diff --git a/FIXES_APPLIED.md b/FIXES_APPLIED.md deleted file mode 100644 index 8d11a651..00000000 --- a/FIXES_APPLIED.md +++ /dev/null @@ -1,217 +0,0 @@ -# 🔧 Game Fixes Applied - November 19, 2025 - -## Issues Fixed - -### 1. ✅ Cannot Answer Card Questions -**Problem**: Card modal wasn't receiving data properly from backend -**Root Cause**: Backend sends `game:card-drawn-self` event with nested structure `{ cardData: {...}, timeLimit: 60 }` but frontend was trying to access fields directly -**Solution**: -- Updated `handleCardDrawn` in GameScreen.jsx to properly extract `cardData` from nested structure -- Added support for `hint` field -- Properly handles both `game:card-drawn` and `game:card-drawn-self` events - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 249-263) - -```javascript -const handleCardDrawn = (data) => { - // Backend sends cardData nested in game:card-drawn-self event - const cardData = data.cardData || data; - setCurrentCard({ - id: cardData.cardId || cardData.id, - type: cardData.cardType || cardData.type, - question: cardData.question || cardData.text || cardData.statement, - answerOptions: cardData.answerOptions || cardData.options || [], - correctAnswer: cardData.correctAnswer, - hint: cardData.hint, - points: cardData.points || 0, - timeLimit: data.timeLimit || cardData.timeLimit || 60 - }) - setIsCardModalOpen(true) -} -``` - ---- - -### 2. ✅ Player Turn Indicator Not Working -**Problem**: Turn indicator wasn't updating properly -**Root Cause**: Frontend didn't know which player was the current user to compare with `gameState.currentPlayer` -**Solution**: -- Added `playerIdentifier` state to GameWebSocketContext -- Decode gameToken on connect to extract `userId` or `playerName` -- Added `isMyTurn` computed value that compares `gameState.currentPlayer` with `playerIdentifier` - -**Files Modified**: -- `SerpentRace_Frontend/src/contexts/GameWebSocketContext.jsx` (lines 16, 57-62, 88-97, 488-489) - -```javascript -// In GameWebSocketContext -const [playerIdentifier, setPlayerIdentifier] = useState(null); - -// Decode token to get player identifier -try { - const payload = JSON.parse(atob(gameToken.split('.')[1])); - const identifier = payload.userId || payload.playerName; - setPlayerIdentifier(identifier); - log('🎼 Player identifier:', identifier); -} catch (err) { - logError('Failed to decode game token:', err); -} - -// Check if it's the current player's turn -const isMyTurn = useMemo(() => { - if (!gameState?.currentPlayer || !playerIdentifier) return false; - return gameState.currentPlayer === playerIdentifier; -}, [gameState?.currentPlayer, playerIdentifier]); -``` - ---- - -### 3. ✅ Current Player Name Not Shown in Indicator -**Problem**: Turn indicator only showed "BetöltĂ©s..." or player ID instead of player name -**Root Cause**: Inconsistent player ID format (some by `userId`, some by `playerName`) -**Solution**: -- Updated player lookup to check multiple possible ID formats -- Highlights current player name in green when it's your turn -- Shows "← Te vagy!" (It's you!) indicator next to your name - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 470-476) - -```javascript -{currentTurn && ( -
- 🎯 Köron: - {players.find(p => p.id === currentTurn || p.playerName === currentTurn || p.name === currentTurn)?.name || currentTurn || 'BetöltĂ©s...'} - - {isMyTurn && ← Te vagy!} -
-)} -``` - ---- - -### 4. ✅ Dice Shown Even When Not Player's Turn -**Problem**: Dice was always interactive regardless of whose turn it was -**Root Cause**: No turn validation on dice display -**Solution**: -- Added conditional rendering based on `isMyTurn` flag -- When it's your turn: Shows green pulsing text "🎯 A te köröd! Kattints a kockĂĄra dobĂĄshoz!" -- When it's NOT your turn: Shows gray text "⏳ VĂĄrd meg a köröd..." and dice is disabled with 50% opacity and `pointer-events-none` - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 609-625) - -```javascript -{isMyTurn ? ( - <> -

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

- - -) : ( - <> -

- ⏳ VĂĄrd meg a köröd... -

-
- -
- -)} -``` - ---- - -## Additional Improvements - -### Debug Panel Enhancement -Added debug information to help verify turn system: -- **🆔 My ID**: Shows current player's identifier (userId or playerName) -- **✅ Is My Turn**: Shows YES/NO to quickly verify turn detection - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 643-644) - ---- - -## Technical Details - -### Token Structure -The gameToken is a JWT containing: -```json -{ - "gameId": "uuid", - "gameCode": "ABC123", - "playerName": "Player1", - "isAuthenticated": true/false, - "userId": "uuid" // only for authenticated players -} -``` - -### Player Identification Logic -Backend uses: `playerIdentifier = socket.userId || socket.playerName` -Frontend now extracts: `payload.userId || payload.playerName` from decoded token - -This ensures both authenticated users (with userId) and guest players (with only playerName) work correctly. - ---- - -## Testing Checklist - -### ✅ Card System -- [ ] Draw a card and verify modal opens with question -- [ ] Verify answer options display correctly (for quiz cards) -- [ ] Submit answer and verify it's sent to backend -- [ ] Check hint displays if available -- [ ] Verify timer countdown works - -### ✅ Turn System -- [ ] Game starts and first player sees "🎯 A te köröd!" -- [ ] Other players see "⏳ VĂĄrd meg a köröd..." -- [ ] Turn indicator shows correct player name -- [ ] "← Te vagy!" appears next to your name when it's your turn -- [ ] Name is highlighted in green when it's your turn - -### ✅ Dice Control -- [ ] Dice is interactive (clickable) only on your turn -- [ ] Dice is grayed out and disabled when not your turn -- [ ] Text changes from green "A te köröd!" to gray "VĂĄrd meg a köröd..." - -### ✅ Multi-Player Testing -- [ ] Test with 2+ authenticated players -- [ ] Test with guest players (no login) -- [ ] Test with mix of authenticated and guest players -- [ ] Verify turn rotation works correctly -- [ ] Each player can only act on their turn - ---- - -## Files Modified Summary - -1. **SerpentRace_Frontend/src/contexts/GameWebSocketContext.jsx** - - Added `playerIdentifier` state - - Added token decoding on connect - - Added `isMyTurn` computed value - - Exported new values in context - -2. **SerpentRace_Frontend/src/pages/Game/GameScreen.jsx** - - Fixed card modal data extraction - - Updated turn indicator with name lookup - - Added turn-based dice control - - Added debug info for turn tracking - - Imported `isMyTurn` and `playerIdentifier` from context - ---- - -## Compilation Status - -✅ **No TypeScript/JavaScript errors** -✅ **All changes backwards compatible** -✅ **Ready for testing** - ---- - -**Last Updated**: November 19, 2025 -**Status**: All 4 issues resolved and tested for compilation errors diff --git a/JavitĂĄs.txt b/JavitĂĄs.txt deleted file mode 100644 index 13b10841..00000000 --- a/JavitĂĄs.txt +++ /dev/null @@ -1,62 +0,0 @@ -JavitĂĄs - -Deckeck: - - KövetkezmĂ©ny csak szerencse kĂĄrtyĂĄnĂĄl - - Egy fajta következmĂ©ny (/lap, automatikusan kerĂŒl vĂ©grehajtĂĄsra) - - HibĂĄs kĂĄrtya pakli mentĂ©sekor is törlödjön - - extra kör, kimarad bĂĄrmennyi 1-tƑl 5-ig - - megnyitĂĄs, szerkesztĂ©s, adatok betöltĂ©se - - MentĂ©si ADATOK CsekkolĂĄsa | ZSOLA - - Closer option - -navbar: - - tegnapiak - -TEGNAPI HIBÁK JAVÍTÁSA: - - kapcs fel routing - - navbar szĂ©thĂșz - - footer kapcsolat - - navabar gomboksorrend - - vagy kontat vagy kapcsolat - - navbar bejelent - - navbar layout finomĂ­t - - palki info get - - -GET /ap/decks/page/:from/:to (0-49) 50db (50-99) 50db ... (0-29) 30db => (30-59) 30db - - from: (oldalsz-1)*dbsz (pl: (1-1)*30=0; (2-1)*30=30) - - to: (oldalsz*dbsz) - 1 (pl: (1*30)-1=29; (2*30)-1 =59) - -email verifikĂĄciĂł: - - verify-email/:code => Email cĂ­me hitelesĂ­tĂ©s alatt: stb - - ha sikeres => login => toastify => email cĂ­me hitelesĂ­tve - - ha sikertelen => home/register => toastify/pushup => sikertelen vegye fel velĂŒnk a kapcsolatot - - - POST api/users/verify-email/:code <= BACKEND URI - - - -HOLNAP ESTE 19:00 => JĂł lenne, ha ezek megvannak -HOLNAPTÓL => JÁTÉK => SOCKET IO mƱködĂ©se - - -Mobil nĂ©zet: - - landing page - - navbar - - footer - - pakli fƑ nĂ©zet => bar - - pakli összerakĂĄs Ă©s szerkesztĂ©s - - bejelentkezĂ©s - - regisztrĂĄciĂł - -User felĂŒlet: - - SajĂĄt adatok lekĂ©rĂ©se - - SajĂĄt adatok mĂłdosĂ­tĂĄsa: - - email-cĂ­m - - telefonszĂĄm - - jelszĂł - - felhasznĂĄlĂł nĂ©v - - SajĂĄt profil törlĂ©se - - Elfelelejtett jelszĂł - - KĂ©rĂ©se => email-cĂ­m alapjĂĄn => POST /api/users/forgot-password - - password-reset/:token => POST /api/users/reset-password diff --git a/README.md b/README.md deleted file mode 100644 index a0fccc0f..00000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# SerpentRace - -- Frontend: React (Vite) -- Backend: Node.js (Express.js) - -## Development Commands - -### Start with File Watchers (Recommended) -```bash -# Windows -.\docker-manage.bat dev:watch - -# Linux/Mac -./docker-manage.sh dev:watch -``` -Automatically syncs file changes and rebuilds containers when needed. - -### Traditional Start -```bash -# Windows -.\docker-manage.bat dev:start - -# Linux/Mac -./docker-manage.sh dev:start -``` - -## Documentation -- [Docker Watcher Guide](./Documentations/DOCKER_WATCHER_GUIDE.md) - Comprehensive guide for file watching functionality \ No newline at end of file diff --git a/SerpentRace_Backend/.dockerignore b/SerpentRace_Backend/.dockerignore deleted file mode 100644 index fb8f96bb..00000000 --- a/SerpentRace_Backend/.dockerignore +++ /dev/null @@ -1,27 +0,0 @@ -node_modules -npm-debug.log -.git -.gitignore -README.md -.env -.nyc_output -coverage -.coverage -.coverage.* -.cache -logs -*.log -.DS_Store -.vscode -.idea -*.swp -*.swo -dist -build -.next -.nuxt -.vuepress/dist -.serverless -.fusebox/ -.dynamodb/ -.tern-port diff --git a/SerpentRace_Backend/.env.dev b/SerpentRace_Backend/.env.dev deleted file mode 100644 index 7f280d5f..00000000 --- a/SerpentRace_Backend/.env.dev +++ /dev/null @@ -1,41 +0,0 @@ -# Development Environment Variables for Local Build -# These are used when running build scripts outside of Docker containers - -NODE_ENV=development -PORT=3000 - -# Database Configuration (Docker containers) -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=postgres - -# Redis Configuration (Docker containers) -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_URL=redis://localhost:6379 - -# JWT Configuration -JWT_SECRET=dev_jwt_secret_change_in_production -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# MinIO Configuration (Docker containers) -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -MINIO_USE_SSL=false - -# Board Generation Configuration -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 - -# EMAIL SERVICE CONFIGURATION -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_USER=your_email@domain.com -EMAIL_PASS=your_email_password -EMAIL_FROM=noreply@serpentrace.com \ No newline at end of file diff --git a/SerpentRace_Backend/.env.example b/SerpentRace_Backend/.env.example deleted file mode 100644 index cd5d9d7c..00000000 --- a/SerpentRace_Backend/.env.example +++ /dev/null @@ -1,62 +0,0 @@ -ï»ż# ============================================== -# SerpentRace Backend Environment Configuration -# ============================================== -# Copy this file to .env and fill in your values - -# APPLICATION CONFIGURATION -NODE_ENV=development -PORT=3000 -APP_BASE_URL=http://localhost:3000 - -# DATABASE CONFIGURATION (PostgreSQL) -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=your_db_password - -# REDIS CONFIGURATION -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_URL=redis://localhost:6379 - -# JWT AUTHENTICATION CONFIGURATION -JWT_SECRET=your-super-secure-secret-key-here -JWT_REFRESH_SECRET=your-super-secure-refresh-secret-key-here - -# Access Token Expiry (choose ONE option, priority order listed): -JWT_ACCESS_TOKEN_EXPIRY=1800 # Seconds (recommended for production) -# JWT_ACCESS_TOKEN_EXPIRATION=30m # Duration string (user-friendly) -# JWT_EXPIRY=1800 # Legacy: seconds -# JWT_EXPIRATION=30m # Legacy: duration string - -# Refresh Token Expiry (choose ONE option, priority order listed): -JWT_REFRESH_TOKEN_EXPIRY=604800 # Seconds (7 days) -# JWT_REFRESH_TOKEN_EXPIRATION=7d # Duration string (recommended) -# JWT_REFRESH_EXPIRATION=7d # Legacy: duration string - -# Cookie Names (optional) -JWT_COOKIE_NAME=auth_token -JWT_REFRESH_COOKIE_NAME=refresh_token - -# Legacy JWT Configuration (deprecated - use above options) -# JWT_EXPIRY=86400 -# JWT_EXPIRATION=24h -GAME_TOKEN_EXPIRY=86400 - -# EMAIL SERVICE CONFIGURATION -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_USER=your_email@domain.com -EMAIL_PASS=your_email_password -EMAIL_FROM=noreply@serpentrace.com - -# CHAT SYSTEM CONFIGURATION -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# GAME CONFIGURATION -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 diff --git a/SerpentRace_Backend/.gitignore b/SerpentRace_Backend/.gitignore deleted file mode 100644 index c8d513b6..00000000 --- a/SerpentRace_Backend/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -./dist/* -./node_modules/* -./Archive_*/* -./Archive_* -./logs/* diff --git a/SerpentRace_Backend/assets/Logo.png b/SerpentRace_Backend/assets/Logo.png deleted file mode 100644 index 480d8c5d..00000000 Binary files a/SerpentRace_Backend/assets/Logo.png and /dev/null differ diff --git a/SerpentRace_Backend/jest.config.js b/SerpentRace_Backend/jest.config.js deleted file mode 100644 index 929a0400..00000000 --- a/SerpentRace_Backend/jest.config.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/tests', '/src'], - testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], - transform: { - '^.+\\.ts$': 'ts-jest', - }, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.d.ts', - '!src/Api/index.ts', - '!src/Infrastructure/ormconfig.ts', - '!src/search-demo.ts' - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html'], - moduleFileExtensions: ['ts', 'js', 'json'], - setupFilesAfterEnv: ['/tests/setup.ts'], - testTimeout: 10000, - setupFiles: ['/tests/jest.setup.ts'], - verbose: true, - moduleNameMapper: { - '^@/(.*)$': '/src/$1' - }, - resolver: undefined, - moduleDirectories: ['node_modules', '/src', '/tests'] -}; diff --git a/SerpentRace_Backend/language-test.js b/SerpentRace_Backend/language-test.js deleted file mode 100644 index 4895e213..00000000 --- a/SerpentRace_Backend/language-test.js +++ /dev/null @@ -1,29 +0,0 @@ -// Quick test to demonstrate the language detection functionality -const { extractLanguageFromAcceptHeader } = require('./src/Api/contactRouter.js'); - -// Test cases to demonstrate Accept-Language parsing -const testCases = [ - 'en-US,en;q=0.9', - 'hu,en;q=0.9', - 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', - 'hu-HU,hu;q=0.9,en-US;q=0.8', - 'fr-FR,fr;q=0.9,en;q=0.8', - 'es,en-US;q=0.9,en;q=0.8', - 'invalid-header', - '' -]; - -console.log('Testing Accept-Language header parsing:\n'); - -testCases.forEach(header => { - const result = extractLanguageFromAcceptHeader(header); - console.log(`Header: "${header}" -> Language: ${result}`); -}); - -console.log('\n✅ Multi-language system is working correctly!'); -console.log('\nFeatures implemented:'); -console.log('- Accept-Language header parsing with quality values'); -console.log('- Support for EN, HU, DE templates'); -console.log('- Custom header detection (X-Language, X-Region, X-Locale)'); -console.log('- Fallback to English for unsupported languages'); -console.log('- Professional email templates in all three languages'); diff --git a/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js b/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js deleted file mode 100644 index 0d7d9dab..00000000 --- a/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js +++ /dev/null @@ -1,513 +0,0 @@ - -/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ - -/*! - * /** - * * Copyright (c) Meta Platforms, Inc. and affiliates. - * * - * * This source code is licensed under the MIT license found in the - * * LICENSE file in the root directory of this source tree. - * * / - */ -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ "./src/runTest.ts": -/***/ ((__unused_webpack_module, exports) => { - - - -Object.defineProperty(exports, "__esModule", ({ - value: true -})); -exports["default"] = runTest; -function _nodeVm() { - const data = require("node:vm"); - _nodeVm = function () { - return data; - }; - return data; -} -function _chalk() { - const data = _interopRequireDefault(require("chalk")); - _chalk = function () { - return data; - }; - return data; -} -function fs() { - const data = _interopRequireWildcard(require("graceful-fs")); - fs = function () { - return data; - }; - return data; -} -function sourcemapSupport() { - const data = _interopRequireWildcard(require("source-map-support")); - sourcemapSupport = function () { - return data; - }; - return data; -} -function _console() { - const data = require("@jest/console"); - _console = function () { - return data; - }; - return data; -} -function _transform() { - const data = require("@jest/transform"); - _transform = function () { - return data; - }; - return data; -} -function docblock() { - const data = _interopRequireWildcard(require("jest-docblock")); - docblock = function () { - return data; - }; - return data; -} -function _jestLeakDetector() { - const data = _interopRequireDefault(require("jest-leak-detector")); - _jestLeakDetector = function () { - return data; - }; - return data; -} -function _jestMessageUtil() { - const data = require("jest-message-util"); - _jestMessageUtil = function () { - return data; - }; - return data; -} -function _jestResolve() { - const data = require("jest-resolve"); - _jestResolve = function () { - return data; - }; - return data; -} -function _jestUtil() { - const data = require("jest-util"); - _jestUtil = function () { - return data; - }; - return data; -} -function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } -function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// eslint-disable-next-line @typescript-eslint/consistent-type-imports - -function freezeConsole(testConsole, config) { - // @ts-expect-error: `_log` is `private` - we should figure out some proper API here - testConsole._log = function fakeConsolePush(_type, message) { - const error = new (_jestUtil().ErrorWithStack)(`${_chalk().default.red(`${_chalk().default.bold('Cannot log after tests are done.')} Did you forget to wait for something async in your test?`)}\nAttempted to log "${message}".`, fakeConsolePush); - const formattedError = (0, _jestMessageUtil().formatExecError)(error, config, { - noStackTrace: false - }, undefined, true); - process.stderr.write(`\n${formattedError}\n`); - process.exitCode = 1; - }; -} - -// Keeping the core of "runTest" as a separate function (as "runTestInternal") -// is key to be able to detect memory leaks. Since all variables are local to -// the function, when "runTestInternal" finishes its execution, they can all be -// freed, UNLESS something else is leaking them (and that's why we can detect -// the leak!). -// -// If we had all the code in a single function, we should manually nullify all -// references to verify if there is a leak, which is not maintainable and error -// prone. That's why "runTestInternal" CANNOT be inlined inside "runTest". -async function runTestInternal(path, globalConfig, projectConfig, resolver, context, sendMessageToJest) { - const testSource = fs().readFileSync(path, 'utf8'); - const docblockPragmas = docblock().parse(docblock().extract(testSource)); - const customEnvironment = docblockPragmas['jest-environment']; - const loadTestEnvironmentStart = Date.now(); - let testEnvironment = projectConfig.testEnvironment; - if (customEnvironment) { - if (Array.isArray(customEnvironment)) { - throw new TypeError(`You can only define a single test environment through docblocks, got "${customEnvironment.join(', ')}"`); - } - testEnvironment = (0, _jestResolve().resolveTestEnvironment)({ - ...projectConfig, - // we wanna avoid webpack trying to be clever - requireResolveFunction: module => require.resolve(module), - testEnvironment: customEnvironment - }); - } - const cacheFS = new Map([[path, testSource]]); - const transformer = await (0, _transform().createScriptTransformer)(projectConfig, cacheFS); - const TestEnvironment = await transformer.requireAndTranspileModule(testEnvironment); - const testFramework = await transformer.requireAndTranspileModule(process.env.JEST_JASMINE === '1' ? require.resolve('jest-jasmine2') : projectConfig.testRunner); - const Runtime = (0, _jestUtil().interopRequireDefault)(projectConfig.runtime ? require(projectConfig.runtime) : require('jest-runtime')).default; - const consoleOut = globalConfig.useStderr ? process.stderr : process.stdout; - const consoleFormatter = (type, message) => (0, _console().getConsoleOutput)( - // 4 = the console call is buried 4 stack frames deep - _console().BufferedConsole.write([], type, message, 4), projectConfig, globalConfig); - let testConsole; - if (globalConfig.silent) { - testConsole = new (_console().NullConsole)(consoleOut, consoleOut, consoleFormatter); - } else if (globalConfig.verbose) { - testConsole = new (_console().CustomConsole)(consoleOut, consoleOut, consoleFormatter); - } else { - testConsole = new (_console().BufferedConsole)(); - } - let extraTestEnvironmentOptions; - const docblockEnvironmentOptions = docblockPragmas['jest-environment-options']; - if (typeof docblockEnvironmentOptions === 'string') { - extraTestEnvironmentOptions = JSON.parse(docblockEnvironmentOptions); - } - const environment = new TestEnvironment({ - globalConfig, - projectConfig: extraTestEnvironmentOptions ? { - ...projectConfig, - testEnvironmentOptions: { - ...projectConfig.testEnvironmentOptions, - ...extraTestEnvironmentOptions - } - } : projectConfig - }, { - console: testConsole, - docblockPragmas, - testPath: path - }); - const loadTestEnvironmentEnd = Date.now(); - if (typeof environment.getVmContext !== 'function') { - console.error(`Test environment found at "${testEnvironment}" does not export a "getVmContext" method, which is mandatory from Jest 27. This method is a replacement for "runScript".`); - process.exit(1); - } - const leakDetector = projectConfig.detectLeaks ? new (_jestLeakDetector().default)(environment) : null; - (0, _jestUtil().setGlobal)(environment.global, 'console', testConsole, 'retain'); - const runtime = new Runtime(projectConfig, environment, resolver, transformer, cacheFS, { - changedFiles: context.changedFiles, - collectCoverage: globalConfig.collectCoverage, - collectCoverageFrom: globalConfig.collectCoverageFrom, - coverageProvider: globalConfig.coverageProvider, - sourcesRelatedToTestsInChangedFiles: context.sourcesRelatedToTestsInChangedFiles - }, path, globalConfig); - let isTornDown = false; - const tearDownEnv = async () => { - if (!isTornDown) { - runtime.teardown(); - - // source-map-support keeps memory leftovers in `Error.prepareStackTrace` - (0, _nodeVm().runInContext)("Error.prepareStackTrace = () => '';", environment.getVmContext()); - sourcemapSupport().resetRetrieveHandlers(); - try { - await environment.teardown(); - } finally { - isTornDown = true; - } - } - }; - const start = Date.now(); - const setupFilesStart = Date.now(); - for (const path of projectConfig.setupFiles) { - const esm = runtime.unstable_shouldLoadAsEsm(path); - if (esm) { - await runtime.unstable_importModule(path); - } else { - const setupFile = runtime.requireModule(path); - if (typeof setupFile === 'function') { - await setupFile(); - } - } - } - const setupFilesEnd = Date.now(); - const sourcemapOptions = { - environment: 'node', - handleUncaughtExceptions: false, - retrieveSourceMap: source => { - const sourceMapSource = runtime.getSourceMaps()?.get(source); - if (sourceMapSource) { - try { - return { - map: JSON.parse(fs().readFileSync(sourceMapSource, 'utf8')), - url: source - }; - } catch {} - } - return null; - } - }; - - // For tests - runtime.requireInternalModule(require.resolve('source-map-support')).install(sourcemapOptions); - - // For runtime errors - sourcemapSupport().install(sourcemapOptions); - if (environment.global && environment.global.process && environment.global.process.exit) { - const realExit = environment.global.process.exit; - environment.global.process.exit = function exit(...args) { - const error = new (_jestUtil().ErrorWithStack)(`process.exit called with "${args.join(', ')}"`, exit); - const formattedError = (0, _jestMessageUtil().formatExecError)(error, projectConfig, { - noStackTrace: false - }, undefined, true); - process.stderr.write(formattedError); - return realExit(...args); - }; - } - - // if we don't have `getVmContext` on the env skip coverage - const collectV8Coverage = globalConfig.collectCoverage && globalConfig.coverageProvider === 'v8' && typeof environment.getVmContext === 'function'; - - // Node's error-message stack size is limited at 10, but it's pretty useful - // to see more than that when a test fails. - Error.stackTraceLimit = 100; - try { - await environment.setup(); - let result; - try { - if (collectV8Coverage) { - await runtime.collectV8Coverage(); - } - result = await testFramework(globalConfig, projectConfig, environment, runtime, path, sendMessageToJest); - } catch (error) { - // Access all stacks before uninstalling sourcemaps - let e = error; - while (typeof e === 'object' && e !== null && 'stack' in e) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - e.stack; - e = e?.cause; - } - throw error; - } finally { - if (collectV8Coverage) { - await runtime.stopCollectingV8Coverage(); - } - } - freezeConsole(testConsole, projectConfig); - const testCount = result.numPassingTests + result.numFailingTests + result.numPendingTests + result.numTodoTests; - const end = Date.now(); - const testRuntime = end - start; - result.perfStats = { - ...result.perfStats, - end, - loadTestEnvironmentEnd, - loadTestEnvironmentStart, - runtime: testRuntime, - setupFilesEnd, - setupFilesStart, - slow: testRuntime / 1000 > projectConfig.slowTestThreshold, - start - }; - result.testFilePath = path; - result.console = testConsole.getBuffer(); - result.skipped = testCount === result.numPendingTests; - result.displayName = projectConfig.displayName; - const coverage = runtime.getAllCoverageInfoCopy(); - if (coverage) { - const coverageKeys = Object.keys(coverage); - if (coverageKeys.length > 0) { - result.coverage = coverage; - } - } - if (collectV8Coverage) { - const v8Coverage = runtime.getAllV8CoverageInfoCopy(); - if (v8Coverage && v8Coverage.length > 0) { - result.v8Coverage = v8Coverage; - } - } - if (globalConfig.logHeapUsage) { - globalThis.gc?.(); - result.memoryUsage = process.memoryUsage().heapUsed; - } - await tearDownEnv(); - - // Delay the resolution to allow log messages to be output. - return await new Promise(resolve => { - setImmediate(() => resolve({ - leakDetector, - result - })); - }); - } finally { - await tearDownEnv(); - } -} -async function runTest(path, globalConfig, config, resolver, context, sendMessageToJest) { - const { - leakDetector, - result - } = await runTestInternal(path, globalConfig, config, resolver, context, sendMessageToJest); - if (leakDetector) { - // We wanna allow a tiny but time to pass to allow last-minute cleanup - await new Promise(resolve => setTimeout(resolve, 100)); - - // Resolve leak detector, outside the "runTestInternal" closure. - result.leaks = await leakDetector.isLeaking(); - } else { - result.leaks = false; - } - return result; -} - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry needs to be wrapped in an IIFE because it uses a non-standard name for the exports (exports). -(() => { -var exports = __webpack_exports__; - - -Object.defineProperty(exports, "__esModule", ({ - value: true -})); -exports.setup = setup; -exports.worker = worker; -function _exitX() { - const data = _interopRequireDefault(require("exit-x")); - _exitX = function () { - return data; - }; - return data; -} -function _jestHasteMap() { - const data = _interopRequireDefault(require("jest-haste-map")); - _jestHasteMap = function () { - return data; - }; - return data; -} -function _jestMessageUtil() { - const data = require("jest-message-util"); - _jestMessageUtil = function () { - return data; - }; - return data; -} -function _jestRuntime() { - const data = _interopRequireDefault(require("jest-runtime")); - _jestRuntime = function () { - return data; - }; - return data; -} -function _jestWorker() { - const data = require("jest-worker"); - _jestWorker = function () { - return data; - }; - return data; -} -var _runTest = _interopRequireDefault(__webpack_require__("./src/runTest.ts")); -function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// Make sure uncaught errors are logged before we exit. -process.on('uncaughtException', err => { - if (err.stack) { - console.error(err.stack); - } else { - console.error(err); - } - (0, _exitX().default)(1); -}); -const formatError = error => { - if (typeof error === 'string') { - const { - message, - stack - } = (0, _jestMessageUtil().separateMessageFromStack)(error); - return { - message, - stack, - type: 'Error' - }; - } - return { - code: error.code || undefined, - message: error.message, - stack: error.stack, - type: 'Error' - }; -}; -const resolvers = new Map(); -const getResolver = config => { - const resolver = resolvers.get(config.id); - if (!resolver) { - throw new Error(`Cannot find resolver for: ${config.id}`); - } - return resolver; -}; -function setup(setupData) { - // Module maps that will be needed for the test runs are passed. - for (const { - config, - serializableModuleMap - } of setupData.serializableResolvers) { - const moduleMap = _jestHasteMap().default.getStatic(config).getModuleMapFromJSON(serializableModuleMap); - resolvers.set(config.id, _jestRuntime().default.createResolver(config, moduleMap)); - } -} -const sendMessageToJest = (eventName, args) => { - (0, _jestWorker().messageParent)([eventName, args]); -}; -async function worker({ - config, - globalConfig, - path, - context -}) { - try { - return await (0, _runTest.default)(path, globalConfig, config, getResolver(config), { - ...context, - changedFiles: context.changedFiles && new Set(context.changedFiles), - sourcesRelatedToTestsInChangedFiles: context.sourcesRelatedToTestsInChangedFiles && new Set(context.sourcesRelatedToTestsInChangedFiles) - }, sendMessageToJest); - } catch (error) { - throw formatError(error); - } -} -})(); - -module.exports = __webpack_exports__; -/******/ })() -; \ No newline at end of file diff --git a/SerpentRace_Backend/node_modules/jest/bin/jest.js b/SerpentRace_Backend/node_modules/jest/bin/jest.js deleted file mode 100644 index 44425d69..00000000 --- a/SerpentRace_Backend/node_modules/jest/bin/jest.js +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env node -/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ - - -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const importLocal = require('import-local'); - -if (!importLocal(__filename)) { - require('jest-cli/bin/jest'); -} diff --git a/SerpentRace_Backend/package-lock.json b/SerpentRace_Backend/package-lock.json deleted file mode 100644 index be1a5942..00000000 --- a/SerpentRace_Backend/package-lock.json +++ /dev/null @@ -1,10435 +0,0 @@ -{ - "name": "serpentrace_backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "serpentrace_backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "bcrypt": "^6.0.0", - "cookie-parser": "^1.4.7", - "express": "^5.1.0", - "helmet": "^8.1.0", - "jsonwebtoken": "^9.0.2", - "minio": "^8.0.5", - "multer": "^2.0.2", - "nodemailer": "^7.0.5", - "pg": "^8.16.3", - "redis": "^5.8.1", - "sharp": "^0.34.4", - "socket.io": "^4.8.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "tsconfig-paths": "^4.2.0", - "typeorm": "^0.3.26", - "uuid": "^11.1.0", - "winston": "^3.17.0" - }, - "devDependencies": { - "@jest/globals": "^30.0.5", - "@types/bcrypt": "^6.0.0", - "@types/cookie-parser": "^1.4.9", - "@types/express": "^5.0.3", - "@types/jest": "^30.0.0", - "@types/jsonwebtoken": "^9.0.10", - "@types/multer": "^2.0.0", - "@types/node": "^24.3.3", - "@types/nodemailer": "^7.0.1", - "@types/pg": "^8.15.5", - "@types/redis": "^4.0.10", - "@types/socket.io": "^3.0.1", - "@types/socket.io-client": "^1.4.36", - "@types/supertest": "^6.0.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.8", - "@types/uuid": "^10.0.0", - "jest": "^30.0.5", - "nodemon": "^3.1.10", - "rimraf": "^5.0.10", - "socket.io-client": "^4.8.1", - "supertest": "^7.1.4", - "ts-jest": "^29.4.1", - "ts-node": "^10.9.2", - "typescript": "^5.9.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "license": "MIT" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.864.0.tgz", - "integrity": "sha512-pwn4/3bs7ccucS9sYpMbzptEhEFQQy8TXtmKNzmyY7OIDBGTiJrxsWYDTULO4nxsMmGXi39mSEowlK4QUCyC+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/credential-provider-node": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/signature-v4-multi-region": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.864.0.tgz", - "integrity": "sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.864.0.tgz", - "integrity": "sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@aws-sdk/xml-builder": "3.862.0", - "@smithy/core": "^3.8.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-utf8": "^4.0.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.864.0.tgz", - "integrity": "sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.864.0.tgz", - "integrity": "sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.864.0.tgz", - "integrity": "sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/credential-provider-env": "3.864.0", - "@aws-sdk/credential-provider-http": "3.864.0", - "@aws-sdk/credential-provider-process": "3.864.0", - "@aws-sdk/credential-provider-sso": "3.864.0", - "@aws-sdk/credential-provider-web-identity": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.864.0.tgz", - "integrity": "sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.864.0", - "@aws-sdk/credential-provider-http": "3.864.0", - "@aws-sdk/credential-provider-ini": "3.864.0", - "@aws-sdk/credential-provider-process": "3.864.0", - "@aws-sdk/credential-provider-sso": "3.864.0", - "@aws-sdk/credential-provider-web-identity": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.864.0.tgz", - "integrity": "sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.864.0.tgz", - "integrity": "sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.864.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/token-providers": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.864.0.tgz", - "integrity": "sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.862.0.tgz", - "integrity": "sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.862.0.tgz", - "integrity": "sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.862.0.tgz", - "integrity": "sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.864.0.tgz", - "integrity": "sha512-GjYPZ6Xnqo17NnC8NIQyvvdzzO7dm+Ks7gpxD/HsbXPmV2aEfuFveJXneGW9e1BheSKFff6FPDWu8Gaj2Iu1yg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/core": "^3.8.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-stream": "^4.2.4", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.864.0.tgz", - "integrity": "sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@smithy/core": "^3.8.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.864.0.tgz", - "integrity": "sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.862.0.tgz", - "integrity": "sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.864.0.tgz", - "integrity": "sha512-w2HIn/WIcUyv1bmyCpRUKHXB5KdFGzyxPkp/YK5g+/FuGdnFFYWGfcO8O+How4jwrZTarBYsAHW9ggoKvwr37w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.864.0.tgz", - "integrity": "sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", - "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", - "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.862.0.tgz", - "integrity": "sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-endpoints": "^3.0.7", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", - "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.862.0.tgz", - "integrity": "sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.864.0.tgz", - "integrity": "sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.862.0.tgz", - "integrity": "sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.5.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", - "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", - "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.5", - "jest-config": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-resolve-dependencies": "30.0.5", - "jest-runner": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "jest-watcher": "30.0.5", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-mock": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.0.5", - "jest-snapshot": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", - "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", - "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", - "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "jest-worker": "30.0.5", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", - "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/test-result": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", - "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/types": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", - "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.0.5", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", - "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@redis/bloom": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.1.tgz", - "integrity": "sha512-hJOJr/yX6BttnyZ+nxD3Ddiu2lPig4XJjyAK1v7OSHOJNUTfn3RHBryB9wgnBMBdkg9glVh2AjItxIXmr600MA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@redis/client": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.1.tgz", - "integrity": "sha512-hD5Tvv7G0t8b3w8ao3kQ4jEPUmUUC6pqA18c8ciYF5xZGfUGBg0olQHW46v6qSt4O5bxOuB3uV7pM6H5wEjBwA==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@redis/json": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.1.tgz", - "integrity": "sha512-kyvM8Vn+WjJI++nRsIoI9TbdfCs1/TgD0Hp7Z7GiG6W4IEBzkXGQakli+R5BoJzUfgh7gED2fkncYy1NLprMNg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@redis/search": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.1.tgz", - "integrity": "sha512-CzuKNTInTNQkxqehSn7QiYcM+th+fhjQn5ilTvksP1wPjpxqK0qWt92oYg3XZc3tO2WuXkqDvTujc4D7kb6r/A==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@redis/time-series": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.1.tgz", - "integrity": "sha512-klvdR96U9oSOyqvcectoAGhYlMOnMS3I5UWUOgdBn1buMODiwM/E4Eds7gxldKmtowe4rLJSF1CyIqyZTjy8Ow==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.40", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", - "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@smithy/abort-controller": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", - "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", - "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", - "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.0.9", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-stream": "^4.2.4", - "@smithy/util-utf8": "^4.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core/node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@smithy/core/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", - "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", - "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", - "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", - "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", - "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", - "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-middleware": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.1.19", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", - "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/protocol-http": "^5.1.3", - "@smithy/service-error-classification": "^4.0.7", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry/node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", - "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", - "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", - "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", - "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", - "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", - "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", - "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-uri-escape": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", - "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", - "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", - "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", - "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", - "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", - "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", - "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", - "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", - "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", - "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.1.5", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", - "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", - "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", - "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.0.7", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", - "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@sqltools/formatter": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", - "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/multer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", - "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/node": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz", - "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/nodemailer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", - "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@aws-sdk/client-sesv2": "^3.839.0", - "@types/node": "*" - } - }, - "node_modules/@types/pg": { - "version": "8.15.5", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", - "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/redis": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.10.tgz", - "integrity": "sha512-7CLy5b5fzzEGVcOccgZjoMlNpPhX6d10jEeRy2YWbFuaMNrSPc9ExRsMYsd+0VxvEHucf4EWx24Ja7cSU1FGUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "redis": "*" - } - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/socket.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", - "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "socket.io": "*" - } - }, - "node_modules/@types/socket.io-client": { - "version": "1.4.36", - "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", - "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", - "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "node_modules/@types/swagger-jsdoc": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", - "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", - "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "license": "(Unlicense OR Apache-2.0)", - "optional": true - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansis": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", - "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", - "license": "ISC", - "engines": { - "node": ">=14" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", - "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.0.5", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/block-stream2": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", - "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bowser": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", - "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-or-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", - "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", - "license": "MIT" - }, - "node_modules/browserslist": { - "version": "4.25.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", - "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001735", - "electron-to-chromium": "^1.5.204", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.207", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz", - "integrity": "sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", - "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.0.5", - "@jest/types": "30.0.5", - "import-local": "^3.2.0", - "jest-cli": "30.0.5" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.0.5", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", - "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "p-limit": "^3.1.0", - "pretty-format": "30.0.5", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", - "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", - "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.0.1", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.5", - "@jest/types": "30.0.5", - "babel-jest": "30.0.5", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.0.5", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-runner": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "micromatch": "^4.0.8", - "parse-json": "^5.2.0", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", - "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", - "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", - "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", - "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "jest-worker": "30.0.5", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-leak-detector": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", - "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", - "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "jest-diff": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", - "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", - "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", - "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/environment": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-leak-detector": "30.0.5", - "jest-message-util": "30.0.5", - "jest-resolve": "30.0.5", - "jest-runtime": "30.0.5", - "jest-util": "30.0.5", - "jest-watcher": "30.0.5", - "jest-worker": "30.0.5", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", - "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/globals": "30.0.5", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", - "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "babel-preset-current-node-syntax": "^1.1.0", - "chalk": "^4.1.2", - "expect": "30.0.5", - "graceful-fs": "^4.2.11", - "jest-diff": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "pretty-format": "30.0.5", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/jest-validate": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", - "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.5", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", - "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.0.5", - "string-length": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", - "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minio": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz", - "integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.4", - "block-stream2": "^2.1.0", - "browser-or-node": "^2.1.1", - "buffer-crc32": "^1.0.0", - "eventemitter3": "^5.0.1", - "fast-xml-parser": "^4.4.1", - "ipaddr.js": "^2.0.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "query-string": "^7.1.3", - "stream-json": "^1.8.0", - "through2": "^4.0.2", - "web-encoding": "^1.1.5", - "xml2js": "^0.5.0 || ^0.6.2" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/minio/node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/minio/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/minio/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minio/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minio/node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/multer/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemailer": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", - "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT", - "peer": true - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redis": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.8.1.tgz", - "integrity": "sha512-RZjBKYX/qFF809x6vDcE5VA6L3MmiuT+BkbXbIyyyeU0lPD47V4z8qTzN+Z/kKFwpojwCItOfaItYuAjNs8pTQ==", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.8.1", - "@redis/client": "5.8.1", - "@redis/json": "5.8.1", - "@redis/search": "5.8.1", - "@redis/time-series": "5.8.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/sql-highlight": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", - "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", - "funding": [ - "https://github.com/scriptcoded/sql-highlight?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/scriptcoded" - } - ], - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/superagent": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", - "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.1", - "cookiejar": "^2.1.4", - "debug": "^4.3.7", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.4", - "formidable": "^3.5.4", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.2" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supertest": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", - "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^10.2.3" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "license": "MIT", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", - "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.2", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/typeorm": { - "version": "0.3.26", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", - "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", - "license": "MIT", - "dependencies": { - "@sqltools/formatter": "^1.2.5", - "ansis": "^3.17.0", - "app-root-path": "^3.1.0", - "buffer": "^6.0.3", - "dayjs": "^1.11.13", - "debug": "^4.4.0", - "dedent": "^1.6.0", - "dotenv": "^16.4.7", - "glob": "^10.4.5", - "sha.js": "^2.4.11", - "sql-highlight": "^6.0.0", - "tslib": "^2.8.1", - "uuid": "^11.1.0", - "yargs": "^17.7.2" - }, - "bin": { - "typeorm": "cli.js", - "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", - "typeorm-ts-node-esm": "cli-ts-node-esm.js" - }, - "engines": { - "node": ">=16.13.0" - }, - "funding": { - "url": "https://opencollective.com/typeorm" - }, - "peerDependencies": { - "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", - "@sap/hana-client": "^2.14.22", - "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", - "ioredis": "^5.0.4", - "mongodb": "^5.8.0 || ^6.0.0", - "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", - "mysql2": "^2.2.5 || ^3.0.1", - "oracledb": "^6.3.0", - "pg": "^8.5.1", - "pg-native": "^3.0.0", - "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", - "reflect-metadata": "^0.1.14 || ^0.2.0", - "sql.js": "^1.4.0", - "sqlite3": "^5.0.3", - "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" - }, - "peerDependenciesMeta": { - "@google-cloud/spanner": { - "optional": true - }, - "@sap/hana-client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mssql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "pg-query-stream": { - "optional": true - }, - "redis": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "ts-node": { - "optional": true - }, - "typeorm-aurora-data-api-driver": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "license": "MIT", - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - } - } -} diff --git a/SerpentRace_Backend/package.json b/SerpentRace_Backend/package.json deleted file mode 100644 index 0999f1bd..00000000 --- a/SerpentRace_Backend/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "serpentrace_backend", - "version": "1.0.0", - "description": "", - "license": "ISC", - "author": "", - "type": "commonjs", - "main": "index.js", - "scripts": { - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:redis": "jest --testNamePattern=\"RedisService\"", - "start": "node ./dist/Api/index.js", - "dev": "nodemon --watch src --ext ts,json --exec ts-node ./src/Api/index.ts", - "build": "npm run build:clean && npm run build:compile && npm run build:copy-assets", - "build:clean": "rimraf dist", - "build:compile": "tsc", - "build:copy-assets": "node scripts/copy-assets.js", - "build:production": "npm run build:clean && npm run lint && npm run test && npm run migration:run && npm run build:compile && npm run build:copy-assets", - "build:docker": "npm run build:clean && npm run build:compile && npm run build:copy-assets", - "build:advanced": "ts-node scripts/build.ts", - "build:advanced:prod": "ts-node scripts/build.ts --production --migrations --test", - "build:advanced:ci": "ts-node scripts/build.ts --production --migrations --test --skip-lint", - "deploy": "node -e \"console.log('Use deploy.bat on Windows or deploy.sh on Linux/Mac')\"", - "deploy:prod": "npm run build:production && echo 'Build completed - ready for deployment'", - "build:help": "node scripts/build-help.js", - "build:status": "node scripts/build-help.js --status", - "build:quick": "node scripts/build-help.js --quick", - "prebuild": "npm run lint", - "postbuild": "echo 'Build completed successfully!'", - "lint": "echo 'Linting...' && echo 'No linter configured - add ESLint if needed'", - "migration:create": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli migration:create", - "migration:generate": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:generate", - "migration:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:run", - "migration:revert": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:revert", - "migration:show": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:show", - "migration:full": "ts-node scripts/generate-migration.ts", - "typecheck": "tsc --noEmit", - "watch": "tsc --watch" - }, - "dependencies": { - "bcrypt": "^6.0.0", - "cookie-parser": "^1.4.7", - "express": "^5.1.0", - "helmet": "^8.1.0", - "jsonwebtoken": "^9.0.2", - "minio": "^8.0.5", - "multer": "^2.0.2", - "nodemailer": "^7.0.5", - "pg": "^8.16.3", - "redis": "^5.8.1", - "sharp": "^0.34.4", - "socket.io": "^4.8.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "tsconfig-paths": "^4.2.0", - "typeorm": "^0.3.26", - "uuid": "^11.1.0", - "winston": "^3.17.0" - }, - "devDependencies": { - "@jest/globals": "^30.0.5", - "@types/bcrypt": "^6.0.0", - "@types/cookie-parser": "^1.4.9", - "@types/express": "^5.0.3", - "@types/jest": "^30.0.0", - "@types/jsonwebtoken": "^9.0.10", - "@types/multer": "^2.0.0", - "@types/node": "^24.3.3", - "@types/nodemailer": "^7.0.1", - "@types/pg": "^8.15.5", - "@types/redis": "^4.0.10", - "@types/socket.io": "^3.0.1", - "@types/socket.io-client": "^1.4.36", - "@types/supertest": "^6.0.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.8", - "@types/uuid": "^10.0.0", - "jest": "^30.0.5", - "nodemon": "^3.1.10", - "rimraf": "^5.0.10", - "socket.io-client": "^4.8.1", - "supertest": "^7.1.4", - "ts-jest": "^29.4.1", - "ts-node": "^10.9.2", - "typescript": "^5.9.2" - } -} diff --git a/SerpentRace_Backend/scripts/build-help.js b/SerpentRace_Backend/scripts/build-help.js deleted file mode 100644 index 183e9dff..00000000 --- a/SerpentRace_Backend/scripts/build-help.js +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -/** - * Build System Helper - Shows available build commands and their descriptions - */ - -const commands = { - 'Development Commands': { - 'npm run dev': 'Start development server with hot reload', - 'npm run watch': 'Watch mode TypeScript compilation', - 'npm run typecheck': 'Type checking without code generation' - }, - 'Build Commands': { - 'npm run build': 'Standard build: clean → compile → copy assets', - 'npm run build:clean': 'Clean the dist directory', - 'npm run build:compile': 'Compile TypeScript to JavaScript', - 'npm run build:copy-assets': 'Copy non-TS files to dist directory', - 'npm run build:docker': 'Build for Docker (no tests/migrations)' - }, - 'Production Build Commands': { - 'npm run build:production': 'Full production build with linting, tests, and migrations', - 'npm run build:advanced': 'Advanced build script with custom options', - 'npm run build:advanced:prod': 'Advanced production build with all validations', - 'npm run build:advanced:ci': 'CI/CD friendly build (skips linting)', - 'npm run deploy:prod': 'Build for production deployment' - }, - 'Database Commands': { - 'npm run migration:run': 'Run pending database migrations', - 'npm run migration:show': 'Show migration status', - 'npm run migration:generate ': 'Generate new migration', - 'npm run migration:create ': 'Create empty migration', - 'npm run migration:revert': 'Revert last migration', - 'npm run migration:full ': 'Create, generate, and run migration' - }, - 'Testing Commands': { - 'npm test': 'Run all tests', - 'npm run test:watch': 'Run tests in watch mode', - 'npm run test:coverage': 'Run tests with coverage report', - 'npm run test:redis': 'Run Redis-specific tests' - }, - 'Deployment Scripts': { - 'scripts/deploy.sh': 'Full Linux/Mac deployment script', - 'scripts/deploy.bat': 'Full Windows deployment script' - } -}; - -function showCommands() { - console.log('🔧 SerpentRace Backend Build System\n'); - - Object.entries(commands).forEach(([category, categoryCommands]) => { - console.log(`\x1b[36m${category}\x1b[0m`); - console.log('=' .repeat(category.length)); - - Object.entries(categoryCommands).forEach(([command, description]) => { - console.log(` \x1b[32m${command.padEnd(35)}\x1b[0m ${description}`); - }); - - console.log(''); - }); - - console.log('\x1b[33mQuick Start:\x1b[0m'); - console.log(' npm run build # Basic build'); - console.log(' npm run build:production # Production build'); - console.log(' npm run dev # Development server\n'); - - console.log('\x1b[33mDocumentation:\x1b[0m'); - console.log(' See BUILD.md for detailed documentation'); -} - -function checkBuildStatus() { - const distPath = path.join(__dirname, '..', 'dist'); - - if (fs.existsSync(distPath)) { - const stats = fs.statSync(distPath); - console.log(`\x1b[32m✅ Last build:\x1b[0m ${stats.mtime.toLocaleString()}`); - - const indexPath = path.join(distPath, 'Api', 'index.js'); - if (fs.existsSync(indexPath)) { - console.log('\x1b[32m✅ Main entry point built successfully\x1b[0m'); - } else { - console.log('\x1b[31m❌ Main entry point missing\x1b[0m'); - } - } else { - console.log('\x1b[33m⚠ No build found - run "npm run build" first\x1b[0m'); - } -} - -// Handle command line arguments -const args = process.argv.slice(2); - -if (args.includes('--help') || args.includes('-h')) { - showCommands(); -} else if (args.includes('--status') || args.includes('-s')) { - checkBuildStatus(); -} else if (args.includes('--quick') || args.includes('-q')) { - console.log('🚀 Quick build starting...'); - try { - execSync('npm run build', { stdio: 'inherit' }); - } catch (error) { - console.error('❌ Quick build failed'); - process.exit(1); - } -} else { - showCommands(); - checkBuildStatus(); - - console.log('\n\x1b[33mOptions:\x1b[0m'); - console.log(' --help, -h Show this help'); - console.log(' --status, -s Show build status only'); - console.log(' --quick, -q Run quick build'); -} diff --git a/SerpentRace_Backend/scripts/build.ts b/SerpentRace_Backend/scripts/build.ts deleted file mode 100644 index 81185407..00000000 --- a/SerpentRace_Backend/scripts/build.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync, rmSync } from 'fs'; -import { join } from 'path'; - -/** - * Comprehensive Build Script for SerpentRace Backend - * Handles TypeScript compilation, migrations, asset copying, and validation - */ - -interface BuildOptions { - runMigrations?: boolean; - runTests?: boolean; - skipLinting?: boolean; - production?: boolean; -} - -class BuildManager { - private distDir = join(__dirname, '..', 'dist'); - - constructor(private options: BuildOptions = {}) {} - - private log(message: string, level: 'info' | 'error' | 'warn' = 'info') { - const timestamp = new Date().toISOString(); - const prefix = { - info: '🔧', - error: '❌', - warn: '⚠' - }[level]; - console.log(`${prefix} [${timestamp}] ${message}`); - } - - private execute(command: string, description: string) { - this.log(`${description}...`); - try { - execSync(command, { - stdio: 'inherit', - cwd: join(__dirname, '..') - }); - this.log(`✅ ${description} completed successfully`); - } catch (error) { - this.log(`❌ ${description} failed`, 'error'); - throw error; - } - } - - async clean() { - this.log('Cleaning previous build...'); - if (existsSync(this.distDir)) { - rmSync(this.distDir, { recursive: true, force: true }); - this.log('✅ Previous build cleaned'); - } else { - this.log('No previous build found'); - } - } - - async typecheck() { - this.execute('npx tsc --noEmit', 'Type checking'); - } - - async lint() { - if (this.options.skipLinting) { - this.log('Skipping linting...', 'warn'); - return; - } - - // For now, just check if TypeScript compiles without errors - this.log('Linting (basic type checking)...'); - await this.typecheck(); - } - - async runTests() { - if (!this.options.runTests) { - this.log('Skipping tests...', 'warn'); - return; - } - - this.execute('npm test', 'Running tests'); - } - - async runMigrations() { - if (!this.options.runMigrations) { - this.log('Skipping database migrations...', 'warn'); - return; - } - - try { - this.log('Checking migration status...'); - execSync('npm run migration:show', { - stdio: 'pipe', - cwd: join(__dirname, '..') - }); - - this.execute('npm run migration:run', 'Running database migrations'); - } catch (error) { - this.log('Migration check/run failed - this might be expected in CI/CD environments', 'warn'); - if (this.options.production) { - throw error; // In production builds, migrations should work - } - } - } - - async compile() { - this.execute('npx tsc', 'Compiling TypeScript'); - } - - async copyAssets() { - this.execute('node scripts/copy-assets.js', 'Copying assets'); - } - - async validateBuild() { - this.log('Validating build output...'); - - const expectedFiles = [ - 'dist/Api/index.js', - 'dist/Api/index.d.ts' - ]; - - const missingFiles = expectedFiles.filter(file => - !existsSync(join(__dirname, '..', file)) - ); - - if (missingFiles.length > 0) { - this.log(`Missing expected build files: ${missingFiles.join(', ')}`, 'error'); - throw new Error('Build validation failed'); - } - - this.log('✅ Build validation completed'); - } - - async build() { - const startTime = Date.now(); - - try { - this.log('🚀 Starting SerpentRace Backend build process...'); - - // Step 1: Clean previous build - await this.clean(); - - // Step 2: Lint code (if not skipped) - await this.lint(); - - // Step 3: Run tests (if enabled) - await this.runTests(); - - // Step 4: Run migrations (if enabled) - await this.runMigrations(); - - // Step 5: Compile TypeScript - await this.compile(); - - // Step 6: Copy assets - await this.copyAssets(); - - // Step 7: Validate build - await this.validateBuild(); - - const duration = ((Date.now() - startTime) / 1000).toFixed(2); - this.log(`🎉 Build completed successfully in ${duration}s`); - - } catch (error) { - const duration = ((Date.now() - startTime) / 1000).toFixed(2); - this.log(`đŸ’„ Build failed after ${duration}s`, 'error'); - - if (error instanceof Error) { - this.log(`Error: ${error.message}`, 'error'); - } - - process.exit(1); - } - } -} - -// Parse command line arguments -const args = process.argv.slice(2); -const options: BuildOptions = { - runMigrations: args.includes('--migrations'), - runTests: args.includes('--test'), - skipLinting: args.includes('--skip-lint'), - production: args.includes('--production') -}; - -// Create and run build -const buildManager = new BuildManager(options); -buildManager.build().catch(error => { - console.error('Unhandled build error:', error); - process.exit(1); -}); diff --git a/SerpentRace_Backend/scripts/copy-assets.js b/SerpentRace_Backend/scripts/copy-assets.js deleted file mode 100644 index 1f9c26e3..00000000 --- a/SerpentRace_Backend/scripts/copy-assets.js +++ /dev/null @@ -1,62 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -/** - * Copy Assets Script for SerpentRace Backend - * Copies non-TypeScript files to the dist directory - */ - -const srcDir = path.join(__dirname, '..', 'src'); -const distDir = path.join(__dirname, '..', 'dist'); - -// File extensions to copy -const assetExtensions = ['.json', '.html', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot']; - -// Directories to exclude from copying -const excludeDirs = ['node_modules', '.git', 'tests', '__tests__']; - -function copyAssets(srcPath, distPath) { - if (!fs.existsSync(srcPath)) { - console.log(`Source directory ${srcPath} does not exist`); - return; - } - - if (!fs.existsSync(distPath)) { - fs.mkdirSync(distPath, { recursive: true }); - } - - const items = fs.readdirSync(srcPath); - - items.forEach(item => { - const srcItemPath = path.join(srcPath, item); - const distItemPath = path.join(distPath, item); - const stat = fs.statSync(srcItemPath); - - if (stat.isDirectory()) { - // Skip excluded directories - if (excludeDirs.includes(item)) { - return; - } - - // Recursively copy subdirectories - copyAssets(srcItemPath, distItemPath); - } else { - const ext = path.extname(item).toLowerCase(); - - // Copy asset files - if (assetExtensions.includes(ext)) { - console.log(`Copying asset: ${srcItemPath} -> ${distItemPath}`); - fs.copyFileSync(srcItemPath, distItemPath); - } - } - }); -} - -try { - console.log('Copying assets from src to dist...'); - copyAssets(srcDir, distDir); - console.log('Asset copying completed successfully!'); -} catch (error) { - console.error('Error copying assets:', error); - process.exit(1); -} diff --git a/SerpentRace_Backend/scripts/deploy.bat b/SerpentRace_Backend/scripts/deploy.bat deleted file mode 100644 index 465ad1dc..00000000 --- a/SerpentRace_Backend/scripts/deploy.bat +++ /dev/null @@ -1,233 +0,0 @@ -@echo off -REM SerpentRace Backend Production Deployment Script for Windows -REM This script handles the complete deployment process - -setlocal EnableDelayedExpansion - -set "SCRIPT_START=%TIME%" - -REM Colors simulation for Windows (using echo with different prefixes) -set "LOG_PREFIX=[INFO]" -set "ERROR_PREFIX=[ERROR]" -set "WARN_PREFIX=[WARN]" - -:log -echo %LOG_PREFIX% [%DATE% %TIME%] %~1 -goto :eof - -:error -echo %ERROR_PREFIX% [%DATE% %TIME%] %~1 -goto :eof - -:warn -echo %WARN_PREFIX% [%DATE% %TIME%] %~1 -goto :eof - -:check_env -call :log "Checking environment variables..." - -set "required_vars=DB_HOST DB_PORT DB_USERNAME DB_PASSWORD DB_NAME JWT_SECRET REDIS_HOST REDIS_PORT" -set "missing_vars=" - -for %%v in (%required_vars%) do ( - call set "var_value=%%!%%v!%%" - if "!var_value!"=="" ( - set "missing_vars=!missing_vars! %%v" - ) -) - -if not "!missing_vars!"==" " ( - call :error "Missing required environment variables:!missing_vars!" - call :error "Please set these variables before running the deployment" - exit /b 1 -) - -call :log "All required environment variables are set" -goto :eof - -:install_dependencies -call :log "Installing production dependencies..." -npm ci --only=production -if !errorlevel! neq 0 ( - call :error "Failed to install dependencies" - exit /b 1 -) -call :log "Dependencies installed successfully" -goto :eof - -:run_build -call :log "Running production build..." -npm run build:production -if !errorlevel! neq 0 ( - call :error "Build failed" - exit /b 1 -) -call :log "Build completed successfully" -goto :eof - -:test_database -call :log "Testing database connectivity..." - -echo import { AppDataSource } from './src/Infrastructure/ormconfig'; > test-db-temp.ts -echo. >> test-db-temp.ts -echo async function testConnection() { >> test-db-temp.ts -echo try { >> test-db-temp.ts -echo await AppDataSource.initialize(); >> test-db-temp.ts -echo console.log('✅ Database connection successful'^); >> test-db-temp.ts -echo await AppDataSource.destroy(); >> test-db-temp.ts -echo process.exit(0^); >> test-db-temp.ts -echo } catch (error^) { >> test-db-temp.ts -echo console.error('❌ Database connection failed:', error^); >> test-db-temp.ts -echo process.exit(1^); >> test-db-temp.ts -echo } >> test-db-temp.ts -echo } >> test-db-temp.ts -echo. >> test-db-temp.ts -echo testConnection(); >> test-db-temp.ts - -npx ts-node test-db-temp.ts -set "db_test_result=!errorlevel!" -del test-db-temp.ts 2>nul - -if !db_test_result! neq 0 ( - call :error "Database connectivity test failed" - exit /b 1 -) - -call :log "Database connectivity test passed" -goto :eof - -:test_redis -call :log "Testing Redis connectivity..." - -echo import { createClient } from 'redis'; > test-redis-temp.ts -echo. >> test-redis-temp.ts -echo async function testRedis() { >> test-redis-temp.ts -echo const client = createClient({ >> test-redis-temp.ts -echo socket: { >> test-redis-temp.ts -echo host: process.env.REDIS_HOST ^|^| 'localhost', >> test-redis-temp.ts -echo port: parseInt(process.env.REDIS_PORT ^|^| '6379'^) >> test-redis-temp.ts -echo } >> test-redis-temp.ts -echo }^); >> test-redis-temp.ts -echo. >> test-redis-temp.ts -echo try { >> test-redis-temp.ts -echo await client.connect(); >> test-redis-temp.ts -echo await client.ping(); >> test-redis-temp.ts -echo console.log('✅ Redis connection successful'^); >> test-redis-temp.ts -echo await client.disconnect(); >> test-redis-temp.ts -echo process.exit(0^); >> test-redis-temp.ts -echo } catch (error^) { >> test-redis-temp.ts -echo console.error('❌ Redis connection failed:', error^); >> test-redis-temp.ts -echo process.exit(1^); >> test-redis-temp.ts -echo } >> test-redis-temp.ts -echo } >> test-redis-temp.ts -echo. >> test-redis-temp.ts -echo testRedis(); >> test-redis-temp.ts - -npx ts-node test-redis-temp.ts -set "redis_test_result=!errorlevel!" -del test-redis-temp.ts 2>nul - -if !redis_test_result! neq 0 ( - call :warn "Redis connectivity test failed - continuing anyway" -) else ( - call :log "Redis connectivity test passed" -) -goto :eof - -:setup_directories -call :log "Setting up required directories..." -if not exist "logs" mkdir logs -if not exist "uploads" mkdir uploads -call :log "Directories created" -goto :eof - -:start_app -call :log "Starting application for validation..." - -REM Start the app in background -start /B "" npm start - -REM Wait for app to start -timeout /t 10 /nobreak >nul - -REM Test if the health endpoint responds (using curl if available) -set "PORT_VAR=!PORT!" -if "!PORT_VAR!"=="" set "PORT_VAR=3000" - -curl -f http://localhost:!PORT_VAR!/health >nul 2>&1 -if !errorlevel! equ 0 ( - call :log "Application health check passed" - REM Try to stop the background process (this is tricky in batch) - taskkill /F /IM node.exe /FI "WINDOWTITLE eq npm start*" >nul 2>&1 -) else ( - call :error "Application health check failed" - taskkill /F /IM node.exe /FI "WINDOWTITLE eq npm start*" >nul 2>&1 - exit /b 1 -) -goto :eof - -:deploy -call :log "🚀 Starting SerpentRace Backend production deployment..." - -call :check_env -if !errorlevel! neq 0 exit /b 1 - -call :install_dependencies -if !errorlevel! neq 0 exit /b 1 - -call :run_build -if !errorlevel! neq 0 exit /b 1 - -call :setup_directories -if !errorlevel! neq 0 exit /b 1 - -call :test_database -if !errorlevel! neq 0 exit /b 1 - -call :test_redis -REM Redis test failure is not fatal - -if not "%SKIP_APP_TEST%"=="true" ( - call :start_app - if !errorlevel! neq 0 exit /b 1 -) else ( - call :warn "Skipping application startup test" -) - -call :log "🎉 Deployment completed successfully!" -call :log "You can now start the application with: npm start" -goto :eof - -:build_only -call :log "Running build-only deployment..." -call :check_env -if !errorlevel! neq 0 exit /b 1 -call :install_dependencies -if !errorlevel! neq 0 exit /b 1 -call :run_build -if !errorlevel! neq 0 exit /b 1 -call :setup_directories -call :log "Build-only deployment completed" -goto :eof - -:test_connections -call :log "Testing connections only..." -call :check_env -if !errorlevel! neq 0 exit /b 1 -call :test_database -if !errorlevel! neq 0 exit /b 1 -call :test_redis -call :log "Connection tests completed" -goto :eof - -REM Main script logic -if "%1"=="" goto deploy -if "%1"=="deploy" goto deploy -if "%1"=="build-only" goto build_only -if "%1"=="test-connections" goto test_connections - -echo Usage: %0 [deploy^|build-only^|test-connections] -echo deploy - Full deployment (default) -echo build-only - Only build, skip tests -echo test-connections - Test database and Redis connections -exit /b 1 diff --git a/SerpentRace_Backend/scripts/deploy.sh b/SerpentRace_Backend/scripts/deploy.sh deleted file mode 100644 index 4af6b641..00000000 --- a/SerpentRace_Backend/scripts/deploy.sh +++ /dev/null @@ -1,237 +0,0 @@ -#!/bin/bash - -# SerpentRace Backend Production Deployment Script -# This script handles the complete deployment process - -set -e # Exit on any error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -log() { - echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" -} - -error() { - echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" -} - -warn() { - echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" -} - -info() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" -} - -# Check if required environment variables are set -check_env() { - log "Checking environment variables..." - - required_vars=( - "DB_HOST" - "DB_PORT" - "DB_USERNAME" - "DB_PASSWORD" - "DB_NAME" - "JWT_SECRET" - "REDIS_HOST" - "REDIS_PORT" - ) - - missing_vars=() - for var in "${required_vars[@]}"; do - if [ -z "${!var}" ]; then - missing_vars+=("$var") - fi - done - - if [ ${#missing_vars[@]} -ne 0 ]; then - error "Missing required environment variables: ${missing_vars[*]}" - error "Please set these variables before running the deployment" - exit 1 - fi - - log "All required environment variables are set" -} - -# Install dependencies -install_dependencies() { - log "Installing production dependencies..." - npm ci --only=production - log "Dependencies installed successfully" -} - -# Run the comprehensive build process -run_build() { - log "Running production build..." - npm run build:production - log "Build completed successfully" -} - -# Test database connectivity -test_database() { - log "Testing database connectivity..." - - # Use a simple TypeScript script to test connection - cat > /tmp/test-db.ts << 'EOF' -import { AppDataSource } from './src/Infrastructure/ormconfig'; - -async function testConnection() { - try { - await AppDataSource.initialize(); - console.log('✅ Database connection successful'); - await AppDataSource.destroy(); - process.exit(0); - } catch (error) { - console.error('❌ Database connection failed:', error); - process.exit(1); - } -} - -testConnection(); -EOF - - npx ts-node /tmp/test-db.ts || { - error "Database connectivity test failed" - exit 1 - } - - rm -f /tmp/test-db.ts - log "Database connectivity test passed" -} - -# Test Redis connectivity -test_redis() { - log "Testing Redis connectivity..." - - # Use a simple script to test Redis connection - cat > /tmp/test-redis.ts << 'EOF' -import { createClient } from 'redis'; - -async function testRedis() { - const client = createClient({ - socket: { - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379') - } - }); - - try { - await client.connect(); - await client.ping(); - console.log('✅ Redis connection successful'); - await client.disconnect(); - process.exit(0); - } catch (error) { - console.error('❌ Redis connection failed:', error); - process.exit(1); - } -} - -testRedis(); -EOF - - npx ts-node /tmp/test-redis.ts || { - warn "Redis connectivity test failed - continuing anyway" - } - - rm -f /tmp/test-redis.ts - log "Redis connectivity test completed" -} - -# Create required directories -setup_directories() { - log "Setting up required directories..." - mkdir -p logs - mkdir -p uploads - log "Directories created" -} - -# Start the application (for testing) -start_app() { - log "Starting application for validation..." - - # Start the app in background and test if it responds - npm start & - APP_PID=$! - - # Wait for app to start - sleep 10 - - # Test if the health endpoint responds - if curl -f http://localhost:${PORT:-3000}/health > /dev/null 2>&1; then - log "Application health check passed" - kill $APP_PID - wait $APP_PID 2>/dev/null - else - error "Application health check failed" - kill $APP_PID 2>/dev/null || true - wait $APP_PID 2>/dev/null || true - exit 1 - fi -} - -# Main deployment function -deploy() { - log "🚀 Starting SerpentRace Backend production deployment..." - - # Check environment - check_env - - # Install dependencies - install_dependencies - - # Run build process - run_build - - # Setup directories - setup_directories - - # Test connections - test_database - test_redis - - # Test application startup - if [ "${SKIP_APP_TEST}" != "true" ]; then - start_app - else - warn "Skipping application startup test" - fi - - log "🎉 Deployment completed successfully!" - info "You can now start the application with: npm start" -} - -# Handle script arguments -case "${1:-deploy}" in - "deploy") - deploy - ;; - "build-only") - log "Running build-only deployment..." - check_env - install_dependencies - run_build - setup_directories - log "Build-only deployment completed" - ;; - "test-connections") - log "Testing connections only..." - check_env - test_database - test_redis - log "Connection tests completed" - ;; - *) - echo "Usage: $0 [deploy|build-only|test-connections]" - echo " deploy - Full deployment (default)" - echo " build-only - Only build, skip tests" - echo " test-connections - Test database and Redis connections" - exit 1 - ;; -esac diff --git a/SerpentRace_Backend/scripts/generate-migration.ts b/SerpentRace_Backend/scripts/generate-migration.ts deleted file mode 100644 index 9ad0a8f3..00000000 --- a/SerpentRace_Backend/scripts/generate-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { execSync } from 'child_process'; - -const migrationName = process.argv[2]; - -if (!migrationName) { - console.error('Please provide a migration name: npm run migration:full '); - process.exit(1); -} - -try { - console.log(`Creating migration: ${migrationName}`); - execSync(`npx ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli migration:create ./src/Infrastructure/Migrationsettings/${migrationName}`, { stdio: 'inherit' }); - - console.log(`Generating migration: ${migrationName}`); - execSync(`npx ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:generate ./src/Infrastructure/Migrations/${migrationName}`, { stdio: 'inherit' }); - - console.log('Migration generated successfully!'); - - console.log('Running migration...'); - execSync(`npx ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:run`, { stdio: 'inherit' }); -} catch (error) { - if (error instanceof Error) { - console.error('Migration failed:', error.message); - } else { - console.error('Migration failed:', error); - } - process.exit(1); -} \ No newline at end of file diff --git a/SerpentRace_Backend/scripts/test-redis.ps1 b/SerpentRace_Backend/scripts/test-redis.ps1 deleted file mode 100644 index 18d8ecfa..00000000 --- a/SerpentRace_Backend/scripts/test-redis.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -# PowerShell script to start Redis and run tests -Write-Host "Starting Redis with Docker Compose..." -ForegroundColor Green -docker-compose up -d redis - -# Wait for Redis to be ready -Write-Host "Waiting for Redis to be ready..." -ForegroundColor Yellow -do { - Write-Host "Checking Redis connection..." -ForegroundColor Gray - $result = docker-compose exec redis redis-cli ping 2>$null - if ($result -ne "PONG") { - Start-Sleep -Seconds 2 - } -} while ($result -ne "PONG") - -Write-Host "Redis is ready!" -ForegroundColor Green - -# Run Redis tests -Write-Host "Running Redis tests..." -ForegroundColor Cyan -npm test -- --testNamePattern="RedisService" - -Write-Host "Done!" -ForegroundColor Green diff --git a/SerpentRace_Backend/scripts/test-redis.sh b/SerpentRace_Backend/scripts/test-redis.sh deleted file mode 100644 index 791362ae..00000000 --- a/SerpentRace_Backend/scripts/test-redis.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Script to start Redis and run tests -echo "Starting Redis with Docker Compose..." -docker-compose up -d redis - -# Wait for Redis to be ready -echo "Waiting for Redis to be ready..." -until docker-compose exec redis redis-cli ping; do - echo "Waiting for Redis..." - sleep 2 -done - -echo "Redis is ready!" - -# Run Redis tests -echo "Running Redis tests..." -npm test -- --testNamePattern="RedisService" - -echo "Done!" diff --git a/SerpentRace_Backend/src/Api/index.ts b/SerpentRace_Backend/src/Api/index.ts deleted file mode 100644 index 5bcc6a6e..00000000 --- a/SerpentRace_Backend/src/Api/index.ts +++ /dev/null @@ -1,314 +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(); - -// Validate required environment variables in production -if (process.env.NODE_ENV === 'production') { - const requiredEnvVars = [ - 'JWT_SECRET', - 'DB_PASSWORD', - 'DB_HOST', - 'DB_NAME', - 'POSTGRES_PASSWORD' - ]; - - const missingVars = requiredEnvVars.filter(varName => !process.env[varName]); - - if (missingVars.length > 0) { - logError('[FATAL] Missing required environment variables in production:', missingVars.join(', ')); - logError('[FATAL] Please configure all required environment variables in .env.server'); - process.exit(1); - } - - // Check for placeholder values that haven't been changed - if (process.env.JWT_SECRET && process.env.JWT_SECRET.includes('CHANGE_THIS')) { - logError('[FATAL] JWT_SECRET still contains placeholder value. Please set a proper secret in .env.server'); - process.exit(1); - } - - if (process.env.POSTGRES_PASSWORD && process.env.POSTGRES_PASSWORD.includes('CHANGE_THIS')) { - logError('[FATAL] POSTGRES_PASSWORD still contains placeholder value. Please set a proper password in .env.server'); - process.exit(1); - } -} - -logStartup('SerpentRace Backend starting up', { - environment: process.env.NODE_ENV || 'development', - port: PORT, - nodeVersion: process.version, - chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30' -}); - -app.use(helmet({ - contentSecurityPolicy: isDevelopment ? false : undefined -})); - -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true, limit: '10mb' })); -app.use(cookieParser()); - -app.use(loggingService.requestLoggingMiddleware()); - -app.use((req, res, next) => { - const origin = req.headers.origin; - const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080', process.env.FRONTEND_URL]; - - if (!origin || allowedOrigins.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin || '*'); - } - - res.setHeader('Access-Control-Allow-Credentials', 'true'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie'); - - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - next(); -}); - -if (isDevelopment) { - app.use((req, res, next) => { - logRequest(`${req.method} ${req.path}`, req, res); - next(); - }); -} - -// Setup Swagger documentation -setupSwagger(app); - -app.get('/', (req, res) => { - res.json({ - service: 'SerpentRace Backend API', - status: 'running', - version: '1.0.0', - endpoints: { - swagger: '/api-docs', - users: '/api/users', - organizations: '/api/organizations', - decks: '/api/decks', - chats: '/api/chats', - contacts: '/api/contacts', - admin: '/api/admin', - deckImportExport: '/api/deck-import-export', - health: '/health' - }, - websocket: { - enabled: true, - events: [ - 'chat:join', 'chat:leave', 'message:send', - 'group:create', 'chat:direct', 'game:chat:create', - 'chat:history' - ] - } - }); -}); - -app.get('/health', async (req, res) => { - try { - const isDbConnected = AppDataSource.isInitialized; - - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - service: 'SerpentRace Backend API', - version: '1.0.0', - environment: process.env.NODE_ENV || 'development', - database: { - connected: isDbConnected, - type: AppDataSource.options.type - }, - websocket: { - enabled: true - }, - uptime: process.uptime() - }); - } catch (error) { - res.status(503).json({ - status: 'unhealthy', - timestamp: new Date().toISOString(), - error: 'Service health check failed' - }); - } -}); - -// API Routes -app.use('/api/users', userRouter); -app.use('/api/organizations', organizationRouter); -app.use('/api/decks', deckRouter); -app.use('/api/chats', chatRouter); -app.use('/api/contacts', contactRouter); -app.use('/api/admin', adminRouter); -app.use('/api/deck-import-export', deckImportExportRouter); -app.use('/api/games', gameRouter); - -// Global error handler (must be after routes) -app.use(loggingService.errorLoggingMiddleware()); -app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { - logError('Global error handler caught unhandled error', error, req, res); - - // Don't expose internal error details in production - const isDevelopment = process.env.NODE_ENV === 'development'; - - res.status(500).json({ - error: 'Internal server error', - timestamp: new Date().toISOString(), - ...(isDevelopment && { details: error.message, stack: error.stack }) - }); -}); - -// Handle 404 routes -app.use((req: express.Request, res: express.Response) => { - res.status(404).json({ - error: 'Route not found', - path: req.originalUrl, - method: req.method, - timestamp: new Date().toISOString() - }); -}); - -// Initialize WebSocket service after database connection -let webSocketService: WebSocketService; -let gameWebSocketService: GameWebSocketService; - -// Initialize database connection -AppDataSource.initialize() - .then(() => { - const dbOptions = AppDataSource.options as any; - logConnection('Database connection established', 'postgresql', 'success', { - type: dbOptions.type, - host: dbOptions.host, - database: dbOptions.database - }); - - // Initialize WebSocket service after database is connected - webSocketService = new WebSocketService(httpServer); - logStartup('WebSocket service initialized', { - chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30' - }); - - // Initialize Game WebSocket service for /game namespace via DIContainer - container.setSocketIO(webSocketService['io']); - gameWebSocketService = container.gameWebSocketService; - logStartup('Game WebSocket service initialized for /game namespace'); - - // Restore active games from snapshots (if any exist) - gameWebSocketService.restoreAllActiveGames() - .then(restoredCount => { - if (restoredCount > 0) { - logStartup(`Restored ${restoredCount} active game(s) from snapshots`); - } - }) - .catch(error => { - logError('Failed to restore games from snapshots', error); - }); - }) - .catch((error) => { - const dbOptions = AppDataSource.options as any; - logConnection('Database connection failed', 'postgresql', 'failure', { - error: error.message, - type: dbOptions.type, - host: dbOptions.host, - database: dbOptions.database - }); - process.exit(1); - }); - -// Start server with WebSocket support -const server = httpServer.listen(PORT, () => { - logStartup('Server started successfully', { - port: PORT, - environment: process.env.NODE_ENV || 'development', - timestamp: new Date().toISOString(), - endpoints: { - health: `/health`, - swagger: `/api-docs`, - users: `/api/users`, - organizations: `/api/organizations`, - decks: `/api/decks`, - chats: `/api/chats` - }, - websocket: { - enabled: true, - chatInactivityTimeout: `${process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'} minutes` - } - }); -}); - -// Graceful shutdown -const gracefulShutdown = async (signal: string) => { - logStartup(`Received ${signal}. Shutting down gracefully...`); - - // Snapshot all active games before shutdown - if (gameWebSocketService) { - try { - const snapshotCount = await gameWebSocketService.snapshotAllActiveGames(); - logStartup(`Created ${snapshotCount} game snapshot(s) before shutdown`); - } catch (error) { - logError('Failed to snapshot games before shutdown', error as Error); - } - } - - server.close(() => { - logStartup('HTTP server closed'); - - if (AppDataSource.isInitialized) { - AppDataSource.destroy() - .then(() => { - logConnection('Database connection closed', 'postgresql', 'success'); - process.exit(0); - }) - .catch((error) => { - logError('Error during database shutdown', error); - process.exit(1); - }); - } else { - process.exit(0); - } - }); -}; - -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); -process.on('SIGINT', () => gracefulShutdown('SIGINT')); - -// Handle uncaught exceptions -process.on('uncaughtException', (error) => { - logError('Uncaught Exception - Server will shut down', error); - process.exit(1); -}); - -// Handle unhandled promise rejections -process.on('unhandledRejection', (reason, promise) => { - logError('Unhandled Rejection - Server will shut down', new Error(String(reason)), undefined, undefined); - process.exit(1); -}); - -// Export WebSocket services for game integration -export { webSocketService, gameWebSocketService }; diff --git a/SerpentRace_Backend/src/Api/middleware/optionalAuth.ts b/SerpentRace_Backend/src/Api/middleware/optionalAuth.ts deleted file mode 100644 index 05953518..00000000 --- a/SerpentRace_Backend/src/Api/middleware/optionalAuth.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { JWTService } from '../../Application/Services/JWTService'; -import { UserState } from '../../Domain/User/UserAggregate'; -import { logAuth, logWarning } from '../../Application/Services/Logger'; - -interface AuthenticatedRequest extends Request { - user?: { - userId: string; - authLevel: 0 | 1; - userStatus: UserState; - orgId: string | null; - }; -} - -/** - * Optional authentication middleware - extracts JWT data if present but doesn't require authentication - * Used for endpoints that work for both authenticated and anonymous users - */ -export const optionalAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - const jwtService = new JWTService(); - - try { - // Try to extract token from Authorization header or cookies - const authHeader = req.headers.authorization; - const token = authHeader?.startsWith('Bearer ') - ? authHeader.substring(7) - : req.cookies?.auth_token; - - if (token) { - // If token exists, try to verify it - const payload = jwtService.verify(req); - - if (payload) { - req.user = { - userId: payload.userId, - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId || null - }; - - logAuth('Optional auth - user authenticated', payload.userId, { - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId - }); - } else { - logWarning('Optional auth - invalid token provided', { - hasToken: true, - tokenLength: token.length - }); - } - } - - // Continue regardless of authentication status - next(); - - } catch (error) { - // Log the error but continue without authentication - logWarning('Optional auth - error processing token', { - error: error instanceof Error ? error.message : String(error), - hasAuthHeader: !!req.headers.authorization, - hasCookie: !!req.cookies?.auth_token - }); - - next(); - } -}; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/routers/adminRouter.ts b/SerpentRace_Backend/src/Api/routers/adminRouter.ts deleted file mode 100644 index 4226687a..00000000 --- a/SerpentRace_Backend/src/Api/routers/adminRouter.ts +++ /dev/null @@ -1,1141 +0,0 @@ -import express, { Request, Response } from 'express'; -import multer from 'multer'; -import { DIContainer } from '../../Application/Services/DIContainer'; -import { adminRequired } from '../../Application/Services/AuthMiddleware'; -import { UserState } from '../../Domain/User/UserAggregate'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { AdminAuditService } from '../../Application/Services/AdminBypassService'; -import { webSocketService } from '../index'; -import { logRequest, logError, logWarning, logAuth } from '../../Application/Services/Logger'; - -// Extend Express Request interface for file uploads -declare global { - namespace Express { - interface Request { - file?: Express.Multer.File; - } - } -} - -const router = express.Router(); -const container = DIContainer.getInstance(); - -// Configure multer for file uploads -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit - }, - fileFilter: (req: any, file: any, cb: any) => { - if (file.mimetype === 'application/json' || file.originalname.endsWith('.spr')) { - cb(null, true); - } else { - cb(new Error('Only JSON and .spr files are allowed')); - } - } -}); - -// Helper function to extract language from Accept-Language header -function extractLanguageFromAcceptHeader(acceptLanguage: string): string | null { - if (!acceptLanguage) return null; - - const languages = acceptLanguage.split(','); - if (languages.length > 0) { - const primaryLanguage = languages[0].split(';')[0].trim().substring(0, 2); - return primaryLanguage; - } - - return null; -} - -// ============================================================================= -// USER MANAGEMENT ROUTES -// ============================================================================= - -// Get users with pagination (RECOMMENDED) -router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ - error: 'Invalid pagination parameters. From and to must be valid numbers with from <= to.' - }); - } - - const limit = to - from + 1; - if (limit > 100) { - return res.status(400).json({ - error: 'Page size too large. Maximum 100 records per request.' - }); - } - - logRequest('Admin paginated users endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = await container.getUsersByPageQueryHandler.execute({ - from, - to, - includeDeleted - }); - - const response = { - users: result.users, - pagination: { - from, - to, - returned: result.users.length, - totalCount: result.totalCount, - includeDeleted - } - }; - - logRequest('Admin users retrieved successfully', req, res, { - returnedUsers: result.users.length, - totalCount: result.totalCount, - from, - to, - includeDeleted - }); - - return res.status(200).json(response); - } catch (error: any) { - logError('Error in admin get users endpoint', error, req, res); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get user by ID including soft-deleted ones -router.get('/users/:userId', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get user by id endpoint accessed', req, res, { targetUserId, includeDeleted }); - - const user = includeDeleted - ? await container.userRepository.findByIdIncludingDeleted(targetUserId) - : await container.userRepository.findById(targetUserId); - - if (!user) { - logWarning('User not found', { targetUserId, includeDeleted }, req, res); - return res.status(404).json({ error: 'User not found' }); - } - - logRequest('Admin user retrieved successfully', req, res, { - targetUserId, - username: user.username, - includeDeleted - }); - - res.json(user); - } catch (error) { - logError('Admin get user by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search users including soft-deleted ones -router.get('/users/search/:searchTerm', - adminRequired, - ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }), - async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const users = includeDeleted - ? await container.userRepository.searchIncludingDeleted(searchTerm) - : await container.userRepository.search(searchTerm); - - logRequest('Admin user search completed', req, res, { - searchTerm, - resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0), - includeDeleted - }); - - res.json(users); - } catch (error) { - logError('Admin search users endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update any user (admin only) -router.patch('/users/:userId', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - - logRequest('Admin update user endpoint accessed', req, res, { - adminUserId, - targetUserId, - fieldsToUpdate: Object.keys(req.body) - }); - - const result = await container.updateUserCommandHandler.execute({ id: targetUserId, ...req.body }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logRequest('User updated by admin', req, res, { - adminUserId, - targetUserId, - username: result.username - }); - - res.json(result); - - } catch (error) { - logError('Admin update user endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Activate user (admin only) -router.post('/users/:userId/activate', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - - logRequest('Admin activate user endpoint accessed', req, res, { adminUserId, targetUserId }); - - const result = await container.activateUserCommandHandler.execute({ id: targetUserId }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logAuth('User activated by admin', targetUserId, { adminUserId }, req, res); - res.json({ message: 'User activated successfully', user: result }); - - } catch (error) { - logError('Admin activate user endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Deactivate user (admin only) -router.post('/users/:userId/deactivate', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - - logRequest('Deactivate user endpoint accessed', req, res, { adminUserId, targetUserId }); - - const result = await container.deactivateUserCommandHandler.execute({ id: targetUserId }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logAuth('User deactivated by admin', targetUserId, { adminUserId }, req, res); - res.json({ message: 'User deactivated successfully', user: result }); - - } catch (error) { - logError('Deactivate user endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Delete user (admin only) -router.delete('/users/:userId', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - const softDelete = req.query.soft === 'true' || req.query.soft === undefined; - - logRequest('Delete user endpoint accessed', req, res, { adminUserId, targetUserId, softDelete }); - - const result = await container.deleteUserCommandHandler.execute({ id: targetUserId, soft: softDelete }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logAuth('User deleted by admin', targetUserId, { adminUserId }, req, res); - res.json({ message: 'User deleted successfully' }); - - } catch (error) { - logError('Delete user endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// DECK MANAGEMENT ROUTES -// ============================================================================= - -// Get decks by page (admin only) - RECOMMENDED -router.get('/decks/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Admin get decks by page endpoint accessed', req, res, { from, to, includeDeleted }); - - // For admin, we need to pass admin context to get unrestricted decks - const adminUserId = (req as any).user.userId; - const result = await container.getDecksByPageQueryHandler.execute({ - userId: adminUserId, - userOrgId: undefined, - isAdmin: true, - from, - to, - includeDeleted - }); - - logRequest('Admin decks page retrieved successfully', req, res, { - from, - to, - count: result.decks.length, - total: result.totalCount, - includeDeleted - }); - - res.json(result); - } catch (error) { - logError('Admin get decks by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get deck by ID including soft-deleted ones -router.get('/decks/:id', adminRequired, async (req: Request, res: Response) => { - try { - const { id } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get deck by id endpoint accessed', req, res, { deckId: id, includeDeleted }); - - const deck = includeDeleted - ? await container.deckRepository.findByIdIncludingDeleted(id) - : await container.deckRepository.findById(id); - - if (!deck) { - logWarning('Deck not found', { deckId: id, includeDeleted }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - logRequest('Admin deck retrieved successfully', req, res, { deckId: id, includeDeleted }); - res.json(deck); - } catch (error) { - logError('Admin get deck by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search decks including soft-deleted ones -router.get('/decks/search/:searchTerm', adminRequired, async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search decks endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const decks = includeDeleted - ? await container.deckRepository.searchIncludingDeleted(searchTerm) - : await container.deckRepository.search(searchTerm); - - logRequest('Admin deck search completed', req, res, { - searchTerm, - resultCount: Array.isArray(decks) ? decks.length : (decks.totalCount || 0), - includeDeleted - }); - - res.json(decks); - } catch (error) { - logError('Admin search decks endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -//modify deck (admin only) -router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) => { - try { - const deckId = req.params.id; - const adminUserId = (req as any).user.userId; - logRequest('Admin update deck endpoint accessed', req, res, { deckId, adminUserId, updateFields: Object.keys(req.body) }); - const result = await container.updateDeckCommandHandler.execute({ id: deckId, userstate: 1 , ...req.body}); - logRequest('Deck updated successfully by admin', req, res, { deckId, adminUserId }); - res.json(result); - } catch (error) { - logError('Admin update deck endpoint error', error as Error, req, res); - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: `Deck not found` }); - } - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Deck with this name already exists' }); - } - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Hard delete deck (admin only) -router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => { - try { - const adminUserId = (req as any).user.userId; - const deckId = req.params.id; - logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId }); - - const result = await container.deleteDeckCommandHandler.execute({ userid: adminUserId, authLevel: 1, id: deckId, soft: false }); - - logRequest('Admin deck hard delete successful', req, res, { deckId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Admin hard delete deck endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Deck not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// ORGANIZATION MANAGEMENT ROUTES -// ============================================================================= - -// Create organization (admin only) -router.post('/organizations', adminRequired, async (req: Request, res: Response) => { - try { - const adminUserId = (req as any).user.userId; - logRequest('Admin create organization endpoint accessed', req, res, { name: req.body.name, adminUserId }); - - const result = await container.createOrganizationCommandHandler.execute(req.body); - - AdminAuditService.logAdminAction('CREATE_ORGANIZATION', adminUserId, { - targetType: 'organization', - targetId: result.id, - operation: 'create', - changes: req.body - }, req, res); - - logRequest('Admin organization created successfully', req, res, { organizationId: result.id, name: req.body.name, adminUserId }); - res.json(result); - } catch (error) { - logError('Admin create organization endpoint error', error as Error, req, res); - - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Organization with this name already exists' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update organization (admin only) - NEW ENDPOINT -router.patch('/organizations/:id', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - const adminUserId = (req as any).user.userId; - - logRequest('Admin update organization endpoint accessed', req, res, { - adminUserId, - organizationId, - fieldsToUpdate: Object.keys(req.body) - }); - - const result = await container.updateOrganizationCommandHandler.execute({ - id: organizationId, - ...req.body - }); - - if (!result) { - return res.status(404).json({ error: 'Organization not found' }); - } - - AdminAuditService.logAdminAction('UPDATE_ORGANIZATION', adminUserId, { - targetType: 'organization', - targetId: organizationId, - operation: 'update', - changes: req.body, - sensitive: req.body.maxOrganizationalDecks !== undefined - }, req, res); - - logRequest('Organization updated by admin', req, res, { - adminUserId, - organizationId, - organizationName: result.name - }); - - res.json(result); - - } catch (error) { - logError('Admin update organization endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get organizations by page (admin only) - RECOMMENDED -router.get('/organizations/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Admin get organizations by page endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = await container.getOrganizationsByPageQueryHandler.execute({ - from, - to, - includeDeleted - }); - - logRequest('Admin organizations page retrieved successfully', req, res, { - from, - to, - count: result.organizations.length, - total: result.totalCount, - includeDeleted - }); - - res.json(result); - } catch (error) { - logError('Admin get organizations by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get organization by ID including soft-deleted ones -router.get('/organizations/:id', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get organization by id endpoint accessed', req, res, { organizationId, includeDeleted }); - - const organization = includeDeleted - ? await container.organizationRepository.findByIdIncludingDeleted(organizationId) - : await container.organizationRepository.findById(organizationId); - - if (!organization) { - logWarning('Organization not found', { organizationId, includeDeleted }, req, res); - return res.status(404).json({ error: 'Organization not found' }); - } - - logRequest('Admin organization retrieved successfully', req, res, { organizationId, includeDeleted }); - res.json(organization); - } catch (error) { - logError('Admin get organization by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search organizations including soft-deleted ones -router.get('/organizations/search/:searchTerm', adminRequired, async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search organizations endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const organizations = includeDeleted - ? await container.organizationRepository.searchIncludingDeleted(searchTerm) - : await container.organizationRepository.search(searchTerm); - - logRequest('Admin organization search completed', req, res, { - searchTerm, - resultCount: Array.isArray(organizations) ? organizations.length : (organizations.totalCount || 0), - includeDeleted - }); - - res.json(organizations); - } catch (error) { - logError('Admin search organizations endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Soft delete organization (admin only) -router.delete('/organizations/:id', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - logRequest('Admin soft delete organization endpoint accessed', req, res, { organizationId }); - - const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: true }); - - logRequest('Admin organization soft delete successful', req, res, { organizationId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Admin soft delete organization endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Organization not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Hard delete organization (admin only) -router.delete('/organizations/:id/hard', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - logRequest('Admin hard delete organization endpoint accessed', req, res, { organizationId }); - - const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: false }); - - logRequest('Admin organization hard delete successful', req, res, { organizationId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Admin hard delete organization endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Organization not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// CHAT MANAGEMENT ROUTES -// ============================================================================= - -// Get chats with pagination (RECOMMENDED) -router.get('/chats/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ - error: 'Invalid pagination parameters. From and to must be valid numbers with from <= to.' - }); - } - - const limit = to - from + 1; - if (limit > 100) { - return res.status(400).json({ - error: 'Page size too large. Maximum 100 records per request.' - }); - } - - logRequest('Admin paginated chats endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = await container.getChatsByPageQueryHandler.execute({ - from, - to, - includeDeleted - }); - - const response = { - chats: result.chats, - pagination: { - from, - to, - returned: result.chats.length, - totalCount: result.totalCount, - includeDeleted - } - }; - - logRequest('Admin chats retrieved successfully', req, res, { - returnedChats: result.chats.length, - totalCount: result.totalCount, - from, - to, - includeDeleted - }); - - return res.status(200).json(response); - } catch (error: any) { - logError('Error in admin get chats endpoint', error, req, res); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get chat by ID including soft-deleted ones -router.get('/chats/:id', adminRequired, async (req: Request, res: Response) => { - try { - const { id } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get chat by id endpoint accessed', req, res, { chatId: id, includeDeleted }); - - const chat = includeDeleted - ? await container.chatRepository.findByIdIncludingDeleted(id) - : await container.chatRepository.findById(id); - - if (!chat) { - logWarning('Chat not found', { chatId: id, includeDeleted }, req, res); - return res.status(404).json({ error: 'Chat not found' }); - } - - logRequest('Admin chat retrieved successfully', req, res, { chatId: id, includeDeleted }); - res.json(chat); - } catch (error) { - logError('Admin get chat by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// CONTACT MANAGEMENT ROUTES -// ============================================================================= - -// Get contacts by page (admin only) - RECOMMENDED (already exists, enhanced) -router.get('/contacts/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Admin get contacts by page endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = includeDeleted - ? await container.contactRepository.findByPageIncludingDeleted(from, to) - : await container.contactRepository.findByPage(from, to); - - logRequest('Admin contacts page retrieved successfully', req, res, { - from, - to, - count: result.contacts.length, - total: result.totalCount, - includeDeleted - }); - - res.json(result); - } catch (error) { - logError('Admin get contacts by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get contact by ID (admin only) -router.get('/contacts/:id', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get contact by ID endpoint accessed', req, res, { contactId, includeDeleted }); - - const result = includeDeleted - ? await container.contactRepository.findByIdIncludingDeleted(contactId) - : await container.getContactByIdQueryHandler.execute({ id: contactId }); - - if (!result) { - logRequest('Contact not found', req, res, { contactId, includeDeleted }); - return res.status(404).json({ error: 'Contact not found' }); - } - - logRequest('Admin contact retrieved successfully', req, res, { contactId, includeDeleted }); - res.json(result); - } catch (error) { - logError('Admin get contact by ID endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search contacts including soft-deleted ones (admin only) -router.get('/contacts/search/:searchTerm', adminRequired, async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search contacts endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const contacts = includeDeleted - ? await container.contactRepository.searchIncludingDeleted(searchTerm) - : await container.contactRepository.search(searchTerm); - - logRequest('Admin contact search completed', req, res, { - searchTerm, - resultCount: contacts.length, - includeDeleted - }); - - res.json(contacts); - } catch (error) { - logError('Admin search contacts endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Respond to contact (admin only) -router.put('/contacts/:id/respond', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - const { adminResponse, sendEmail, language } = req.body; - - if (!adminResponse) { - return res.status(400).json({ error: 'Admin response is required' }); - } - - // Determine language from body, headers, or default to English - let selectedLanguage = language; - if (!selectedLanguage) { - // Try to get language from Accept-Language header - const acceptLanguage = req.headers['accept-language'] as string; - // Try to get language from custom headers (common frontend patterns) - const regionHeader = req.headers['x-region'] as string; - const languageHeader = req.headers['x-language'] as string; - const localeHeader = req.headers['x-locale'] as string; - - selectedLanguage = languageHeader || - localeHeader || - regionHeader || - extractLanguageFromAcceptHeader(acceptLanguage) || - 'en'; - } - - // Validate and normalize language parameter - if (!['en', 'hu', 'de'].includes(selectedLanguage.toLowerCase())) { - selectedLanguage = 'en'; // Fallback to English for unsupported languages - } else { - selectedLanguage = selectedLanguage.toLowerCase(); - } - - logRequest('Admin respond to contact endpoint accessed', req, res, { - contactId, - adminUserId, - sendEmail, - language: selectedLanguage, - headerLanguage: req.headers['accept-language'] || req.headers['x-language'] || 'none' - }); - - // Update contact with response - const result = await container.updateContactCommandHandler.execute({ - id: contactId, - adminResponse, - respondedBy: adminUserId - }); - - if (!result) { - logWarning('Contact not found for response', { contactId }, req, res); - return res.status(404).json({ error: 'Contact not found' }); - } - - // Send email if requested - let emailSent = false; - let emailError = null; - - if (sendEmail === true && adminResponse) { - try { - await container.contactEmailService.sendResponse({ - to: result.email, - message: adminResponse, - contactId: contactId, - adminUserId: adminUserId, - contactName: result.name, - contactType: result.type, - originalMessage: result.txt, - language: selectedLanguage - }); - emailSent = true; - - logRequest('Contact response email sent successfully', req, res, { - contactId, - recipientEmail: result.email, - language: selectedLanguage - }); - } catch (emailErr) { - emailError = emailErr instanceof Error ? emailErr.message : 'Email sending failed'; - logError('Contact response email failed', emailErr as Error, req, res); - } - } - - AdminAuditService.logAdminAction('RESPOND_TO_CONTACT', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'update', - changes: { adminResponse, sendEmail, language: selectedLanguage }, - metadata: { emailSent, emailError } - }, req, res); - - logRequest('Admin contact response saved successfully', req, res, { - contactId, - sendEmail, - emailSent, - language: selectedLanguage - }); - - res.json({ - success: true, - message: 'Response saved successfully', - contact: result, - emailSent, - emailError: emailSent ? null : emailError - }); - } catch (error) { - logError('Admin respond to contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Contact not found' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Resend contact email (admin only) - NEW ENDPOINT -router.post('/contacts/:id/resend-email', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - const { language } = req.body; - - logRequest('Admin resend contact email endpoint accessed', req, res, { - contactId, - adminUserId, - language - }); - - // Get contact details - const contact = await container.getContactByIdQueryHandler.execute({ id: contactId }); - - if (!contact) { - return res.status(404).json({ error: 'Contact not found' }); - } - - if (!contact.adminResponse) { - return res.status(400).json({ error: 'No admin response found to resend' }); - } - - const selectedLanguage = language || 'en'; - - try { - await container.contactEmailService.sendResponse({ - to: contact.email, - message: contact.adminResponse, - contactId: contactId, - adminUserId: adminUserId, - contactName: contact.name, - contactType: contact.type, - originalMessage: contact.txt, - language: selectedLanguage - }); - - AdminAuditService.logAdminAction('RESEND_CONTACT_EMAIL', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'create', - metadata: { language: selectedLanguage, action: 'resend' } - }, req, res); - - logRequest('Contact email resent successfully', req, res, { - contactId, - recipientEmail: contact.email, - language: selectedLanguage - }); - - res.json({ - success: true, - message: 'Email resent successfully' - }); - } catch (emailErr) { - logError('Contact email resend failed', emailErr as Error, req, res); - res.status(500).json({ - error: 'Failed to resend email', - details: emailErr instanceof Error ? emailErr.message : 'Unknown error' - }); - } - } catch (error) { - logError('Admin resend contact email endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Soft delete contact (admin only) - NEW ENDPOINT -router.delete('/contacts/:id', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - - logRequest('Admin soft delete contact endpoint accessed', req, res, { contactId, adminUserId }); - - const result = await container.deleteContactCommandHandler.execute({ - id: contactId, - hard: false - }); - - AdminAuditService.logAdminAction('SOFT_DELETE_CONTACT', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'update' - }, req, res); - - logAuth('Contact soft deleted by admin', contactId, { adminUserId }, req, res); - res.json({ success: result }); - } catch (error) { - logError('Admin soft delete contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Contact not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Hard delete contact (admin only) - NEW ENDPOINT -router.delete('/contacts/:id/hard', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - - logRequest('Admin hard delete contact endpoint accessed', req, res, { contactId, adminUserId }); - - const result = await container.deleteContactCommandHandler.execute({ - id: contactId, - hard: true - }); - - AdminAuditService.logAdminAction('HARD_DELETE_CONTACT', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'delete', - sensitive: true - }, req, res); - - logAuth('Contact hard deleted by admin', contactId, { adminUserId }, req, res); - res.json({ success: result }); - } catch (error) { - logError('Admin hard delete contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Contact not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// DECK IMPORT/EXPORT ROUTES (ADMIN) -// ============================================================================= - -// Import deck from JSON file (unencrypted, admin only) -router.post('/decks/import', adminRequired, upload.single('file'), async (req: Request, res: Response) => { - try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - const userId = (req as any).user.userId; - const fileContent = req.file!.buffer.toString('utf-8'); - - logRequest('Admin deck import from JSON endpoint accessed', req, res, { fileName: req.file.originalname }); - - let jsonData; - try { - jsonData = JSON.parse(fileContent); - } catch (parseError) { - return res.status(400).json({ error: 'Invalid JSON format' }); - } - - // For admin import, we need to specify both target user and admin user - // Let's assume the deck will be owned by the admin user doing the import - const result = await container.deckImportExportService.adminImportFromJson(jsonData, userId, userId); - - logRequest('Admin deck import successful', req, res, { deckId: result.id, fileName: req.file.originalname }); - - res.json({ - success: true, - message: 'Deck imported successfully', - deckId: result.id - }); - } catch (error) { - logError('Admin deck import from JSON error', error as Error, req, res); - if (error instanceof Error && error.message.includes('Invalid')) { - res.status(400).json({ error: 'Invalid deck data structure' }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } -}); - -// Export deck as JSON (unencrypted, admin only) -router.get('/decks/:deckId/export', adminRequired, async (req: Request, res: Response) => { - try { - const { deckId } = req.params; - - logRequest('Admin deck export as JSON endpoint accessed', req, res, { deckId }); - - const deck = await container.deckRepository.findById(deckId); - if (!deck) { - logWarning('Deck not found for export', { deckId }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - logRequest('Admin deck export successful', req, res, { deckId, deckName: deck.name }); - - // Return deck as JSON for admin export - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Content-Disposition', `attachment; filename="${deck.name || 'deck'}.json"`); - res.json(deck); - } catch (error) { - logError('Admin deck export as JSON error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/SerpentRace_Backend/src/Api/routers/chatRouter.ts b/SerpentRace_Backend/src/Api/routers/chatRouter.ts deleted file mode 100644 index 140d6791..00000000 --- a/SerpentRace_Backend/src/Api/routers/chatRouter.ts +++ /dev/null @@ -1,287 +0,0 @@ -import express from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { logAuth, logError, logRequest, logWarning } from '../../Application/Services/Logger'; - -const chatRouter = express.Router(); - -// Get user's chats -chatRouter.get('/user-chats', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const includeArchived = req.query.includeArchived === 'true'; - - logRequest('Get user chats endpoint accessed', req, res, { userId, includeArchived }); - - const chats = await container.getUserChatsQueryHandler.execute({ - userId, - includeArchived - }); - - logRequest('User chats retrieved successfully', req, res, { - userId, - chatCount: chats.length - }); - - res.json(chats); - } catch (error) { - logError('Get user chats endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Get chat history -chatRouter.get('/history/:chatId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['chatId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const chatId = req.params.chatId; - - logRequest('Get chat history endpoint accessed', req, res, { userId, chatId }); - - const history = await container.getChatHistoryQueryHandler.execute({ - chatId, - userId - }); - - if (!history) { - logWarning('Chat history not found or unauthorized', { userId, chatId }, req, res); - return ErrorResponseService.sendNotFound(res, 'Chat not found or unauthorized'); - } - - logRequest('Chat history retrieved successfully', req, res, { - userId, - chatId, - messageCount: history.messages.length, - isArchived: history.isArchived - }); - - res.json(history); - } catch (error) { - logError('Get chat history endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Create new chat (direct/group) -chatRouter.post('/create', - authRequired, - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['type', 'userIds']), - ValidationMiddleware.validateAllowedValues({ type: ['direct', 'group'] }), - ValidationMiddleware.validateNonEmptyArrays(['userIds']) - ]), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const { type, name, userIds } = req.body; - - logRequest('Create chat endpoint accessed', req, res, { - userId, - type, - targetUserCount: userIds?.length || 0 - }); - - if (type === 'group' && !name?.trim()) { - return ErrorResponseService.sendBadRequest(res, 'Group name is required'); - } - - const chat = await container.createChatCommandHandler.execute({ - type, - name: name?.trim(), - createdBy: userId, - userIds - }); - - if (!chat) { - return ErrorResponseService.sendBadRequest(res, 'Failed to create chat'); - } - - logRequest('Chat created successfully', req, res, { - userId, - chatId: chat.id, - chatType: chat.type - }); - - res.json({ - id: chat.id, - type: chat.type, - name: chat.name, - users: chat.users, - messages: chat.messages - }); - } catch (error) { - logError('Create chat endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Premium subscription required')) { - return ErrorResponseService.sendForbidden(res, 'Premium subscription required to create groups'); - } - if (error.message.includes('not found')) { - return ErrorResponseService.sendNotFound(res, 'One or more users not found'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Send message (REST endpoint - mainly for testing, real messaging is via WebSocket) -chatRouter.post('/message', - authRequired, - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['chatId', 'message']), - ValidationMiddleware.validateUUIDFormat(['chatId']), - ValidationMiddleware.validateStringLength({ message: { min: 1, max: 2000 } }) - ]), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const { chatId, message } = req.body; - - logRequest('Send message endpoint accessed', req, res, { - userId, - chatId, - messageLength: message?.length || 0 - }); - - const sentMessage = await container.sendMessageCommandHandler.execute({ - chatId, - userId, - message - }); - - if (!sentMessage) { - return ErrorResponseService.sendBadRequest(res, 'Failed to send message'); - } - - logRequest('Message sent successfully', req, res, { - userId, - chatId, - messageId: sentMessage.id - }); - - res.json(sentMessage); - } catch (error) { - logError('Send message endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Chat not found')) { - return ErrorResponseService.sendNotFound(res, 'Chat not found'); - } - if (error.message.includes('not a member')) { - return ErrorResponseService.sendForbidden(res, 'Not authorized to send messages to this chat'); - } - if (error.message.includes('non-empty string')) { - return ErrorResponseService.sendBadRequest(res, 'Message must be a non-empty string'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Archive chat manually -chatRouter.post('/archive/:chatId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['chatId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const chatId = req.params.chatId; - - logRequest('Archive chat endpoint accessed', req, res, { userId, chatId }); - - // Check if user has access to this chat - const chat = await container.chatRepository.findById(chatId); - if (!chat) { - return ErrorResponseService.sendNotFound(res, 'Chat not found'); - } - - if (!chat.users.includes(userId)) { - return ErrorResponseService.sendForbidden(res, 'Not authorized to archive this chat'); - } - - const success = await container.archiveChatCommandHandler.execute({ chatId }); - - if (!success) { - return ErrorResponseService.sendBadRequest(res, 'Failed to archive chat'); - } - - logRequest('Chat archived successfully', req, res, { userId, chatId }); - res.json({ success: true, message: 'Chat archived successfully' }); - - } catch (error) { - logError('Archive chat endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Restore chat from archive -chatRouter.post('/restore/:chatId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['chatId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const chatId = req.params.chatId; - - logRequest('Restore chat endpoint accessed', req, res, { userId, chatId }); - - // Check if user has access to this archived chat - const archive = await container.chatArchiveRepository.findByChatId(chatId); - const userArchive = archive.find((a: any) => a.participants.includes(userId)); - - if (!userArchive) { - return ErrorResponseService.sendNotFound(res, 'Archived chat not found or unauthorized'); - } - - const success = await container.restoreChatCommandHandler.execute({ chatId }); - - if (!success) { - return ErrorResponseService.sendBadRequest(res, 'Failed to restore chat (game chats cannot be restored)'); - } - - logRequest('Chat restored successfully', req, res, { userId, chatId }); - res.json({ success: true, message: 'Chat restored successfully' }); - - } catch (error) { - logError('Restore chat endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Get archived chats for a game -chatRouter.get('/archived/game/:gameId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['gameId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const gameId = req.params.gameId; - - logRequest('Get archived game chats endpoint accessed', req, res, { userId, gameId }); - - const archivedChats = await container.getArchivedChatsQueryHandler.execute({ - userId, - gameId - }); - - logRequest('Archived game chats retrieved successfully', req, res, { - userId, - gameId, - chatCount: archivedChats.length - }); - - res.json(archivedChats); - } catch (error) { - logError('Get archived game chats endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -export default chatRouter; diff --git a/SerpentRace_Backend/src/Api/routers/contactRouter.ts b/SerpentRace_Backend/src/Api/routers/contactRouter.ts deleted file mode 100644 index d7fcbb53..00000000 --- a/SerpentRace_Backend/src/Api/routers/contactRouter.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Router } from 'express'; -import { container } from '../../Application/Services/DIContainer'; -import { logRequest, logError } from '../../Application/Services/Logger'; -import { ContactType } from '../../Domain/Contact/ContactAggregate'; - -const contactRouter = Router(); - -// Public endpoint - anyone can create a contact -contactRouter.post('/', async (req, res) => { - try { - // Get user ID if authenticated (optional) - const userId = (req as any).user?.userId || null; - - const { name, email, type, txt } = req.body; - - // Validate required fields - if (!name || !email || type === undefined || !txt) { - return res.status(400).json({ - error: 'Missing required fields: name, email, type, and txt are required' - }); - } - - // Validate type - if (!Object.values(ContactType).includes(Number(type))) { - return res.status(400).json({ - error: 'Invalid contact type. Must be one of: 0 (Bug), 1 (Problem), 2 (Question), 3 (Sales), 4 (Other)' - }); - } - - logRequest('Create contact endpoint accessed', req, res, { name, email, type, userId }); - - const result = await container.createContactCommandHandler.execute({ - name, - email, - userid: userId, - type: Number(type), - txt - }); - - logRequest('Contact created successfully', req, res, { contactId: result.id, name, email, type }); - res.status(201).json(result); - } catch (error) { - logError('Create contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default contactRouter; diff --git a/SerpentRace_Backend/src/Api/routers/deckImportExportRouter.ts b/SerpentRace_Backend/src/Api/routers/deckImportExportRouter.ts deleted file mode 100644 index cf205d6d..00000000 --- a/SerpentRace_Backend/src/Api/routers/deckImportExportRouter.ts +++ /dev/null @@ -1,124 +0,0 @@ -import express, { Request, Response } from 'express'; -import multer from 'multer'; -import { DIContainer } from '../../Application/Services/DIContainer'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; - -// Extend Express Request interface for file uploads -declare global { - namespace Express { - interface Request { - file?: Express.Multer.File; - } - } -} - -const router = express.Router(); -const container = DIContainer.getInstance(); - -// Configure multer for file uploads -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit - }, - fileFilter: (req: any, file: any, cb: any) => { - if (file.mimetype === 'application/json' || file.originalname.endsWith('.spr')) { - cb(null, true); - } else { - cb(new Error('Only JSON and .spr files are allowed')); - } - } -}); - -// Export deck to .spr file (encrypted) - users can only export their own decks -router.get('/export/:deckId', authRequired, async (req: Request, res: Response) => { - try { - const { deckId } = req.params; - const userId = (req as any).user.userId; - - logRequest('Export deck endpoint accessed', req, res, { deckId, userId }); - - // Check if user owns the deck - const deck = await container.deckRepository.findById(deckId); - if (!deck) { - logWarning('Deck not found for export', { deckId, userId }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - // Users can only export their own decks - if (deck.userid !== userId) { - logWarning('Access denied - user attempted to export deck they do not own', { - deckId, - userId, - deckOwnerId: deck.userid - }, req, res); - return res.status(403).json({ error: 'Access denied - you can only export your own decks' }); - } - - const sprData = await container.deckImportExportService.exportDeckToSpr(deckId, userId); - - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', `attachment; filename="${deck.name || 'deck'}.spr"`); - - logRequest('Deck exported successfully', req, res, { - deckId, - userId, - deckName: deck.name, - fileSize: sprData.length - }); - - res.send(sprData); - } catch (error) { - logError('Export deck endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Import deck from .spr file (encrypted) - imported deck will be owned by the importing user -router.post('/import', authRequired, upload.single('file'), async (req: Request, res: Response) => { - try { - const userId = (req as any).user.userId; - - logRequest('Import deck endpoint accessed', req, res, { - userId, - hasFile: !!req.file, - fileName: req.file?.originalname, - fileSize: req.file?.size - }); - - if (!req.file) { - logWarning('No file uploaded for deck import', { userId }, req, res); - return res.status(400).json({ error: 'No file uploaded' }); - } - - const fileBuffer = req.file!.buffer; - - // Import the deck and assign ownership to the current user - const result = await container.deckImportExportService.importDeckFromSpr(fileBuffer, userId); - - logRequest('Deck imported successfully', req, res, { - userId, - deckId: result.id, - deckName: result.name || 'Unknown', - fileName: req.file.originalname, - fileSize: req.file.size - }); - - res.json({ - success: true, - message: 'Deck imported successfully and added to your collection', - deckId: result.id - }); - } catch (error) { - logError('Import deck endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('Invalid')) { - return res.status(400).json({ error: 'Invalid file format or corrupted data' }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } -}); - -export default router; diff --git a/SerpentRace_Backend/src/Api/routers/deckRouter.ts b/SerpentRace_Backend/src/Api/routers/deckRouter.ts deleted file mode 100644 index 67632a33..00000000 --- a/SerpentRace_Backend/src/Api/routers/deckRouter.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { GeneralSearchService } from '../../Application/Search/Generalsearch'; -import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; -import { Type, CType } from '../../Domain/Deck/DeckAggregate'; - -const deckRouter = Router(); - -/** - * Helper function to convert string enum values to integer enum values - */ -function convertEnumValues(data: any): any { - const converted = { ...data }; - - // Convert Type enum - if (converted.type && typeof converted.type === 'string') { - switch (converted.type.toUpperCase()) { - case 'LUCK': - converted.type = Type.LUCK; - break; - case 'JOKER': - converted.type = Type.JOKER; - break; - case 'QUESTION': - converted.type = Type.QUESTION; - break; - default: - throw new Error('Invalid deck type. Must be LUCK, JOKER, or QUESTION'); - } - } - - // Convert CType enum - if (converted.ctype && typeof converted.ctype === 'string') { - switch (converted.ctype.toUpperCase()) { - case 'PUBLIC': - converted.ctype = CType.PUBLIC; - break; - case 'PRIVATE': - converted.ctype = CType.PRIVATE; - break; - case 'ORGANIZATION': - converted.ctype = CType.ORGANIZATION; - break; - default: - throw new Error('Invalid deck ctype. Must be PUBLIC, PRIVATE, or ORGANIZATION'); - } - } - - return converted; -} - -// Create search service that isn't in the container yet -const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository); - -// Authenticated routes - Get decks with pagination (RECOMMENDED) -deckRouter.get('/page/:from/:to', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const userOrgId = (req as any).user.orgId; - const isAdmin = (req as any).user.authLevel === 1; - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Get decks by page endpoint accessed', req, res, { - userId, - userOrgId, - isAdmin, - from, - to - }); - - // Use paginated query handler for memory efficiency - const result = await container.getDecksByPageQueryHandler.execute({ - userId, - userOrgId, - isAdmin, - from, - to - }); - - logRequest('Get decks page completed successfully', req, res, { - userId, - from, - to, - returnedCount: result.decks.length, - totalCount: result.totalCount - }); - - res.json(result); - } catch (error) { - logError('Get decks by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.post('/', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - logRequest('Create deck endpoint accessed', req, res, { name: req.body.name, userId }); - - // Convert string enum values to integers - const command = convertEnumValues({ - ...req.body, - userid: userId - }); - - const result = await container.createDeckCommandHandler.execute(command); - - logRequest('Deck created successfully', req, res, { deckId: result.id, name: req.body.name, userId }); - res.json(result); - } catch (error) { - logError('Create deck endpoint error', error as Error, req, res); - - // Handle enum validation errors - if (error instanceof Error && error.message.includes('Invalid deck')) { - return res.status(400).json({ error: error.message }); - } - - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Deck with this name already exists' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.get('/search', authRequired, async (req, res) => { - try { - const { query, limit, offset } = req.query; - logRequest('Search decks endpoint accessed', req, res, { query, limit, offset }); - - if (!query || typeof query !== 'string') { - logWarning('Deck search attempted without query', { query, hasQuery: !!query }, req, res); - return res.status(400).json({ error: 'Search query is required' }); - } - - const searchQuery = { - query: query.trim(), - limit: limit ? parseInt(limit as string) : 20, - offset: offset ? parseInt(offset as string) : 0 - }; - - // Validate pagination parameters - if (searchQuery.limit < 1 || searchQuery.limit > 100) { - logWarning('Invalid deck search limit parameter', { limit: searchQuery.limit }, req, res); - return res.status(400).json({ error: 'Limit must be between 1 and 100' }); - } - - if (searchQuery.offset < 0) { - logWarning('Invalid deck search offset parameter', { offset: searchQuery.offset }, req, res); - return res.status(400).json({ error: 'Offset must be non-negative' }); - } - - const result = await searchService.searchFromUrl(req.originalUrl, searchQuery); - - logRequest('Deck search completed successfully', req, res, { - query: searchQuery.query, - resultCount: Array.isArray(result) ? result.length : 0 - }); - res.json(result); - } catch (error) { - logError('Search decks endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.get('/:id', authRequired, async (req, res) => { - try { - const deckId = req.params.id; - logRequest('Get deck by id endpoint accessed', req, res, { deckId }); - - const result = await container.getDeckByIdQueryHandler.execute({ id: deckId }); - - if (!result) { - logWarning('Deck not found', { deckId }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - logRequest('Deck retrieved successfully', req, res, { deckId }); - res.json(result); - } catch (error) { - logError('Get deck by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.patch('/:id', authRequired, async (req, res) => { - try { - const deckId = req.params.id; - const userId = (req as any).user.userId; - const authLevel = (req as any).user.authLevel; - logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) }); - - // Convert string enum values to integers - const updateData = convertEnumValues(req.body); - - const result = await container.updateDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, ...updateData }); - - logRequest('Deck updated successfully', req, res, { deckId, userId }); - res.json(result); - } catch (error) { - logError('Update deck endpoint error', error as Error, req, res); - - // Handle enum validation errors - if (error instanceof Error && error.message.includes('Invalid deck')) { - return res.status(400).json({ error: error.message }); - } - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Deck not found' }); - } - - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Deck with this name already exists' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - if (error instanceof Error && error.message.includes('admin')) { - return res.status(403).json({ error: 'Forbidden: ' + error.message }); - } - - if (error instanceof Error && error.message.includes('admin')) { - return res.status(403).json({ error: 'Forbidden: ' + error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.delete('/:id', authRequired, async (req, res) => { - try { - const deckId = req.params.id; - const userId = (req as any).user.userId; - const authLevel = (req as any).user.authLevel; - logRequest('Soft delete deck endpoint accessed', req, res, { deckId, userId }); - - const result = await container.deleteDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, soft: true }); - - logRequest('Deck soft delete successful', req, res, { deckId, userId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Soft delete deck endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Deck not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default deckRouter; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/routers/gameRouter.ts b/SerpentRace_Backend/src/Api/routers/gameRouter.ts deleted file mode 100644 index 982cf163..00000000 --- a/SerpentRace_Backend/src/Api/routers/gameRouter.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { optionalAuth } from '../middleware/optionalAuth'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; -import { LoginType } from '../../Domain/Game/GameAggregate'; - -const gameRouter = Router(); - -gameRouter.post('/start', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const orgId = (req as any).user.orgId; - const { deckids, maxplayers, logintype } = req.body; - - logRequest('Start game endpoint accessed', req, res, { - userId, - orgId, - deckCount: deckids?.length, - maxplayers, - logintype - }); - - // Validate required fields - if (!deckids || !Array.isArray(deckids) || deckids.length === 0) { - return res.status(400).json({ error: 'deckids is required and must be a non-empty array' }); - } - - if (!maxplayers || typeof maxplayers !== 'number') { - return res.status(400).json({ error: 'maxplayers is required and must be a number' }); - } - - if (logintype === undefined || typeof logintype !== 'number') { - return res.status(400).json({ error: 'logintype is required and must be a number (0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION)' }); - } - - // Start the game using the GameService - const game = await container.gameService.startGame( - deckids, - maxplayers, - logintype as LoginType, - userId, - orgId - ); - - logRequest('Game started successfully', req, res, { - userId, - gameId: game.id, - gameCode: game.gamecode, - deckCount: game.gamedecks.length, - totalCards: game.gamedecks.reduce((sum, deck) => sum + deck.cards.length, 0) - }); - - res.json(game); - } catch (error) { - logError('Start game endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('not found')) { - return res.status(404).json({ error: error.message }); - } - if (error.message.includes('validation') || - error.message.includes('must be') || - error.message.includes('required') || - error.message.includes('Invalid')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -gameRouter.post('/join', optionalAuth, async (req, res) => { - try { - const user = (req as any).user; - const { gameCode, playerName } = req.body; - - logRequest('Join game endpoint accessed', req, res, { - gameCode, - playerName, - hasAuth: !!user, - userId: user?.userId, - orgId: user?.orgId - }); - - // Validate required fields - if (!gameCode || typeof gameCode !== 'string') { - return res.status(400).json({ error: 'gameCode is required and must be a string' }); - } - - if (gameCode.length !== 6) { - return res.status(400).json({ error: 'gameCode must be exactly 6 characters long' }); - } - - // First, we need to find the game to determine its type - const gameRepository = container.gameRepository; - const gameToJoin = await gameRepository.findByGameCode(gameCode); - - if (!gameToJoin) { - return res.status(404).json({ error: 'Game not found' }); - } - - // Determine join requirements based on game login type - let actualPlayerId: string | undefined; - let actualPlayerName: string | undefined; - let actualOrgId: string | null = null; - - switch (gameToJoin.logintype) { - case LoginType.PUBLIC: - // Public games: playerName required, authentication optional - // If user is logged in and no playerName provided, use their username - if (!playerName || typeof playerName !== 'string' || !playerName.trim()) { - if (user && user.userId) { - // User is logged in, fetch their username to use as playerName - try { - const userDetails = await container.getUserByIdQueryHandler.execute({ id: user.userId }); - if (userDetails && userDetails.username) { - actualPlayerName = userDetails.username; - logRequest('Using logged-in user\'s username as playerName', req, res, { - userId: user.userId, - username: userDetails.username - }); - } else { - return res.status(400).json({ - error: 'playerName is required for public games' - }); - } - } catch (error) { - logError('Failed to fetch user details for playerName', error as Error, req, res); - return res.status(400).json({ - error: 'playerName is required for public games' - }); - } - } else { - // User is not logged in, playerName is required - return res.status(400).json({ - error: 'playerName is required for public games' - }); - } - } else { - // playerName was provided, use it - actualPlayerName = playerName.trim(); - } - actualPlayerId = user?.userId; // Use authenticated user ID if available, otherwise undefined - break; - - case LoginType.PRIVATE: - // Private games: authentication required - if (!user || !user.userId) { - return res.status(401).json({ - error: 'Authentication required to join private games' - }); - } - actualPlayerId = user.userId; - actualPlayerName = playerName; - break; - - case LoginType.ORGANIZATION: - // Organization games: authentication + organization membership required - if (!user || !user.userId) { - return res.status(401).json({ - error: 'Authentication required to join organization games' - }); - } - - if (!user.orgId) { - return res.status(403).json({ - error: 'Organization membership required to join organization games' - }); - } - - if (gameToJoin.orgid && user.orgId !== gameToJoin.orgid) { - return res.status(403).json({ - error: 'You must be a member of the same organization to join this game' - }); - } - - actualPlayerId = user.userId; - actualPlayerName = playerName; - actualOrgId = user.orgId; - break; - - default: - return res.status(400).json({ error: 'Invalid game type' }); - } - - // Join the game using the GameService with determined parameters - const game = await container.gameService.joinGame( - gameCode, - actualPlayerId, - actualPlayerName, - actualOrgId, - gameToJoin.logintype - ); - - logRequest('Player joined game successfully', req, res, { - userId: actualPlayerId || 'anonymous', - gameId: game.id, - gameCode: game.gamecode, - gameType: LoginType[gameToJoin.logintype], - playerCount: game.players.length, - maxPlayers: game.maxplayers, - playerName: actualPlayerName - }); - - // Create game token for WebSocket authentication - const gameTokenService = container.gameTokenService; - const gameToken = gameTokenService.createGameToken( - game.id, - game.gamecode, - actualPlayerName || 'Anonymous', - actualPlayerId - ); - - // Return clean response with essential data + game token - res.json({ - id: game.id, - gamecode: game.gamecode, - playerName: actualPlayerName, - playerCount: game.players.length, - maxPlayers: game.maxplayers, - gameType: LoginType[gameToJoin.logintype], - isAuthenticated: !!actualPlayerId, - gameToken: gameToken - }); - } catch (error) { - logError('Join game endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('not found')) { - return res.status(404).json({ error: error.message }); - } - if (error.message.includes('Authentication required')) { - return res.status(401).json({ error: error.message }); - } - if (error.message.includes('Organization') || error.message.includes('organization')) { - return res.status(403).json({ error: error.message }); - } - if (error.message.includes('full') || - error.message.includes('already in') || - error.message.includes('not accepting')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation') || - error.message.includes('must be') || - error.message.includes('required') || - error.message.includes('Invalid')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -gameRouter.post('/:gameId/start', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const { gameId } = req.params; - - logRequest('Start gameplay endpoint accessed', req, res, { - userId, - gameId - }); - - // Validate required fields - if (!gameId || typeof gameId !== 'string') { - return res.status(400).json({ error: 'gameId is required and must be a string' }); - } - - // Start the gameplay using the GameService - const result = await container.gameService.startGamePlay(gameId, userId); - - logRequest('Game gameplay started successfully', req, res, { - userId, - gameId, - playerCount: result.game.players.length - }); - - res.json({ - message: 'Game started successfully', - gameId: gameId, - playerCount: result.game.players.length, - game: result.game, - boardData: result.boardData - }); - } catch (error) { - logError('Start gameplay endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('not found')) { - return res.status(404).json({ error: error.message }); - } - if (error.message.includes('Only') || error.message.includes('master')) { - return res.status(403).json({ error: error.message }); - } - if (error.message.includes('already started') || - error.message.includes('not ready') || - error.message.includes('minimum players') || - error.message.includes('not in waiting state') || - error.message.includes('cannot be started')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation') || - error.message.includes('must be') || - error.message.includes('required') || - error.message.includes('Invalid')) { - return res.status(400).json({ error: error.message }); - } - // Board generation specific errors - if (error.message.includes('Board generation') || - error.message.includes('board not found') || - error.message.includes('BoardGenerationService') || - error.message.includes('Failed to wait for board generation') || - error.message.includes('board generation timeout')) { - return res.status(500).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default gameRouter; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/routers/organizationRouter.ts b/SerpentRace_Backend/src/Api/routers/organizationRouter.ts deleted file mode 100644 index eceaad01..00000000 --- a/SerpentRace_Backend/src/Api/routers/organizationRouter.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { GeneralSearchService } from '../../Application/Search/Generalsearch'; -import { logRequest, logError, logWarning, logAuth } from '../../Application/Services/Logger'; - -const organizationRouter = Router(); - -// Create search service that isn't in the container yet -const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository); - -// Auth routes - Get organizations with pagination (RECOMMENDED) -organizationRouter.get('/page/:from/:to', authRequired, async (req, res) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Get organizations by page endpoint accessed', req, res, { from, to }); - - const result = await container.getOrganizationsByPageQueryHandler.execute({ from, to }); - - logRequest('Organizations page retrieved successfully', req, res, { - from, - to, - count: result.organizations.length, - totalCount: result.totalCount - }); - - res.json(result); - } catch (error) { - logError('Get organizations by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -organizationRouter.get('/search', authRequired, async (req, res) => { - try { - const { query, limit, offset } = req.query; - logRequest('Search organizations endpoint accessed', req, res, { query, limit, offset }); - - if (!query || typeof query !== 'string') { - logWarning('Organization search attempted without query', { query, hasQuery: !!query }, req, res); - return res.status(400).json({ error: 'Search query is required' }); - } - - const searchQuery = { - query: query.trim(), - limit: limit ? parseInt(limit as string) : 20, - offset: offset ? parseInt(offset as string) : 0 - }; - - // Validate pagination parameters - if (searchQuery.limit < 1 || searchQuery.limit > 100) { - logWarning('Invalid organization search limit parameter', { limit: searchQuery.limit }, req, res); - return res.status(400).json({ error: 'Limit must be between 1 and 100' }); - } - - if (searchQuery.offset < 0) { - logWarning('Invalid organization search offset parameter', { offset: searchQuery.offset }, req, res); - return res.status(400).json({ error: 'Offset must be non-negative' }); - } - - const result = await searchService.searchFromUrl(req.originalUrl, searchQuery); - - logRequest('Organization search completed successfully', req, res, { - query: searchQuery.query, - resultCount: Array.isArray(result) ? result.length : 0 - }); - res.json(result); - } catch (error) { - logError('Search organizations endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get organization login URL -organizationRouter.get('/:orgId/login-url', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const { orgId } = req.params; - - logRequest('Get organization login URL endpoint accessed', req, res, { - userId, - organizationId: orgId - }); - - const result = await container.getOrganizationLoginUrlQueryHandler.execute({ - organizationId: orgId - }); - - if (!result) { - logWarning('Organization login URL not found', { - organizationId: orgId, - userId - }, req, res); - return ErrorResponseService.sendNotFound(res, 'Organization login URL not found'); - } - - logRequest('Organization login URL retrieved successfully', req, res, { - organizationId: orgId, - organizationName: result.organizationName, - hasUrl: !!result.loginUrl, - userId - }); - - res.json(result); - } catch (error) { - logError('Get organization login URL endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Process third-party authentication callback -organizationRouter.post('/auth-callback', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const { organizationId, status, authToken } = req.body; - - logRequest('Organization auth callback endpoint accessed', req, res, { - userId, - organizationId, - status, - hasAuthToken: !!authToken - }); - - // Validate required fields - if (!organizationId || !status) { - logWarning('Missing required fields for organization auth callback', { - organizationId: !!organizationId, - status: !!status, - userId - }, req, res); - return ErrorResponseService.sendBadRequest(res, 'organizationId and status are required'); - } - - if (status !== 'ok' && status !== 'not_ok') { - logWarning('Invalid status value for organization auth callback', { - status, - userId, - organizationId - }, req, res); - return ErrorResponseService.sendBadRequest(res, 'status must be either "ok" or "not_ok"'); - } - - const result = await container.processOrgAuthCallbackCommandHandler.execute({ - organizationId, - userId, - status, - authToken - }); - - if (!result.success) { - if (result.message.includes('not found')) { - logWarning('Organization auth callback failed - entity not found', { - userId, - organizationId, - message: result.message - }, req, res); - return ErrorResponseService.sendNotFound(res, result.message); - } - if (result.message.includes('does not belong')) { - logWarning('Organization auth callback failed - authorization error', { - userId, - organizationId, - message: result.message - }, req, res); - return ErrorResponseService.sendForbidden(res, result.message); - } - if (result.message.includes('authentication failed')) { - logAuth('Organization authentication failed via callback', userId, { - organizationId, - status - }, req, res); - return ErrorResponseService.sendUnauthorized(res, result.message); - } - - logError('Organization auth callback internal error', new Error(result.message), req, res); - return ErrorResponseService.sendInternalServerError(res); - } - - logAuth('Organization auth callback processed successfully', userId, { - organizationId, - status, - updatedFields: result.updatedFields - }, req, res); - - res.json({ - success: result.success, - message: result.message, - updatedFields: result.updatedFields - }); - } catch (error) { - logError('Organization auth callback endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -export default organizationRouter; diff --git a/SerpentRace_Backend/src/Api/routers/userRouter.ts b/SerpentRace_Backend/src/Api/routers/userRouter.ts deleted file mode 100644 index facc906d..00000000 --- a/SerpentRace_Backend/src/Api/routers/userRouter.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { GeneralSearchService } from '../../Application/Search/Generalsearch'; -import { logRequest, logError, logAuth, logWarning } from '../../Application/Services/Logger'; - -const userRouter = Router(); - -// Create search service that isn't in the container yet -const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository); - -// Login endpoint -userRouter.post('/login', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'password']), - ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 50 }, - password: { min: 6, max: 100 } - }) - ]), - async (req, res) => { - try { - logRequest('Login endpoint accessed', req, res, { username: req.body.username }); - - const { username, password } = req.body; - - const result = await container.loginCommandHandler.execute({ username, password }, res); - - if (result) { - logAuth('User login successful', undefined, { username: result.user.username }, req, res); - res.json(result); - } else { - throw new Error(`Login failed: ${result}`); - } - - } catch (error) { - logError('Login endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Invalid username')) { - return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password'); - } - if (error.message.includes('Invalid password')) { - return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password'); - } - if (error.message.includes('not verified')) { - return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address'); - } - if (error.message.includes('restriction')) { - return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address'); - } - if (error.message.includes('deactivated')) { - return ErrorResponseService.sendUnauthorized(res, 'Account has been deactivated'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Create user endpoint -userRouter.post('/create', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'email', 'password']), - ValidationMiddleware.validateEmailFormat(['email']), - ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 50 }, - password: { min: 6, max: 100 } - }) - ]), - async (req, res) => { - try { - logRequest('Create user endpoint accessed', req, res, { - username: req.body.username, - email: req.body.email - }); - - const acceptLanguage = req.header('Accept-Language') || 'en'; - const language : 'hu' | 'de' | 'en' = acceptLanguage.toLowerCase().startsWith('hu') ? 'hu' : - acceptLanguage.toLowerCase().startsWith('de') ? 'de' : 'en'; - - const result = await container.createUserCommandHandler.execute({ ...req.body, language }); - - logRequest('User created successfully', req, res, { - username: result.username - }); - - res.status(201).json(result); - - } catch (error) { - // Don't log here since CreateUserCommandHandler already logs system errors - // Only log validation/user input errors at router level - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return ErrorResponseService.sendConflict(res, error.message); - } - if (error.message.includes('validation')) { - return ErrorResponseService.sendBadRequest(res, error.message); - } - // Log unexpected errors that weren't handled by the command handler - if (!error.message.includes('Failed to create user')) { - logError('Unexpected create user endpoint error', error as Error, req, res); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Get user profile (current user) -userRouter.get('/profile', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - - logRequest('Get user profile endpoint accessed', req, res, { userId }); - - const result = await container.getUserByIdQueryHandler.execute({ id: userId }); - - if (!result) { - logWarning('User profile not found', { userId }, req, res); - return ErrorResponseService.sendNotFound(res, 'User not found'); - } - - logRequest('User profile retrieved successfully', req, res, { - userId, - username: result.username - }); - - res.json(result); - - } catch (error) { - logError('Get user profile endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Update user profile (current user) -userRouter.patch('/profile', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - - logRequest('Update user profile endpoint accessed', req, res, { - userId, - fieldsToUpdate: Object.keys(req.body) - }); - - const result = await container.updateUserCommandHandler.execute({ id: userId, ...req.body }); - - if (!result) { - return ErrorResponseService.sendNotFound(res, 'User not found'); - } - - logRequest('User profile updated successfully', req, res, { - userId, - username: result.username - }); - - res.json(result); - - } catch (error) { - logError('Update user profile endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return ErrorResponseService.sendConflict(res, error.message); - } - if (error.message.includes('validation')) { - return ErrorResponseService.sendBadRequest(res, error.message); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -//Soft delete user (current user) -userRouter.delete('/profile', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const result = await container.deleteUserCommandHandler.execute({ id: userId, soft: true }); - logRequest('User soft deleted successfully', req, res, { userId }); - res.json({ success: result }); - } catch (error) { - logError('Soft delete user endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -//logout user (current user) -userRouter.post('/logout', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - await container.logoutCommandHandler.execute(userId, res, req); - logRequest('User logged out successfully', req, res, { userId }); - res.json({ success: true }); - } catch (error) { - logError('Logout user endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Refresh token endpoint -userRouter.post('/refresh-token', async (req, res) => { - try { - logRequest('Token refresh endpoint accessed', req, res); - - const jwtService = container.jwtService; - const newTokenPair = jwtService.attemptTokenRefresh(req, res); - - if (newTokenPair) { - logRequest('Token refresh successful', req, res); - res.json({ - success: true, - message: 'Tokens refreshed successfully', - accessToken: newTokenPair.accessToken, - refreshToken: newTokenPair.refreshToken - }); - } else { - logWarning('Token refresh failed - invalid or missing refresh token', undefined, req, res); - return ErrorResponseService.sendUnauthorized(res, 'Invalid or expired refresh token'); - } - } catch (error) { - logError('Refresh token endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Email verification endpoint -userRouter.post('/verify-email/:token', async (req, res) => { - try { - const { token } = req.params; - - logRequest('Email verification endpoint accessed', req, res, { - tokenPrefix: token.substring(0, 8) + '...' - }); - - if (!token) { - return ErrorResponseService.sendBadRequest(res, 'Verification token is required'); - } - - const result = await container.verifyEmailCommandHandler.execute({ token }); - - if (result) { - logAuth('Email verification successful', undefined, { tokenPrefix: token.substring(0, 8) + '...' }, req, res); - res.json({ success: true, message: 'Email verified successfully' }); - } else { - throw new Error('Email verification failed'); - } - - } catch (error) { - logError('Email verification endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Invalid') || error.message.includes('expired')) { - return ErrorResponseService.sendBadRequest(res, 'Invalid or expired verification token'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Forgot password request endpoint -userRouter.post('/forgot-password', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['email']), - ValidationMiddleware.validateEmailFormat(['email']) - ]), - async (req, res) => { - try { - const { email } = req.body; - const acceptLanguage = req.header('Accept-Language') || 'en'; - const language: 'hu' | 'de' | 'en' = acceptLanguage.toLowerCase().startsWith('hu') ? 'hu' : - acceptLanguage.toLowerCase().startsWith('de') ? 'de' : 'en'; - - logRequest('Forgot password endpoint accessed', req, res, { email }); - - const result = await container.requestPasswordResetCommandHandler.execute({ language, email }); - - if (result) { - logAuth('Password reset request successful', undefined, { email }, req, res); - res.json({ - success: true, - message: 'If an account with this email exists, a password reset link has been sent' - }); - } else { - throw new Error('Password reset request failed'); - } - - } catch (error) { - logError('Forgot password endpoint error', error as Error, req, res); - - // Always return success for security (don't reveal if email exists) - res.json({ - success: true, - message: 'If an account with this email exists, a password reset link has been sent' - }); - } -}); - -// Reset password endpoint -userRouter.post('/reset-password', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['token', 'newPassword']), - ValidationMiddleware.validateStringLength({ - newPassword: { min: 6, max: 100 } - }) - ]), - async (req, res) => { - try { - const { token, newPassword } = req.body; - - logRequest('Reset password endpoint accessed', req, res, { - tokenPrefix: token.substring(0, 8) + '...' - }); - - const result = await container.resetPasswordCommandHandler.execute({ token, newPassword }); - - if (result) { - logAuth('Password reset successful', undefined, { tokenPrefix: token.substring(0, 8) + '...' }, req, res); - res.json({ success: true, message: 'Password reset successfully' }); - } else { - throw new Error('Password reset failed'); - } - - } catch (error) { - logError('Reset password endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Invalid') || error.message.includes('expired')) { - return ErrorResponseService.sendBadRequest(res, 'Invalid or expired reset token'); - } - if (error.message.includes('Password validation')) { - return ErrorResponseService.sendBadRequest(res, error.message); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -export default userRouter; diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerConfig.ts b/SerpentRace_Backend/src/Api/swagger/swaggerConfig.ts deleted file mode 100644 index 1f2e31ef..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerConfig.ts +++ /dev/null @@ -1,101 +0,0 @@ -import swaggerJSDoc from 'swagger-jsdoc'; -import path from 'path'; - -export const swaggerOptions = { - definition: { - openapi: '3.0.0', - info: { - title: 'SerpentRace API', - version: '1.0.0', - description: 'Comprehensive API documentation for SerpentRace Backend', - contact: { - name: 'SerpentRace Development Team', - email: 'dev@serpentrace.com' - }, - license: { - name: 'MIT', - url: 'https://opensource.org/licenses/MIT' - } - }, - servers: [ - { - url: 'http://localhost:3001', - description: 'Local development server' - }, - { - url: 'http://localhost:3000', - description: 'Local development server (alt)' - }, - { - url: 'https://api.serpentrace.com', - description: 'Production server' - } - ], - components: { - securitySchemes: { - bearerAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - description: 'Enter JWT token obtained from /api/users/login' - } - } - }, - security: [{ bearerAuth: [] }], - tags: [ - { - name: 'Users', - description: 'User authentication and profile management' - }, - { - name: 'Organizations', - description: 'Organization management and authentication' - }, - { - name: 'Decks', - description: 'Deck creation, management, and gameplay' - }, - { - name: 'Chats', - description: 'Real-time chat and messaging system' - }, - { - name: 'Contacts', - description: 'Contact form and support requests' - }, - { - name: 'Deck Import/Export', - description: 'Import and export deck functionality' - }, - { - name: 'Games', - description: 'Game management and gameplay' - }, - { - name: 'Admin - Users', - description: 'Admin user management operations' - }, - { - name: 'Admin - Decks', - description: 'Admin deck management operations' - }, - { - name: 'Admin - Organizations', - description: 'Admin organization management operations' - }, - { - name: 'Admin - Chats', - description: 'Admin chat management operations' - }, - { - name: 'Admin - Contacts', - description: 'Admin contact management operations' - } - ] - }, - apis: [ - './src/Api/swagger/swaggerDefinitionsFixed.ts' - ], -}; - -export const swaggerSpec = swaggerJSDoc(swaggerOptions); diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitions.ts b/SerpentRace_Backend/src/Api/swagger/swaggerDefinitions.ts deleted file mode 100644 index eef90dac..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitions.ts +++ /dev/null @@ -1,1694 +0,0 @@ -/** - * @swagger - * components: -<<<<<<< HEAD -<<<<<<< HEAD -======= - * securitySchemes: - * bearerAuth: - * type: http - * scheme: bearer - * bearerFormat: JWT ->>>>>>> origin/main -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * schemas: - * User: - * type: object - * properties: - * id: - * type: string - * format: uuid - * username: - * type: string - * email: - * type: string - * format: email - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * nullable: true - * type: - * type: string - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * orgid: - * type: string - * nullable: true - * - * CreateUserRequest: - * type: object - * required: - * - username - * - email - * - password - * - fname - * - lname - * - type - * properties: - * username: - * type: string - * email: - * type: string - * format: email - * password: - * type: string - * format: password - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * type: - * type: string - * - * LoginRequest: - * type: object - * required: - * - username - * - password - * properties: - * username: - * type: string - * password: - * type: string - * format: password - * - * LoginResponse: - * type: object - * properties: - * token: - * type: string - * user: - * $ref: '#/components/schemas/User' - * requiresOrgReauth: - * type: boolean - * orgLoginUrl: - * type: string - * organizationName: - * type: string - * - * UpdateProfileRequest: - * type: object - * properties: - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * email: - * type: string - * format: email - * -<<<<<<< HEAD - * ForgotPasswordRequest: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * format: email - * - * ResetPasswordRequest: - * type: object - * required: - * - token - * - newPassword - * properties: - * token: - * type: string - * newPassword: - * type: string - * format: password - * minLength: 6 - * maxLength: 100 - * - * AuthSuccessResponse: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * -======= ->>>>>>> origin/main - * Organization: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * contactfname: - * type: string - * contactlname: - * type: string - * contactphone: - * type: string - * contactemail: - * type: string - * format: email - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * url: - * type: string - * nullable: true - * userinorg: - * type: integer - * maxOrganizationalDecks: - * type: integer - * - * Deck: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * enum: [0, 1, 2, 3] - * description: 0=JOKER, 1=LUCK, 2=QUESTION, 3=GENERAL - * userid: - * type: string - * format: uuid - * creationdate: - * type: string - * format: date-time - * cards: - * type: array - * items: - * type: object - * playedNumber: - * type: integer - * ctype: - * type: integer - * enum: [0, 1, 2] - * description: 0=PUBLIC, 1=ORGANIZATIONAL, 2=PRIVATE - * updatedate: - * type: string - * format: date-time - * state: - * type: integer - * enum: [0, 1, 2] - * description: 0=ACTIVE, 1=INACTIVE, 2=SOFT_DELETE - * organization: - * $ref: '#/components/schemas/Organization' - * nullable: true - * - * CreateDeckRequest: - * type: object - * required: - * - name - * - type - * - cards - * properties: - * name: - * type: string - * type: - * type: integer - * cards: - * type: array - * items: - * type: object - * ctype: - * type: integer - * - * Contact: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * email: - * type: string - * format: email - * userid: - * type: string - * format: uuid - * nullable: true - * type: - * type: integer - * enum: [0, 1, 2] - * description: 0=QUESTION, 1=BUG_REPORT, 2=SUGGESTION - * txt: - * type: string - * state: - * type: integer - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * adminResponse: - * type: string - * nullable: true - * responseDate: - * type: string - * format: date-time - * nullable: true - * respondedBy: - * type: string - * nullable: true - * - * CreateContactRequest: - * type: object - * required: - * - name - * - email - * - type - * - txt - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * txt: - * type: string - * - * Chat: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * participants: - * type: array - * items: - * type: string - * creatorId: - * type: string - * gameId: - * type: string - * nullable: true - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * state: - * type: integer - * - * ChatMessage: - * type: object - * properties: - * id: - * type: string - * format: uuid - * senderId: - * type: string - * senderName: - * type: string - * message: - * type: string - * timestamp: - * type: string - * format: date-time - * chatId: - * type: string - * -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * Game: - * type: object - * properties: - * id: - * type: string - * format: uuid - * gamecode: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: integer - * gamedecks: - * type: array - * players: - * type: array - * items: - * type: string - * started: - * type: boolean - * finished: - * type: boolean - * state: - * type: integer - * createdate: - * type: string - * format: date-time - * -<<<<<<< HEAD -======= ->>>>>>> origin/main -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * Error: - * type: object - * properties: - * error: - * type: string - * timestamp: - * type: string - * format: date-time - * details: - * type: string -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - */ -/** - * @swagger - * - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * description: Authenticate user and return JWT token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginRequest' - * responses: - * 200: - * description: Login successful -<<<<<<< HEAD - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * -======= - * - * paths: - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * description: Authenticate user and return JWT token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginRequest' - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' ->>>>>>> origin/main -======= - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * - * /api/users/create: - * post: - * tags: [Users] - * summary: Create new user - * description: Register a new user account - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateUserRequest' - * responses: - * 201: - * description: User created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * - * /api/users/profile: - * get: - * tags: [Users] - * summary: Get user profile - * description: Get current user's profile information - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User profile data - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 401: - * description: Unauthorized - * patch: - * tags: [Users] - * summary: Update user profile - * description: Update current user's profile information - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/UpdateProfileRequest' - * responses: - * 200: - * description: Profile updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * 401: - * description: Unauthorized - * - * /api/organizations/page/{from}/{to}: - * get: - * tags: [Organizations] - * summary: Get organizations by page - * description: Retrieve paginated list of organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated organizations - * content: - * application/json: - * schema: - * type: object - * properties: - * organizations: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - * - * /api/organizations/search: - * get: - * tags: [Organizations] - * summary: Search organizations - * description: Search organizations by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - * - * /api/organizations/{orgId}/login-url: - * get: - * tags: [Organizations] - * summary: Get organization login URL - * description: Get OAuth login URL for organization - * security: - * - bearerAuth: [] - * parameters: - * - name: orgId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Login URL - * content: - * application/json: - * schema: - * type: object - * properties: - * loginUrl: - * type: string - * - * /api/organizations/auth-callback: - * post: - * tags: [Organizations] - * summary: OAuth callback - * description: Handle OAuth callback from organization - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * state: - * type: string - * responses: - * 200: - * description: Authentication successful - * 400: - * description: Invalid callback data - * - * /api/decks/page/{from}/{to}: - * get: - * tags: [Decks] - * summary: Get decks by page - * description: Retrieve paginated list of decks - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - * - * /api/decks: - * post: - * tags: [Decks] - * summary: Create deck - * description: Create a new deck - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateDeckRequest' - * responses: - * 201: - * description: Deck created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * - * /api/decks/search: - * get: - * tags: [Decks] - * summary: Search decks - * description: Search decks by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - * - * /api/decks/{id}: - * get: - * tags: [Decks] - * summary: Get deck by ID - * description: Retrieve a specific deck by ID - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - * put: - * tags: [Decks] - * summary: Update deck - * description: Update an existing deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateDeckRequest' - * responses: - * 200: - * description: Deck updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * delete: - * tags: [Decks] - * summary: Delete deck - * description: Delete a deck (soft delete) - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Deck deleted successfully - * 404: - * description: Deck not found - * - * /api/chats/user-chats: - * get: - * tags: [Chats] - * summary: Get user chats - * description: Get all chats for the current user - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User chats - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * - * /api/chats/history/{chatId}: - * get: - * tags: [Chats] - * summary: Get chat history - * description: Get message history for a chat - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: integer - * - name: limit - * in: query - * schema: - * type: integer - * responses: - * 200: - * description: Chat history - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - * - * /api/chats/create: - * post: - * tags: [Chats] - * summary: Create chat - * description: Create a new chat room - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - gameId - * properties: - * name: - * type: string - * gameId: - * type: string - * password: - * type: string - * nullable: true - * responses: - * 201: - * description: Chat created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - * - * /api/chats/message: - * post: - * tags: [Chats] - * summary: Send message - * description: Send a message to a chat - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - chatId - * - message - * properties: - * chatId: - * type: string - * message: - * type: string - * responses: - * 201: - * description: Message sent successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ChatMessage' - * - * /api/chats/archive/{chatId}: - * post: - * tags: [Chats] - * summary: Archive chat - * description: Archive a chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat archived successfully - * 404: - * description: Chat not found - * - * /api/chats/restore/{chatId}: - * post: - * tags: [Chats] - * summary: Restore chat - * description: Restore an archived chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat restored successfully - * 404: - * description: Chat not found - * - * /api/chats/archived/game/{gameId}: - * get: - * tags: [Chats] - * summary: Get archived chats by game - * description: Get all archived chats for a specific game - * security: - * - bearerAuth: [] - * parameters: - * - name: gameId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Archived chats - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * - * /api/deck-import-export/export/{deckId}: - * get: - * tags: [Import/Export] - * summary: Export deck - * description: Export a deck as JSON or .spr file - * security: - * - bearerAuth: [] - * parameters: - * - name: deckId - * in: path - * required: true - * schema: - * type: string - * - name: format - * in: query - * schema: - * type: string - * enum: [json, spr] - * default: json - * responses: - * 200: - * description: Deck exported successfully - * content: - * application/json: - * schema: - * type: object - * application/octet-stream: - * schema: - * type: string - * format: binary - * - * /api/deck-import-export/import: - * post: - * tags: [Import/Export] - * summary: Import deck - * description: Import a deck from JSON or .spr file - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * file: - * type: string - * format: binary - * responses: - * 201: - * description: Deck imported successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * - * /api/admin/users/page/{from}/{to}: - * get: - * tags: [Admin - Users] - * summary: Get users by page (Admin) - * description: Admin endpoint to retrieve paginated list of users - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated users - * content: - * application/json: - * schema: - * type: object - * properties: - * users: - * type: array - * items: - * $ref: '#/components/schemas/User' - * totalCount: - * type: integer - * - * /api/admin/users/{userId}: - * get: - * tags: [Admin - Users] - * summary: Get user by ID (Admin) - * description: Admin endpoint to get specific user details - * security: - * - bearerAuth: [] - * parameters: - * - name: userId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * delete: - * tags: [Admin - Users] - * summary: Delete user (Admin) - * description: Admin endpoint to delete a user - * security: - * - bearerAuth: [] - * parameters: - * - name: userId - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: User deleted successfully - * - * /api/admin/users/search/{searchTerm}: - * get: - * tags: [Admin - Users] - * summary: Search users (Admin) - * description: Admin endpoint to search users - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - * - * /api/admin/users/{userId}/deactivate: - * post: - * tags: [Admin - Users] - * summary: Deactivate user (Admin) - * description: Admin endpoint to deactivate a user - * security: - * - bearerAuth: [] - * parameters: - * - name: userId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User deactivated successfully - * - * /api/admin/decks/page/{from}/{to}: - * get: - * tags: [Admin - Decks] - * summary: Get decks by page (Admin) - * description: Admin endpoint to retrieve paginated list of decks - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - * - * /api/admin/decks/{id}: - * get: - * tags: [Admin - Decks] - * summary: Get deck by ID (Admin) - * description: Admin endpoint to get specific deck details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * - * /api/admin/decks/search/{searchTerm}: - * get: - * tags: [Admin - Decks] - * summary: Search decks (Admin) - * description: Admin endpoint to search decks - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * - * /api/admin/decks/{id}/hard: - * delete: - * tags: [Admin - Decks] - * summary: Hard delete deck (Admin) - * description: Admin endpoint to permanently delete a deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Deck permanently deleted - * - * /api/admin/organizations: - * post: - * tags: [Admin - Organizations] - * summary: Create organization (Admin) - * description: Admin endpoint to create a new organization - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * properties: - * name: - * type: string - * description: - * type: string - * responses: - * 201: - * description: Organization created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * - * /api/admin/organizations/page/{from}/{to}: - * get: - * tags: [Admin - Organizations] - * summary: Get organizations by page (Admin) - * description: Admin endpoint to retrieve paginated list of organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated organizations - * content: - * application/json: - * schema: - * type: object - * properties: - * organizations: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - * - * /api/admin/organizations/{id}: - * get: - * tags: [Admin - Organizations] - * summary: Get organization by ID (Admin) - * description: Admin endpoint to get specific organization details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * delete: - * tags: [Admin - Organizations] - * summary: Delete organization (Admin) - * description: Admin endpoint to soft delete an organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Organization deleted successfully - * - * /api/admin/organizations/search/{searchTerm}: - * get: - * tags: [Admin - Organizations] - * summary: Search organizations (Admin) - * description: Admin endpoint to search organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * - * /api/admin/organizations/{id}/hard: - * delete: - * tags: [Admin - Organizations] - * summary: Hard delete organization (Admin) - * description: Admin endpoint to permanently delete an organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Organization permanently deleted - * - * /api/admin/chats/page/{from}/{to}: - * get: - * tags: [Admin - Chats] - * summary: Get chats by page (Admin) - * description: Admin endpoint to retrieve paginated list of chats - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated chats - * content: - * application/json: - * schema: - * type: object - * properties: - * chats: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * totalCount: - * type: integer - * - * /api/admin/chats/{id}: - * get: - * tags: [Admin - Chats] - * summary: Get chat by ID (Admin) - * description: Admin endpoint to get specific chat details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - * - * /api/admin/contacts/page/{from}/{to}: - * get: - * tags: [Admin - Contacts] - * summary: Get contacts by page (Admin) - * description: Admin endpoint to retrieve paginated list of contacts - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated contacts - * content: - * application/json: - * schema: - * type: object - * properties: - * contacts: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - * totalCount: - * type: integer - * - * /api/admin/contacts/{id}: - * get: - * tags: [Admin - Contacts] - * summary: Get contact by ID (Admin) - * description: Admin endpoint to get specific contact details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Contact details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - * - * /api/admin/contacts/search/{searchTerm}: - * get: - * tags: [Admin - Contacts] - * summary: Search contacts (Admin) - * description: Admin endpoint to search contacts - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * - name: includeDeleted - * in: query - * required: true - * schema: - * type: boolean - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - * - * /api/contacts: - * post: - * tags: [Contacts] - * summary: Create contact - * description: Create a new contact message - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateContactRequest' - * responses: - * 201: - * description: Contact created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * - * /api/games/start: - * post: - * summary: Start a new game - * tags: [Games] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - deckids - * - maxplayers - * - logintype - * properties: - * deckids: - * type: array - * items: - * type: string - * description: Array of deck IDs (must include all 3 types LUCK, JOKER, QUESTION) - * maxplayers: - * type: integer - * minimum: 2 - * maximum: 8 - * description: Maximum number of players allowed in the game - * logintype: - * type: integer - * enum: [0, 1, 2] - * description: How players can join (PUBLIC=0, PRIVATE=1, ORGANIZATION=2) - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * type: string - * description: Game UUID - * gamecode: - * type: string - * description: 6-character game code for joining - * maxplayers: - * type: integer - * logintype: - * type: integer - * gamedecks: - * type: array - * description: Shuffled game decks - * players: - * type: array - * items: - * type: string - * started: - * type: boolean - * finished: - * type: boolean - * state: - * type: integer - * description: Game state (WAITING=0, ACTIVE=1, FINISHED=2, CANCELLED=3) - * createdate: - * type: string - * format: date-time - * 400: - * description: Invalid input parameters - * 401: - * description: Authentication required - * 500: - * description: Internal server error - * - * /api/games/join: - * post: - * summary: Join a game (automatically detects game type) - * description: Join any game by providing the game code. The system automatically determines if authentication is required based on the game type. - * tags: [Games] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - gameCode - * properties: - * gameCode: - * type: string - * description: 6-character game code - * example: "ABC123" - * playerName: - * type: string - * description: Display name for the player (required for public games, optional for authenticated games) - * example: "John Doe" - * responses: - * 200: - * description: Successfully joined the game - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 400: - * description: Invalid input or missing required fields - * 401: - * description: Authentication required for this game type - * 403: - * description: Organization membership required - * 404: - * description: Game not found - * 409: - * description: Game is full or not accepting players - * 500: - * description: Internal server error - * - * /api/games/{gameId}/start: - * post: - * summary: Start gameplay for an existing game - * description: Initialize gameplay by setting all player positions to 0 and assigning random turn order. This is separate from game creation. - * tags: [Games] - * parameters: - * - in: path - * name: gameId - * required: true - * schema: - * type: string - * description: The ID of the game to start - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Game started successfully" - * gameId: - * type: string - * example: "game123" - * playerCount: - * type: number - * example: 4 - * 400: - * description: Invalid input or game cannot be started - * 401: - * description: Authentication required - * 403: - * description: Only game master can start the game - * 404: - * description: Game not found - * 409: - * description: Game already started or not ready to start - * 500: - * description: Internal server error -<<<<<<< HEAD -======= ->>>>>>> origin/main -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - */ - -export {}; diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitionsFixed.ts b/SerpentRace_Backend/src/Api/swagger/swaggerDefinitionsFixed.ts deleted file mode 100644 index 1f019805..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitionsFixed.ts +++ /dev/null @@ -1,2788 +0,0 @@ -/** - * @swagger - * components: - * schemas: - * User: - * type: object - * properties: - * id: - * type: string - * format: uuid - * username: - * type: string - * email: - * type: string - * format: email - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * nullable: true - * type: - * type: string - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * orgid: - * type: string - * nullable: true - * - * CreateUserRequest: - * type: object - * required: - * - username - * - email - * - password - * - fname - * - lname - * - type - * properties: - * username: - * type: string - * email: - * type: string - * format: email - * password: - * type: string - * format: password - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * type: - * type: string - * - * LoginRequest: - * type: object - * required: - * - username - * - password - * properties: - * username: - * type: string - * password: - * type: string - * format: password - * - * LoginResponse: - * type: object - * properties: - * token: - * type: string - * user: - * $ref: '#/components/schemas/User' - * requiresOrgReauth: - * type: boolean - * orgLoginUrl: - * type: string - * organizationName: - * type: string - * - * UpdateProfileRequest: - * type: object - * properties: - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * email: - * type: string - * format: email - * - * ForgotPasswordRequest: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * format: email - * - * ResetPasswordRequest: - * type: object - * required: - * - token - * - newPassword - * properties: - * token: - * type: string - * newPassword: - * type: string - * format: password - * minLength: 6 - * maxLength: 100 - * - * AuthSuccessResponse: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * - * Organization: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * contactfname: - * type: string - * contactlname: - * type: string - * contactphone: - * type: string - * contactemail: - * type: string - * format: email - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * url: - * type: string - * nullable: true - * userinorg: - * type: integer - * maxOrganizationalDecks: - * type: integer - * - * Deck: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * enum: [0, 1, 2, 3] - * description: 0=JOKER, 1=LUCK, 2=QUESTION, 3=GENERAL - * userid: - * type: string - * format: uuid - * creationdate: - * type: string - * format: date-time - * cards: - * type: array - * items: - * type: object - * playedNumber: - * type: integer - * ctype: - * type: integer - * enum: [0, 1, 2] - * description: 0=PUBLIC, 1=ORGANIZATIONAL, 2=PRIVATE - * updatedate: - * type: string - * format: date-time - * state: - * type: integer - * enum: [0, 1, 2] - * description: 0=ACTIVE, 1=INACTIVE, 2=SOFT_DELETE - * organization: - * $ref: '#/components/schemas/Organization' - * nullable: true - * - * CreateDeckRequest: - * type: object - * required: - * - name - * - type - * - cards - * properties: - * name: - * type: string - * type: - * type: integer - * cards: - * type: array - * items: - * type: object - * ctype: - * type: integer - * - * Chat: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * participants: - * type: array - * items: - * type: string - * creatorId: - * type: string - * gameId: - * type: string - * nullable: true - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * state: - * type: integer - * - * ChatMessage: - * type: object - * properties: - * id: - * type: string - * format: uuid - * senderId: - * type: string - * senderName: - * type: string - * message: - * type: string - * timestamp: - * type: string - * format: date-time - * chatId: - * type: string - * - * Contact: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * email: - * type: string - * format: email - * userid: - * type: string - * format: uuid - * nullable: true - * type: - * type: integer - * enum: [0, 1, 2] - * description: 0=QUESTION, 1=BUG_REPORT, 2=SUGGESTION - * txt: - * type: string - * state: - * type: integer - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * adminResponse: - * type: string - * nullable: true - * responseDate: - * type: string - * format: date-time - * nullable: true - * respondedBy: - * type: string - * nullable: true - * - * CreateContactRequest: - * type: object - * required: - * - name - * - email - * - type - * - txt - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * txt: - * type: string - * - * Game: - * type: object - * properties: - * id: - * type: string - * format: uuid - * gamecode: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: integer - * gamedecks: - * type: array - * players: - * type: array - * items: - * type: string - * started: - * type: boolean - * finished: - * type: boolean - * state: - * type: integer - * createdate: - * type: string - * format: date-time - * - * Error: - * type: object - * properties: - * error: - * type: string - * timestamp: - * type: string - * format: date-time - * details: - * type: string - */ - -/** - * @swagger - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * description: Authenticate user and return JWT token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginRequest' - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - */ - -/** - * @swagger - * /api/users/create: - * post: - * tags: [Users] - * summary: Create new user - * description: Register a new user account - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateUserRequest' - * responses: - * 201: - * description: User created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * 409: - * description: User already exists - */ - -/** - * @swagger - * /api/users/profile: - * get: - * tags: [Users] - * summary: Get user profile - * description: Get current user's profile information - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User profile data - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 401: - * description: Unauthorized - * patch: - * tags: [Users] - * summary: Update user profile - * description: Update current user's profile information - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/UpdateProfileRequest' - * responses: - * 200: - * description: Profile updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * 401: - * description: Unauthorized - */ - -/** - * @swagger - * /api/users/verify-email/{token}: - * get: - * tags: [Users] - * summary: Verify email address - * description: Verify user's email address using verification token - * parameters: - * - name: token - * in: path - * required: true - * schema: - * type: string - * description: Email verification token - * responses: - * 200: - * description: Email verified successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AuthSuccessResponse' - * 400: - * description: Invalid or expired verification token - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/users/forgot-password: - * post: - * tags: [Users] - * summary: Request password reset - * description: Send password reset email to user - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ForgotPasswordRequest' - * responses: - * 200: - * description: Password reset email sent (if email exists) - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AuthSuccessResponse' - * 400: - * description: Validation error - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/users/reset-password: - * post: - * tags: [Users] - * summary: Reset password - * description: Reset user password using reset token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ResetPasswordRequest' - * responses: - * 200: - * description: Password reset successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AuthSuccessResponse' - * 400: - * description: Invalid token or password validation failed - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/organizations/search: - * get: - * tags: [Organizations] - * summary: Search organizations - * description: Search organizations by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - */ - -/** - * @swagger - * /api/organizations/{orgId}/login-url: - * get: - * tags: [Organizations] - * summary: Get organization login URL - * description: Get OAuth login URL for organization - * security: - * - bearerAuth: [] - * parameters: - * - name: orgId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Login URL - * content: - * application/json: - * schema: - * type: object - * properties: - * loginUrl: - * type: string - */ - -/** - * @swagger - * /api/organizations/auth-callback: - * post: - * tags: [Organizations] - * summary: OAuth callback - * description: Handle OAuth callback from organization - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * state: - * type: string - * responses: - * 200: - * description: Authentication successful - * 400: - * description: Invalid callback data - */ - -/** - * @swagger - * /api/decks/page/{from}/{to}: - * get: - * tags: [Decks] - * summary: Get decks by page - * description: Retrieve paginated list of decks - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - */ - -/** - * @swagger - * /api/decks/search: - * get: - * tags: [Decks] - * summary: Search decks - * description: Search decks by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - */ - -/** - * @swagger - * /api/decks/{id}: - * get: - * tags: [Decks] - * summary: Get deck by ID - * description: Retrieve a specific deck by ID - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - * put: - * tags: [Decks] - * summary: Update deck - * description: Update an existing deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateDeckRequest' - * responses: - * 200: - * description: Deck updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * delete: - * tags: [Decks] - * summary: Delete deck - * description: Delete a deck (soft delete) - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Deck deleted successfully - * 404: - * description: Deck not found - */ - -/** - * @swagger - * /api/chats/user-chats: - * get: - * tags: [Chats] - * summary: Get user chats - * description: Get all chats for the current user - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User chats - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - */ - -/** - * @swagger - * /api/chats/history/{chatId}: - * get: - * tags: [Chats] - * summary: Get chat history - * description: Get message history for a chat - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: integer - * - name: limit - * in: query - * schema: - * type: integer - * responses: - * 200: - * description: Chat history - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/chats/message: - * post: - * tags: [Chats] - * summary: Send message - * description: Send a message to a chat - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - chatId - * - message - * properties: - * chatId: - * type: string - * message: - * type: string - * responses: - * 201: - * description: Message sent successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/chats/archive/{chatId}: - * post: - * tags: [Chats] - * summary: Archive chat - * description: Archive a chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat archived successfully - * 404: - * description: Chat not found - */ - -/** - * @swagger - * /api/chats/restore/{chatId}: - * post: - * tags: [Chats] - * summary: Restore chat - * description: Restore an archived chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat restored successfully - * 404: - * description: Chat not found - */ - -/** - * @swagger - * /api/contact/create: - * post: - * tags: [Contact] - * summary: Create contact message - * description: Send a contact message to the system - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - email - * - message - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * subject: - * type: string - * nullable: true - * message: - * type: string - * responses: - * 201: - * description: Contact message sent successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/admin/users: - * get: - * tags: [Admin - Users] - * summary: Get all users (Admin) - * description: Retrieve all users in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Users retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * users: - * type: array - * items: - * $ref: '#/components/schemas/User' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/users/{id}: - * get: - * tags: [Admin - Users] - * summary: Get user by ID (Admin) - * description: Get detailed information about a specific user - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 404: - * description: User not found - * put: - * tags: [Admin - Users] - * summary: Update user (Admin) - * description: Update user information by admin - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * username: - * type: string - * email: - * type: string - * format: email - * firstName: - * type: string - * lastName: - * type: string - * isActive: - * type: boolean - * role: - * type: string - * enum: [user, admin, moderator] - * responses: - * 200: - * description: User updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * delete: - * tags: [Admin - Users] - * summary: Delete user (Admin) - * description: Delete a user from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User deleted successfully - * 404: - * description: User not found - */ - -/** - * @swagger - * /api/admin/users/{id}/ban: - * post: - * tags: [Admin - Users] - * summary: Ban user (Admin) - * description: Ban a user from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * content: - * application/json: - * schema: - * type: object - * properties: - * reason: - * type: string - * duration: - * type: string - * description: Ban duration in ISO format or 'permanent' - * responses: - * 200: - * description: User banned successfully - */ - -/** - * @swagger - * /api/admin/users/{id}/unban: - * post: - * tags: [Admin - Users] - * summary: Unban user (Admin) - * description: Remove ban from a user - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User unbanned successfully - */ - -/** - * @swagger - * /api/admin/decks: - * get: - * tags: [Admin - Decks] - * summary: Get all decks (Admin) - * description: Retrieve all decks in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * - name: status - * in: query - * schema: - * type: string - * enum: [public, private, reported] - * responses: - * 200: - * description: Decks retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/decks/{id}: - * get: - * tags: [Admin - Decks] - * summary: Get deck by ID (Admin) - * description: Get detailed information about a specific deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - * put: - * tags: [Admin - Decks] - * summary: Update deck (Admin) - * description: Update deck information by admin - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: - * type: string - * isPublic: - * type: boolean - * isActive: - * type: boolean - * responses: - * 200: - * description: Deck updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * delete: - * tags: [Admin - Decks] - * summary: Delete deck (Admin) - * description: Delete a deck from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck deleted successfully - * 404: - * description: Deck not found - */ - -/** - * @swagger - * /api/admin/decks/{id}/moderate: - * post: - * tags: [Admin - Decks] - * summary: Moderate deck (Admin) - * description: Approve or reject a deck for public visibility - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - action - * properties: - * action: - * type: string - * enum: [approve, reject, flag] - * reason: - * type: string - * responses: - * 200: - * description: Deck moderated successfully - */ - -/** - * @swagger - * /api/admin/organizations: - * get: - * tags: [Admin - Organizations] - * summary: Get all organizations (Admin) - * description: Retrieve all organizations in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Organizations retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * organizations: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/organizations/{id}: - * get: - * tags: [Admin - Organizations] - * summary: Get organization by ID (Admin) - * description: Get detailed information about a specific organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * 404: - * description: Organization not found - * put: - * tags: [Admin - Organizations] - * summary: Update organization (Admin) - * description: Update organization information by admin - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: - * type: string - * isActive: - * type: boolean - * responses: - * 200: - * description: Organization updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * delete: - * tags: [Admin - Organizations] - * summary: Delete organization (Admin) - * description: Delete an organization from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization deleted successfully - * 404: - * description: Organization not found - */ - -/** - * @swagger - * /api/admin/organizations/{id}/members: - * get: - * tags: [Admin - Organizations] - * summary: Get organization members (Admin) - * description: Get all members of an organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization members - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - */ - -/** - * @swagger - * /api/admin/chats: - * get: - * tags: [Admin - Chats] - * summary: Get all chats (Admin) - * description: Retrieve all chats in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * - name: status - * in: query - * schema: - * type: string - * enum: [active, archived, reported] - * responses: - * 200: - * description: Chats retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * chats: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/chats/{id}: - * get: - * tags: [Admin - Chats] - * summary: Get chat by ID (Admin) - * description: Get detailed information about a specific chat - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - * 404: - * description: Chat not found - * delete: - * tags: [Admin - Chats] - * summary: Delete chat (Admin) - * description: Delete a chat from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat deleted successfully - * 404: - * description: Chat not found - */ - -/** - * @swagger - * /api/admin/chats/{id}/messages: - * get: - * tags: [Admin - Chats] - * summary: Get chat messages (Admin) - * description: Get all messages in a chat - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: integer - * - name: limit - * in: query - * schema: - * type: integer - * responses: - * 200: - * description: Chat messages - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/admin/chats/{id}/moderate: - * post: - * tags: [Admin - Chats] - * summary: Moderate chat (Admin) - * description: Moderate a chat room (archive, restore, flag) - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - action - * properties: - * action: - * type: string - * enum: [archive, restore, flag, unflag] - * reason: - * type: string - * responses: - * 200: - * description: Chat moderated successfully - */ - -/** - * @swagger - * /api/admin/contacts: - * get: - * tags: [Admin - Contacts] - * summary: Get all contact messages (Admin) - * description: Retrieve all contact messages with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: status - * in: query - * schema: - * type: string - * enum: [unread, read, resolved] - * - name: search - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Contact messages retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * contacts: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/contacts/{id}: - * get: - * tags: [Admin - Contacts] - * summary: Get contact message by ID (Admin) - * description: Get detailed information about a specific contact message - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Contact message found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - * 404: - * description: Contact message not found - * put: - * tags: [Admin - Contacts] - * summary: Update contact message status (Admin) - * description: Update the status of a contact message - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * enum: [unread, read, resolved] - * adminNotes: - * type: string - * responses: - * 200: - * description: Contact message updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - * delete: - * tags: [Admin - Contacts] - * summary: Delete contact message (Admin) - * description: Delete a contact message from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Contact message deleted successfully - * 404: - * description: Contact message not found - */ - -/** - * @swagger - * /api/admin/contacts/{id}/reply: - * post: - * tags: [Admin - Contacts] - * summary: Reply to contact message (Admin) - * description: Send a reply to a contact message - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - reply - * properties: - * reply: - * type: string - * responses: - * 200: - * description: Reply sent successfully - */ - -/** - * @swagger - * /api/admin/games: - * get: - * tags: [Admin - Games] - * summary: Get all games (Admin) - * description: Retrieve all games in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: status - * in: query - * schema: - * type: string - * enum: [active, completed, abandoned] - * responses: - * 200: - * description: Games retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * games: - * type: array - * items: - * $ref: '#/components/schemas/Game' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/games/{id}: - * get: - * tags: [Admin - Games] - * summary: Get game by ID (Admin) - * description: Get detailed information about a specific game - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Game found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 404: - * description: Game not found - * delete: - * tags: [Admin - Games] - * summary: Delete game (Admin) - * description: Delete a game from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Game deleted successfully - * 404: - * description: Game not found - */ - -/** - * @swagger - * /api/admin/system/stats: - * get: - * tags: [Admin - System] - * summary: Get system statistics (Admin) - * description: Get comprehensive system statistics - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: System statistics - * content: - * application/json: - * schema: - * type: object - * properties: - * uptime: - * type: string - * version: - * type: string - * memoryUsage: - * type: object - * activeConnections: - * type: integer - * databaseHealth: - * type: string - * redisHealth: - * type: string - */ - -/** - * @swagger - * /api/admin/system/logs: - * get: - * tags: [Admin - System] - * summary: Get system logs (Admin) - * description: Retrieve system logs with filtering - * security: - * - bearerAuth: [] - * parameters: - * - name: level - * in: query - * schema: - * type: string - * enum: [error, warn, info, debug] - * - name: limit - * in: query - * schema: - * type: integer - * default: 100 - * - name: since - * in: query - * schema: - * type: string - * format: date-time - * responses: - * 200: - * description: System logs - * content: - * application/json: - * schema: - * type: array - * items: - * type: object - * properties: - * timestamp: - * type: string - * level: - * type: string - * message: - * type: string - * metadata: - * type: object - */ - -/** - * @swagger - * /api/health: - * get: - * tags: [System] - * summary: Health check - * description: Check the health status of the API - * responses: - * 200: - * description: Service is healthy - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "OK" - * timestamp: - * type: string - * format: date-time - * version: - * type: string - */ - -/** - * @swagger - * components: - * securitySchemes: - * bearerAuth: - * type: http - * scheme: bearer - * schemas: - * User: - * type: object - * properties: - * id: - * type: string - * format: uuid - * username: - * type: string - * email: - * type: string - * format: email - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * nullable: true - * type: - * type: string - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * orgid: - * type: string - * nullable: true - * Deck: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * userid: - * type: string - * format: uuid - * creationdate: - * type: string - * format: date-time - * cards: - * type: array - * items: - * type: object - * Organization: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * description: - * type: string - * members: - * type: array - * items: - * $ref: '#/components/schemas/User' - * Contact: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * txt: - * type: string - * userid: - * type: string - * nullable: true - * createdate: - * type: string - * format: date-time - * Chat: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * gameId: - * type: string - * format: uuid - * messages: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - * ChatMessage: - * type: object - * properties: - * id: - * type: string - * format: uuid - * chatId: - * type: string - * format: uuid - * senderId: - * type: string - * format: uuid - * message: - * type: string - * timestamp: - * type: string - * format: date-time - * Game: - * type: object - * properties: - * id: - * type: string - * format: uuid - * deckids: - * type: array - * items: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: string - * state: - * type: string - * createdate: - * type: string - * format: date-time - */ - -/** - * @swagger - * tags: - * - name: Users - * - name: Decks - * - name: Organizations - * - name: Contact - * - name: Chats - * - name: Games - * - name: Admin - Users - * - name: Admin - Decks - * - name: Admin - Organizations - * - name: Admin - Contacts - * - name: Admin - Chats - * - name: Admin - Games - * - name: System - */ - -// User endpoints -/** - * @swagger - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [username, password] - * properties: - * username: - * type: string - * password: - * type: string - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * type: object - * properties: - * token: - * type: string - */ - -/** - * @swagger - * /api/users/create: - * post: - * tags: [Users] - * summary: Create user - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [username, email, password] - * properties: - * username: - * type: string - * email: - * type: string - * password: - * type: string - * responses: - * 201: - * description: User created - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - */ - -// ...existing code... -/** - * @swagger - * /api/decks/page/{from}/{to}: - * get: - * tags: [Decks] - * summary: Get decks with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/decks/search: - * get: - * tags: [Decks] - * summary: Search decks - * security: - * - bearerAuth: [] - * parameters: - * - name: q - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/decks/{id}: - * get: - * tags: [Decks] - * summary: Get deck by ID - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - */ - -/** - * @swagger - * /api/organizations/page/{from}/{to}: - * get: - * tags: [Organizations] - * summary: Get organizations with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated organizations - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - */ - -/** - * @swagger - * /api/organizations/search: - * get: - * tags: [Organizations] - * summary: Search organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: q - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - */ - -/** - * @swagger - * /api/contact/create: - * post: - * tags: [Contact] - * summary: Create contact message - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [name, email, txt] - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * txt: - * type: string - * responses: - * 201: - * description: Contact created - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/chats/create: - * post: - * tags: [Chats] - * summary: Create chat - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [name, gameId] - * properties: - * name: - * type: string - * gameId: - * type: string - * responses: - * 201: - * description: Chat created - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - */ - -/** - * @swagger - * /api/chats/history/{chatId}: - * get: - * tags: [Chats] - * summary: Get chat history - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat history - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/games/start: - * post: - * tags: [Games] - * summary: Start a new game - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [deckids, maxplayers, logintype] - * properties: - * deckids: - * type: array - * items: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: string - * responses: - * 201: - * description: Game started - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - */ - -/** - * @swagger - * /api/games/join: - * post: - * tags: [Games] - * summary: Join a game - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [gameId] - * properties: - * gameId: - * type: string - * responses: - * 200: - * description: Joined game - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - */ - -// Admin endpoints (examples) -/** - * @swagger - * /api/admin/users: - * get: - * tags: [Admin - Users] - * summary: Get all users (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Users retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - */ - -/** - * @swagger - * /api/admin/decks: - * get: - * tags: [Admin - Decks] - * summary: Get all decks (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Decks retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/admin/organizations: - * get: - * tags: [Admin - Organizations] - * summary: Get all organizations (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Organizations retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - */ - -/** - * @swagger - * /api/admin/contacts: - * get: - * tags: [Admin - Contacts] - * summary: Get all contact messages (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Contacts retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/admin/chats: - * get: - * tags: [Admin - Chats] - * summary: Get all chats (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Chats retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - */ - -/** - * @swagger - * /api/admin/games: - * get: - * tags: [Admin - Games] - * summary: Get all games (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Games retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Game' - */ - -/** - * @swagger - * /api/contacts: - * post: - * tags: [Contacts] - * summary: Create contact - * description: Create a new contact message - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - email - * - type - * - txt - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * enum: [0, 1, 2] - * description: 0=QUESTION, 1=BUG_REPORT, 2=SUGGESTION - * txt: - * type: string - * responses: - * 201: - * description: Contact created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/deck-import-export/export/{deckId}: - * get: - * tags: [Deck Import/Export] - * summary: Export deck - * description: Export a deck as JSON or .spr file - * security: - * - bearerAuth: [] - * parameters: - * - name: deckId - * in: path - * required: true - * schema: - * type: string - * - name: format - * in: query - * schema: - * type: string - * enum: [json, spr] - * default: json - * responses: - * 200: - * description: Deck exported successfully - * content: - * application/json: - * schema: - * type: object - * application/octet-stream: - * schema: - * type: string - * format: binary - */ - -/** - * @swagger - * /api/deck-import-export/import: - * post: - * tags: [Deck Import/Export] - * summary: Import deck - * description: Import a deck from JSON or .spr file - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * file: - * type: string - * format: binary - * responses: - * 201: - * description: Deck imported successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/games/start: - * post: - * summary: Start a new game - * tags: [Games] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - deckids - * - maxplayers - * - logintype - * properties: - * deckids: - * type: array - * items: - * type: string - * description: Array of deck IDs (must include all 3 types LUCK, JOKER, QUESTION) - * maxplayers: - * type: integer - * minimum: 2 - * maximum: 8 - * description: Maximum number of players allowed in the game - * logintype: - * type: integer - * enum: [0, 1, 2] - * description: How players can join (PUBLIC=0, PRIVATE=1, ORGANIZATION=2) - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 400: - * description: Invalid input parameters - * 401: - * description: Authentication required - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/games/join: - * post: - * summary: Join a game (automatically detects game type) - * description: Join any game by providing the game code. The system automatically determines if authentication is required based on the game type. - * tags: [Games] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - gameCode - * properties: - * gameCode: - * type: string - * description: 6-character game code - * example: "ABC123" - * playerName: - * type: string - * description: Display name for the player (required for public games, optional for authenticated games) - * example: "John Doe" - * responses: - * 200: - * description: Successfully joined the game - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 400: - * description: Invalid input or missing required fields - * 401: - * description: Authentication required for this game type - * 403: - * description: Organization membership required - * 404: - * description: Game not found - * 409: - * description: Game is full or not accepting players - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/games/{gameId}/start: - * post: - * summary: Start gameplay for an existing game - * description: Initialize gameplay by setting all player positions to 0 and assigning random turn order. This is separate from game creation. - * tags: [Games] - * parameters: - * - in: path - * name: gameId - * required: true - * schema: - * type: string - * description: The ID of the game to start - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Game started successfully" - * gameId: - * type: string - * example: "game123" - * playerCount: - * type: number - * example: 4 - * 400: - * description: Invalid input or game cannot be started - * 401: - * description: Authentication required - * 403: - * description: Only game master can start the game - * 404: - * description: Game not found - * 409: - * description: Game already started or not ready to start - * 500: - * description: Internal server error - */ - -export {}; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerUiSetup.ts b/SerpentRace_Backend/src/Api/swagger/swaggerUiSetup.ts deleted file mode 100644 index f474fe87..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerUiSetup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import express from 'express'; -import swaggerUi from 'swagger-ui-express'; -import { swaggerSpec } from './swaggerConfig'; - -export function setupSwagger(app: express.Application) { - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/ChatArchiveCommandHandlers.ts b/SerpentRace_Backend/src/Application/Chat/commands/ChatArchiveCommandHandlers.ts deleted file mode 100644 index c9456564..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/ChatArchiveCommandHandlers.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ArchiveChatCommand, RestoreChatCommand } from './ChatCommands'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { ChatType } from '../../../Domain/Chat/ChatAggregate'; -import { logAuth, logError, logWarning } from '../../Services/Logger'; - -export class ArchiveChatCommandHandler { - constructor(private chatRepository: IChatRepository) {} - - async execute(command: ArchiveChatCommand): Promise { - try { - const chat = await this.chatRepository.findById(command.chatId); - if (!chat) { - throw new Error('Chat not found'); - } - - await this.chatRepository.archiveChat(chat); - - logAuth('Chat archived manually', undefined, { - chatId: command.chatId, - chatType: chat.type, - messageCount: chat.messages.length - }); - - return true; - - } catch (error) { - logError('ArchiveChatCommandHandler error', error as Error); - return false; - } - } -} - -export class RestoreChatCommandHandler { - constructor(private chatRepository: IChatRepository) {} - - async execute(command: RestoreChatCommand): Promise { - try { - const archive = await this.chatRepository.getArchivedChat(command.chatId); - if (!archive) { - throw new Error('Archived chat not found'); - } - - // Game chats cannot be restored, only viewed - if (archive.chatType === ChatType.GAME) { - logWarning('Attempt to restore game chat blocked', { - chatId: command.chatId, - chatType: archive.chatType - }); - return false; - } - - const restoredChat = await this.chatRepository.restoreFromArchive(command.chatId); - if (!restoredChat) { - throw new Error('Failed to restore chat from archive'); - } - - logAuth('Chat restored from archive', undefined, { - chatId: command.chatId, - messageCount: archive.archivedMessages.length - }); - - return true; - - } catch (error) { - logError('RestoreChatCommandHandler error', error as Error); - return false; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/ChatCommands.ts b/SerpentRace_Backend/src/Application/Chat/commands/ChatCommands.ts deleted file mode 100644 index 41d2bb47..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/ChatCommands.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface CreateChatCommand { - type: 'direct' | 'group' | 'game'; - name?: string; - gameId?: string; - createdBy: string; - userIds: string[]; -} - -export interface SendMessageCommand { - chatId: string; - userId: string; - message: string; -} - -export interface ArchiveChatCommand { - chatId: string; -} - -export interface RestoreChatCommand { - chatId: string; -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/CreateChatCommandHandler.ts b/SerpentRace_Backend/src/Application/Chat/commands/CreateChatCommandHandler.ts deleted file mode 100644 index d007c28b..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/CreateChatCommandHandler.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CreateChatCommand } from './ChatCommands'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { ChatType, ChatAggregate } from '../../../Domain/Chat/ChatAggregate'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { logAuth, logError } from '../../Services/Logger'; - -export class CreateChatCommandHandler { - constructor( - private chatRepository: IChatRepository, - private userRepository: IUserRepository - ) {} - - async execute(command: CreateChatCommand): Promise { - try { - // Validate creator exists - const creator = await this.userRepository.findById(command.createdBy); - if (!creator) { - throw new Error('Creator not found'); - } - - // For group chats, check if creator is premium - if (command.type === 'group' && creator.state !== UserState.VERIFIED_PREMIUM) { - throw new Error('Premium subscription required to create groups'); - } - - // Validate all target users exist - const targetUsers = await Promise.all( - command.userIds.map(id => this.userRepository.findById(id)) - ); - - if (targetUsers.some(user => !user)) { - throw new Error('One or more target users not found'); - } - - // For direct chats, check if already exists - if (command.type === 'direct' && command.userIds.length === 1) { - const existingChats = await this.chatRepository.findByUserId(command.createdBy); - const existingDirectChat = existingChats.find(chat => - chat.type === ChatType.DIRECT && - chat.users.length === 2 && - chat.users.includes(command.userIds[0]) - ); - - if (existingDirectChat) { - return existingDirectChat; - } - } - - // For game chats, check if already exists - if (command.type === 'game' && command.gameId) { - const existingGameChat = await this.chatRepository.findByGameId(command.gameId); - if (existingGameChat) { - return existingGameChat; - } - } - - // Create chat - const chatData: Partial = { - type: command.type as any, - name: command.name, - gameId: command.gameId, - createdBy: command.createdBy, - users: [command.createdBy, ...command.userIds], - messages: [], - lastActivity: new Date() - }; - - const chat = await this.chatRepository.create(chatData); - - logAuth('Chat created successfully', command.createdBy, { - chatId: chat.id, - chatType: command.type, - participantCount: chat.users.length, - gameId: command.gameId - }); - - return chat; - - } catch (error) { - logError('CreateChatCommandHandler error', error as Error); - return null; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/SendMessageCommandHandler.ts b/SerpentRace_Backend/src/Application/Chat/commands/SendMessageCommandHandler.ts deleted file mode 100644 index 8bd6903a..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/SendMessageCommandHandler.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { SendMessageCommand } from './ChatCommands'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { Message } from '../../../Domain/Chat/ChatAggregate'; -import { logAuth, logError } from '../../Services/Logger'; -import { v4 as uuidv4 } from 'uuid'; - -export class SendMessageCommandHandler { - constructor(private chatRepository: IChatRepository) {} - - async execute(command: SendMessageCommand): Promise { - try { - // Validate message is non-empty string - if (typeof command.message !== 'string' || !command.message.trim()) { - throw new Error('Message must be a non-empty string'); - } - - const chat = await this.chatRepository.findById(command.chatId); - if (!chat) { - throw new Error('Chat not found'); - } - - // Check if user is member of this chat - if (!chat.users.includes(command.userId)) { - throw new Error('User is not a member of this chat'); - } - - // Create message - const message: Message = { - id: uuidv4(), - date: new Date(), - userid: command.userId, - text: command.message.trim() - }; - - // Manage message history (keep last 10 per user, up to 2 weeks) - let updatedMessages = [...chat.messages, message]; - updatedMessages = this.pruneMessages(updatedMessages); - - // Update chat - await this.chatRepository.update(command.chatId, { - messages: updatedMessages, - lastActivity: new Date() - }); - - logAuth('Message sent successfully', command.userId, { - chatId: command.chatId, - messageLength: command.message.length, - totalMessages: updatedMessages.length - }); - - return message; - - } catch (error) { - logError('SendMessageCommandHandler error', error as Error); - return null; - } - } - - private pruneMessages(messages: Message[]): Message[] { - const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); - - // Remove messages older than 2 weeks - let prunedMessages = messages.filter(msg => new Date(msg.date) > twoWeeksAgo); - - // Group by user and keep last 10 messages per user - const messagesByUser = new Map(); - prunedMessages.forEach(msg => { - if (!messagesByUser.has(msg.userid)) { - messagesByUser.set(msg.userid, []); - } - messagesByUser.get(msg.userid)!.push(msg); - }); - - // Keep only last 10 messages per user - const finalMessages: Message[] = []; - messagesByUser.forEach((userMessages, userId) => { - const last10 = userMessages.slice(-10); - finalMessages.push(...last10); - }); - - // Sort by date - return finalMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/SoftDeleteCommandHandlers.ts b/SerpentRace_Backend/src/Application/Chat/commands/SoftDeleteCommandHandlers.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/SerpentRace_Backend/src/Application/Chat/queries/ChatHistoryQueryHandlers.ts b/SerpentRace_Backend/src/Application/Chat/queries/ChatHistoryQueryHandlers.ts deleted file mode 100644 index 5f215037..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/ChatHistoryQueryHandlers.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { GetChatHistoryQuery, GetArchivedChatsQuery } from './ChatQueries'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { IChatArchiveRepository } from '../../../Domain/IRepository/IChatArchiveRepository'; -import { Message } from '../../../Domain/Chat/ChatAggregate'; -import { logAuth, logError, logWarning } from '../../Services/Logger'; - -interface ChatHistoryResult { - chatId: string; - messages: Message[]; - isArchived: boolean; - chatInfo: { - type: string; - name: string | null; - gameId: string | null; - users: string[]; - }; -} - -export class GetChatHistoryQueryHandler { - constructor( - private chatRepository: IChatRepository, - private chatArchiveRepository: IChatArchiveRepository - ) {} - - async execute(query: GetChatHistoryQuery): Promise { - try { - // First try to find active chat - const chat = await this.chatRepository.findById(query.chatId); - - if (chat) { - // Check authorization - if (!chat.users.includes(query.userId)) { - logWarning('Unauthorized chat history access attempt', { - chatId: query.chatId, - userId: query.userId - }); - return null; - } - - logAuth('Chat history retrieved', query.userId, { - chatId: query.chatId, - messageCount: chat.messages.length, - isArchived: false - }); - - return { - chatId: query.chatId, - messages: chat.messages, - isArchived: false, - chatInfo: { - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users - } - }; - } - - // Try to find in archives - const archives = await this.chatArchiveRepository.findByChatId(query.chatId); - const userArchive = archives.find(archive => - archive.participants.includes(query.userId) - ); - - if (userArchive) { - logAuth('Archived chat history retrieved', query.userId, { - chatId: query.chatId, - messageCount: userArchive.archivedMessages.length, - isArchived: true - }); - - return { - chatId: query.chatId, - messages: userArchive.archivedMessages, - isArchived: true, - chatInfo: { - type: userArchive.chatType, - name: userArchive.chatName, - gameId: userArchive.gameId, - users: userArchive.participants - } - }; - } - - logWarning('Chat history not found', { - chatId: query.chatId, - userId: query.userId - }); - - return null; - - } catch (error) { - logError('GetChatHistoryQueryHandler error', error as Error); - return null; - } - } -} - -export class GetArchivedChatsQueryHandler { - constructor(private chatArchiveRepository: IChatArchiveRepository) {} - - async execute(query: GetArchivedChatsQuery): Promise { - try { - let archives: any[] = []; - - if (query.gameId) { - // Get archived game chats - archives = await this.chatArchiveRepository.findByGameId(query.gameId); - } else { - // Get all archived chats for user (would need different query) - // For now, return empty - this would need a new repository method - archives = []; - } - - const result = archives - .filter(archive => archive.participants.includes(query.userId)) - .map(archive => ({ - chatId: archive.chatId, - messages: archive.archivedMessages, - isArchived: true, - chatInfo: { - type: archive.chatType, - name: archive.chatName, - gameId: archive.gameId, - users: archive.participants - } - })); - - logAuth('Archived chats retrieved', query.userId, { - count: result.length, - gameId: query.gameId - }); - - return result; - - } catch (error) { - logError('GetArchivedChatsQueryHandler error', error as Error); - return []; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/ChatQueries.ts b/SerpentRace_Backend/src/Application/Chat/queries/ChatQueries.ts deleted file mode 100644 index 69e0b36c..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/ChatQueries.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface GetUserChatsQuery { - userId: string; - includeArchived?: boolean; -} - -export interface GetChatHistoryQuery { - chatId: string; - userId: string; // For authorization -} - -export interface GetArchivedChatsQuery { - userId: string; - gameId?: string; -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQuery.ts b/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQuery.ts deleted file mode 100644 index 18fc595d..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetChatsByPageQuery { - from: number; - to: number; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQueryHandler.ts deleted file mode 100644 index fbafa33c..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQueryHandler.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { GetChatsByPageQuery } from './GetChatsByPageQuery'; -import { ShortChatDto } from '../../DTOs/ChatDto'; -import { ChatMapper } from '../../DTOs/Mappers/ChatMapper'; -import { logRequest, logError } from '../../Services/Logger'; - -export class GetChatsByPageQueryHandler { - constructor(private readonly chatRepo: IChatRepository) {} - - async execute(query: GetChatsByPageQuery): Promise<{ chats: ShortChatDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - logRequest('Get chats by page query started', undefined, undefined, { - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - const result = query.includeDeleted - ? await this.chatRepo.findByPageIncludingDeleted(query.from, query.to) - : await this.chatRepo.findByPage(query.from, query.to); - - logRequest('Get chats by page query completed', undefined, undefined, { - from: query.from, - to: query.to, - returned: result.chats.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - chats: ChatMapper.toShortDtoList(result.chats), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetChatsByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve chats page'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/GetUserChatsQueryHandler.ts b/SerpentRace_Backend/src/Application/Chat/queries/GetUserChatsQueryHandler.ts deleted file mode 100644 index abb6fad0..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/GetUserChatsQueryHandler.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { GetUserChatsQuery } from './ChatQueries'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { IChatArchiveRepository } from '../../../Domain/IRepository/IChatArchiveRepository'; -import { ChatAggregate } from '../../../Domain/Chat/ChatAggregate'; -import { ChatArchiveAggregate } from '../../../Domain/Chat/ChatArchiveAggregate'; -import { logAuth, logError } from '../../Services/Logger'; - -interface ChatWithMetadata { - id: string; - type: string; - name: string | null; - gameId: string | null; - users: string[]; - lastActivity: Date | null; - isArchived: boolean; - messageCount: number; - unreadCount?: number; -} - -export class GetUserChatsQueryHandler { - constructor( - private chatRepository: IChatRepository, - private chatArchiveRepository: IChatArchiveRepository - ) {} - - async execute(query: GetUserChatsQuery): Promise { - try { - const result: ChatWithMetadata[] = []; - - // Get active chats - const activeChats = await this.chatRepository.findActiveChatsForUser(query.userId); - result.push(...activeChats.map(chat => ({ - id: chat.id, - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users, - lastActivity: chat.lastActivity, - isArchived: false, - messageCount: chat.messages.length, - unreadCount: this.calculateUnreadMessages(chat, query.userId) - }))); - - // Get archived chats if requested - if (query.includeArchived) { - const userActiveChats = await this.chatRepository.findByUserId(query.userId); - const archivedChatIds = userActiveChats - .filter(chat => chat.archiveDate !== null) - .map(chat => chat.id); - - const archives = await Promise.all( - archivedChatIds.map(id => this.chatArchiveRepository.findByChatId(id)) - ); - - archives.forEach(archiveArray => { - archiveArray.forEach(archive => { - if (archive.participants.includes(query.userId)) { - result.push({ - id: archive.chatId, - type: archive.chatType, - name: archive.chatName, - gameId: archive.gameId, - users: archive.participants, - lastActivity: archive.archivedAt, - isArchived: true, - messageCount: archive.archivedMessages.length, - unreadCount: 0 // Archived chats have no unread messages - }); - } - }); - }); - } - - logAuth('User chats retrieved', query.userId, { - activeCount: activeChats.length, - totalCount: result.length, - includeArchived: query.includeArchived - }); - - return result.sort((a, b) => { - if (!a.lastActivity) return 1; - if (!b.lastActivity) return -1; - return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime(); - }); - - } catch (error) { - logError('GetUserChatsQueryHandler error', error as Error); - return []; - } - } - - private calculateUnreadMessages(chat: ChatAggregate, userId: string): number { - // Simple implementation - count messages from other users - // In production, you'd store lastSeen timestamp per user per chat - return chat.messages.filter(msg => msg.userid !== userId).length; - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommand.ts b/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommand.ts deleted file mode 100644 index 54ceeee5..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommand.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ContactType } from '../../../Domain/Contact/ContactAggregate'; - -export interface CreateContactCommand { - name: string; - email: string; - userid?: string; - type: ContactType; - txt: string; -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommandHandler.ts b/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommandHandler.ts deleted file mode 100644 index ddc2a40b..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommandHandler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { CreateContactCommand } from './CreateContactCommand'; -import { ShortContactDto } from '../../DTOs/ContactDto'; -import { ContactAggregate, ContactState } from '../../../Domain/Contact/ContactAggregate'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; - -export class CreateContactCommandHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(cmd: CreateContactCommand): Promise { - try { - const contact = new ContactAggregate(); - contact.name = cmd.name; - contact.email = cmd.email; - contact.userid = cmd.userid || null; - contact.type = cmd.type; - contact.txt = cmd.txt; - contact.state = ContactState.ACTIVE; - - const created = await this.contactRepo.create(contact); - return ContactMapper.toShortDto(created); - } catch (error) { - throw new Error('Failed to create contact'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommand.ts b/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommand.ts deleted file mode 100644 index 99b1e0b7..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteContactCommand { - id: string; - hard?: boolean; // true for permanent delete, false/undefined for soft delete -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommandHandler.ts b/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommandHandler.ts deleted file mode 100644 index c2a3f9f9..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommandHandler.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { DeleteContactCommand } from './DeleteContactCommand'; -import { AdminAuditService } from '../../Services/AdminBypassService'; -import { logRequest } from '../../Services/Logger'; - -export class DeleteContactCommandHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(cmd: DeleteContactCommand): Promise { - try { - const existingContact = await this.contactRepo.findById(cmd.id); - if (!existingContact) { - throw new Error('Contact not found'); - } - - if (cmd.hard) { - // Permanent delete - await this.contactRepo.delete(cmd.id); - logRequest('Contact hard deleted', undefined, undefined, { - contactId: cmd.id, - contactEmail: existingContact.email, - deleteType: 'hard' - }); - } else { - // Soft delete (default) - await this.contactRepo.softDelete(cmd.id); - logRequest('Contact soft deleted', undefined, undefined, { - contactId: cmd.id, - contactEmail: existingContact.email, - deleteType: 'soft' - }); - } - - return true; - } catch (error) { - if (error instanceof Error && error.message === 'Contact not found') { - throw error; - } - throw new Error('Failed to delete contact'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommand.ts b/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommand.ts deleted file mode 100644 index 6d66e809..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface UpdateContactCommand { - id: string; - adminResponse?: string; - state?: number; - respondedBy?: string; -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommandHandler.ts b/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommandHandler.ts deleted file mode 100644 index e25ec319..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommandHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { UpdateContactCommand } from './UpdateContactCommand'; -import { DetailContactDto } from '../../DTOs/ContactDto'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; -import { ContactState } from '../../../Domain/Contact/ContactAggregate'; - -export class UpdateContactCommandHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(cmd: UpdateContactCommand): Promise { - try { - const existingContact = await this.contactRepo.findById(cmd.id); - if (!existingContact) { - throw new Error('Contact not found'); - } - - const updateData: any = {}; - - if (cmd.adminResponse !== undefined) { - updateData.adminResponse = cmd.adminResponse; - updateData.responseDate = new Date(); - } - - if (cmd.state !== undefined) { - updateData.state = cmd.state; - } - - if (cmd.respondedBy !== undefined) { - updateData.respondedBy = cmd.respondedBy; - } - - const updated = await this.contactRepo.update(cmd.id, updateData); - if (!updated) { - throw new Error('Failed to update contact'); - } - - return ContactMapper.toDetailDto(updated); - } catch (error) { - if (error instanceof Error && error.message === 'Contact not found') { - throw error; - } - throw new Error('Failed to update contact'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQuery.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQuery.ts deleted file mode 100644 index f8686379..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetContactByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQueryHandler.ts deleted file mode 100644 index d20f3c7a..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQueryHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { GetContactByIdQuery } from './GetContactByIdQuery'; -import { DetailContactDto } from '../../DTOs/ContactDto'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; - -export class GetContactByIdQueryHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(query: GetContactByIdQuery): Promise { - const contact = await this.contactRepo.findById(query.id); - if (!contact) { - return null; - } - return ContactMapper.toDetailDto(contact); - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQuery.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQuery.ts deleted file mode 100644 index cc5850ab..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQuery.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface GetContactsByPageQuery { - from: number; - to: number; -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQueryHandler.ts deleted file mode 100644 index e39234df..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQueryHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { GetContactsByPageQuery } from './GetContactsByPageQuery'; -import { ContactPageDto } from '../../DTOs/ContactDto'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; - -export class GetContactsByPageQueryHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(query: GetContactsByPageQuery): Promise { - const result = await this.contactRepo.findByPage(query.from, query.to); - return { - contacts: ContactMapper.toShortDtoList(result.contacts), - totalCount: result.totalCount, - from: query.from, - to: query.to, - }; - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/ChatDto.ts b/SerpentRace_Backend/src/Application/DTOs/ChatDto.ts deleted file mode 100644 index f3f59bf3..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/ChatDto.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface CreateChatDto { - users: string[]; - messages: import('../../Domain/Chat/ChatAggregate').Message[]; - state?: number; -} - -export interface UpdateChatDto { - id: string; - users?: string[]; - messages?: import('../../Domain/Chat/ChatAggregate').Message[]; - state?: number; -} - -export interface ShortChatDto { - id: string; - userCount: number; - state: number; -} - -export interface DetailChatDto { - id: string; - users: string[]; - messages: import('../../Domain/Chat/ChatAggregate').Message[]; - updateDate: Date; - state: number; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/ContactDto.ts b/SerpentRace_Backend/src/Application/DTOs/ContactDto.ts deleted file mode 100644 index 44100357..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/ContactDto.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ContactType } from '../../Domain/Contact/ContactAggregate'; - -export interface CreateContactDto { - name: string; - email: string; - userid?: string; - type: ContactType; - txt: string; -} - -export interface UpdateContactDto { - id: string; - adminResponse?: string; - state?: number; - respondedBy?: string; -} - -export interface ShortContactDto { - id: string; - name: string; - email: string; - type: ContactType; - createDate: Date; - state: number; -} - -export interface DetailContactDto { - id: string; - name: string; - email: string; - userid: string | null; - type: ContactType; - txt: string; - state: number; - createDate: Date; - updateDate: Date; - adminResponse: string | null; - responseDate: Date | null; - respondedBy: string | null; -} - -export interface ContactPageDto { - contacts: ShortContactDto[]; - totalCount: number; - from: number; - to: number; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/DeckDto.ts b/SerpentRace_Backend/src/Application/DTOs/DeckDto.ts deleted file mode 100644 index 68bd74d2..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/DeckDto.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface CreateDeckDto { - name: string; - description?: string; -} - -export interface UpdateDeckDto { - id: string; - name?: string; - description?: string; -} - -export interface ShortDeckDto { - id: string; - name: string; - type: number; - playedNumber: number; - ctype: number; - cardCount: number; - creator: string; - creationdate: Date; - editable?: boolean; -} - -export interface DetailDeckDto { - id: string; - name: string; - type: number; - userid: string; - creationdate: Date; - cards: any[]; - playedNumber: number; - ctype: number; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/GameDto.ts b/SerpentRace_Backend/src/Application/DTOs/GameDto.ts deleted file mode 100644 index 02bc0f61..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/GameDto.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as DeckAggregate from "../../Domain/Deck/DeckAggregate"; - -export interface GameStartDto { - gameid: string; - maxplayers: number; - logintype: number; - gamecode: string; - deck: gamedeck[]; -} - -enum decktype { - JOCKER = 0, - LUCK = 1, - QUEST = 2 -} - -export interface cards { - cardid: string; - question?: string; - answer?: string; - consequence?: DeckAggregate.Consequence | null; - played?: boolean; - playerid?: string; -} - -export interface gamedeck { - deckid: string; - decktype: decktype; - cards: cards[]; -} - -export interface GameDataDto { - id: string; - gamecode: string; - maxplayers: number; - logintype: number; - gamedecks: gamedeck[]; - players: string[]; - started: boolean; - finished: boolean; - winner?: string; - currentplayer?: string; - createdate: Date; - startdate?: Date; - enddate?: Date; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/BaseMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/BaseMapper.ts deleted file mode 100644 index d11c5ea4..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/BaseMapper.ts +++ /dev/null @@ -1,19 +0,0 @@ -export abstract class BaseMapper { - abstract toShortDto(entity: TEntity): TShortDto; - abstract toDetailDto(entity: TEntity): TDetailDto; - - toShortDtoList(entities: TEntity[]): TShortDto[] { - return entities.map(entity => this.toShortDto(entity)); - } - - toDetailDtoList(entities: TEntity[]): TDetailDto[] { - return entities.map(entity => this.toDetailDto(entity)); - } - - static toShortDtoListStatic( - entities: T[], - mapperFn: (entity: T) => TDto - ): TDto[] { - return entities.map(mapperFn); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/ChatMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/ChatMapper.ts deleted file mode 100644 index 60507b2f..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/ChatMapper.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChatAggregate } from '../../../Domain/Chat/ChatAggregate'; -import { ShortChatDto, DetailChatDto } from '../ChatDto'; - -export class ChatMapper { - static toShortDto(chat: ChatAggregate): ShortChatDto { - return { - id: chat.id, - userCount: chat.users?.length ?? 0, - state: chat.state, - }; - } - - static toDetailDto(chat: ChatAggregate): DetailChatDto { - return { - id: chat.id, - users: chat.users ?? [], - messages: chat.messages, - updateDate: chat.updateDate, - state: chat.state, - }; - } - - static toShortDtoList(chats: ChatAggregate[]): ShortChatDto[] { - return chats.map(this.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/ContactMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/ContactMapper.ts deleted file mode 100644 index b9a23ed1..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/ContactMapper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ContactAggregate } from '../../../Domain/Contact/ContactAggregate'; -import { CreateContactDto, UpdateContactDto, ShortContactDto, DetailContactDto } from '../ContactDto'; - -export class ContactMapper { - static toShortDto(contact: ContactAggregate): ShortContactDto { - return { - id: contact.id, - name: contact.name, - email: contact.email, - type: contact.type, - createDate: contact.createDate, - state: contact.state, - }; - } - - static toDetailDto(contact: ContactAggregate): DetailContactDto { - return { - id: contact.id, - name: contact.name, - email: contact.email, - userid: contact.userid, - type: contact.type, - txt: contact.txt, - state: contact.state, - createDate: contact.createDate, - updateDate: contact.updateDate, - adminResponse: contact.adminResponse, - responseDate: contact.responseDate, - respondedBy: contact.respondedBy, - }; - } - - static toShortDtoList(contacts: ContactAggregate[]): ShortContactDto[] { - return contacts.map(this.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/DeckMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/DeckMapper.ts deleted file mode 100644 index 87946870..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/DeckMapper.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; -import { UserAggregate } from '../../../Domain/User/UserAggregate'; -import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto'; - -export class DeckMapper { - static toShortDto(deck: DeckAggregate, userId?: string): ShortDeckDto { - return { - id: deck.id, - name: deck.name, - type: deck.type, - playedNumber: deck.playedNumber, - ctype: deck.ctype, - cardCount: deck.cards.length, - creator: deck.user?.username || 'Unknown', - creationdate: deck.creationdate, - editable: deck.isEditable(userId!) ? deck.isEditable(userId!) : undefined - }; - } - - static toDetailDto(deck: DeckAggregate): DetailDeckDto { - return { - id: deck.id, - name: deck.name, - type: deck.type, - userid: deck.userid, - creationdate: deck.creationdate, - cards: deck.cards, - playedNumber: deck.playedNumber, - ctype: deck.ctype, - }; - } - - static toShortDtoList(decks: DeckAggregate[], userId?: string): ShortDeckDto[] { - return decks.map(deck => ({ - id: deck.id, - name: deck.name, - type: deck.type, - playedNumber: deck.playedNumber, - ctype: deck.ctype, - cardCount: deck.cards.length, - creator: deck.user?.username || 'Unknown', - creationdate: deck.creationdate, - editable: deck.isEditable(userId!) ? deck.isEditable(userId!) : undefined - })); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/OrganizationMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/OrganizationMapper.ts deleted file mode 100644 index 70ff5352..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/OrganizationMapper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { OrganizationAggregate } from '../../../Domain/Organization/OrganizationAggregate'; -import { CreateOrganizationDto, UpdateOrganizationDto, ShortOrganizationDto, DetailOrganizationDto } from '../OrganizationDto'; - -export class OrganizationMapper { - static toShortDto(org: OrganizationAggregate): ShortOrganizationDto { - return { - id: org.id, - name: org.name, - state: org.state, - userinorg: org.userinorg, - maxOrganizationalDecks: org.maxOrganizationalDecks, - }; - } - - static toDetailDto(org: OrganizationAggregate): DetailOrganizationDto { - return { - id: org.id, - name: org.name, - contactfname: org.contactfname, - contactlname: org.contactlname, - contactphone: org.contactphone, - contactemail: org.contactemail, - state: org.state, - regdate: org.regdate, - updateDate: org.updateDate, - url: org.url, - userinorg: org.userinorg, - maxOrganizationalDecks: org.maxOrganizationalDecks, - users: org.users?.map(u => u.id) ?? [], - }; - } - - static toShortDtoList(orgs: OrganizationAggregate[]): ShortOrganizationDto[] { - return orgs.map(this.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/UserMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/UserMapper.ts deleted file mode 100644 index 877ccc0f..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/UserMapper.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UserAggregate, UserState } from '../../../Domain/User/UserAggregate'; -import { CreateUserDto, UpdateUserDto, ShortUserDto, DetailUserDto } from '../UserDto'; -import { BaseMapper } from './BaseMapper'; - -export class UserMapper { - static toShortDto(user: UserAggregate): ShortUserDto { - return { - username: user.username, - authLevel: (user.state === UserState.ADMIN ? 1 : 0) as 0 | 1, - }; - } - - static toDetailDto(user: UserAggregate): DetailUserDto { - return { - id: user.id, - orgid: user.orgid, - username: user.username, - email: user.email, - fname: user.fname, - lname: user.lname, - code: user.token, - phone: user.phone, - state: user.state, - }; - } - - static toShortDtoList(users: UserAggregate[]): ShortUserDto[] { - return BaseMapper.toShortDtoListStatic(users, UserMapper.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/OrganizationDto.ts b/SerpentRace_Backend/src/Application/DTOs/OrganizationDto.ts deleted file mode 100644 index c1eb4aaf..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/OrganizationDto.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface CreateOrganizationDto { - name: string; - description?: string; - maxOrganizationalDecks?: number | null; -} - -export interface UpdateOrganizationDto { - id: string; - name?: string; - description?: string; -} - -export interface ShortOrganizationDto { - id: string; - name: string; - state: number; - userinorg: number; - maxOrganizationalDecks?: number | null; -} - -export interface DetailOrganizationDto { - id: string; - name: string; - contactfname: string; - contactlname: string; - contactphone: string; - contactemail: string; - state: number; - regdate: Date; - updateDate: Date; - url: string | null; - userinorg: number; - maxOrganizationalDecks: number | null; - users: string[]; -} - -export interface OrganizationLoginUrlDto { - organizationId: string; - organizationName: string; - loginUrl: string; -} - -export interface OrganizationAuthCallbackDto { - organizationId: string; - userId: string; - status: 'ok' | 'not_ok'; - authToken?: string; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/SearchDto.ts b/SerpentRace_Backend/src/Application/DTOs/SearchDto.ts deleted file mode 100644 index acb616d8..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/SearchDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface SearchQuery { - query: string; - limit?: number; - offset?: number; -} - -export interface SearchResult { - results: T[]; - totalCount: number; - hasMore: boolean; - searchQuery: string; - searchType: 'users' | 'organizations' | 'decks'; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/UserDto.ts b/SerpentRace_Backend/src/Application/DTOs/UserDto.ts deleted file mode 100644 index d3a68ab7..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/UserDto.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface CreateUserDto { - username: string; - email: string; -} - -export interface UpdateUserDto { - id: string; - username?: string; - email?: string; -} - -export interface ShortUserDto { - username: string; - authLevel: 0 | 1; -} - -export interface DetailUserDto { - id: string; - orgid: string | null; - username: string; - email: string; - fname: string; - lname: string; - code: string | null; - phone: string | null; - state: number; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommand.ts b/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommand.ts deleted file mode 100644 index 9e4ec5ad..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommand.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface CreateDeckCommand { - name: string; - type: number; - userid: string; - cards: any[]; - ctype?: number; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommandHandler.ts b/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommandHandler.ts deleted file mode 100644 index c6c75d2f..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommandHandler.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { CreateDeckCommand } from './CreateDeckCommand'; -import { ShortDeckDto } from '../../DTOs/DeckDto'; -import { DeckAggregate, State, CType } from '../../../Domain/Deck/DeckAggregate'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; -import { AdminBypassService } from '../../Services/AdminBypassService'; -import { logRequest } from '../../Services/Logger'; - -export class CreateDeckCommandHandler { - constructor( - private readonly deckRepo: IDeckRepository, - private readonly userRepo: IUserRepository, - private readonly orgRepo: IOrganizationRepository - ) {} - - async execute(cmd: CreateDeckCommand): Promise { - try { - // 1. Get user details - const user = await this.userRepo.findById(cmd.userid); - if (!user) { - throw new Error('User not found'); - } - - // 2. ADMIN BYPASS - Skip all restrictions - if (AdminBypassService.shouldBypassRestrictions(user.state)) { - AdminBypassService.logAdminBypass( - 'CREATE_DECK_BYPASS', - user.id, - 'new-deck', - { - deckName: cmd.name, - deckType: cmd.type, - cardCount: cmd.cards.length, - ctype: cmd.ctype - } - ); - return this.createDeck(cmd); - } - - // 3. Check deck count limits for regular users - const userDeckCount = await this.deckRepo.countActiveByUserId(cmd.userid); - const maxDecks = user.state === UserState.VERIFIED_PREMIUM ? 12 : 8; - - if (userDeckCount >= maxDecks) { - throw new Error(`Deck limit exceeded. Maximum ${maxDecks} decks allowed for your account type.`); - } - - // 4. Organizational deck restrictions - if (cmd.ctype === CType.ORGANIZATION) { - // Only premium users can create organizational decks - if (user.state !== UserState.VERIFIED_PREMIUM) { - throw new Error('Only premium users can create organizational decks.'); - } - - // User must belong to an organization - if (!user.orgid) { - throw new Error('You must be a member of an organization to create organizational decks.'); - } - - // Check organization limits - const org = await this.orgRepo.findById(user.orgid); - if (!org) { - throw new Error('Organization not found.'); - } - - if (org.maxOrganizationalDecks === null) { - throw new Error('Organization deck limit not configured. Contact administrator.'); - } - - const userOrgDeckCount = await this.deckRepo.countOrganizationalByUserId(cmd.userid); - if (userOrgDeckCount >= org.maxOrganizationalDecks) { - throw new Error(`Organization deck limit exceeded. Maximum ${org.maxOrganizationalDecks} organizational decks allowed.`); - } - } - - // 5. Create deck with restrictions passed - return this.createDeck(cmd); - } catch (error) { - if (error instanceof Error) { - throw error; // Re-throw known errors with original message - } - throw new Error('Failed to create deck'); - } - } - - /** - * Private method to create deck after all validations - */ - private async createDeck(cmd: CreateDeckCommand): Promise { - const deck = new DeckAggregate(); - deck.name = cmd.name; - deck.type = cmd.type; - deck.userid = cmd.userid; - deck.cards = cmd.cards; - deck.ctype = cmd.ctype ?? CType.PUBLIC; - deck.state = State.ACTIVE; - - // Set organization reference for organizational decks - if (cmd.ctype === CType.ORGANIZATION) { - const user = await this.userRepo.findById(cmd.userid); - if (user?.orgid) { - const org = await this.orgRepo.findById(user.orgid); - if (org) { - deck.organization = org; - } - } - } - - const created = await this.deckRepo.create(deck); - - logRequest('Deck created successfully', undefined, undefined, { - deckId: created.id, - userId: cmd.userid, - deckName: cmd.name, - deckType: cmd.type, - ctype: cmd.ctype, - cardCount: cmd.cards.length - }); - - return DeckMapper.toShortDto(created); - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommand.ts b/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommand.ts deleted file mode 100644 index d41de235..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface DeleteDeckCommand { - userid: string; - authLevel: number; - id: string; - soft?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommandHandler.ts b/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommandHandler.ts deleted file mode 100644 index 07309b19..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommandHandler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { logAuth, logError } from '../../Services/Logger'; -import { DeleteDeckCommand } from './DeleteDeckCommand'; - -export class DeleteDeckCommandHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(cmd: DeleteDeckCommand): Promise { - - //get decks userid - const deck = await this.deckRepo.findById(cmd.id); - if (!deck) { - logError(`Deck not found with ID: ${cmd.id}`); - throw new Error('Deck not found'); - } - - if(cmd.authLevel !==1 && deck.userid !== cmd.userid) { - logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`); - throw new Error('Unauthorized'); - } - - if (cmd.soft) { - await this.deckRepo.softDelete(cmd.id); - } else { - await this.deckRepo.delete(cmd.id); - } - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommand.ts b/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommand.ts deleted file mode 100644 index 9fbbb3a9..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface UpdateDeckCommand { - userid: string; - authLevel: number; - id: string; - userstate?: number; - name?: string; - type?: number; - cards?: any[]; - ctype?: number; - state?: number; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommandHandler.ts b/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommandHandler.ts deleted file mode 100644 index 85cc4202..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommandHandler.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { UpdateDeckCommand } from './UpdateDeckCommand'; -import { ShortDeckDto } from '../../DTOs/DeckDto'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; -import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; -import { logAuth, logError } from '../../Services/Logger'; - -export class UpdateDeckCommandHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(cmd: UpdateDeckCommand): Promise { - if(cmd.state !== undefined && cmd.authLevel !== 1) { - throw new Error('Only admin users can change deck state'); - } - try { - let existingDeck: DeckAggregate | null = null; - if (cmd.authLevel === 1) { - existingDeck = await this.deckRepo.findByIdIncludingDeleted(cmd.id); - } else { - existingDeck = await this.deckRepo.findById(cmd.id); - } - if (!existingDeck) { - logError(`Deck not found with ID: ${cmd.id}`); - throw new Error('Deck not found'); - } - - if(cmd.authLevel !== 1 && existingDeck.userid !== cmd.userid) { - logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`); - throw new Error('Unauthorized'); - } - - const for_update: Partial = {}; - if(cmd.name !== undefined) for_update.name = cmd.name; - if(cmd.type !== undefined) for_update.type = cmd.type; - if(cmd.cards !== undefined) for_update.cards = cmd.cards; - if(cmd.ctype !== undefined) for_update.ctype = cmd.ctype; - if(cmd.state !== undefined) for_update.state = cmd.state; - - // Ensure we have something to update - if (Object.keys(for_update).length === 0) { - throw new Error('No fields provided for update'); - } - - const deck = await this.deckRepo.update(cmd.id, { ...for_update }); - if(!deck) { - logError(`Deck update failed for ID: ${cmd.id}. Update returned null.`); - throw new Error('Failed to update deck'); - } - return DeckMapper.toShortDto(deck); - } catch (error: any) { - logError(`Error updating deck: ${cmd.id}`, error); - throw error; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQuery.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQuery.ts deleted file mode 100644 index 49c192e0..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetDeckByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQueryHandler.ts deleted file mode 100644 index 9ea429b4..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQueryHandler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { GetDeckByIdQuery } from './GetDeckByIdQuery'; -import { DetailDeckDto } from '../../DTOs/DeckDto'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; - -export class GetDeckByIdQueryHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(query: GetDeckByIdQuery): Promise { - const deck = await this.deckRepo.findById(query.id); - if (!deck) return null; - return DeckMapper.toDetailDto(deck); - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQuery.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQuery.ts deleted file mode 100644 index 370fe350..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQuery.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface GetDecksByPageQuery { - from: number; - to: number; - userId: string; - userOrgId?: string; - isAdmin: boolean; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQueryHandler.ts deleted file mode 100644 index a4e6086a..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQueryHandler.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { GetDecksByPageQuery } from './GetDecksByPageQuery'; -import { ShortDeckDto } from '../../DTOs/DeckDto'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; -import { AdminBypassService } from '../../Services/AdminBypassService'; -import { logRequest, logError } from '../../Services/Logger'; - -export class GetDecksByPageQueryHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(query: GetDecksByPageQuery): Promise<{ decks: ShortDeckDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - // Log admin bypass if applicable - if (query.isAdmin) { - AdminBypassService.logAdminBypass( - 'GET_DECKS_PAGE_BYPASS', - query.userId, - 'paginated-decks', - { - from: query.from, - to: query.to, - includesDeleted: query.includeDeleted || false, - operation: 'read' - } - ); - } - - logRequest('Get decks by page query started', undefined, undefined, { - userId: query.userId, - userOrgId: query.userOrgId, - isAdmin: query.isAdmin, - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - // Use paginated filtered deck finding method - const result = await this.deckRepo.findFilteredDecks( - query.userId, - query.userOrgId, - query.isAdmin, - query.from, - query.to - ); - - logRequest('Get decks by page query completed', undefined, undefined, { - userId: query.userId, - userOrgId: query.userOrgId, - isAdmin: query.isAdmin, - from: query.from, - to: query.to, - returned: result.decks.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - decks: DeckMapper.toShortDtoList(result.decks, query.userId), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetDecksByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve decks page'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Game/BoardGenerationService.ts b/SerpentRace_Backend/src/Application/Game/BoardGenerationService.ts deleted file mode 100644 index e952bfe7..00000000 --- a/SerpentRace_Backend/src/Application/Game/BoardGenerationService.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { GameField, BoardData } from '../../Domain/Game/GameAggregate'; -import { logOther, logError } from '../Services/Logger'; - -interface SpecialFieldInfo { - position: number; - type: 'positive' | 'negative' | 'luck'; -} - -export class BoardGenerationService { - async generateBoard( - positiveFieldCount: number, - negativeFieldCount: number, - luckFieldCount: number - ): Promise { - // Pattern-based approach has 100% success rate, no retry needed - const result = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount); - - logOther('Pattern-based board generation completed', { - totalFields: result.fields.length, - specialFields: result.fields.filter((f: GameField) => f.type !== 'regular').length, - positiveFields: result.fields.filter((f: GameField) => f.type === 'positive').length, - negativeFields: result.fields.filter((f: GameField) => f.type === 'negative').length, - luckFields: result.fields.filter((f: GameField) => f.type === 'luck').length - }); - - return result; - } - - private generateSingleAttempt( - positiveFieldCount: number, - negativeFieldCount: number, - luckFieldCount: number - ): BoardData { - // Step 1: Choose special field positions - const specialFieldPositions = this.chooseSpecialFieldPositions( - positiveFieldCount, - negativeFieldCount, - luckFieldCount - ); - - // Step 2: Calculate step values using pattern-based approach - const fields = this.calculatePatternBasedStepValues(specialFieldPositions); - - return { - fields - }; - } - - private chooseSpecialFieldPositions( - positiveFieldCount: number, - negativeFieldCount: number, - luckFieldCount: number - ): SpecialFieldInfo[] { - const totalSpecial = positiveFieldCount + negativeFieldCount + luckFieldCount; - const specialFields: SpecialFieldInfo[] = []; - - // Generate unique random positions - const positions = new Set(); - while (positions.size < totalSpecial) { - const position = Math.floor(Math.random() * 100) + 1; // 1-100 - positions.add(position); - } - - // Convert to sorted array - const sortedPositions = Array.from(positions).sort((a, b) => a - b); - - // Distribute types randomly - const types: ('positive' | 'negative' | 'luck')[] = [ - ...Array(positiveFieldCount).fill('positive'), - ...Array(negativeFieldCount).fill('negative'), - ...Array(luckFieldCount).fill('luck') - ]; - - // Shuffle types - for (let i = types.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [types[i], types[j]] = [types[j], types[i]]; - } - - sortedPositions.forEach((position, index) => { - specialFields.push({ - position, - type: types[index] || 'positive' - }); - }); - - return specialFields; - } - - private calculatePatternBasedStepValues(specialFields: SpecialFieldInfo[]): GameField[] { - // Initialize all fields as regular - const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({ - position: i + 1, - type: 'regular' as const - })); - - // Update special fields with pattern-based step values - specialFields.forEach(specialField => { - const fieldIndex = specialField.position - 1; // Convert to 0-based index - fields[fieldIndex].type = specialField.type; - - if (specialField.type === 'luck') { - // Luck fields don't need step values - return; - } - - // Calculate step values based on position rules - let maxStepValue: number; - let minStepValue: number; - - if (specialField.position <= 80) { - // Positions 1-80: step values can be ±20 - maxStepValue = 20; - minStepValue = -20; - } else { - // Positions 81-100: step values can be -30 to +10 - maxStepValue = 10; - minStepValue = -30; - } - - // Generate appropriate step value for field type - if (specialField.type === 'positive') { - // Positive fields: use positive step values (1-3 range for balanced gameplay) - // Max movement: 3 × 6 (dice) = 18 steps - const stepValue = Math.floor(Math.random() * 3) + 1; // 1-3 - fields[fieldIndex].stepValue = Math.min(stepValue, maxStepValue); - } else { - // Negative fields: use negative step values (-1 to -3 range) - // Max backward: -3 × 6 (dice) = -18 steps - const stepValue = -(Math.floor(Math.random() * 3) + 1); // -1 to -3 - fields[fieldIndex].stepValue = Math.max(stepValue, minStepValue); - } - }); - - return fields; - } - - // This method can be used by FieldEffectService for movement calculations - public calculatePatternBasedMovement( - currentPosition: number, - stepValue: number, - diceValue: number - ): number { - // Calculate pattern modifier based on current position - const patternModifier = this.getPatternModifier(currentPosition, stepValue > 0); - - // Calculate final position: currentPosition + (stepValue × dice) + patternModifier - const movement = stepValue * diceValue; - let finalPosition = currentPosition + movement + patternModifier; - - // Ensure position stays within board bounds (1-100) - if (finalPosition < 1) { - finalPosition = 1; - } else if (finalPosition > 100) { - finalPosition = 100; - } - - return finalPosition; - } - - public getPatternModifier(position: number, positiveField: boolean): number { - // Pattern modifiers STACK for strategic complexity: - // - Positions ending in 0 (10, 20, 30...): No modifier - // - Positions ending in 5 (15, 25, 35...): ±3 modifier - // - Positions divisible by 3 (9, 12, 21...): ±2 modifier - // - Odd positions (1, 7, 11...): ±1 modifier - // Multiple conditions can apply and stack - - if (position % 10 === 0) { - return 0; // Positions ending in 0 - no modifier - } - - let modifier = 0; - const direction = positiveField ? 1 : -1; - - // Check each condition and stack modifiers - if (position % 10 === 5) { - modifier += 3 * direction; // Positions ending in 5 - } - if (position % 3 === 0) { - modifier += 2 * direction; // Divisible by 3 - } - if (position % 2 === 1) { - modifier += 1 * direction; // Odd positions - } - - return modifier; - } - - private validate20_30Rule(currentPosition: number, targetPosition: number, distance: number): boolean { - // Fields 1-85: max 20 fields in any direction - if (currentPosition <= 85) { - return distance <= 20; - } - - // Fields 86-100: max 30 fields backward, max 20 fields forward - if (currentPosition > 85) { - if (targetPosition > currentPosition) { - // Moving forward: max 20 fields - return distance <= 20; - } else { - // Moving backward: max 30 fields - return distance <= 30; - } - } - - return false; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/GameService.ts b/SerpentRace_Backend/src/Application/Game/GameService.ts deleted file mode 100644 index dd941e6a..00000000 --- a/SerpentRace_Backend/src/Application/Game/GameService.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { StartGameCommand } from './commands/StartGameCommand'; -import { StartGameCommandHandler } from './commands/StartGameCommandHandler'; -import { JoinGameCommand } from './commands/JoinGameCommand'; -import { JoinGameCommandHandler } from './commands/JoinGameCommandHandler'; -import { StartGamePlayCommand } from './commands/StartGamePlayCommand'; -import { StartGamePlayCommandHandler, GameStartResult } from './commands/StartGamePlayCommandHandler'; -import { GameAggregate, LoginType } from '../../Domain/Game/GameAggregate'; -import { logOther, logError } from '../Services/Logger'; - -export class GameService { - private startGameHandler: StartGameCommandHandler; - private joinGameHandler: JoinGameCommandHandler; - private startGamePlayHandler: StartGamePlayCommandHandler; - - constructor() { - this.startGameHandler = new StartGameCommandHandler(); - this.joinGameHandler = new JoinGameCommandHandler(); - this.startGamePlayHandler = new StartGamePlayCommandHandler(); - } - - /** - * Starts a new game with the provided deck IDs - * @param deckids Array of deck IDs (should contain 3 types: LUCK, JOKER, QUESTION) - * @param maxplayers Maximum number of players allowed in the game - * @param logintype How players can join the game (PUBLIC, PRIVATE, ORGANIZATION) - * @param userid Optional ID of the user creating the game - * @returns Promise The created game - */ - async startGame( - deckids: string[], - maxplayers: number, - logintype: LoginType, - userid?: string, - orgid?: string | null - ): Promise { - const startTime = performance.now(); - - try { - logOther('GameService.startGame called', { - deckCount: deckids.length, - maxplayers, - logintype, - userid, - orgid - }); - - // Validate input parameters - this.validateStartGameInput(deckids, maxplayers, logintype); - - // Create and execute the command - const command: StartGameCommand = { - deckids, - maxplayers, - logintype, - userid, - orgid - }; - - const game = await this.startGameHandler.handle(command); - - const endTime = performance.now(); - logOther('Game started successfully', { - gameId: game.id, - gameCode: game.gamecode, - deckCount: game.gamedecks.length, - totalCards: game.gamedecks.reduce((sum, deck) => sum + deck.cards.length, 0), - executionTime: Math.round(endTime - startTime) - }); - - return game; - - } catch (error) { - const endTime = performance.now(); - logError('GameService.startGame failed', error instanceof Error ? error : new Error(String(error))); - logOther('Game start failed', { - executionTime: Math.round(endTime - startTime), - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - } - - /** - * Join an existing game using game code - * @param gameCode 6-character game code - * @param playerId ID of the player joining (optional for public games) - * @param playerName Display name for the player - * @param orgId Organization ID (for organization games) - * @param loginType Type of join being attempted - * @returns Promise The updated game with new player - */ - async joinGame( - gameCode: string, - playerId?: string, - playerName?: string, - orgId?: string | null, - loginType?: LoginType - ): Promise { - const startTime = performance.now(); - - try { - logOther('GameService.joinGame called', { - gameCode, - playerId: playerId || 'anonymous', - playerName, - orgId, - loginType - }); - - // Validate input parameters - this.validateJoinGameInput(gameCode, playerId, loginType); - - // Create and execute the command - const command: JoinGameCommand = { - gameCode, - playerId, - playerName, - orgId, - loginType: loginType || LoginType.PUBLIC - }; - - const game = await this.joinGameHandler.handle(command); - - const endTime = performance.now(); - logOther('Player joined game successfully', { - gameId: game.id, - gameCode: game.gamecode, - playerId, - playerCount: game.players.length, - maxPlayers: game.maxplayers, - executionTime: Math.round(endTime - startTime) - }); - - return game; - - } catch (error) { - const endTime = performance.now(); - logError('GameService.joinGame failed', error instanceof Error ? error : new Error(String(error))); - logOther('Game join failed', { - gameCode, - playerId, - executionTime: Math.round(endTime - startTime), - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - } - - /** - * Start an existing game (move from WAITING to ACTIVE) - * Initializes all player positions to 0 and assigns random turn order - * @param gameId Game ID to start - * @param userId User ID of the game master (optional for public games) - * @returns Promise The updated game - */ - async startGamePlay( - gameId: string, - userId?: string - ): Promise { - const startTime = performance.now(); - - try { - logOther('GameService.startGamePlay called', { - gameId, - userId: userId || 'system' - }); - - // Validate input parameters - this.validateStartGamePlayInput(gameId); - - // Create and execute the command - const command: StartGamePlayCommand = { - gameId, - userId - }; - - const result = await this.startGamePlayHandler.handle(command); - - const endTime = performance.now(); - logOther('Game play started successfully', { - gameId: result.game.id, - gameCode: result.game.gamecode, - playerCount: result.game.players.length, - gameState: result.game.state, - executionTime: Math.round(endTime - startTime) - }); - - return result; - - } catch (error) { - const endTime = performance.now(); - logError('GameService.startGamePlay failed', error instanceof Error ? error : new Error(String(error))); - logOther('Game play start failed', { - gameId, - userId, - executionTime: Math.round(endTime - startTime), - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - } - - private validateStartGamePlayInput(gameId: string): void { - // Validate game ID - if (!gameId || typeof gameId !== 'string') { - throw new Error('Game ID is required and must be a string'); - } - - logOther('Start game play input validation passed', { - gameId - }); - } - - private validateJoinGameInput(gameCode: string, playerId?: string, loginType?: LoginType): void { - // Validate game code - if (!gameCode || typeof gameCode !== 'string') { - throw new Error('Game code is required and must be a string'); - } - - if (gameCode.length !== 6) { - throw new Error('Game code must be exactly 6 characters long'); - } - - // Validate login type specific requirements - if (loginType === LoginType.PRIVATE || loginType === LoginType.ORGANIZATION) { - if (!playerId || typeof playerId !== 'string') { - throw new Error(`Player ID is required for ${LoginType[loginType]} games`); - } - } - - logOther('Join game input validation passed', { - gameCode, - playerId: playerId || 'anonymous', - loginType - }); - } - - private validateStartGameInput(deckids: string[], maxplayers: number, logintype: LoginType): void { - // Validate deck IDs - if (!deckids || deckids.length === 0) { - throw new Error('At least one deck ID must be provided'); - } - - if (deckids.length < 3) { - throw new Error('At least 3 decks are required to start a game (one for each type: LUCK, JOKER, QUESTION)'); - } - - // Validate max players - if (!maxplayers || maxplayers < 2) { - throw new Error('Maximum players must be at least 2'); - } - - if (maxplayers > 8) { - throw new Error('Maximum players cannot exceed 8'); - } - - // Validate login type - if (logintype < 0 || logintype > 2) { - throw new Error('Invalid login type. Must be PUBLIC (0), PRIVATE (1), or ORGANIZATION (2)'); - } - - // Check for duplicate deck IDs - const uniqueIds = new Set(deckids); - if (uniqueIds.size !== deckids.length) { - throw new Error('Duplicate deck IDs are not allowed'); - } - - logOther('Start game input validation passed', { - deckCount: deckids.length, - maxplayers, - logintype - }); - } - - /** - * Game flow explanation (to be implemented later): - * - * 1. START GAME (implemented above): - * - Input: deckids, maxplayers, logintype, gamecode - * - Process: Fetch decks, validate types, shuffle cards, create game - * - Output: Game with shuffled deck objects - * - * 2. JOIN GAME (to be implemented): - * - Input: gamecode, playerid - * - Process: Find game, validate capacity, add player - * - Output: Updated game with new player - * - * 3. GAME ROUNDS (to be implemented): - * - Input: gameid, current player - * - Process: Manage turn order, track game state - * - Output: Current player information - * - * 4. PICK CARD (to be implemented): - * - Input: gameid, playerid, deck type - * - Process: Draw card from specific deck, apply consequence - * - Output: Card details and consequence effects - * - * 5. END GAME (to be implemented): - * - Input: gameid, winner - * - Process: Set game as finished, record winner - * - Output: Final game state - */ -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommand.ts deleted file mode 100644 index b3c909b4..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface GenerateBoardCommand { - gameId: string; - positiveFieldCount: number; - negativeFieldCount: number; - luckFieldCount: number; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommandHandler.ts deleted file mode 100644 index f5e454a9..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommandHandler.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { GenerateBoardCommand } from './GenerateBoardCommand'; -import { BoardGenerationService } from '../BoardGenerationService'; -import { RedisService } from '../../Services/RedisService'; -import { logOther, logError } from '../../Services/Logger'; -import { BoardData } from '../../../Domain/Game/GameAggregate'; - -export class GenerateBoardCommandHandler { - constructor( - private readonly boardGenerationService: BoardGenerationService, - private readonly redisService: RedisService - ) {} - - async execute(cmd: GenerateBoardCommand): Promise { - try { - logOther(`Starting board generation for game ${cmd.gameId}`); - const startTime = Date.now(); - - // Generate board with 20-30 rule validation - const boardData = await this.boardGenerationService.generateBoard( - cmd.positiveFieldCount, - cmd.negativeFieldCount, - cmd.luckFieldCount - ); - - // Store in Redis - const boardDataWithMetadata: BoardData = { - ...boardData, - gameId: cmd.gameId, - generatedAt: new Date(), - generationComplete: true - }; - - await this.redisService.setWithExpiry( - `game_board_${cmd.gameId}`, - JSON.stringify(boardDataWithMetadata), - 24 * 60 * 60 // 24 hours - ); - - const executionTime = Date.now() - startTime; - logOther(`Board generation completed for game ${cmd.gameId} in ${executionTime}ms using pattern-based approach`); - - } catch (error) { - logError(`Board generation failed for game ${cmd.gameId}:`, error as Error); - - // Store error state in Redis - const errorData: BoardData = { - gameId: cmd.gameId, - fields: [], - generationComplete: false, - error: error instanceof Error ? error.message : 'Unknown error', - generatedAt: new Date() - }; - - await this.redisService.setWithExpiry( - `game_board_${cmd.gameId}`, - JSON.stringify(errorData), - 24 * 60 * 60 - ); - - throw error; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommand.ts deleted file mode 100644 index b59e633c..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommand.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LoginType } from '../../../Domain/Game/GameAggregate'; - -export interface JoinGameCommand { - gameCode: string; // 6-character game code - playerId?: string; // User ID of the player joining (optional for public games) - playerName?: string; // Display name for the player (required for public games) - orgId?: string | null; // Organization ID (for organization games) - loginType: LoginType; // Type of join being attempted -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommandHandler.ts deleted file mode 100644 index 2fa4dc6a..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommandHandler.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { JoinGameCommand } from './JoinGameCommand'; -import { GameAggregate, GameState, LoginType } from '../../../Domain/Game/GameAggregate'; -import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; -import { DIContainer } from '../../Services/DIContainer'; -import { RedisService } from '../../Services/RedisService'; -import { logOther, logError } from '../../Services/Logger'; -import { v4 as uuidv4 } from 'uuid'; - -export interface GamePlayerData { - playerId: string; - playerName?: string; - joinedAt: Date; - isOnline: boolean; - position?: number; // For game board position (to be used later) -} - -export interface ActiveGameData { - gameId: string; - gameCode: string; - hostId?: string; - maxPlayers: number; - currentPlayers: GamePlayerData[]; - state: GameState; - createdAt: Date; - startedAt?: Date; - currentTurn?: string; // Player ID whose turn it is - websocketRoom: string; // WebSocket room name for real-time updates -} - -export class JoinGameCommandHandler { - private gameRepository: IGameRepository; - private redisService: RedisService; - - constructor() { - this.gameRepository = DIContainer.getInstance().gameRepository; - this.redisService = RedisService.getInstance(); - } - - async handle(command: JoinGameCommand): Promise { - const startTime = performance.now(); - - try { - logOther('Joining game', `gameCode: ${command.gameCode}, playerId: ${command.playerId || 'anonymous'}, loginType: ${command.loginType}`); - - // Find the game by game code - const game = await this.gameRepository.findByGameCode(command.gameCode); - if (!game) { - throw new Error(`Game with code ${command.gameCode} not found`); - } - - // Generate player ID for public games or use provided one - // For anonymous players (no playerId), use playerName as the identifier to allow rejoining - const actualPlayerId = command.playerId || `guest_${command.playerName}`; - - // Validate game joinability (authentication/org checks done in router) - this.validateGameJoinability(game, actualPlayerId, command); - - // Add player to database - const updatedGame = await this.gameRepository.addPlayerToGame(game.id, actualPlayerId); - if (!updatedGame) { - throw new Error('Failed to add player to game'); - } - - // Update Redis with the new player - await this.updateGameInRedis(updatedGame, { ...command, playerId: actualPlayerId }); - - const endTime = performance.now(); - logOther('Player joined game successfully', { - gameId: game.id, - gameCode: game.gamecode, - playerId: actualPlayerId, - playerCount: updatedGame.players.length, - maxPlayers: updatedGame.maxplayers, - loginType: game.logintype, - executionTime: Math.round(endTime - startTime) - }); - - return updatedGame; - - } catch (error) { - const endTime = performance.now(); - logError('Failed to join game', error instanceof Error ? error : new Error(String(error))); - logOther('Game join failed', { - gameCode: command.gameCode, - playerId: command.playerId || 'anonymous', - loginType: command.loginType, - executionTime: Math.round(endTime - startTime) - }); - throw error; - } - } - - private validateGameJoinability(game: GameAggregate, playerId: string, command: JoinGameCommand): void { - // Check if game is in waiting state - if (game.state !== GameState.WAITING) { - throw new Error('Game is not accepting new players'); - } - - // Check if player is already in the game - if (game.players.includes(playerId)) { - throw new Error('Player is already in this game'); - } - - // Check if game is full - if (game.players.length >= game.maxplayers) { - throw new Error('Game is full'); - } - - // Note: Login type validation is now handled in the router before reaching this handler - // This ensures proper authentication and organization membership checks are done first - - logOther('Game join validation passed', { - gameId: game.id, - gameCode: game.gamecode, - currentPlayers: game.players.length, - maxPlayers: game.maxplayers, - gameState: game.state, - loginType: game.logintype, - playerId: playerId, - isAuthenticated: !!command.playerId - }); - } - - private async updateGameInRedis(game: GameAggregate, command: JoinGameCommand & { playerId: string }): Promise { - try { - const redisKey = `game:${game.gamecode}`; - - // Get existing game data from Redis or create new - let gameData: ActiveGameData; - const existingData = await this.redisService.get(redisKey); - - if (existingData) { - gameData = JSON.parse(existingData) as ActiveGameData; - } else { - // Create new game data structure - gameData = { - gameId: game.id, - gameCode: game.gamecode, - maxPlayers: game.maxplayers, - currentPlayers: [], - state: game.state, - createdAt: game.createdate, - websocketRoom: `game_${game.gamecode}` - }; - } - - // Add the new player - const newPlayer: GamePlayerData = { - playerId: command.playerId, - playerName: command.playerName, - joinedAt: new Date(), - isOnline: true - }; - - // Check if player name is already in use by a different player - const existingPlayerWithName = gameData.currentPlayers.find( - p => p.playerName === command.playerName && p.playerId !== command.playerId - ); - - if (existingPlayerWithName) { - throw new Error(`Player name "${command.playerName}" is already in use in this game`); - } - - // Update players list (remove if exists, then add) - gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId); - gameData.currentPlayers.push(newPlayer); - - // Update game state and player count - gameData.state = game.state; - - // Store updated data in Redis with TTL (24 hours) - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - logOther('Game data updated in Redis', { - gameId: game.id, - gameCode: game.gamecode, - redisKey, - playerCount: gameData.currentPlayers.length, - websocketRoom: gameData.websocketRoom, - playerId: command.playerId - }); - - } catch (error) { - logError('Failed to update game in Redis', error instanceof Error ? error : new Error(String(error))); - // Don't throw error here - Redis failure shouldn't prevent game join - logOther('Game join completed despite Redis error', { - gameId: game.id, - playerId: command.playerId - }); - } - } - - async getGameFromRedis(gameCode: string): Promise { - try { - const redisKey = `game:${gameCode}`; - const data = await this.redisService.get(redisKey); - return data ? JSON.parse(data) as ActiveGameData : null; - } catch (error) { - logError('Failed to get game from Redis', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - async removePlayerFromRedis(gameCode: string, playerId: string): Promise { - try { - const redisKey = `game:${gameCode}`; - const existingData = await this.redisService.get(redisKey); - - if (existingData) { - const gameData = JSON.parse(existingData) as ActiveGameData; - gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId); - - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - } - } catch (error) { - logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error))); - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGameCommand.ts deleted file mode 100644 index e10fad32..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommand.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LoginType } from '../../../Domain/Game/GameAggregate'; - -export interface StartGameCommand { - deckids: string[]; // Array of deck IDs (3 types, multiple decks per type) - maxplayers: number; // Maximum number of players - logintype: LoginType; // How players can join the game - userid?: string; // Optional user who created the game (becomes game master) - orgid?: string | null; // Organization ID (for organization games) -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGameCommandHandler.ts deleted file mode 100644 index 931f05fe..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommandHandler.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { StartGameCommand } from './StartGameCommand'; -import { GameAggregate, GameDeck, GameCard, DeckType, GameState } from '../../../Domain/Game/GameAggregate'; -import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; -import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { DIContainer } from '../../Services/DIContainer'; -import { RedisService } from '../../Services/RedisService'; -import { logOther, logError } from '../../Services/Logger'; -import { randomBytes } from 'crypto'; -import { GenerateBoardCommand } from './GenerateBoardCommand'; - -export interface ActiveGameData { - gameId: string; - gameCode: string; - hostId?: string; - maxPlayers: number; - currentPlayers: GamePlayerData[]; - state: GameState; - createdAt: Date; - startedAt?: Date; - currentTurn?: string; - websocketRoom: string; -} - -export interface GamePlayerData { - playerId: string; - playerName?: string; - joinedAt: Date; - isOnline: boolean; - position?: number; -} - -export class StartGameCommandHandler { - private gameRepository: IGameRepository; - private deckRepository: IDeckRepository; - private redisService: RedisService; - - constructor() { - this.gameRepository = DIContainer.getInstance().gameRepository; - this.deckRepository = DIContainer.getInstance().deckRepository; - this.redisService = RedisService.getInstance(); - } - - async handle(command: StartGameCommand): Promise { - const startTime = performance.now(); - - try { - logOther('Starting game creation', `deckCount: ${command.deckids.length}, maxPlayers: ${command.maxplayers}, loginType: ${command.logintype}`); - - // Generate unique game code - const gamecode = this.generateGameCode(); - - // Fetch all decks by IDs - const decks = await this.fetchDecks(command.deckids); - - // Validate we have 3 deck types - this.validateDeckTypes(decks); - - // Group decks by type and shuffle cards within each type - const gamedecks = await this.createShuffledGameDecks(decks); - - // Create the game aggregate - const gameData: Partial = { - gamecode, - maxplayers: command.maxplayers, - logintype: command.logintype, - createdby: command.userid!, - orgid: command.orgid || null, - gamedecks, - players: [], - winner: null, - state: GameState.WAITING, - startdate: null, - enddate: null - }; - - // Save the game to database - const savedGame = await this.gameRepository.create(gameData); - - // Create Redis object for real-time game management - await this.createGameInRedis(savedGame, command.userid); - - // Trigger async board generation (don't block game creation) - this.triggerAsyncBoardGeneration(savedGame.id).catch((error: Error) => { - logError('Async board generation failed', error); - }); - - const endTime = performance.now(); - logOther('Game created successfully', `gameId: ${savedGame.id}, gameCode: ${savedGame.gamecode}, executionTime: ${Math.round(endTime - startTime)}ms`); - - return savedGame; - - } catch (error) { - const endTime = performance.now(); - logError('Failed to create game', error instanceof Error ? error : new Error(String(error))); - logOther('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - throw new Error('Failed to start game: ' + (error instanceof Error ? error.message : String(error))); - } - } - - private generateGameCode(): string { - // Generate a 6-character alphanumeric game code - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let result = ''; - const randomBytesArray = randomBytes(6); - - for (let i = 0; i < 6; i++) { - result += chars[randomBytesArray[i] % chars.length]; - } - - return result; - } - - private async fetchDecks(deckIds: string[]): Promise { - const decks: DeckAggregate[] = []; - - for (const deckId of deckIds) { - const deck = await this.deckRepository.findById(deckId); - if (!deck) { - throw new Error(`Deck with ID ${deckId} not found`); - } - decks.push(deck); - } - - return decks; - } - - private validateDeckTypes(decks: DeckAggregate[]): void { - const deckTypes = new Set(decks.map(deck => deck.type)); - - // Check if we have all 3 required deck types (LUCK=0, JOKER=1, QUESTION=2) - const requiredTypes = [0, 1, 2]; // Based on Type enum in DeckAggregate - const missingTypes = requiredTypes.filter(type => !deckTypes.has(type)); - - if (missingTypes.length > 0) { - throw new Error(`Missing required deck types: ${missingTypes.join(', ')}. Game requires LUCK, JOKER, and QUESTION deck types.`); - } - - logOther('Deck types validation passed', `foundTypes: [${Array.from(deckTypes).join(', ')}]`); - } - - private async createShuffledGameDecks(decks: DeckAggregate[]): Promise { - // Group decks by type - const decksByType = new Map(); - - decks.forEach(deck => { - if (!decksByType.has(deck.type)) { - decksByType.set(deck.type, []); - } - decksByType.get(deck.type)!.push(deck); - }); - - const gamedecks: GameDeck[] = []; - - // Process each deck type - for (const [deckType, typeDecks] of decksByType) { - // Collect all cards from decks of this type - const allCards: GameCard[] = []; - - typeDecks.forEach(deck => { - deck.cards.forEach(card => { - const gameCard: GameCard = { - cardid: this.generateCardId(), - question: card.text, - answer: card.answer || undefined, - type: card.type, // Include card type for proper processing - consequence: card.consequence || null, - played: false, - playerid: undefined - }; - allCards.push(gameCard); - }); - }); - - // Shuffle all cards of this type - const shuffledCards = this.shuffleArray(allCards); - - // Create game deck for this type - const gameDeck: GameDeck = { - deckid: typeDecks[0].id, // Use first deck ID as representative - decktype: this.mapDeckTypeToGameDeckType(deckType), - cards: shuffledCards - }; - - gamedecks.push(gameDeck); - - logOther('Created shuffled game deck', `type: ${deckType}, cardCount: ${shuffledCards.length}, sourceDecks: ${typeDecks.length}`); - } - - return gamedecks; - } - - private mapDeckTypeToGameDeckType(deckType: number): DeckType { - // Map DeckAggregate.Type to GameAggregate.DeckType - switch (deckType) { - case 0: return DeckType.LUCK; // LUCK = 0 - case 1: return DeckType.JOCKER; // JOKER = 1 - case 2: return DeckType.QUEST; // QUESTION = 2 - default: throw new Error(`Unknown deck type: ${deckType}`); - } - } - - private shuffleArray(array: T[]): T[] { - const shuffled = [...array]; - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled; - } - - private generateCardId(): string { - return randomBytes(8).toString('hex'); - } - - private async createGameInRedis(game: GameAggregate, hostId?: string): Promise { - try { - const redisKey = `game:${game.id}`; - - const gameData: ActiveGameData = { - gameId: game.id, - gameCode: game.gamecode, - hostId: hostId, - maxPlayers: game.maxplayers, - currentPlayers: [], - state: game.state, - createdAt: game.createdate, - websocketRoom: `game_${game.gamecode}` - }; - - // Store game data in Redis with TTL (24 hours) - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - // Create game room for WebSocket connections - await this.redisService.set(`game_room:${game.gamecode}`, game.id); - - logOther('Game created in Redis', { - gameId: game.id, - gameCode: game.gamecode, - hostId: hostId, - websocketRoom: gameData.websocketRoom, - redisKey - }); - - } catch (error) { - logError('Failed to create game in Redis', error instanceof Error ? error : new Error(String(error))); - // Don't throw error here - Redis failure shouldn't prevent game creation - logOther('Game created successfully despite Redis error', { - gameId: game.id, - gameCode: game.gamecode - }); - } - } - - private async triggerAsyncBoardGeneration(gameId: string): Promise { - try { - // Calculate default field counts based on game configuration - // For now, use reasonable defaults - this should be configurable by host in the future - const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67'); - const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100); - - // Default distribution: 60% positive, 25% negative, 15% luck - const positiveFieldCount = Math.floor(maxSpecialFields * 0.6); - const negativeFieldCount = Math.floor(maxSpecialFields * 0.25); - const luckFieldCount = Math.floor(maxSpecialFields * 0.15); - - const command: GenerateBoardCommand = { - gameId, - positiveFieldCount, - negativeFieldCount, - luckFieldCount - }; - - logOther(`Triggering async board generation for game ${gameId}`, { - positiveFieldCount, - negativeFieldCount, - luckFieldCount, - totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount - }); - - // Execute board generation in background - await DIContainer.getInstance().generateBoardCommandHandler.execute(command); - - } catch (error) { - logError(`Async board generation failed for game ${gameId}`, error as Error); - // Don't propagate error - board generation failure shouldn't affect game creation - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommand.ts deleted file mode 100644 index af62a030..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface StartGamePlayCommand { - gameId: string; // Game ID to start - userId?: string; // User who is starting the game (should be game master) -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommandHandler.ts deleted file mode 100644 index 6d352403..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommandHandler.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { StartGamePlayCommand } from './StartGamePlayCommand'; -import { GameAggregate, GameState, BoardData, GameField } from '../../../Domain/Game/GameAggregate'; -import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; -import { DIContainer } from '../../Services/DIContainer'; -import { RedisService } from '../../Services/RedisService'; -import { WebSocketService } from '../../Services/WebSocketService'; -import { logOther, logError } from '../../Services/Logger'; - -export interface GamePlayerPosition { - playerId: string; - playerName?: string; - position: number; // Board position (starts at 0) - turnOrder: number; // Random number to determine turn sequence - isOnline: boolean; - joinedAt: Date; -} - -export interface ActiveGamePlayData { - gameId: string; - gameCode: string; - hostId?: string; - maxPlayers: number; - players: GamePlayerPosition[]; - state: GameState; - createdAt: Date; - startedAt: Date; - currentTurn: number; // Index of current player in turn order - currentPlayer: string; // ID of the player whose turn it is - turnSequence: string[]; // Ordered array of player IDs based on turnOrder - websocketRoom: string; - gamePhase: 'starting' | 'playing' | 'paused' | 'finished'; - boardData: BoardData; // Generated board with fields -} - -export interface GameStartResult { - game: GameAggregate; - boardData: BoardData; -} - -export class StartGamePlayCommandHandler { - private gameRepository: IGameRepository; - private redisService: RedisService; - - constructor() { - this.gameRepository = DIContainer.getInstance().gameRepository; - this.redisService = RedisService.getInstance(); - } - - async handle(command: StartGamePlayCommand): Promise { - const startTime = performance.now(); - - try { - logOther('Starting game play', `gameId: ${command.gameId}, userId: ${command.userId || 'system'}`); - - // Find the game - const game = await this.gameRepository.findById(command.gameId); - if (!game) { - throw new Error(`Game with ID ${command.gameId} not found`); - } - - // Validate game can be started - this.validateGameCanStart(game, command.userId); - - // Wait for board generation to complete (max 20 seconds) - const boardData = await this.waitForBoardGeneration(game.id); - - // Update game state in database - const updatedGame = await this.gameRepository.update(game.id, { - state: GameState.ACTIVE, - startdate: new Date() - }); - - if (!updatedGame) { - throw new Error('Failed to update game state'); - } - - // Initialize game play in Redis with board data - await this.initializeGamePlayInRedis(updatedGame, boardData); - - // Notify all players via WebSocket - await this.notifyGameStart(updatedGame); - - const endTime = performance.now(); - logOther('Game play started successfully', { - gameId: updatedGame.id, - gameCode: updatedGame.gamecode, - playerCount: updatedGame.players.length, - executionTime: Math.round(endTime - startTime) - }); - - return { - game: updatedGame, - boardData: boardData - }; - - } catch (error) { - const endTime = performance.now(); - logError('Failed to start game play', error instanceof Error ? error : new Error(String(error))); - logOther('Game start failed', { - gameId: command.gameId, - userId: command.userId, - executionTime: Math.round(endTime - startTime) - }); - throw error; - } - } - - private validateGameCanStart(game: GameAggregate, userId?: string): void { - // Check if game is in waiting state - if (game.state !== GameState.WAITING) { - throw new Error('Game is not in waiting state and cannot be started'); - } - - // Check if there are enough players (at least 2) - if (game.players.length < 2) { - throw new Error('Game needs at least 2 players to start'); - } - - // For private and organization games, check if user is game master - if (game.createdby && userId && game.createdby !== userId) { - throw new Error('Only the game master can start this game'); - } - - logOther('Game start validation passed', { - gameId: game.id, - gameCode: game.gamecode, - playerCount: game.players.length, - gameState: game.state, - isGameMaster: !game.createdby || (userId && game.createdby === userId) - }); - } - - private async initializeGamePlayInRedis(game: GameAggregate, boardData: BoardData): Promise { - try { - const redisKey = `gameplay:${game.gamecode}`; - - // Get connected player names from Redis (stored by WebSocket) - const playerNamesMap = await this.getPlayerNames(game.gamecode); - - // Generate random turn orders for all players - const playersWithPositions = this.initializePlayerPositions(game.players, playerNamesMap); - - // Sort by turn order to create turn sequence - const turnSequence = [...playersWithPositions] - .sort((a, b) => a.turnOrder - b.turnOrder) - .map(p => p.playerId); - - const gamePlayData: ActiveGamePlayData = { - gameId: game.id, - gameCode: game.gamecode, - hostId: game.createdby || undefined, - maxPlayers: game.maxplayers, - players: playersWithPositions, - state: GameState.ACTIVE, - createdAt: game.createdate, - startedAt: new Date(), - currentTurn: 0, // Start with first player in sequence - currentPlayer: turnSequence[0], // First player in turn sequence - turnSequence, - websocketRoom: `game_${game.gamecode}`, - gamePhase: 'starting', - boardData - }; - - // Store game play data in Redis with TTL (24 hours) - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gamePlayData), 24 * 60 * 60); - - logOther('Game play initialized in Redis', { - gameId: game.id, - gameCode: game.gamecode, - playerCount: playersWithPositions.length, - turnSequence, - currentPlayer: turnSequence[0], - redisKey - }); - - } catch (error) { - logError('Failed to initialize game play in Redis', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to initialize game session'); - } - } - - private initializePlayerPositions(playerIds: string[], playerNamesMap: Map): GamePlayerPosition[] { - const players: GamePlayerPosition[] = []; - - // Generate random turn orders (1 to playerCount) - const turnOrders = this.generateRandomTurnOrders(playerIds.length); - - playerIds.forEach((playerId, index) => { - players.push({ - playerId, - playerName: playerNamesMap.get(playerId) || playerId, // Use mapped name or fallback to ID - position: 0, // All players start at position 0 - turnOrder: turnOrders[index], - isOnline: true, // Assume online when game starts - joinedAt: new Date() - }); - }); - - logOther('Player positions initialized', { - playerCount: players.length, - turnOrders: turnOrders, - playersData: players.map(p => ({ - playerId: p.playerId, - playerName: p.playerName, - position: p.position, - turnOrder: p.turnOrder - })) - }); - - return players; - } - - private generateRandomTurnOrders(playerCount: number): number[] { - // Create array [1, 2, 3, ..., playerCount] - const orders = Array.from({ length: playerCount }, (_, i) => i + 1); - - // Fisher-Yates shuffle - for (let i = orders.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [orders[i], orders[j]] = [orders[j], orders[i]]; - } - - return orders; - } - - private async notifyGameStart(game: GameAggregate): Promise { - try { - // Get game play data from Redis (contains board data) - const gamePlayData = await this.getGamePlayFromRedis(game.gamecode); - if (!gamePlayData) { - logError('Game play data not found in Redis', new Error('Missing game play data')); - return; - } - - const boardData = gamePlayData.boardData; - if (!boardData) { - logError('Board data not found in game play data', new Error('Missing board data')); - return; - } - - // Get WebSocket service from DIContainer and broadcast game start - const gameWebSocketService = DIContainer.getInstance().gameWebSocketService; - await gameWebSocketService.broadcastGameStart( - game.gamecode, - boardData, - gamePlayData.turnSequence, - game - ); - - logOther('Game start notifications sent via WebSocket', { - gameId: game.id, - gameCode: game.gamecode, - playerCount: game.players.length, - websocketRoom: `game_${game.gamecode}`, - firstPlayer: gamePlayData.turnSequence[0] - }); - - } catch (error) { - logError('Failed to send game start notifications', error instanceof Error ? error : new Error(String(error))); - // Don't throw error here - notification failure shouldn't prevent game start - } - } - - async getGamePlayFromRedis(gameCode: string): Promise { - try { - const redisKey = `gameplay:${gameCode}`; - const data = await this.redisService.get(redisKey); - return data ? JSON.parse(data) as ActiveGamePlayData : null; - } catch (error) { - logError('Failed to get game play from Redis', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - async updatePlayerPosition(gameCode: string, playerId: string, newPosition: number): Promise { - try { - const gameData = await this.getGamePlayFromRedis(gameCode); - if (!gameData) { - throw new Error('Game session not found'); - } - - // Update player position - const player = gameData.players.find(p => p.playerId === playerId); - if (player) { - player.position = newPosition; - - // Save back to Redis - const redisKey = `gameplay:${gameCode}`; - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - logOther('Player position updated', { - gameCode, - playerId, - newPosition - }); - } - } catch (error) { - logError('Failed to update player position', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } - - async getNextPlayer(gameCode: string): Promise { - try { - const gameData = await this.getGamePlayFromRedis(gameCode); - if (!gameData) { - return null; - } - - const nextTurnIndex = (gameData.currentTurn + 1) % gameData.turnSequence.length; - return gameData.turnSequence[nextTurnIndex]; - } catch (error) { - logError('Failed to get next player', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - private async getPlayerNames(gameCode: string): Promise> { - try { - // Get active game data from Redis which contains player names - const activeGameKey = `game:${gameCode}`; - const activeGameStr = await this.redisService.get(activeGameKey); - - const playerNamesMap = new Map(); - - if (activeGameStr) { - const activeGame = JSON.parse(activeGameStr); - if (activeGame.currentPlayers && Array.isArray(activeGame.currentPlayers)) { - // Map playerIds to playerNames from active game data - activeGame.currentPlayers.forEach((player: any) => { - if (player.playerId && player.playerName) { - playerNamesMap.set(player.playerId, player.playerName); - } - }); - } - } - - logOther('Retrieved player names map', { - gameCode, - playerCount: playerNamesMap.size, - players: Array.from(playerNamesMap.entries()).map(([id, name]) => ({ id, name })) - }); - - return playerNamesMap; - } catch (error) { - logError('Failed to get player names', error instanceof Error ? error : new Error(String(error))); - return new Map(); - } - } - - async advanceTurn(gameId: string): Promise { - try { - const gameData = await this.getGamePlayFromRedis(gameId); - if (!gameData) { - return null; - } - - // Advance to next player - gameData.currentTurn = (gameData.currentTurn + 1) % gameData.turnSequence.length; - const currentPlayer = gameData.turnSequence[gameData.currentTurn]; - - // Save back to Redis - const redisKey = `gameplay:${gameId}`; - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - logOther('Turn advanced', { - gameId, - currentTurn: gameData.currentTurn, - currentPlayer - }); - - return currentPlayer; - } catch (error) { - logError('Failed to advance turn', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - private async waitForBoardGeneration(gameId: string): Promise { - const maxWaitTime = parseInt(process.env.MAX_GENERATION_TIME_SECONDS || '20') * 1000; - const pollInterval = 500; // Check every 500ms - const startTime = Date.now(); - - logOther(`Waiting for board generation for game ${gameId}`, { - maxWaitTime: maxWaitTime / 1000, - pollInterval, - redisKey: `game_board_${gameId}` - }); - - while (Date.now() - startTime < maxWaitTime) { - try { - const redisKey = `game_board_${gameId}`; - const boardDataStr = await this.redisService.get(redisKey); - - logOther(`Board generation check for game ${gameId}`, { - attempt: Math.floor((Date.now() - startTime) / pollInterval) + 1, - hasData: !!boardDataStr, - dataLength: boardDataStr ? boardDataStr.length : 0, - waitTime: Date.now() - startTime - }); - - if (boardDataStr) { - const boardData: BoardData = JSON.parse(boardDataStr); - - logOther(`Board data found for game ${gameId}`, { - generationComplete: boardData.generationComplete, - hasError: !!boardData.error, - fieldsCount: boardData.fields ? boardData.fields.length : 0 - }); - - if (boardData.generationComplete) { - if (boardData.error) { - logError(`Board generation failed for game ${gameId}`, new Error(boardData.error)); - throw new Error(`Board generation failed: ${boardData.error}`); - } - - logOther(`Board generation completed for game ${gameId}`, { - fieldCount: boardData.fields.length, - waitTime: Date.now() - startTime - }); - - return boardData; - } - } else { - // No board data found yet - check if we need to trigger generation - logOther(`No board data found yet for game ${gameId}, checking if generation was triggered...`, { - waitTime: Date.now() - startTime, - redisKey - }); - - // If we've waited for 2 seconds and still no data, try to trigger generation manually - if (Date.now() - startTime > 2000) { - await this.ensureBoardGenerationTriggered(gameId); - } - } - - // Wait before next poll - await new Promise(resolve => setTimeout(resolve, pollInterval)); - - } catch (error) { - logError(`Error checking board generation status for game ${gameId}`, error as Error); - throw new Error(`Failed to retrieve board data: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // Timeout reached - logError(`Board generation timeout for game ${gameId}`, new Error(`Generation took longer than ${maxWaitTime / 1000} seconds`)); - throw new Error(`Board generation timeout. Game ${gameId} is not ready to start. Please try again later.`); - } - - private async ensureBoardGenerationTriggered(gameId: string): Promise { - try { - logOther(`Ensuring board generation is triggered for game ${gameId}`); - - // Check if generation was already triggered by looking for any board data - const redisKey = `game_board_${gameId}`; - const existingData = await this.redisService.get(redisKey); - - if (!existingData) { - // No data at all - trigger generation manually - logOther(`No board generation found for game ${gameId}, triggering manually`); - - // Use DIContainer to trigger board generation - const generateBoardCommand = { - gameId, - positiveFieldCount: Math.floor(67 * 0.6), // Default: 60% positive - negativeFieldCount: Math.floor(67 * 0.25), // Default: 25% negative - luckFieldCount: Math.floor(67 * 0.15) // Default: 15% luck - }; - - await DIContainer.getInstance().generateBoardCommandHandler.execute(generateBoardCommand); - logOther(`Board generation manually triggered for game ${gameId}`); - } - } catch (error) { - logError(`Failed to ensure board generation for game ${gameId}`, error as Error); - // Don't throw here - let the main wait loop handle the timeout - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommand.ts deleted file mode 100644 index 8e0f95fc..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommand.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface CreateOrganizationCommand { - name: string; - contactfname: string; - contactlname: string; - contactphone: string; - contactemail: string; - url?: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommandHandler.ts deleted file mode 100644 index d77ed9de..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommandHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { CreateOrganizationCommand } from './CreateOrganizationCommand'; -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationAggregate, OrganizationState } from '../../../Domain/Organization/OrganizationAggregate'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; - -export class CreateOrganizationCommandHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(cmd: CreateOrganizationCommand): Promise { - try { - const org = new OrganizationAggregate(); - org.name = cmd.name; - org.contactfname = cmd.contactfname; - org.contactlname = cmd.contactlname; - org.contactphone = cmd.contactphone; - org.contactemail = cmd.contactemail; - org.url = cmd.url || null; - org.state = OrganizationState.REGISTERED; - - const created = await this.orgRepo.create(org); - return OrganizationMapper.toShortDto(created); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('duplicate key value violates unique constraint')) { - throw new Error('Organization with this name or contact email already exists'); - } - } - throw new Error('Failed to create organization'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommand.ts deleted file mode 100644 index 60a31806..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteOrganizationCommand { - id: string; - soft?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommandHandler.ts deleted file mode 100644 index a9e1b965..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommandHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { DeleteOrganizationCommand } from './DeleteOrganizationCommand'; - - -export class DeleteOrganizationCommandHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(cmd: DeleteOrganizationCommand): Promise { - if (cmd.soft) { - await this.orgRepo.softDelete(cmd.id); - } else { - await this.orgRepo.delete(cmd.id); - } - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommand.ts deleted file mode 100644 index 6fa97b73..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ProcessOrgAuthCallbackCommand { - organizationId: string; - userId: string; - status: 'ok' | 'not_ok'; - authToken?: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommandHandler.ts deleted file mode 100644 index 39a41abe..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommandHandler.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { ProcessOrgAuthCallbackCommand } from './ProcessOrgAuthCallbackCommand'; -import { logAuth, logDatabase, logError, logWarning } from '../../Services/Logger'; - -export interface ProcessOrgAuthCallbackResponse { - success: boolean; - message: string; - updatedFields?: string[]; -} - -export class ProcessOrgAuthCallbackCommandHandler { - constructor( - private readonly userRepo: IUserRepository, - private readonly orgRepo: IOrganizationRepository - ) {} - - async execute(cmd: ProcessOrgAuthCallbackCommand): Promise { - const startTime = Date.now(); - - try { - logAuth('Processing organization authentication callback', cmd.userId, { - organizationId: cmd.organizationId, - status: cmd.status, - hasAuthToken: !!cmd.authToken - }); - - // Verify organization exists - const organization = await this.orgRepo.findById(cmd.organizationId); - if (!organization) { - logWarning('Organization not found for auth callback', { - organizationId: cmd.organizationId, - userId: cmd.userId - }); - return { - success: false, - message: 'Organization not found' - }; - } - - // Verify user exists - const user = await this.userRepo.findById(cmd.userId); - if (!user) { - logWarning('User not found for auth callback', { - organizationId: cmd.organizationId, - userId: cmd.userId - }); - return { - success: false, - message: 'User not found' - }; - } - - // Verify user belongs to the organization - if (user.orgid !== cmd.organizationId) { - logWarning('User does not belong to organization for auth callback', { - organizationId: cmd.organizationId, - userId: cmd.userId, - userOrgId: user.orgid - }); - return { - success: false, - message: 'User does not belong to this organization' - }; - } - - if (cmd.status === 'not_ok') { - logAuth('Organization authentication failed', cmd.userId, { - organizationId: cmd.organizationId, - organizationName: organization.name - }); - return { - success: false, - message: 'Organization authentication failed' - }; - } - - // Update user's organization login date - const now = new Date(); - const updatedUser = await this.userRepo.update(cmd.userId, { - Orglogindate: now - }); - - if (!updatedUser) { - logError('Failed to update user organization login date', new Error('User update returned null')); - return { - success: false, - message: 'Failed to update user login information' - }; - } - - logAuth('Organization authentication successful', cmd.userId, { - organizationId: cmd.organizationId, - organizationName: organization.name, - orgLoginDate: now.toISOString(), - executionTime: Date.now() - startTime - }); - - logDatabase('User organization login date updated', - `userId: ${cmd.userId}, orgId: ${cmd.organizationId}`, - Date.now() - startTime, - { - userId: cmd.userId, - organizationId: cmd.organizationId, - newOrgLoginDate: now.toISOString() - } - ); - - return { - success: true, - message: 'Organization authentication successful', - updatedFields: ['Orglogindate'] - }; - - } catch (error) { - logError('ProcessOrgAuthCallbackCommandHandler error', error as Error); - return { - success: false, - message: 'Internal error processing authentication callback' - }; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommand.ts deleted file mode 100644 index 040f893d..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommand.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { OrganizationStateType } from '../../../Domain/Organization/OrganizationAggregate'; - -export interface UpdateOrganizationCommand { - id: string; - name?: string; - contactfname?: string; - contactlname?: string; - contactphone?: string; - contactemail?: string; - url?: string; - state?: OrganizationStateType; - userinorg?: number; - maxOrganizationalDecks?: number | null; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommandHandler.ts deleted file mode 100644 index 32a145a8..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommandHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { UpdateOrganizationCommand } from './UpdateOrganizationCommand'; - -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; - -export class UpdateOrganizationCommandHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(cmd: UpdateOrganizationCommand): Promise { - const updated = await this.orgRepo.update(cmd.id, { ...cmd }); - if (!updated) return null; - return OrganizationMapper.toShortDto(updated); - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQuery.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQuery.ts deleted file mode 100644 index e8473c15..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetOrganizationByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQueryHandler.ts deleted file mode 100644 index 60c82768..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQueryHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { GetOrganizationByIdQuery } from './GetOrganizationByIdQuery'; - -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; - -export class GetOrganizationByIdQueryHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(query: GetOrganizationByIdQuery): Promise { - const org = await this.orgRepo.findById(query.id); - if (!org) return null; - return OrganizationMapper.toShortDto(org); - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQuery.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQuery.ts deleted file mode 100644 index 26370b47..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetOrganizationLoginUrlQuery { - organizationId: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQueryHandler.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQueryHandler.ts deleted file mode 100644 index f7de9707..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQueryHandler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { GetOrganizationLoginUrlQuery } from './GetOrganizationLoginUrlQuery'; -import { OrganizationLoginUrlDto } from '../../DTOs/OrganizationDto'; -import { logDatabase, logError, logWarning } from '../../Services/Logger'; - -export class GetOrganizationLoginUrlQueryHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(query: GetOrganizationLoginUrlQuery): Promise { - const startTime = Date.now(); - - try { - logDatabase('Getting organization login URL', `organizationId: ${query.organizationId}`, 0, { - organizationId: query.organizationId - }); - - const organization = await this.orgRepo.findById(query.organizationId); - - if (!organization) { - logWarning('Organization not found for login URL request', { - organizationId: query.organizationId - }); - return null; - } - - if (!organization.url) { - logWarning('Organization has no configured login URL', { - organizationId: query.organizationId, - organizationName: organization.name - }); - return null; - } - - const result: OrganizationLoginUrlDto = { - organizationId: organization.id, - organizationName: organization.name, - loginUrl: organization.url - }; - - logDatabase('Organization login URL retrieved successfully', - `organizationId: ${query.organizationId}`, - Date.now() - startTime, - { - organizationId: organization.id, - organizationName: organization.name, - hasUrl: !!organization.url - } - ); - - return result; - } catch (error) { - logError('GetOrganizationLoginUrlQueryHandler error', error as Error); - throw new Error('Failed to retrieve organization login URL'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQuery.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQuery.ts deleted file mode 100644 index ec22eb96..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetOrganizationsByPageQuery { - from: number; - to: number; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQueryHandler.ts deleted file mode 100644 index b838d81f..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQueryHandler.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { GetOrganizationsByPageQuery } from './GetOrganizationsByPageQuery'; -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; -import { logError, logRequest } from '../../Services/Logger'; - -export class GetOrganizationsByPageQueryHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(query: GetOrganizationsByPageQuery): Promise<{ organizations: ShortOrganizationDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - logRequest('Get organizations by page query started', undefined, undefined, { - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - const result = query.includeDeleted - ? await this.orgRepo.findByPageIncludingDeleted(query.from, query.to) - : await this.orgRepo.findByPage(query.from, query.to); - - logRequest('Get organizations by page query completed', undefined, undefined, { - from: query.from, - to: query.to, - returned: result.organizations.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - organizations: OrganizationMapper.toShortDtoList(result.organizations), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetOrganizationsByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Handle database errors - if (error instanceof Error && error.message.includes('database')) { - throw new Error('Database connection error'); - } - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve organizations'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Search/Generalsearch.ts b/SerpentRace_Backend/src/Application/Search/Generalsearch.ts deleted file mode 100644 index 0d940191..00000000 --- a/SerpentRace_Backend/src/Application/Search/Generalsearch.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { IUserRepository } from '../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { SearchQuery, SearchResult } from '../DTOs/SearchDto'; -import { ShortUserDto, DetailUserDto } from '../DTOs/UserDto'; -import { ShortOrganizationDto, DetailOrganizationDto } from '../DTOs/OrganizationDto'; -import { ShortDeckDto, DetailDeckDto } from '../DTOs/DeckDto'; -import { UserMapper } from '../DTOs/Mappers/UserMapper'; -import { OrganizationMapper } from '../DTOs/Mappers/OrganizationMapper'; -import { DeckMapper } from '../DTOs/Mappers/DeckMapper'; - -export type SearchType = 'users' | 'organizations' | 'decks'; - -export interface IGeneralSearchService { - searchUsers(searchQuery: SearchQuery): Promise>; - searchOrganizations(searchQuery: SearchQuery): Promise>; - searchDecks(searchQuery: SearchQuery): Promise>; - searchByType(searchType: SearchType, searchQuery: SearchQuery): Promise>; -} - -export class GeneralSearchService implements IGeneralSearchService { - constructor( - private userRepo: IUserRepository, - private organizationRepo: IOrganizationRepository, - private deckRepo: IDeckRepository - ) {} - - static getSearchTypeFromUrl(url: string): SearchType { - if (url.includes('/users/') || url.includes('/api/users/')) { - return 'users'; - } else if (url.includes('/organizations/') || url.includes('/api/organizations/')) { - return 'organizations'; - } else if (url.includes('/decks/') || url.includes('/api/decks/')) { - return 'decks'; - } - return 'users'; - } - - async searchUsers(searchQuery: SearchQuery): Promise> { - const { query, limit = 20, offset = 0 } = searchQuery; - - if (!query || query.trim().length === 0) { - return { - results: [], - totalCount: 0, - hasMore: false, - searchQuery: query, - searchType: 'users' - }; - } - - // Ensure limit is at least 1 to prevent database issues - const effectiveLimit = Math.max(limit || 20, 1); - const effectiveOffset = Math.max(offset || 0, 0); - - try { - const { users, totalCount } = await this.userRepo.search(query.trim(), effectiveLimit, effectiveOffset); - const results = users.map(user => UserMapper.toShortDto(user)); - const hasMore = (effectiveOffset + effectiveLimit) < totalCount; - - return { - results, - totalCount, - hasMore, - searchQuery: query, - searchType: 'users' - }; - } catch (error) { - throw new Error('Failed to search users'); - } - } - - async searchOrganizations(searchQuery: SearchQuery): Promise> { - const { query, limit = 20, offset = 0 } = searchQuery; - - if (!query || query.trim().length === 0) { - return { - results: [], - totalCount: 0, - hasMore: false, - searchQuery: query, - searchType: 'organizations' - }; - } - - const { organizations, totalCount } = await this.organizationRepo.search(query.trim(), limit, offset); - const results = organizations.map(org => OrganizationMapper.toShortDto(org)); - const hasMore = (offset + limit) < totalCount; - - return { - results, - totalCount, - hasMore, - searchQuery: query, - searchType: 'organizations' - }; - } - - async searchDecks(searchQuery: SearchQuery): Promise> { - const { query, limit = 20, offset = 0 } = searchQuery; - - if (!query || query.trim().length === 0) { - return { - results: [], - totalCount: 0, - hasMore: false, - searchQuery: query, - searchType: 'decks' - }; - } - - // Ensure limit is at least 1 to prevent database issues - const effectiveLimit = Math.max(limit || 20, 1); - const effectiveOffset = Math.max(offset || 0, 0); - - try { - const { decks, totalCount } = await this.deckRepo.search(query.trim(), effectiveLimit, effectiveOffset); - const results = decks.map(deck => DeckMapper.toShortDto(deck)); - const hasMore = (effectiveOffset + effectiveLimit) < totalCount; - - return { - results, - totalCount, - hasMore, - searchQuery: query, - searchType: 'decks' - }; - } catch (error) { - throw new Error('Failed to search decks'); - } - } - - async searchByType( - searchType: SearchType, - searchQuery: SearchQuery - ): Promise> { - switch (searchType) { - case 'users': - return await this.searchUsers(searchQuery) as SearchResult; - case 'organizations': - return await this.searchOrganizations(searchQuery) as SearchResult; - case 'decks': - return await this.searchDecks(searchQuery) as SearchResult; - default: - throw new Error(`Unsupported search type: ${searchType}`); - } - } - - async searchFromUrl( - url: string, - searchQuery: SearchQuery - ): Promise> { - const searchType = GeneralSearchService.getSearchTypeFromUrl(url); - return await this.searchByType(searchType, searchQuery); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/AdminBypassService.ts b/SerpentRace_Backend/src/Application/Services/AdminBypassService.ts deleted file mode 100644 index 6732c15f..00000000 --- a/SerpentRace_Backend/src/Application/Services/AdminBypassService.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { UserState } from '../../Domain/User/UserAggregate'; -import { logAuth } from './Logger'; -import { Request, Response } from 'express'; - -/** - * Admin Bypass Service - Centralized admin privilege checking and logging - */ -export class AdminBypassService { - /** - * Check if user has admin privileges - * @param userState - User's current state - * @returns true if user is admin - */ - static isAdmin(userState: UserState): boolean { - return userState === UserState.ADMIN; - } - - /** - * Check if user should bypass all restrictions - * @param userState - User's current state - * @returns true if restrictions should be bypassed - */ - static shouldBypassRestrictions(userState: UserState): boolean { - return this.isAdmin(userState); - } - - /** - * Log admin bypass action for audit trail - * @param action - Description of the action being bypassed - * @param adminUserId - ID of the admin user - * @param targetId - ID of the target resource - * @param details - Additional details about the bypass - * @param req - Optional request object for context - * @param res - Optional response object for context - */ - static logAdminBypass( - action: string, - adminUserId: string, - targetId: string, - details?: any, - req?: Request, - res?: Response - ): void { - logAuth(`ADMIN_BYPASS: ${action}`, adminUserId, { - targetId, - action, - bypassReason: 'Admin privileges', - timestamp: new Date().toISOString(), - ...details - }, req, res); - } -} - -/** - * Admin Audit Service - Enhanced logging for all admin actions - */ -export class AdminAuditService { - /** - * Log comprehensive admin action for audit trail - * @param action - Action being performed - * @param adminUserId - ID of the admin user - * @param details - Detailed information about the action - * @param req - Request object for context - * @param res - Response object for context - */ - static logAdminAction( - action: string, - adminUserId: string, - details: { - targetType: 'user' | 'organization' | 'deck' | 'contact' | 'chat'; - targetId: string; - operation: 'create' | 'read' | 'update' | 'delete' | 'bypass' | 'export' | 'import'; - changes?: any; - sensitive?: boolean; - metadata?: any; - }, - req?: Request, - res?: Response - ): void { - - const auditData = { - timestamp: new Date().toISOString(), - adminUserId, - action, - ...details, - ip: req?.ip, - userAgent: req?.get('User-Agent'), - endpoint: req?.path, - method: req?.method, - requestId: req?.headers['x-request-id'] || 'unknown' - }; - - // Enhanced logging for admin actions - logAuth(`ADMIN_AUDIT: ${action}`, adminUserId, auditData, req, res); - - // Additional security logging for sensitive operations - if (details.sensitive) { - logAuth(`ADMIN_SENSITIVE: ${action}`, adminUserId, { - ...auditData, - alertLevel: 'HIGH', - requiresReview: true - }, req, res); - } - } - - /** - * Log bulk admin operations - * @param action - Bulk action being performed - * @param adminUserId - ID of the admin user - * @param affectedCount - Number of resources affected - * @param targetType - Type of resources affected - * @param req - Request object for context - * @param res - Response object for context - */ - static logBulkAdminAction( - action: string, - adminUserId: string, - affectedCount: number, - targetType: string, - req?: Request, - res?: Response - ): void { - this.logAdminAction(`BULK_${action}`, adminUserId, { - targetType: targetType as any, - targetId: `bulk-${affectedCount}-items`, - operation: 'update' as any, - metadata: { affectedCount }, - sensitive: affectedCount > 10 // Mark large bulk operations as sensitive - }, req, res); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/AuthMiddleware.ts b/SerpentRace_Backend/src/Application/Services/AuthMiddleware.ts deleted file mode 100644 index 46496685..00000000 --- a/SerpentRace_Backend/src/Application/Services/AuthMiddleware.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { JWTService } from './JWTService'; -import { RedisService } from './RedisService'; -import { logAuth, logWarning } from './Logger'; - -export const jwtService = new JWTService(); -const redisService = RedisService.getInstance(); - -/** - * Check if a token is blacklisted - */ -async function isTokenBlacklisted(token: string): Promise { - try { - const result = await redisService.get(`blacklist:${token}`); - return result === 'true'; - } catch (error) { - // If Redis is down, allow the request to proceed (fail open) - logWarning('Failed to check token blacklist - allowing request', { error: (error as Error).message }); - return false; - } -} - -/** - * Extract token from request (cookie or Authorization header) - */ -function extractToken(req: Request, type: 'auth' | 'refresh'): string | null { - // First try to get token from cookie - const cookieToken = req.cookies[`${type}_token`]; - if (cookieToken) { - return cookieToken; - } - - // Fallback to Authorization header - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - return authHeader.substring(7); - } - - return null; -} - -export async function authRequired(req: Request, res: Response, next: NextFunction) { - try { - // Extract token from request - const token = extractToken(req, "auth"); - const refreshToken = extractToken(req, "refresh"); - if (!token || !refreshToken) { - logAuth('Authentication failed - No token provided', undefined, { - ip: req.ip, - userAgent: req.get ? req.get('User-Agent') : 'unknown', - path: req.path - }, req); - return res.status(401).json({ error: 'Unauthorized' }); - } - - // Check if token is blacklisted - const isBlacklisted = await isTokenBlacklisted(token); - if (isBlacklisted) { - logAuth('Authentication failed - Token blacklisted', undefined, { - ip: req.ip, - userAgent: req.get ? req.get('User-Agent') : 'unknown', - path: req.path - }, req); - return res.status(401).json({ error: 'Token has been invalidated' }); - } - - // Verify token - const payload = jwtService.verify(req); - if (!payload) { - logAuth('Authentication failed - Invalid token', undefined, { - ip: req.ip, - userAgent: req.get ? req.get('User-Agent') : 'unknown', - path: req.path - }, req); - return res.status(401).json({ error: 'Unauthorized' }); - } - - logAuth('Authentication successful', payload.userId, { - authLevel: payload.authLevel, - orgId: payload.orgId - }, req); - - const refreshed = jwtService.refreshIfNeeded(payload, res, req); - if (refreshed) { - logAuth('Token refreshed', payload.userId, undefined, req); - } - - (req as any).user = payload; - next(); - } catch (error) { - logWarning('Authentication middleware error', { error: (error as Error).message }, req); - return res.status(500).json({ error: 'Internal server error' }); - } -} - -export async function adminRequired(req: Request, res: Response, next: NextFunction) { - try { - // Extract token from request - const token = extractToken(req, "auth"); - const refreshToken = extractToken(req, "refresh"); - if (!token || !refreshToken) { - logWarning('Admin access denied - No token provided', { - ip: req.ip, - path: req.path - }, req); - return res.status(401).json({ error: 'Unauthorized' }); - } - - // Check if token is blacklisted - const isBlacklisted = await isTokenBlacklisted(token); - if (isBlacklisted) { - logWarning('Admin access denied - Token blacklisted', { - ip: req.ip, - path: req.path - }, req); - return res.status(401).json({ error: 'Token has been invalidated' }); - } - - // Verify token and check admin privileges - const payload = jwtService.verify(req); - if (!payload || payload.authLevel !== 1) { - logWarning('Admin access denied', { - hasPayload: !!payload, - authLevel: payload?.authLevel, - userId: payload?.userId, - ip: req.ip, - path: req.path - }, req); - return res.status(403).json({ error: 'Forbidden' }); - } - - logAuth('Admin authentication successful', payload.userId, { - authLevel: payload.authLevel, - orgId: payload.orgId - }, req); - - const refreshed = jwtService.refreshIfNeeded(payload, res, req); - if (refreshed) { - logAuth('Admin token refreshed', payload.userId, undefined, req); - } - - (req as any).user = payload; - next(); - } catch (error) { - logWarning('Admin authentication middleware error', { error: (error as Error).message }, req); - return res.status(500).json({ error: 'Internal server error' }); - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/CardDrawingService.ts b/SerpentRace_Backend/src/Application/Services/CardDrawingService.ts deleted file mode 100644 index cbb0d3bf..00000000 --- a/SerpentRace_Backend/src/Application/Services/CardDrawingService.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { GameAggregate, GameCard, DeckType, GameDeck } from '../../Domain/Game/GameAggregate'; -import { ConsequenceType } from '../../Domain/Deck/DeckAggregate'; -import { CardProcessingService, CardClientData, CardValidationResult } from './CardProcessingService'; - -export interface CardDrawResult { - success: boolean; - card?: GameCard; - clientData?: CardClientData; // Prepared data for client - error?: string; -} - -export interface CardAnswerResult { - correct: boolean; - consequence: ConsequenceType; - description: string; - validationDetails?: CardValidationResult; // Detailed validation info -} - -export interface PendingCardAnswer { - gameId: string; - playerId: string; - card: GameCard; - timeoutId: NodeJS.Timeout; - startTime: Date; -} - -/** - * Service responsible for handling card drawing mechanics during special field landings - * Integrates with existing GameCard interface and DeckType enum - */ -export class CardDrawingService { - private pendingAnswers: Map = new Map(); - private readonly ANSWER_TIMEOUT_MS = 60000; // 1 minute - private cardProcessingService: CardProcessingService; - - constructor() { - this.cardProcessingService = new CardProcessingService(); - } - - /** - * Draw a card from the appropriate deck based on field type - * @param game Game aggregate containing the deck information - * @param fieldType Type of field the player landed on - * @param playerId ID of the player who needs to draw the card - * @returns Card draw result with the drawn card or error - */ - drawCard(game: GameAggregate, fieldType: 'positive' | 'negative' | 'luck', playerId: string): CardDrawResult { - try { - // Determine which deck type to use based on field type - const deckType = this.getRequiredDeckType(fieldType); - - // Find the appropriate deck in the game - const gameDecks: GameDeck[] = typeof game.gamedecks === 'string' - ? JSON.parse(game.gamedecks) - : game.gamedecks; - - const targetDeck = gameDecks.find((deck: GameDeck) => deck.decktype === deckType); - - if (!targetDeck) { - return { - success: false, - error: `No ${this.getDeckTypeName(deckType)} deck found in game` - }; - } - - // Filter available cards (not played by this player yet) - const availableCards = targetDeck.cards.filter((card: GameCard) => !card.played || card.playerid !== playerId); - - if (availableCards.length === 0) { - return { - success: false, - error: `No more cards available in ${this.getDeckTypeName(deckType)} deck` - }; - } - - // Randomly select a card - const randomIndex = Math.floor(Math.random() * availableCards.length); - const drawnCard = availableCards[randomIndex]; - - // Mark card as drawn by this player - drawnCard.played = true; - drawnCard.playerid = playerId; - - // Check if card has consequence field (joker/luck card) even without type - const hasConsequence = drawnCard.consequence !== undefined && drawnCard.consequence !== null; - - // Prepare client data based on card type - // Only prepare for question cards (cards without consequence and with defined type) - let clientData: CardClientData | undefined; - if (!hasConsequence && drawnCard.type !== undefined) { - try { - clientData = this.cardProcessingService.prepareCardForClient(drawnCard); - } catch (error) { - // If client data preparation fails, still return the card but log the error - console.warn(`Failed to prepare client data for card ${drawnCard.cardid}:`, error); - } - } else if (!hasConsequence && drawnCard.type === undefined) { - // Card is missing type field - this shouldn't happen, log error - console.error(`Card ${drawnCard.cardid} is missing type field. Card data:`, { - cardId: drawnCard.cardid, - hasQuestion: !!drawnCard.question, - hasAnswer: !!drawnCard.answer, - hasConsequence, - cardKeys: Object.keys(drawnCard) - }); - } - - return { - success: true, - card: drawnCard, - clientData: clientData - }; - } catch (error) { - return { - success: false, - error: `Failed to draw card: ${error instanceof Error ? error.message : 'Unknown error'}` - }; - } - } - - /** - * Draw a joker card for secondary landings on special fields - * @param game Game aggregate containing the deck information - * @param playerId ID of the player who needs to draw the joker card - * @returns Card draw result with the joker card or error - */ - drawJokerCard(game: GameAggregate, playerId: string): CardDrawResult { - try { - const gameDecks: GameDeck[] = typeof game.gamedecks === 'string' - ? JSON.parse(game.gamedecks) - : game.gamedecks; - - const jokerDeck = gameDecks.find((deck: GameDeck) => deck.decktype === DeckType.JOCKER); - - if (!jokerDeck) { - return { - success: false, - error: 'No joker deck found in game' - }; - } - - // Filter available joker cards - const availableCards = jokerDeck.cards.filter((card: GameCard) => !card.played || card.playerid !== playerId); - - if (availableCards.length === 0) { - return { - success: false, - error: 'No more joker cards available' - }; - } - - // Randomly select a joker card - const randomIndex = Math.floor(Math.random() * availableCards.length); - const drawnCard = availableCards[randomIndex]; - - // Mark card as drawn by this player - drawnCard.played = true; - drawnCard.playerid = playerId; - - return { - success: true, - card: drawnCard - }; - } catch (error) { - return { - success: false, - error: `Failed to draw joker card: ${error instanceof Error ? error.message : 'Unknown error'}` - }; - } - } - - /** - * Start the answer timeout for a question card - * @param gameId Game ID - * @param playerId Player ID who needs to answer - * @param card The card with the question - * @param onTimeout Callback function when timeout occurs - * @returns Unique key for tracking this pending answer - */ - startAnswerTimeout( - gameId: string, - playerId: string, - card: GameCard, - onTimeout: (gameId: string, playerId: string, card: GameCard) => void - ): string { - const key = `${gameId}:${playerId}`; - - // Clear any existing timeout for this player - this.clearAnswerTimeout(key); - - // Set new timeout - const timeoutId = setTimeout(() => { - onTimeout(gameId, playerId, card); - this.pendingAnswers.delete(key); - }, this.ANSWER_TIMEOUT_MS); - - // Store pending answer - this.pendingAnswers.set(key, { - gameId, - playerId, - card, - timeoutId, - startTime: new Date() - }); - - return key; - } - - /** - * Clear an answer timeout - * @param key The key returned from startAnswerTimeout - */ - clearAnswerTimeout(key: string): void { - const pending = this.pendingAnswers.get(key); - if (pending) { - clearTimeout(pending.timeoutId); - this.pendingAnswers.delete(key); - } - } - - /** - * Process player's answer to a question card - * @param card The question card - * @param playerAnswer Player's submitted answer - * @returns Result indicating if answer was correct and consequence to apply - */ - processAnswer(card: GameCard, playerAnswer: any): CardAnswerResult { - if (!card.answer) { - throw new Error('Card has no answer to compare against'); - } - - let validationResult: CardValidationResult; - - try { - // Use CardProcessingService for type-specific validation - validationResult = this.cardProcessingService.validateAnswer(card, playerAnswer); - } catch (error) { - // Fallback to simple string comparison if type-specific validation fails - console.warn(`Card validation failed, using fallback: ${error}`); - validationResult = this.fallbackValidation(card, playerAnswer); - } - - // For question cards, the consequence is applied only if the answer is correct - // If wrong, we apply a default negative consequence - const consequence = validationResult.isCorrect - ? (card.consequence?.type || ConsequenceType.EXTRA_TURN) - : ConsequenceType.LOSE_TURN; // Default penalty for wrong answer - - return { - correct: validationResult.isCorrect, - consequence: consequence, - description: validationResult.explanation || (validationResult.isCorrect - ? '✅ Correct!' - : '❌ Wrong answer!'), - validationDetails: validationResult - }; - } - - /** - * Process automatic wrong answer (timeout occurred) - * @param card The question card that timed out - * @returns Result with wrong consequence applied - */ - processTimeoutAnswer(card: GameCard): CardAnswerResult { - if (!card.answer) { - throw new Error('Card has no answer to compare against'); - } - - const consequence = ConsequenceType.LOSE_TURN; // Default penalty for timeout - - return { - correct: false, - consequence: consequence, - description: `⏰ Time's up! The correct answer was "${card.answer}". ${this.getConsequenceDescription(consequence, false)}` - }; - } - - /** - * Process luck card effect (no answer required) - * @param card The luck card - * @returns Result with the luck consequence to apply - */ - processLuckCard(card: GameCard): CardAnswerResult { - const consequence = card.consequence?.type || ConsequenceType.EXTRA_TURN; - - return { - correct: true, // Luck cards are always "correct" since no answer is needed - consequence: consequence, - description: `🍀 ${this.getConsequenceDescription(consequence, true)}` - }; - } - - /** - * Get the required deck type based on field type - */ - private getRequiredDeckType(fieldType: 'positive' | 'negative' | 'luck'): DeckType { - switch (fieldType) { - case 'positive': - case 'negative': - return DeckType.QUEST; // Question cards for positive/negative fields - case 'luck': - return DeckType.LUCK; // Luck cards for luck fields - default: - throw new Error(`Unsupported field type: ${fieldType}`); - } - } - - /** - * Get human-readable deck type name - */ - private getDeckTypeName(deckType: DeckType): string { - switch (deckType) { - case DeckType.QUEST: - return 'question'; - case DeckType.LUCK: - return 'luck'; - case DeckType.JOCKER: - return 'joker'; - default: - return 'unknown'; - } - } - - /** - * Get human-readable consequence description - */ - private getConsequenceDescription(consequence: ConsequenceType, isPositive: boolean): string { - switch (consequence) { - case ConsequenceType.MOVE_FORWARD: - return isPositive ? 'Move forward!' : 'Move forward anyway!'; - case ConsequenceType.MOVE_BACKWARD: - return 'Move backward!'; - case ConsequenceType.LOSE_TURN: - return 'Lose your next turn!'; - case ConsequenceType.EXTRA_TURN: - return 'Get an extra turn!'; - case ConsequenceType.GO_TO_START: - return 'Go back to start!'; - default: - return 'Unknown effect!'; - } - } - - /** - * Get remaining time for a pending answer - * @param key The key for the pending answer - * @returns Remaining time in seconds, or -1 if not found - */ - getRemainingTime(key: string): number { - const pending = this.pendingAnswers.get(key); - if (!pending) { - return -1; - } - - const elapsed = Date.now() - pending.startTime.getTime(); - const remaining = Math.max(0, this.ANSWER_TIMEOUT_MS - elapsed); - return Math.ceil(remaining / 1000); // Return in seconds - } - - /** - * Check if a player has a pending answer - * @param gameId Game ID - * @param playerId Player ID - * @returns True if player has a pending answer - */ - hasPendingAnswer(gameId: string, playerId: string): boolean { - const key = `${gameId}:${playerId}`; - return this.pendingAnswers.has(key); - } - - /** - * Fallback validation for cards without proper type information - * @param card The card to validate - * @param playerAnswer Player's answer - * @returns Basic validation result - */ - private fallbackValidation(card: GameCard, playerAnswer: any): CardValidationResult { - if (typeof card.answer !== 'string' || typeof playerAnswer !== 'string') { - return { - isCorrect: false, - submittedAnswer: playerAnswer, - explanation: 'Cannot validate non-string answers without card type information' - }; - } - - const cleanPlayerAnswer = playerAnswer.toLowerCase().trim(); - const cleanCorrectAnswer = card.answer.toLowerCase().trim(); - const isCorrect = cleanPlayerAnswer === cleanCorrectAnswer; - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: card.answer, - explanation: isCorrect - ? '✅ Correct!' - : `❌ Wrong! The correct answer was "${card.answer}".` - }; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/CardProcessingService.ts b/SerpentRace_Backend/src/Application/Services/CardProcessingService.ts deleted file mode 100644 index 65a0fa27..00000000 --- a/SerpentRace_Backend/src/Application/Services/CardProcessingService.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { GameCard } from '../../Domain/Game/GameAggregate'; -import { CardType } from '../../Domain/Deck/DeckAggregate'; - -// Type-specific answer structures -export interface QuizOption { - answer: string; // A, B, C, D - text: string; - correct: boolean; -} - -export interface CloserAnswer { - correct: number; - percent: number; -} - -/** - * Sentence pair for matching left to right - */ -export interface SentencePair { - id: string; // Unique identifier for this pair - left: string; // Left part to match - right: string; // Right part (scrambled position) -} - -/** - * Player's answer for sentence pairing (array of matches) - */ -export interface SentencePairingAnswer { - pairId: string; // ID of the pair - leftText: string; // Left part - rightText: string; // Player's chosen right part -} - -export interface CardClientData { - cardid: string; - question: string; - type: CardType; - timeLimit: number; - // Type-specific client data - answerOptions?: QuizOption[]; // For QUIZ - words?: string[]; // For SENTENCE_PAIRING (legacy scrambled words) - sentencePairs?: SentencePair[]; // For SENTENCE_PAIRING (left-right matching) - acceptableAnswers?: string[]; // For OWN_ANSWER (not sent to client) - // CLOSER and TRUE_FALSE send only question -} - -export interface CardValidationResult { - isCorrect: boolean; - submittedAnswer: any; - correctAnswer?: any; - explanation?: string; -} - -/** - * Service responsible for handling type-specific card processing - * Prepares cards for clients and validates answers based on CardType - */ -export class CardProcessingService { - - /** - * Prepare card data for client based on card type - * @param card The game card to prepare - * @returns Client-safe card data with type-specific information - */ - prepareCardForClient(card: GameCard): CardClientData { - if (!card.question || card.type === undefined) { - throw new Error('Card must have question and type defined'); - } - - const baseData: CardClientData = { - cardid: card.cardid, - question: card.question, - type: card.type, - timeLimit: 60 // Default 60 seconds for question cards - }; - - switch (card.type) { - case CardType.QUIZ: - return this.prepareQuizCard(card, baseData); - - case CardType.SENTENCE_PAIRING: - return this.prepareSentencePairingCard(card, baseData); - - case CardType.OWN_ANSWER: - return this.prepareOwnAnswerCard(card, baseData); - - case CardType.TRUE_FALSE: - return this.prepareTrueFalseCard(card, baseData); - - case CardType.CLOSER: - return this.prepareCloserCard(card, baseData); - - default: - throw new Error(`Unsupported card type: ${card.type}`); - } - } - - /** - * Validate player's answer based on card type - * @param card The game card - * @param playerAnswer Player's submitted answer - * @returns Validation result with correctness and explanation - */ - validateAnswer(card: GameCard, playerAnswer: any): CardValidationResult { - if (card.type === undefined) { - throw new Error('Card type is required for validation'); - } - - switch (card.type) { - case CardType.QUIZ: - return this.validateQuizAnswer(card, playerAnswer); - - case CardType.SENTENCE_PAIRING: - return this.validateSentencePairingAnswer(card, playerAnswer); - - case CardType.OWN_ANSWER: - return this.validateOwnAnswerAnswer(card, playerAnswer); - - case CardType.TRUE_FALSE: - return this.validateTrueFalseAnswer(card, playerAnswer); - - case CardType.CLOSER: - return this.validateCloserAnswer(card, playerAnswer); - - default: - throw new Error(`Unsupported card type for validation: ${card.type}`); - } - } - - /** - * Prepare QUIZ card with multiple choice options - */ - private prepareQuizCard(card: GameCard, baseData: CardClientData): CardClientData { - if (!Array.isArray(card.answer)) { - throw new Error('Quiz card answer must be an array of options'); - } - - return { - ...baseData, - answerOptions: card.answer as QuizOption[] - }; - } - - /** - * Prepare SENTENCE_PAIRING card with scrambled left/right pairs - * - * Expected card.answer format: - * [ - * { left: "Apple", right: "Red" }, - * { left: "Banana", right: "Yellow" }, - * { left: "Orange", right: "Orange color" } - * ] - * - * OR legacy string format: "word1 word2 word3" (will be split and scrambled) - */ - private prepareSentencePairingCard(card: GameCard, baseData: CardClientData): CardClientData { - // NEW FORMAT: Array of pairs (left-right matching) - if (Array.isArray(card.answer)) { - // Validate structure - const pairs = card.answer as Array<{ left: string; right: string }>; - if (!pairs.every(p => p.left && p.right)) { - throw new Error('Sentence pairing card answer must be array of {left, right} objects'); - } - - // Create pairs with IDs and scramble the right parts - const leftParts = pairs.map((p, idx) => ({ id: `pair_${idx}`, left: p.left, right: p.right })); - const rightParts = this.scrambleArray([...pairs.map(p => p.right)]); - - // Send left parts in order, right parts scrambled - const sentencePairs: SentencePair[] = leftParts.map((lp, idx) => ({ - id: lp.id, - left: lp.left, - right: rightParts[idx] // Scrambled position - })); - - return { - ...baseData, - sentencePairs - }; - } - - // LEGACY FORMAT: Single sentence to reconstruct (backward compatibility) - if (typeof card.answer === 'string') { - const words = card.answer.split(' ').filter(word => word.trim() !== ''); - const scrambledWords = this.scrambleArray([...words]); - - return { - ...baseData, - words: scrambledWords - }; - } - - throw new Error('Sentence pairing card answer must be array of pairs or string'); - } - - /** - * Prepare OWN_ANSWER card (only question, acceptable answers hidden) - */ - private prepareOwnAnswerCard(card: GameCard, baseData: CardClientData): CardClientData { - // Don't send acceptable answers to client - return baseData; - } - - /** - * Prepare TRUE_FALSE card (only question) - */ - private prepareTrueFalseCard(card: GameCard, baseData: CardClientData): CardClientData { - return baseData; - } - - /** - * Prepare CLOSER card (only question) - */ - private prepareCloserCard(card: GameCard, baseData: CardClientData): CardClientData { - return baseData; - } - - /** - * Validate QUIZ answer (A, B, C, D) - */ - private validateQuizAnswer(card: GameCard, playerAnswer: string): CardValidationResult { - if (!Array.isArray(card.answer)) { - throw new Error('Quiz card answer must be an array'); - } - - const options = card.answer as QuizOption[]; - const correctOption = options.find(opt => opt.correct); - - if (!correctOption) { - throw new Error('Quiz card must have one correct answer'); - } - - const isCorrect = playerAnswer.toUpperCase() === correctOption.answer.toUpperCase(); - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: correctOption.answer, - explanation: isCorrect - ? `✅ Correct! ${correctOption.text}` - : `❌ Wrong! Correct answer was ${correctOption.answer}: ${correctOption.text}` - }; - } - - /** - * Validate SENTENCE_PAIRING answer - * - * Supports two formats: - * 1. NEW: Array of { pairId, leftText, rightText } matches - * 2. LEGACY: Reconstructed sentence string or array of words - */ - private validateSentencePairingAnswer(card: GameCard, playerAnswer: any): CardValidationResult { - // NEW FORMAT: Array of pairs (left-right matching) - if (Array.isArray(card.answer) && card.answer.every((p: any) => p.left && p.right)) { - const correctPairs = card.answer as Array<{ left: string; right: string }>; - - // Player answer should be array of SentencePairingAnswer objects - if (!Array.isArray(playerAnswer)) { - throw new Error('Player answer must be array of pair matches'); - } - - const playerMatches = playerAnswer as SentencePairingAnswer[]; - - // Check if all pairs match correctly - let correctCount = 0; - const results: string[] = []; - - for (const correctPair of correctPairs) { - const playerMatch = playerMatches.find(pm => - pm.leftText.toLowerCase().trim() === correctPair.left.toLowerCase().trim() - ); - - if (playerMatch) { - const isMatch = playerMatch.rightText.toLowerCase().trim() === - correctPair.right.toLowerCase().trim(); - if (isMatch) { - correctCount++; - results.push(`✓ "${correctPair.left}" → "${correctPair.right}"`); - } else { - results.push(`✗ "${correctPair.left}" → "${playerMatch.rightText}" (should be "${correctPair.right}")`); - } - } else { - results.push(`✗ "${correctPair.left}" → (not matched)`); - } - } - - const isCorrect = correctCount === correctPairs.length; - - return { - isCorrect, - submittedAnswer: playerMatches, - correctAnswer: correctPairs, - explanation: isCorrect - ? `✅ Perfect! All ${correctCount} pairs matched correctly!\n${results.join('\n')}` - : `❌ Only ${correctCount}/${correctPairs.length} pairs correct:\n${results.join('\n')}` - }; - } - - // LEGACY FORMAT: Single sentence to reconstruct (backward compatibility) - if (typeof card.answer === 'string') { - // Handle both array of words and joined string - const reconstructed = Array.isArray(playerAnswer) - ? playerAnswer.join(' ').toLowerCase().trim() - : (typeof playerAnswer === 'string' ? playerAnswer.toLowerCase().trim() : ''); - - const correctSentence = card.answer.toLowerCase().trim(); - const isCorrect = reconstructed === correctSentence; - - return { - isCorrect, - submittedAnswer: reconstructed, - correctAnswer: card.answer, - explanation: isCorrect - ? '✅ Perfect! You arranged the sentence correctly!' - : `❌ Wrong order! Correct sentence: "${card.answer}"` - }; - } - - throw new Error('Sentence pairing card answer must be array of pairs or string'); - } - - /** - * Validate OWN_ANSWER (check against acceptable answers array) - */ - private validateOwnAnswerAnswer(card: GameCard, playerAnswer: string): CardValidationResult { - if (!Array.isArray(card.answer)) { - throw new Error('Own answer card must have array of acceptable answers'); - } - - const acceptableAnswers = card.answer as string[]; - const cleanPlayerAnswer = playerAnswer.toLowerCase().trim(); - - const isCorrect = acceptableAnswers.some(acceptable => - acceptable.toLowerCase().trim() === cleanPlayerAnswer - ); - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: acceptableAnswers, - explanation: isCorrect - ? '✅ Correct! Your answer is acceptable.' - : `❌ Your answer doesn't match any acceptable responses.` - }; - } - - /** - * Validate TRUE_FALSE answer - */ - private validateTrueFalseAnswer(card: GameCard, playerAnswer: string): CardValidationResult { - if (typeof card.answer !== 'boolean' && typeof card.answer !== 'string') { - throw new Error('True/false card answer must be boolean or string'); - } - - // Convert player answer to boolean - const playerBool = this.convertToBoolean(playerAnswer); - const correctBool = typeof card.answer === 'boolean' - ? card.answer - : this.convertToBoolean(card.answer); - - const isCorrect = playerBool === correctBool; - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: correctBool ? 'True' : 'False', - explanation: isCorrect - ? '✅ Correct!' - : `❌ Wrong! The correct answer is ${correctBool ? 'True' : 'False'}.` - }; - } - - /** - * Validate CLOSER answer (numerical proximity) - */ - private validateCloserAnswer(card: GameCard, playerAnswer: string | number): CardValidationResult { - if (typeof card.answer !== 'object' || !card.answer.correct || !card.answer.percent) { - throw new Error('Closer card answer must have correct and percent fields'); - } - - const closerAnswer = card.answer as CloserAnswer; - const playerNumber = typeof playerAnswer === 'number' - ? playerAnswer - : parseFloat(playerAnswer.toString()); - - if (isNaN(playerNumber)) { - return { - isCorrect: false, - submittedAnswer: playerAnswer, - correctAnswer: closerAnswer.correct, - explanation: '❌ Invalid number! Please enter a valid numeric answer.' - }; - } - - const tolerance = closerAnswer.correct * (closerAnswer.percent / 100); - const minValue = closerAnswer.correct - tolerance; - const maxValue = closerAnswer.correct + tolerance; - - const isCorrect = playerNumber >= minValue && playerNumber <= maxValue; - - return { - isCorrect, - submittedAnswer: playerNumber, - correctAnswer: closerAnswer.correct, - explanation: isCorrect - ? `✅ Close enough! Correct answer: ${closerAnswer.correct}` - : `❌ Not close enough! Correct answer: ${closerAnswer.correct} (±${closerAnswer.percent}%)` - }; - } - - /** - * Convert string to boolean for TRUE_FALSE validation - */ - private convertToBoolean(value: string): boolean { - const lowerValue = value.toLowerCase().trim(); - return ['true', 'yes', '1', 'correct', 'right', 'igaz'].includes(lowerValue); - } - - /** - * Scramble array elements randomly - */ - private scrambleArray(array: T[]): T[] { - const scrambled = [...array]; - for (let i = scrambled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [scrambled[i], scrambled[j]] = [scrambled[j], scrambled[i]]; - } - return scrambled; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/ContactEmailService.ts b/SerpentRace_Backend/src/Application/Services/ContactEmailService.ts deleted file mode 100644 index 969537e4..00000000 --- a/SerpentRace_Backend/src/Application/Services/ContactEmailService.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { IContactRepository } from '../../Domain/IRepository/IContactRepository'; -import { EmailService } from './EmailService'; -import { ContactType } from '../../Domain/Contact/ContactAggregate'; -import { logOther, logError } from './Logger'; -import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper'; - -export interface EmailResponseData { - to: string; - message: string; - contactId: string; - adminUserId: string; - contactName: string; - contactType: ContactType; - originalMessage: string; - language?: 'en' | 'hu' | 'de'; // Default to 'en' if not specified -} - -export class ContactEmailService { - constructor( - private readonly contactRepo: IContactRepository, - private readonly emailService: EmailService - ) {} - - async sendResponse(responseData: EmailResponseData): Promise { - try { - // First update the contact with the response - await this.contactRepo.update(responseData.contactId, { - adminResponse: responseData.message, - responseDate: new Date(), - respondedBy: responseData.adminUserId, - }); - - // Determine language and template - const language = responseData.language || 'en'; - const templateName = language === 'en' ? 'contact-response' : `contact-response-${language}`; - - // Prepare template data - const templateData = { - contactName: responseData.contactName, - contactTypeString: this.getContactTypeString(responseData.contactType, language), - contactTypeBadge: this.getContactTypeBadge(responseData.contactType), - originalMessage: responseData.originalMessage, - adminResponse: responseData.message, - companyName: 'SerpentRace', - supportEmail: 'support@serpentrace.com' - }; - - // Send email using EmailService with template - const emailSent = await this.emailService.sendEmail({ - to: responseData.to, - subject: this.getLocalizedContactResponseSubject(language), - template: templateName, - templateData - }); - - if (emailSent) { - logOther('Contact response email sent successfully', { - to: responseData.to, - subject: this.getLocalizedContactResponseSubject(language), - contactId: responseData.contactId, - respondedBy: responseData.adminUserId, - language - }); - } else { - throw new Error('Email service failed to send email'); - } - - } catch (error) { - logError('Failed to send contact response email', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to send email response'); - } - } - - private getLocalizedContactResponseSubject(language: 'en' | 'hu' | 'de'): string { - const subjects: LocalizedSubjects = { - contactResponse: { - en: 'SerpentRace - Response to Your Message', - hu: 'SerpentRace - VĂĄlasz az ĂŒzenetĂ©re', - de: 'SerpentRace - Antwort auf Ihre Nachricht' - } - }; - return EmailTemplateHelper.getLocalizedSubject('contactResponse', subjects, language); - } - - private getContactTypeString(type: ContactType, language: 'en' | 'hu' | 'de' = 'en'): string { - const translations = { - [ContactType.BUG]: { - en: 'Bug Report', - hu: 'Hiba bejelentĂ©s', - de: 'Fehlerbericht' - }, - [ContactType.PROBLEM]: { - en: 'Problem', - hu: 'ProblĂ©ma', - de: 'Problem' - }, - [ContactType.QUESTION]: { - en: 'Question', - hu: 'KĂ©rdĂ©s', - de: 'Frage' - }, - [ContactType.SALES]: { - en: 'Sales Inquiry', - hu: 'ÉrtĂ©kesĂ­tĂ©si kĂ©rdĂ©s', - de: 'Verkaufsanfrage' - }, - [ContactType.OTHER]: { - en: 'General Inquiry', - hu: 'ÁltalĂĄnos kĂ©rdĂ©s', - de: 'Allgemeine Anfrage' - } - }; - - return translations[type]?.[language] || translations[type]?.['en'] || 'Contact'; - } - - private getContactTypeBadge(type: ContactType): string { - switch (type) { - case ContactType.BUG: - return 'bug'; - case ContactType.PROBLEM: - return 'problem'; - case ContactType.QUESTION: - return 'question'; - case ContactType.SALES: - return 'sales'; - case ContactType.OTHER: - return 'other'; - default: - return 'other'; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/DIContainer.ts b/SerpentRace_Backend/src/Application/Services/DIContainer.ts deleted file mode 100644 index 0962d215..00000000 --- a/SerpentRace_Backend/src/Application/Services/DIContainer.ts +++ /dev/null @@ -1,600 +0,0 @@ -// Repository Interfaces -import { IUserRepository } from '../../Domain/IRepository/IUserRepository'; -import { IChatRepository } from '../../Domain/IRepository/IChatRepository'; -import { IChatArchiveRepository } from '../../Domain/IRepository/IChatArchiveRepository'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository'; -import { IContactRepository } from '../../Domain/IRepository/IContactRepository'; -import { IGameRepository } from '../../Domain/IRepository/IGameRepository'; -import { ITurnHistoryRepository } from '../../Domain/IRepository/ITurnHistoryRepository'; -import { IGameSnapshotRepository } from '../../Domain/IRepository/IGameSnapshotRepository'; - -// Repository Implementations -import { UserRepository } from '../../Infrastructure/Repository/UserRepository'; -import { ChatRepository } from '../../Infrastructure/Repository/ChatRepository'; -import { ChatArchiveRepository } from '../../Infrastructure/Repository/ChatArchiveRepository'; -import { DeckRepository } from '../../Infrastructure/Repository/DeckRepository'; -import { OrganizationRepository } from '../../Infrastructure/Repository/OrganizationRepository'; -import { ContactRepository } from '../../Infrastructure/Repository/ContactRepository'; -import { GameRepository } from '../../Infrastructure/Repository/GameRepository'; -import { TurnHistoryRepository } from '../../Infrastructure/Repository/TurnHistoryRepository'; -import { GameSnapshotRepository } from '../../Infrastructure/Repository/GameSnapshotRepository'; - -// Command Handlers -import { CreateUserCommandHandler } from '../User/commands/CreateUserCommandHandler'; -import { LoginCommandHandler } from '../User/commands/LoginCommandHandler'; -import { LogoutCommandHandler } from '../User/commands/LogoutCommandHandler'; -import { UpdateUserCommandHandler } from '../User/commands/UpdateUserCommandHandler'; -import { DeactivateUserCommandHandler } from '../User/commands/DeactivateUserCommandHandler'; -import { DeleteUserCommandHandler } from '../User/commands/DeleteUserCommandHandler'; -import { VerifyEmailCommandHandler } from '../User/commands/VerifyEmailCommandHandler'; -import { RequestPasswordResetCommandHandler } from '../User/commands/RequestPasswordResetCommandHandler'; -import { ResetPasswordCommandHandler } from '../User/commands/ResetPasswordCommandHandler'; -import { CreateChatCommandHandler } from '../Chat/commands/CreateChatCommandHandler'; -import { SendMessageCommandHandler } from '../Chat/commands/SendMessageCommandHandler'; -import { ArchiveChatCommandHandler, RestoreChatCommandHandler } from '../Chat/commands/ChatArchiveCommandHandlers'; -import { CreateDeckCommandHandler } from '../Deck/commands/CreateDeckCommandHandler'; -import { UpdateDeckCommandHandler } from '../Deck/commands/UpdateDeckCommandHandler'; -import { DeleteDeckCommandHandler } from '../Deck/commands/DeleteDeckCommandHandler'; -import { CreateOrganizationCommandHandler } from '../Organization/commands/CreateOrganizationCommandHandler'; -import { UpdateOrganizationCommandHandler } from '../Organization/commands/UpdateOrganizationCommandHandler'; -import { DeleteOrganizationCommandHandler } from '../Organization/commands/DeleteOrganizationCommandHandler'; -import { ProcessOrgAuthCallbackCommandHandler } from '../Organization/commands/ProcessOrgAuthCallbackCommandHandler'; -import { CreateContactCommandHandler } from '../Contact/commands/CreateContactCommandHandler'; -import { UpdateContactCommandHandler } from '../Contact/commands/UpdateContactCommandHandler'; -import { DeleteContactCommandHandler } from '../Contact/commands/DeleteContactCommandHandler'; -import { ActivateUserCommandHandler } from '../User/commands/ActivateUserCommandHandler'; - -// Query Handlers -import { GetUserByIdQueryHandler } from '../User/queries/GetUserByIdQueryHandler'; -import { GetUsersByPageQueryHandler } from '../User/queries/GetUsersByPageQueryHandler'; -import { GetUserChatsQueryHandler } from '../Chat/queries/GetUserChatsQueryHandler'; -import { GetChatHistoryQueryHandler, GetArchivedChatsQueryHandler } from '../Chat/queries/ChatHistoryQueryHandlers'; -import { GetChatsByPageQueryHandler } from '../Chat/queries/GetChatsByPageQueryHandler'; -import { GetDeckByIdQueryHandler } from '../Deck/queries/GetDeckByIdQueryHandler'; -import { GetDecksByPageQueryHandler } from '../Deck/queries/GetDecksByPageQueryHandler'; -import { GetOrganizationByIdQueryHandler } from '../Organization/queries/GetOrganizationByIdQueryHandler'; -import { GetOrganizationsByPageQueryHandler } from '../Organization/queries/GetOrganizationsByPageQueryHandler'; -import { GetOrganizationLoginUrlQueryHandler } from '../Organization/queries/GetOrganizationLoginUrlQueryHandler'; -import { GetContactByIdQueryHandler } from '../Contact/queries/GetContactByIdQueryHandler'; -import { GetContactsByPageQueryHandler } from '../Contact/queries/GetContactsByPageQueryHandler'; - -// Services -import { JWTService } from './JWTService'; -import { EmailService } from './EmailService'; -import { GameTokenService } from './GameTokenService'; -import { ContactEmailService } from './ContactEmailService'; -import { DeckImportExportService } from './DeckImportExportService'; -import { FieldEffectService } from './FieldEffectService'; -import { CardDrawingService } from './CardDrawingService'; -import { GamemasterService } from './GamemasterService'; -import { RedisService } from './RedisService'; -import { GameService } from '../Game/GameService'; -import { BoardGenerationService } from '../Game/BoardGenerationService'; -import { GenerateBoardCommandHandler } from '../Game/commands/GenerateBoardCommandHandler'; -import { GameWebSocketService } from './GameWebSocketService'; -import type { Server as SocketIOServer } from 'socket.io'; - -/** - * Central Dependency Injection Container - * Manages all repositories, command handlers, and query handlers as singletons - */ -export class DIContainer { - private static instance: DIContainer; - - // Repositories - Using interfaces for better abstraction - private _userRepository: IUserRepository | null = null; - private _chatRepository: IChatRepository | null = null; - private _chatArchiveRepository: IChatArchiveRepository | null = null; - private _deckRepository: IDeckRepository | null = null; - private _organizationRepository: IOrganizationRepository | null = null; - private _contactRepository: IContactRepository | null = null; - private _gameRepository: IGameRepository | null = null; - private _turnHistoryRepository: ITurnHistoryRepository | null = null; - private _gameSnapshotRepository: IGameSnapshotRepository | null = null; - - // Services - private _jwtService: JWTService | null = null; - private _emailService: EmailService | null = null; - private _gameTokenService: GameTokenService | null = null; - private _contactEmailService: ContactEmailService | null = null; - private _deckImportExportService: DeckImportExportService | null = null; - private _cardDrawingService: CardDrawingService | null = null; - private _gamemasterService: GamemasterService | null = null; - private _fieldEffectService: FieldEffectService | null = null; - private _gameService: GameService | null = null; - private _boardGenerationService: BoardGenerationService | null = null; - private _gameWebSocketService: GameWebSocketService | null = null; - private _socketIOInstance: SocketIOServer | null = null; - - // Command Handlers - private _createUserCommandHandler: CreateUserCommandHandler | null = null; - private _loginCommandHandler: LoginCommandHandler | null = null; - private _logoutCommandHandler: LogoutCommandHandler | null = null; - private _updateUserCommandHandler: UpdateUserCommandHandler | null = null; - private _deactivateUserCommandHandler: DeactivateUserCommandHandler | null = null; - private _deleteUserCommandHandler: DeleteUserCommandHandler | null = null; - private _verifyEmailCommandHandler: VerifyEmailCommandHandler | null = null; - private _requestPasswordResetCommandHandler: RequestPasswordResetCommandHandler | null = null; - private _resetPasswordCommandHandler: ResetPasswordCommandHandler | null = null; - private _createChatCommandHandler: CreateChatCommandHandler | null = null; - private _sendMessageCommandHandler: SendMessageCommandHandler | null = null; - private _archiveChatCommandHandler: ArchiveChatCommandHandler | null = null; - private _restoreChatCommandHandler: RestoreChatCommandHandler | null = null; - private _createDeckCommandHandler: CreateDeckCommandHandler | null = null; - private _updateDeckCommandHandler: UpdateDeckCommandHandler | null = null; - private _deleteDeckCommandHandler: DeleteDeckCommandHandler | null = null; - private _createOrganizationCommandHandler: CreateOrganizationCommandHandler | null = null; - private _updateOrganizationCommandHandler: UpdateOrganizationCommandHandler | null = null; - private _deleteOrganizationCommandHandler: DeleteOrganizationCommandHandler | null = null; - private _processOrgAuthCallbackCommandHandler: ProcessOrgAuthCallbackCommandHandler | null = null; - private _createContactCommandHandler: CreateContactCommandHandler | null = null; - private _updateContactCommandHandler: UpdateContactCommandHandler | null = null; - private _deleteContactCommandHandler: DeleteContactCommandHandler | null = null; - private _generateBoardCommandHandler: GenerateBoardCommandHandler | null = null; - private _activateUserCommandHandler: ActivateUserCommandHandler | null = null; - - // Query Handlers - private _getUserByIdQueryHandler: GetUserByIdQueryHandler | null = null; - private _getUsersByPageQueryHandler: GetUsersByPageQueryHandler | null = null; - private _getUserChatsQueryHandler: GetUserChatsQueryHandler | null = null; - private _getChatHistoryQueryHandler: GetChatHistoryQueryHandler | null = null; - private _getArchivedChatsQueryHandler: GetArchivedChatsQueryHandler | null = null; - private _getChatsByPageQueryHandler: GetChatsByPageQueryHandler | null = null; - private _getDeckByIdQueryHandler: GetDeckByIdQueryHandler | null = null; - private _getDecksByPageQueryHandler: GetDecksByPageQueryHandler | null = null; - private _getOrganizationByIdQueryHandler: GetOrganizationByIdQueryHandler | null = null; - private _getOrganizationsByPageQueryHandler: GetOrganizationsByPageQueryHandler | null = null; - private _getOrganizationLoginUrlQueryHandler: GetOrganizationLoginUrlQueryHandler | null = null; - private _getContactByIdQueryHandler: GetContactByIdQueryHandler | null = null; - private _getContactsByPageQueryHandler: GetContactsByPageQueryHandler | null = null; - - private constructor() {} - - public static getInstance(): DIContainer { - if (!DIContainer.instance) { - DIContainer.instance = new DIContainer(); - } - return DIContainer.instance; - } - - // Repository getters - Return interfaces for better abstraction - public get userRepository(): IUserRepository { - if (!this._userRepository) { - this._userRepository = new UserRepository(); - } - return this._userRepository; - } - - public get chatRepository(): IChatRepository { - if (!this._chatRepository) { - this._chatRepository = new ChatRepository(); - } - return this._chatRepository; - } - - public get chatArchiveRepository(): IChatArchiveRepository { - if (!this._chatArchiveRepository) { - this._chatArchiveRepository = new ChatArchiveRepository(); - } - return this._chatArchiveRepository; - } - - public get deckRepository(): IDeckRepository { - if (!this._deckRepository) { - this._deckRepository = new DeckRepository(); - } - return this._deckRepository; - } - - public get organizationRepository(): IOrganizationRepository { - if (!this._organizationRepository) { - this._organizationRepository = new OrganizationRepository(); - } - return this._organizationRepository; - } - - public get contactRepository(): IContactRepository { - if (!this._contactRepository) { - this._contactRepository = new ContactRepository(); - } - return this._contactRepository; - } - - public get gameRepository(): IGameRepository { - if (!this._gameRepository) { - this._gameRepository = new GameRepository(); - } - return this._gameRepository; - } - - public get turnHistoryRepository(): ITurnHistoryRepository { - if (!this._turnHistoryRepository) { - this._turnHistoryRepository = new TurnHistoryRepository(); - } - return this._turnHistoryRepository; - } - - public get gameSnapshotRepository(): IGameSnapshotRepository { - if (!this._gameSnapshotRepository) { - this._gameSnapshotRepository = new GameSnapshotRepository(); - } - return this._gameSnapshotRepository; - } - - // Services getters - public get jwtService(): JWTService { - if (!this._jwtService) { - this._jwtService = new JWTService(); - } - return this._jwtService; - } - - public get emailService(): EmailService { - if (!this._emailService) { - this._emailService = new EmailService(); - } - return this._emailService; - } - - public get gameTokenService(): GameTokenService { - if (!this._gameTokenService) { - this._gameTokenService = new GameTokenService(); - } - return this._gameTokenService; - } - - public get contactEmailService(): ContactEmailService { - if (!this._contactEmailService) { - this._contactEmailService = new ContactEmailService(this.contactRepository, this.emailService); - } - return this._contactEmailService; - } - - public get deckImportExportService(): DeckImportExportService { - if (!this._deckImportExportService) { - this._deckImportExportService = new DeckImportExportService(this.deckRepository); - } - return this._deckImportExportService; - } - - public get cardDrawingService(): CardDrawingService { - if (!this._cardDrawingService) { - this._cardDrawingService = new CardDrawingService(); - } - return this._cardDrawingService; - } - - public get gamemasterService(): GamemasterService { - if (!this._gamemasterService) { - this._gamemasterService = new GamemasterService(); - } - return this._gamemasterService; - } - - public get fieldEffectService(): FieldEffectService { - if (!this._fieldEffectService) { - this._fieldEffectService = new FieldEffectService( - this.boardGenerationService, - this.gamemasterService - ); - } - return this._fieldEffectService; - } - - public get gameService(): GameService { - if (!this._gameService) { - this._gameService = new GameService(); - } - return this._gameService; - } - - public get boardGenerationService(): BoardGenerationService { - if (!this._boardGenerationService) { - this._boardGenerationService = new BoardGenerationService(); - } - return this._boardGenerationService; - } - - /** - * Set the Socket.IO instance (must be called before accessing gameWebSocketService) - */ - public setSocketIO(io: SocketIOServer): void { - this._socketIOInstance = io; - // Reset gameWebSocketService so it gets recreated with new IO instance - this._gameWebSocketService = null; - } - - public get gameWebSocketService(): GameWebSocketService { - if (!this._gameWebSocketService) { - if (!this._socketIOInstance) { - throw new Error('Socket.IO instance must be set before accessing gameWebSocketService. Call setSocketIO() first.'); - } - this._gameWebSocketService = new GameWebSocketService( - this._socketIOInstance, - this.gameRepository as any, // Cast to concrete type - this.userRepository as any, // Cast to concrete type - RedisService.getInstance(), - this.turnHistoryRepository as any, // Cast to concrete type - this.gameSnapshotRepository as any // Cast to concrete type - ); - } - return this._gameWebSocketService; - } - - // Command Handler getters - public get createUserCommandHandler(): CreateUserCommandHandler { - if (!this._createUserCommandHandler) { - this._createUserCommandHandler = new CreateUserCommandHandler(this.userRepository, this.emailService); - } - return this._createUserCommandHandler; - } - - public get loginCommandHandler(): LoginCommandHandler { - if (!this._loginCommandHandler) { - this._loginCommandHandler = new LoginCommandHandler(this.userRepository, this.jwtService, this.organizationRepository); - } - return this._loginCommandHandler; - } - - public get logoutCommandHandler(): LogoutCommandHandler { - if (!this._logoutCommandHandler) { - this._logoutCommandHandler = new LogoutCommandHandler(this.userRepository); - } - return this._logoutCommandHandler; - } - - public get updateUserCommandHandler(): UpdateUserCommandHandler { - if (!this._updateUserCommandHandler) { - this._updateUserCommandHandler = new UpdateUserCommandHandler(this.userRepository); - } - return this._updateUserCommandHandler; - } - - public get deactivateUserCommandHandler(): DeactivateUserCommandHandler { - if (!this._deactivateUserCommandHandler) { - this._deactivateUserCommandHandler = new DeactivateUserCommandHandler(this.userRepository); - } - return this._deactivateUserCommandHandler; - } - - public get activateUserCommandHandler(): ActivateUserCommandHandler { - if (!this._activateUserCommandHandler) { - this._activateUserCommandHandler = new ActivateUserCommandHandler(this.userRepository); - } - return this._activateUserCommandHandler; - } - - public get deleteUserCommandHandler(): DeleteUserCommandHandler { - if (!this._deleteUserCommandHandler) { - this._deleteUserCommandHandler = new DeleteUserCommandHandler(this.userRepository); - } - return this._deleteUserCommandHandler; - } - - public get verifyEmailCommandHandler(): VerifyEmailCommandHandler { - if (!this._verifyEmailCommandHandler) { - this._verifyEmailCommandHandler = new VerifyEmailCommandHandler(this.userRepository); - } - return this._verifyEmailCommandHandler; - } - - public get requestPasswordResetCommandHandler(): RequestPasswordResetCommandHandler { - if (!this._requestPasswordResetCommandHandler) { - this._requestPasswordResetCommandHandler = new RequestPasswordResetCommandHandler(this.userRepository, this.emailService); - } - return this._requestPasswordResetCommandHandler; - } - - public get resetPasswordCommandHandler(): ResetPasswordCommandHandler { - if (!this._resetPasswordCommandHandler) { - this._resetPasswordCommandHandler = new ResetPasswordCommandHandler(this.userRepository); - } - return this._resetPasswordCommandHandler; - } - - public get createChatCommandHandler(): CreateChatCommandHandler { - if (!this._createChatCommandHandler) { - this._createChatCommandHandler = new CreateChatCommandHandler(this.chatRepository, this.userRepository); - } - return this._createChatCommandHandler; - } - - public get sendMessageCommandHandler(): SendMessageCommandHandler { - if (!this._sendMessageCommandHandler) { - this._sendMessageCommandHandler = new SendMessageCommandHandler(this.chatRepository); - } - return this._sendMessageCommandHandler; - } - - public get archiveChatCommandHandler(): ArchiveChatCommandHandler { - if (!this._archiveChatCommandHandler) { - this._archiveChatCommandHandler = new ArchiveChatCommandHandler(this.chatRepository); - } - return this._archiveChatCommandHandler; - } - - public get restoreChatCommandHandler(): RestoreChatCommandHandler { - if (!this._restoreChatCommandHandler) { - this._restoreChatCommandHandler = new RestoreChatCommandHandler(this.chatRepository); - } - return this._restoreChatCommandHandler; - } - - public get createDeckCommandHandler(): CreateDeckCommandHandler { - if (!this._createDeckCommandHandler) { - this._createDeckCommandHandler = new CreateDeckCommandHandler( - this.deckRepository, - this.userRepository, - this.organizationRepository - ); - } - return this._createDeckCommandHandler; - } - - public get updateDeckCommandHandler(): UpdateDeckCommandHandler { - if (!this._updateDeckCommandHandler) { - this._updateDeckCommandHandler = new UpdateDeckCommandHandler(this.deckRepository); - } - return this._updateDeckCommandHandler; - } - - public get deleteDeckCommandHandler(): DeleteDeckCommandHandler { - if (!this._deleteDeckCommandHandler) { - this._deleteDeckCommandHandler = new DeleteDeckCommandHandler(this.deckRepository); - } - return this._deleteDeckCommandHandler; - } - - public get createOrganizationCommandHandler(): CreateOrganizationCommandHandler { - if (!this._createOrganizationCommandHandler) { - this._createOrganizationCommandHandler = new CreateOrganizationCommandHandler(this.organizationRepository); - } - return this._createOrganizationCommandHandler; - } - - public get updateOrganizationCommandHandler(): UpdateOrganizationCommandHandler { - if (!this._updateOrganizationCommandHandler) { - this._updateOrganizationCommandHandler = new UpdateOrganizationCommandHandler(this.organizationRepository); - } - return this._updateOrganizationCommandHandler; - } - - public get deleteOrganizationCommandHandler(): DeleteOrganizationCommandHandler { - if (!this._deleteOrganizationCommandHandler) { - this._deleteOrganizationCommandHandler = new DeleteOrganizationCommandHandler(this.organizationRepository); - } - return this._deleteOrganizationCommandHandler; - } - - public get processOrgAuthCallbackCommandHandler(): ProcessOrgAuthCallbackCommandHandler { - if (!this._processOrgAuthCallbackCommandHandler) { - this._processOrgAuthCallbackCommandHandler = new ProcessOrgAuthCallbackCommandHandler(this.userRepository, this.organizationRepository); - } - return this._processOrgAuthCallbackCommandHandler; - } - - public get createContactCommandHandler(): CreateContactCommandHandler { - if (!this._createContactCommandHandler) { - this._createContactCommandHandler = new CreateContactCommandHandler(this.contactRepository); - } - return this._createContactCommandHandler; - } - - public get updateContactCommandHandler(): UpdateContactCommandHandler { - if (!this._updateContactCommandHandler) { - this._updateContactCommandHandler = new UpdateContactCommandHandler(this.contactRepository); - } - return this._updateContactCommandHandler; - } - - public get deleteContactCommandHandler(): DeleteContactCommandHandler { - if (!this._deleteContactCommandHandler) { - this._deleteContactCommandHandler = new DeleteContactCommandHandler(this.contactRepository); - } - return this._deleteContactCommandHandler; - } - - public get generateBoardCommandHandler(): GenerateBoardCommandHandler { - if (!this._generateBoardCommandHandler) { - this._generateBoardCommandHandler = new GenerateBoardCommandHandler(this.boardGenerationService, RedisService.getInstance()); - } - return this._generateBoardCommandHandler; - } - - // Query Handler getters - public get getUserByIdQueryHandler(): GetUserByIdQueryHandler { - if (!this._getUserByIdQueryHandler) { - this._getUserByIdQueryHandler = new GetUserByIdQueryHandler(this.userRepository); - } - return this._getUserByIdQueryHandler; - } - - public get getUserChatsQueryHandler(): GetUserChatsQueryHandler { - if (!this._getUserChatsQueryHandler) { - this._getUserChatsQueryHandler = new GetUserChatsQueryHandler(this.chatRepository, this.chatArchiveRepository); - } - return this._getUserChatsQueryHandler; - } - - public get getChatHistoryQueryHandler(): GetChatHistoryQueryHandler { - if (!this._getChatHistoryQueryHandler) { - this._getChatHistoryQueryHandler = new GetChatHistoryQueryHandler(this.chatRepository, this.chatArchiveRepository); - } - return this._getChatHistoryQueryHandler; - } - - public get getArchivedChatsQueryHandler(): GetArchivedChatsQueryHandler { - if (!this._getArchivedChatsQueryHandler) { - this._getArchivedChatsQueryHandler = new GetArchivedChatsQueryHandler(this.chatArchiveRepository); - } - return this._getArchivedChatsQueryHandler; - } - - public get getDeckByIdQueryHandler(): GetDeckByIdQueryHandler { - if (!this._getDeckByIdQueryHandler) { - this._getDeckByIdQueryHandler = new GetDeckByIdQueryHandler(this.deckRepository); - } - return this._getDeckByIdQueryHandler; - } - - public get getOrganizationByIdQueryHandler(): GetOrganizationByIdQueryHandler { - if (!this._getOrganizationByIdQueryHandler) { - this._getOrganizationByIdQueryHandler = new GetOrganizationByIdQueryHandler(this.organizationRepository); - } - return this._getOrganizationByIdQueryHandler; - } - - public get getOrganizationLoginUrlQueryHandler(): GetOrganizationLoginUrlQueryHandler { - if (!this._getOrganizationLoginUrlQueryHandler) { - this._getOrganizationLoginUrlQueryHandler = new GetOrganizationLoginUrlQueryHandler(this.organizationRepository); - } - return this._getOrganizationLoginUrlQueryHandler; - } - - public get getContactByIdQueryHandler(): GetContactByIdQueryHandler { - if (!this._getContactByIdQueryHandler) { - this._getContactByIdQueryHandler = new GetContactByIdQueryHandler(this.contactRepository); - } - return this._getContactByIdQueryHandler; - } - - public get getContactsByPageQueryHandler(): GetContactsByPageQueryHandler { - if (!this._getContactsByPageQueryHandler) { - this._getContactsByPageQueryHandler = new GetContactsByPageQueryHandler(this.contactRepository); - } - return this._getContactsByPageQueryHandler; - } - - // New paginated query handlers - public get getUsersByPageQueryHandler(): GetUsersByPageQueryHandler { - if (!this._getUsersByPageQueryHandler) { - this._getUsersByPageQueryHandler = new GetUsersByPageQueryHandler(this.userRepository); - } - return this._getUsersByPageQueryHandler; - } - - public get getDecksByPageQueryHandler(): GetDecksByPageQueryHandler { - if (!this._getDecksByPageQueryHandler) { - this._getDecksByPageQueryHandler = new GetDecksByPageQueryHandler(this.deckRepository); - } - return this._getDecksByPageQueryHandler; - } - - public get getOrganizationsByPageQueryHandler(): GetOrganizationsByPageQueryHandler { - if (!this._getOrganizationsByPageQueryHandler) { - this._getOrganizationsByPageQueryHandler = new GetOrganizationsByPageQueryHandler(this.organizationRepository); - } - return this._getOrganizationsByPageQueryHandler; - } - - public get getChatsByPageQueryHandler(): GetChatsByPageQueryHandler { - if (!this._getChatsByPageQueryHandler) { - this._getChatsByPageQueryHandler = new GetChatsByPageQueryHandler(this.chatRepository); - } - return this._getChatsByPageQueryHandler; - } -} - -// Export singleton instance -export const container = DIContainer.getInstance(); diff --git a/SerpentRace_Backend/src/Application/Services/DeckImportExportService.ts b/SerpentRace_Backend/src/Application/Services/DeckImportExportService.ts deleted file mode 100644 index 3d3f1978..00000000 --- a/SerpentRace_Backend/src/Application/Services/DeckImportExportService.ts +++ /dev/null @@ -1,208 +0,0 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import { DeckAggregate, State, CType } from '../../Domain/Deck/DeckAggregate'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { logError, logAuth } from './Logger'; - -export interface SprDeckData { - name: string; - type: number; - cards: any[]; - ctype: number; - exportDate: string; - version: string; -} - -export interface ImportDeckCommand { - name: string; - type: number; - cards: any[]; - ctype?: number; - userid: string; -} - -export class DeckImportExportService { - private readonly encryptionKey: string; - private readonly algorithm = 'aes-256-gcm'; - - constructor(private readonly deckRepo: IDeckRepository) { - this.encryptionKey = process.env.DECK_ENCRYPTION_KEY || 'your-32-byte-encryption-key-here!!'; - - if (this.encryptionKey.length !== 32) { - throw new Error('DECK_ENCRYPTION_KEY must be exactly 32 characters long'); - } - } - - async exportDeckToSpr(deckId: string, userId: string): Promise { - try { - const deck = await this.deckRepo.findByIdIncludingDeleted(deckId); - - if (!deck) { - throw new Error('Deck not found'); - } - - if (deck.userid !== userId) { - throw new Error('Unauthorized: You can only export your own decks'); - } - - const deckData: SprDeckData = { - name: deck.name, - type: deck.type, - cards: deck.cards, - ctype: deck.ctype, - exportDate: new Date().toISOString(), - version: '1.0' - }; - - const jsonString = JSON.stringify(deckData); - const encrypted = this.encrypt(jsonString); - - logAuth('Deck exported to SPR format', userId, { - deckId: deck.id, - deckName: deck.name, - cardCount: deck.cards.length - }); - - return encrypted; - } catch (error) { - logError('Failed to export deck to SPR', error as Error); - throw error; - } - } - - async importDeckFromSpr(sprData: Buffer, userId: string): Promise { - try { - const decrypted = this.decrypt(sprData); - const deckData: SprDeckData = JSON.parse(decrypted); - - // Validate required fields - if (!deckData.name || !deckData.cards || deckData.type === undefined) { - throw new Error('Invalid SPR file format: missing required fields'); - } - - // Create new deck - const newDeck = new DeckAggregate(); - newDeck.name = deckData.name; - newDeck.type = deckData.type; - newDeck.userid = userId; - newDeck.cards = deckData.cards; - newDeck.ctype = deckData.ctype || CType.PUBLIC; - newDeck.state = State.ACTIVE; - - const createdDeck = await this.deckRepo.create(newDeck); - - logAuth('Deck imported from SPR format', userId, { - deckId: createdDeck.id, - deckName: createdDeck.name, - cardCount: createdDeck.cards.length, - originalExportDate: deckData.exportDate - }); - - return createdDeck; - } catch (error) { - logError('Failed to import deck from SPR', error as Error); - throw error; - } - } - - async importDeckFromJson(jsonData: any, userId: string): Promise { - try { - // Validate required fields - if (!jsonData.name || !jsonData.cards || jsonData.type === undefined) { - throw new Error('Invalid JSON format: missing required fields (name, cards, type)'); - } - - // Create new deck - const newDeck = new DeckAggregate(); - newDeck.name = jsonData.name; - newDeck.type = jsonData.type; - newDeck.userid = userId; - newDeck.cards = jsonData.cards; - newDeck.ctype = jsonData.ctype || CType.PUBLIC; - newDeck.state = State.ACTIVE; - - const createdDeck = await this.deckRepo.create(newDeck); - - logAuth('Deck imported from JSON format', userId, { - deckId: createdDeck.id, - deckName: createdDeck.name, - cardCount: createdDeck.cards.length - }); - - return createdDeck; - } catch (error) { - logError('Failed to import deck from JSON', error as Error); - throw error; - } - } - - // Admin-only function to import JSON without encryption - async adminImportFromJson(jsonData: any, targetUserId: string, adminUserId: string): Promise { - try { - if (!jsonData.name || !jsonData.cards || jsonData.type === undefined) { - throw new Error('Invalid JSON format: missing required fields (name, cards, type)'); - } - - const newDeck = new DeckAggregate(); - newDeck.name = jsonData.name; - newDeck.type = jsonData.type; - newDeck.userid = targetUserId; - newDeck.cards = jsonData.cards; - newDeck.ctype = jsonData.ctype || CType.PUBLIC; - newDeck.state = jsonData.state || State.ACTIVE; - - const createdDeck = await this.deckRepo.create(newDeck); - - logAuth('Deck imported by admin from JSON', adminUserId, { - deckId: createdDeck.id, - deckName: createdDeck.name, - cardCount: createdDeck.cards.length, - targetUserId: targetUserId - }); - - return createdDeck; - } catch (error) { - logError('Failed to admin import deck from JSON', error as Error); - throw error; - } - } - - private encrypt(text: string): Buffer { - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv(this.algorithm, this.encryptionKey, iv); - cipher.setAAD(Buffer.from('SerpentRace-Deck', 'utf8')); - - let encrypted = cipher.update(text, 'utf8'); - encrypted = Buffer.concat([encrypted, cipher.final()]); - - const authTag = cipher.getAuthTag(); - - return Buffer.concat([iv, authTag, encrypted]); - } - - private decrypt(encryptedData: Buffer): string { - if (encryptedData.length < 32) { - throw new Error('Invalid SPR file: file too short'); - } - - const iv = encryptedData.slice(0, 16); - const authTag = encryptedData.slice(16, 32); - const encrypted = encryptedData.slice(32); - - const decipher = crypto.createDecipheriv(this.algorithm, this.encryptionKey, iv); - decipher.setAAD(Buffer.from('SerpentRace-Deck', 'utf8')); - decipher.setAuthTag(authTag); - - let decrypted = decipher.update(encrypted, undefined, 'utf8'); - decrypted += decipher.final('utf8'); - - return decrypted; - } - - generateFilename(deckName: string): string { - // Sanitize deck name for filename - const sanitized = deckName.replace(/[^a-zA-Z0-9\-_]/g, '_'); - const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD - return `${sanitized}_${timestamp}.spr`; - } -} diff --git a/SerpentRace_Backend/src/Application/Services/EmailService.ts b/SerpentRace_Backend/src/Application/Services/EmailService.ts deleted file mode 100644 index d4c25396..00000000 --- a/SerpentRace_Backend/src/Application/Services/EmailService.ts +++ /dev/null @@ -1,312 +0,0 @@ -import * as nodemailer from 'nodemailer'; -import * as fs from 'fs'; -import * as path from 'path'; -import sharp from 'sharp'; -import { logError, logAuth, logStartup } from './Logger'; -import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper'; - - -export interface EmailOptions { - to: string; - subject: string; - html?: string; - text?: string; - template?: string; - templateData?: any; -} - -export interface EmailConfig { - host: string; - port: number; - secure: boolean; - auth: { - user: string; - pass: string; - }; - from: string; -} - -export class EmailService { - private transporter!: nodemailer.Transporter; - private config: EmailConfig; - private templatesPath: string; - private logoPath: string; - private resizedLogoBuffer?: Buffer; - - constructor() { - this.templatesPath = path.join(__dirname, '../../Templates'); - this.logoPath = path.join(__dirname, '../../../assets/Logo.png'); - // Load logo asynchronously after initialization - this.loadLogo().catch(err => logError('Error loading logo:', err)); - - this.config = { - host: process.env.EMAIL_HOST || 'smtp.gmail.com', - port: parseInt(process.env.EMAIL_PORT || '587'), - secure: process.env.EMAIL_SECURE === 'true', - auth: { - user: process.env.EMAIL_USER || '', - pass: process.env.EMAIL_PASS || '' - }, - from: process.env.EMAIL_FROM || 'noreply@serpentrace.com' - }; - - this.initializeTransporter(); - } - - private initializeTransporter(): void { - try { - this.transporter = nodemailer.createTransport({ - host: this.config.host, - port: this.config.port, - secure: this.config.secure, - auth: { - user: this.config.auth.user, - pass: this.config.auth.pass - } - }); - } catch (error) { - logError('EmailService initialization failed', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to initialize email service'); - } - } - - /** - * Load and resize logo for email attachments - 60x60 pixels - */ - private async loadLogo(): Promise { - try { - if (fs.existsSync(this.logoPath)) { - const logoBuffer = fs.readFileSync(this.logoPath); - - // Resize to 60x60 pixels with high quality and centered - this.resizedLogoBuffer = await sharp(logoBuffer) - .resize(60, 60, { - fit: 'contain', - background: { r: 255, g: 255, b: 255, alpha: 1 }, - position: 'center' - }) - .png() - .toBuffer(); - } - } catch (error) { - logError('Failed to load logo for emails', error instanceof Error ? error : new Error(String(error))); - } - } - - /** - * Send email with template - * @param options - Email options including template and data - */ - async sendEmail(options: EmailOptions): Promise { - try { - // Ensure logo is loaded before sending - if (!this.resizedLogoBuffer) { - await this.loadLogo(); - } - - let htmlContent = options.html; - let textContent = options.text; - - if (options.template) { - const templateResult = await this.loadTemplate(options.template, options.templateData); - htmlContent = templateResult.html; - textContent = templateResult.text; - } - - const mailOptions: any = { - from: this.config.from, - to: options.to, - subject: options.subject, - html: htmlContent, - text: textContent, - attachments: [] - }; - - // Add logo as CID attachment if available - if (this.resizedLogoBuffer) { - mailOptions.attachments.push({ - filename: 'logo.png', - content: this.resizedLogoBuffer, - cid: 'logo@serpentrace' // Content-ID for referencing in HTML - }); - console.log('[EmailService] 📎 Logo attached to email as CID: logo@serpentrace'); - } else { - console.warn('[EmailService] ⚠ Logo buffer not available, email will be sent without logo'); - } - - const result = await this.transporter.sendMail(mailOptions); - logAuth('Email sent successfully', undefined, { - messageId: result.messageId, - to: options.to, - subject: options.subject - }); - return true; - } catch (error) { - logError('Email sending failed', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Send verification email to user - * @param userEmail - User's email address - * @param userName - User's name - * @param verificationToken - Verification token - * @param verificationUrl - Complete verification URL - * @param language - Language code ('en', 'hu', 'de') - */ - async sendVerificationEmail( - userEmail: string, - userName: string, - verificationToken: string, - verificationUrl: string, - language: 'en' | 'hu' | 'de' = 'en' - ): Promise { - try { - const templateName = language === 'en' ? 'verification' : `verification-${language}`; - const subject = this.getLocalizedVerificationSubject(language); - - return await this.sendEmail({ - to: userEmail, - subject, - template: templateName, - templateData: { - userName, - verificationToken, - verificationUrl, - companyName: 'SerpentRace', - supportEmail: 'support@serpentrace.com' - } - }); - } catch (error) { - logError('Verification email sending failed', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Send password reset email - * @param userEmail - User's email address - * @param userName - User's name - * @param resetToken - Password reset token - * @param resetUrl - Complete password reset URL - * @param language - Language code ('en', 'hu', 'de') - */ - async sendPasswordResetEmail( - userEmail: string, - userName: string, - resetToken: string, - resetUrl: string, - language: 'en' | 'hu' | 'de' = 'en' - ): Promise { - try { - const templateName = language === 'en' ? 'password-reset' : `password-reset-${language}`; - const subject = this.getLocalizedPasswordResetSubject(language); - - return await this.sendEmail({ - to: userEmail, - subject, - template: templateName, - templateData: { - userName, - resetToken, - resetUrl, - companyName: 'SerpentRace', - supportEmail: 'support@serpentrace.com' - } - }); - } catch (error) { - logError('Password reset email sending failed', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Load and compile email template with language support - * @param templateName - Name of the template file (with or without language suffix) - * @param data - Data to replace placeholders in the template - */ - private async loadTemplate(templateName: string, data: any): Promise<{ html: string; text: string }> { - try { - // Try the specified template first - let htmlTemplatePath = path.join(this.templatesPath, `${templateName}.html`); - let textTemplatePath = path.join(this.templatesPath, `${templateName}.txt`); - - let htmlTemplate = ''; - let textTemplate = ''; - - // Load HTML template if it exists - if (fs.existsSync(htmlTemplatePath)) { - htmlTemplate = fs.readFileSync(htmlTemplatePath, 'utf8'); - } else { - // If language-specific template doesn't exist, try fallback to English - const baseName = templateName.replace(/-[a-z]{2}$/, ''); // Remove language suffix - const fallbackHtmlPath = path.join(this.templatesPath, `${baseName}.html`); - if (fs.existsSync(fallbackHtmlPath)) { - htmlTemplate = fs.readFileSync(fallbackHtmlPath, 'utf8'); - } - } - - // Load text template if it exists - if (fs.existsSync(textTemplatePath)) { - textTemplate = fs.readFileSync(textTemplatePath, 'utf8'); - } else { - // If language-specific template doesn't exist, try fallback to English - const baseName = templateName.replace(/-[a-z]{2}$/, ''); // Remove language suffix - const fallbackTextPath = path.join(this.templatesPath, `${baseName}.txt`); - if (fs.existsSync(fallbackTextPath)) { - textTemplate = fs.readFileSync(fallbackTextPath, 'utf8'); - } - } - - // If no templates found, throw error - if (!htmlTemplate && !textTemplate) { - throw new Error(`Template '${templateName}' not found`); - } - - // Replace placeholders in templates - const processedTemplate = EmailTemplateHelper.processTemplate( - { html: htmlTemplate, text: textTemplate }, - data - ); - - return { - html: processedTemplate.html, - text: processedTemplate.text - }; - } catch (error) { - logError('Email template loading failed', error instanceof Error ? error : new Error(String(error))); - throw new Error(`Failed to load email template: ${templateName}`); - } - } - - /** - * Get localized verification email subject - * @param language - Language code ('en', 'hu', 'de') - */ - private getLocalizedVerificationSubject(language: 'en' | 'hu' | 'de'): string { - const subjects: LocalizedSubjects = { - verification: { - en: 'SerpentRace - Verify Your Account', - hu: 'SerpentRace - FiĂłk megerƑsĂ­tĂ©se', - de: 'SerpentRace - Konto verifizieren' - } - }; - return EmailTemplateHelper.getLocalizedSubject('verification', subjects, language); - } - - /** - * Get localized password reset email subject - * @param language - Language code ('en', 'hu', 'de') - */ - private getLocalizedPasswordResetSubject(language: 'en' | 'hu' | 'de'): string { - const subjects: LocalizedSubjects = { - passwordReset: { - en: 'SerpentRace - Password Reset Request', - hu: 'SerpentRace - JelszĂł visszaĂĄllĂ­tĂĄs kĂ©rĂ©se', - de: 'SerpentRace - Passwort zurĂŒcksetzen' - } - }; - return EmailTemplateHelper.getLocalizedSubject('passwordReset', subjects, language); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/EmailTemplateHelper.ts b/SerpentRace_Backend/src/Application/Services/EmailTemplateHelper.ts deleted file mode 100644 index d58a1784..00000000 --- a/SerpentRace_Backend/src/Application/Services/EmailTemplateHelper.ts +++ /dev/null @@ -1,39 +0,0 @@ -export interface LocalizedSubjects { - [key: string]: { - en: string; - hu: string; - de: string; - }; -} - -export interface TemplateData { - [key: string]: any; -} - -export interface EmailTemplate { - html: string; - text: string; -} - -export class EmailTemplateHelper { - public static getLocalizedSubject( - subjectKey: string, - subjects: LocalizedSubjects, - language: 'en' | 'hu' | 'de' - ): string { - return subjects[subjectKey]?.[language] || subjects[subjectKey]?.['en'] || 'SerpentRace'; - } - - public static replaceTemplatePlaceholders(template: string, data: TemplateData): string { - return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => { - return data[key] !== undefined ? String(data[key]) : match; - }); - } - - public static processTemplate(templateContent: EmailTemplate, data: TemplateData): EmailTemplate { - return { - html: this.replaceTemplatePlaceholders(templateContent.html, data), - text: this.replaceTemplatePlaceholders(templateContent.text, data) - }; - } -} diff --git a/SerpentRace_Backend/src/Application/Services/ErrorResponseService.ts b/SerpentRace_Backend/src/Application/Services/ErrorResponseService.ts deleted file mode 100644 index e4614f42..00000000 --- a/SerpentRace_Backend/src/Application/Services/ErrorResponseService.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Response } from 'express'; - -export class ErrorResponseService { - static sendError(res: Response, statusCode: number, message: string, details?: any): Response { - const errorResponse: any = { error: message }; - if (details) { - errorResponse.details = details; - } - return res.status(statusCode).json(errorResponse); - } - - static sendInternalServerError(res: Response): Response { - return this.sendError(res, 500, 'Internal server error'); - } - - static sendBadRequest(res: Response, message: string = 'Bad request', details?: any): Response { - return this.sendError(res, 400, message, details); - } - - static sendUnauthorized(res: Response, message: string = 'Unauthorized'): Response { - return this.sendError(res, 401, message); - } - - static sendForbidden(res: Response, message: string = 'Forbidden'): Response { - return this.sendError(res, 403, message); - } - - static sendNotFound(res: Response, message: string = 'Not found'): Response { - return this.sendError(res, 404, message); - } - - static sendConflict(res: Response, message: string = 'Conflict'): Response { - return this.sendError(res, 409, message); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/FieldEffectService.ts b/SerpentRace_Backend/src/Application/Services/FieldEffectService.ts deleted file mode 100644 index 22039a4d..00000000 --- a/SerpentRace_Backend/src/Application/Services/FieldEffectService.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { GameCard, GameField } from '../../Domain/Game/GameAggregate'; -import { Consequence } from '../../Domain/Deck/DeckAggregate'; -import { BoardGenerationService } from '../Game/BoardGenerationService'; -import { GamemasterService, GamemasterDecisionResult } from './GamemasterService'; -import { FieldEffectRequest, FieldEffectResult } from './Interfaces/GameInterfaces'; - -// Interfaces for different card processing results -export interface GuessResult { - guessedPosition: number; - actualPosition: number; - isCorrect: boolean; - penaltyApplied: boolean; // true if moved back 2 fields - description: string; -} - -export interface TurnEffect { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - playerId: string; - value: number; // Number of turns to lose/gain -} - -export interface CardProcessingResult { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: GuessResult; - gamemasterResult?: GamemasterDecisionResult; - turnEffect?: TurnEffect; // Turn-based consequences that need game state changes - description: string; - effects: string[]; // Array of all effects applied -} - -/** - * Service responsible for processing card-based field effects with step calculations - * Integrates pattern-based movement with test/guess mechanism and gamemaster decisions - */ -export class FieldEffectService { - constructor( - private boardGenerationService: BoardGenerationService, - private gamemasterService: GamemasterService - ) {} - - /** - * Process a card-based field effect with complete movement calculation - * @param request The field effect request containing all necessary data - * @returns Promise with complete processing result - */ - async processFieldEffect(request: FieldEffectRequest): Promise { - const { currentPosition, card, field, dice } = request; - - // Ensure stepValue is defined - const stepValue = field.stepValue || 1; - - // Calculate base movement using pattern-based system - const finalPosition = this.boardGenerationService.calculatePatternBasedMovement( - currentPosition, - stepValue, - dice - ); - - // Calculate pattern modifier manually for tracking - const patternModifier = this.getPatternModifier(currentPosition); - - let result: CardProcessingResult = { - finalPosition, - stepValue, - dice, - patternModifier, - consequenceModifier: 0, - description: `Moved from position ${currentPosition} to ${finalPosition}`, - effects: [] - }; - - // Process card based on type - if (this.isQuestionCard(card.type)) { - result = await this.processQuestionCard(request, result); - } else if (this.isJokerCard(card.type)) { - result = await this.processJokerCard(request, result); - } else if (this.isLuckCard(card.type)) { - result = await this.processLuckCard(request, result); - } - - return result; - } - - /** - * Get pattern modifier based on position (duplicated from BoardGenerationService) - * @param position Current position - * @returns Pattern modifier value - */ - private getPatternModifier(position: number): number { - // Pattern modifiers for strategic complexity: - // Positions 1-20: +2 bonus (easier start) - // Positions 21-40: -1 penalty (early game challenge) - // Positions 41-60: +1 bonus (mid-game boost) - // Positions 61-80: -2 penalty (late game challenge) - // Positions 81-100: +3 bonus (final stretch boost) - - if (position <= 20) { - return 2; - } else if (position <= 40) { - return -1; - } else if (position <= 60) { - return 1; - } else if (position <= 80) { - return -2; - } else { - return 3; - } - } - - /** - * Check if card is a question card (types 0-4) - * @param cardType Card type - * @returns True if question card - */ - private isQuestionCard(cardType?: number): boolean { - return cardType !== undefined && cardType >= 0 && cardType <= 4; - } - - /** - * Check if card is a joker card - * @param cardType Card type - * @returns True if joker card - */ - private isJokerCard(cardType?: number): boolean { - return cardType === 5; // Assuming joker cards have type 5 - } - - /** - * Check if card is a luck card - * @param cardType Card type - * @returns True if luck card - */ - private isLuckCard(cardType?: number): boolean { - return cardType === 6; // Assuming luck cards have type 6 - } - - /** - * Process question card with test/guess mechanism - * @param request The field effect request - * @param baseResult The base movement calculation result - * @returns Updated result with guess processing - */ - private async processQuestionCard( - request: FieldEffectRequest, - baseResult: CardProcessingResult - ): Promise { - const { guessedPosition } = request; - - if (guessedPosition === undefined) { - throw new Error('Question cards require a position guess'); - } - - // Apply test/guess mechanism - const guessResult = this.processGuess( - guessedPosition, - baseResult.finalPosition, - baseResult.finalPosition - ); - - let finalPosition = baseResult.finalPosition; - let effects = [...baseResult.effects]; - - if (!guessResult.isCorrect) { - // Apply guess penalty: move back exactly 2 fields - finalPosition = Math.max(1, baseResult.finalPosition - 2); - effects.push(`Wrong guess penalty: moved back 2 fields`); - } else { - effects.push(`Correct guess: no penalty`); - } - - return { - ...baseResult, - finalPosition, - guessResult, - effects, - description: `Question card: ${guessResult.description}` - }; - } - - /** - * Process joker card with same guess mechanism as question cards + gamemaster decision - * @param request The field effect request - * @param baseResult The base movement calculation result - * @returns Updated result with guess processing and gamemaster decision - */ - private async processJokerCard( - request: FieldEffectRequest, - baseResult: CardProcessingResult - ): Promise { - const { guessedPosition, gameId, playerId, playerName, card } = request; - - if (guessedPosition === undefined) { - throw new Error('Joker cards require a position guess'); - } - - // Joker cards always use dice = 6, recalculate with correct dice value - const jokerDice = 6; - const correctBasePosition = this.boardGenerationService.calculatePatternBasedMovement( - request.currentPosition, - baseResult.stepValue, - jokerDice - ); - - let finalPosition = correctBasePosition; - let effects = [`Joker card: dice counted as 6`]; - - // Step 1: Process guess penalty (same as question cards) - const guessResult = this.processGuess( - guessedPosition, - correctBasePosition, - correctBasePosition - ); - - if (!guessResult.isCorrect) { - // Apply guess penalty: move back exactly 2 fields - finalPosition = Math.max(1, correctBasePosition - 2); - effects.push(`Wrong guess penalty: moved back 2 fields`); - } else { - effects.push(`Correct guess: no penalty`); - } - - // Step 2: Process gamemaster decision (replaces player answer effect) - const gamemasterResult = await this.requestGamemasterDecision( - gameId, - playerId, - playerName, - card - ); - - let consequenceModifier = 0; - let turnEffect: TurnEffect | undefined; - - if (gamemasterResult.consequence && card.consequence) { - // Apply consequence based on gamemaster decision using new processing method - const consequenceResult = this.processConsequence(playerId, finalPosition, card.consequence); - finalPosition = consequenceResult.newPosition; - consequenceModifier = consequenceResult.positionChange; - turnEffect = consequenceResult.turnEffect; - - effects.push(`Gamemaster decision: ${gamemasterResult.description}`); - effects.push(`Consequence applied: ${this.getConsequenceDescription(card.consequence)}`); - } else { - effects.push(`No consequence applied: ${gamemasterResult.description}`); - } - - return { - ...baseResult, - finalPosition, - dice: jokerDice, // Update to show dice was 6 - consequenceModifier, - turnEffect, - guessResult, - gamemasterResult, - effects, - description: `Joker card: ${guessResult.description} | ${gamemasterResult.description}` - }; - } - - /** - * Process luck card with immediate effects - * @param request The field effect request - * @param baseResult The base movement calculation result - * @returns Updated result with luck card effects - */ - private async processLuckCard( - request: FieldEffectRequest, - baseResult: CardProcessingResult - ): Promise { - const { card, playerId } = request; - - let finalPosition = baseResult.finalPosition; - let consequenceModifier = 0; - let turnEffect: TurnEffect | undefined; - let effects = [...baseResult.effects]; - - if (card.consequence) { - // Apply immediate consequence using new processing method - const consequenceResult = this.processConsequence(playerId, finalPosition, card.consequence); - finalPosition = consequenceResult.newPosition; - consequenceModifier = consequenceResult.positionChange; - turnEffect = consequenceResult.turnEffect; - effects.push(`Luck effect: ${this.getConsequenceDescription(card.consequence)}`); - } - - return { - ...baseResult, - finalPosition, - consequenceModifier, - turnEffect, - effects, - description: `Luck card: immediate effect applied` - }; - } - - /** - * Process position guess and determine if penalty should be applied - * @param guessedPosition Player's position guess - * @param actualPosition The calculated final position - * @param basePosition The position before guess penalty - * @returns Guess processing result - */ - private processGuess( - guessedPosition: number, - actualPosition: number, - basePosition: number - ): GuessResult { - // Validate guess range - if (guessedPosition < 1 || guessedPosition > 100) { - throw new Error('Position guess must be between 1 and 100'); - } - - const isCorrect = guessedPosition === actualPosition; - const penaltyApplied = !isCorrect; - - return { - guessedPosition, - actualPosition, - isCorrect, - penaltyApplied, - description: isCorrect - ? `Correct guess (${guessedPosition})!` - : `Wrong guess (${guessedPosition} ≠ ${actualPosition})` - }; - } - - /** - * Request gamemaster decision for joker card - * @param gameId Game ID - * @param playerId Player ID - * @param playerName Player name - * @param card Joker card - * @returns Promise with gamemaster decision result - */ - private async requestGamemasterDecision( - gameId: string, - playerId: string, - playerName: string, - card: GameCard - ): Promise { - // For now, return a default decision - this will be replaced with actual async gamemaster interaction - // TODO: Implement proper WebSocket-based gamemaster decision flow - return { - decision: 'reject' as any, - consequence: false, - description: '🎭 Gamemaster decision pending...' - }; - } - - /** - * Process consequence and separate position changes from turn effects - * @param playerId Player ID who drew the card - * @param currentPosition Current position before consequence - * @param consequence Card consequence - * @returns Object with position changes and turn effects - */ - private processConsequence(playerId: string, currentPosition: number, consequence: Consequence): { - newPosition: number; - positionChange: number; - turnEffect?: TurnEffect; - } { - // Handle position-affecting consequences - if (consequence.type === 0 || consequence.type === 1 || consequence.type === 5) { - const newPosition = this.applyConsequenceToPosition(currentPosition, consequence); - return { - newPosition, - positionChange: newPosition - currentPosition - }; - } - - // Handle turn-based consequences - if (consequence.type === 2 || consequence.type === 3) { - const turnEffect: TurnEffect = { - type: consequence.type === 2 ? 'LOSE_TURN' : 'EXTRA_TURN', - playerId, - value: consequence.value || 1 - }; - return { - newPosition: currentPosition, // No position change - positionChange: 0, - turnEffect - }; - } - - // Unknown consequence type - return { - newPosition: currentPosition, - positionChange: 0 - }; - } - - /** - * Apply consequence to position with proper boundary handling - * @param currentPosition Current position - * @param consequence Card consequence - * @returns New position after consequence - */ - private applyConsequenceToPosition(currentPosition: number, consequence: Consequence): number { - switch (consequence.type) { - case 0: // MOVE_FORWARD - return Math.min(100, currentPosition + (consequence.value || 1)); - case 1: // MOVE_BACKWARD - return Math.max(1, currentPosition - (consequence.value || 1)); - case 5: // GO_TO_START - return 1; - default: - return currentPosition; // Other consequences don't change position - } - } - - /** - * Get human-readable description for consequence - * @param consequence Card consequence - * @returns Description string - */ - private getConsequenceDescription(consequence: Consequence): string { - switch (consequence.type) { - case 0: // MOVE_FORWARD - return `Move forward ${consequence.value || 1} steps`; - case 1: // MOVE_BACKWARD - return `Move backward ${consequence.value || 1} steps`; - case 2: // LOSE_TURN - const lostTurns = consequence.value || 1; - return lostTurns === 1 ? 'Lose next turn' : `Lose next ${lostTurns} turns`; - case 3: // EXTRA_TURN - const extraTurns = consequence.value || 1; - return extraTurns === 1 ? 'Get extra turn' : `Get ${extraTurns} extra turns`; - case 5: // GO_TO_START - return 'Go back to start'; - default: - return 'Unknown effect'; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/GameSnapshotService.ts b/SerpentRace_Backend/src/Application/Services/GameSnapshotService.ts deleted file mode 100644 index e208485f..00000000 --- a/SerpentRace_Backend/src/Application/Services/GameSnapshotService.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { IGameSnapshotRepository } from '../../Domain/IRepository/IGameSnapshotRepository'; -import { GameSnapshotAggregate, SnapshotTrigger, GameStateSnapshot, PlayerSnapshot } from '../../Domain/Game/GameSnapshotAggregate'; -import { RedisService } from './RedisService'; -import { logOther, logError } from './Logger'; - -export class GameSnapshotService { - private static readonly SNAPSHOT_INTERVAL = 5; // Every 5 turns - private static readonly MAX_SNAPSHOTS_PER_GAME = 20; // Keep last 20 snapshots - - constructor( - private snapshotRepository: IGameSnapshotRepository, - private redisService: RedisService - ) {} - - /** - * Create a game state snapshot - */ - async createSnapshot( - gameId: string, - turnNumber: number, - trigger: SnapshotTrigger, - notes?: string - ): Promise { - try { - // Gather current game state from Redis - const gameState = await this.getCurrentGameState(gameId); - if (!gameState) { - logError('Cannot create snapshot: game state not found', new Error(`Game ${gameId} not in Redis`)); - return; - } - - // Gather Redis state (pending actions, timers, etc.) - const redisState = await this.getRedisState(gameId); - - // Create snapshot - const snapshot = new GameSnapshotAggregate(); - snapshot.gameid = gameId; - snapshot.turnNumber = turnNumber; - snapshot.trigger = trigger; - snapshot.gameState = gameState; - snapshot.redisState = redisState; - snapshot.notes = notes || null; - - await this.snapshotRepository.save(snapshot); - - // Cleanup old snapshots - await this.snapshotRepository.deleteOldSnapshots( - gameId, - GameSnapshotService.MAX_SNAPSHOTS_PER_GAME - ); - - logOther(`Game snapshot created: ${trigger}`, { - gameId, - turnNumber, - trigger - }); - } catch (error) { - logError('Failed to create game snapshot', error as Error); - // Don't throw - snapshots shouldn't break game flow - } - } - - /** - * Check if snapshot should be created (every N turns) - */ - shouldCreateSnapshot(turnNumber: number): boolean { - return turnNumber % GameSnapshotService.SNAPSHOT_INTERVAL === 0; - } - - /** - * Restore game state from latest snapshot - */ - async restoreFromSnapshot(gameId: string): Promise { - try { - const snapshot = await this.snapshotRepository.findLatestByGameId(gameId); - if (!snapshot) { - logOther(`No snapshot found for game ${gameId}`); - return false; - } - - // Restore game state to Redis - await this.restoreGameState(gameId, snapshot.gameState); - - // Restore Redis state (pending actions, timers) - if (snapshot.redisState) { - await this.restoreRedisState(gameId, snapshot.redisState); - } - - logOther(`Game state restored from snapshot`, { - gameId, - turnNumber: snapshot.turnNumber, - trigger: snapshot.trigger, - age: Date.now() - snapshot.createdat.getTime() - }); - - return true; - } catch (error) { - logError('Failed to restore game from snapshot', error as Error); - return false; - } - } - - /** - * Get current game state from Redis - */ - private async getCurrentGameState(gameId: string): Promise { - try { - // Get game state - const gameStateKey = `game_state:${gameId}`; - const gameStateJson = await this.redisService.get(gameStateKey); - if (!gameStateJson) return null; - - const gameState = JSON.parse(gameStateJson); - - // Get player positions - const playerPositions: PlayerSnapshot[] = []; - const positionsKey = `player_positions:${gameId}`; - const positionsJson = await this.redisService.get(positionsKey); - - if (positionsJson) { - const positions = JSON.parse(positionsJson); - for (const [playerId, data] of Object.entries(positions)) { - const posData = data as any; - - // Get extra turns - const extraTurnsKey = `extra_turns:${gameId}:${playerId}`; - const extraTurns = parseInt(await this.redisService.get(extraTurnsKey) || '0'); - - // Get turns to lose - const turnsToLoseKey = `turns_to_lose:${gameId}:${playerId}`; - const turnsToLose = parseInt(await this.redisService.get(turnsToLoseKey) || '0'); - - playerPositions.push({ - playerId: playerId, - playerName: posData.playerName || 'Unknown', - boardPosition: posData.boardPosition || 0, - extraTurns, - turnsToLose, - isOnline: posData.isOnline !== false - }); - } - } - - // Get board data - const boardKey = `board_data:${gameId}`; - const boardJson = await this.redisService.get(boardKey); - const boardFields = boardJson ? JSON.parse(boardJson).fields : undefined; - - return { - currentPlayer: gameState.currentPlayer, - currentPlayerName: gameState.currentPlayerName || 'Unknown', - turnNumber: gameState.turnNumber || 1, - turnOrder: gameState.turnOrder || [], - playerPositions, - boardFields, - deckStates: undefined, // TODO: Add deck states if needed - pendingActions: undefined - }; - } catch (error) { - logError('Error getting current game state', error as Error); - return null; - } - } - - /** - * Get Redis state (pending cards, decisions, etc.) - */ - private async getRedisState(gameId: string): Promise { - const redisState: any = { - pendingCards: {}, - pendingDecisions: {}, - timers: {} - }; - - try { - // Get all keys for this game - const pattern = `*${gameId}*`; - const keys = await this.redisService['client'].keys(pattern); - - for (const key of keys) { - // Store non-critical state for reference - if (key.includes('pending_card') || key.includes('pending_decision')) { - const value = await this.redisService.get(key); - if (value) { - redisState.pendingCards[key] = value; - } - } - } - } catch (error) { - logError('Error getting Redis state', error as Error); - } - - return redisState; - } - - /** - * Restore game state to Redis - */ - private async restoreGameState(gameId: string, state: GameStateSnapshot): Promise { - // Restore game state - const gameStateKey = `game_state:${gameId}`; - await this.redisService.setWithExpiry(gameStateKey, JSON.stringify({ - currentPlayer: state.currentPlayer, - currentPlayerName: state.currentPlayerName, - turnNumber: state.turnNumber, - turnOrder: state.turnOrder - }), 3600); - - // Restore player positions - const positionsKey = `player_positions:${gameId}`; - const positions: any = {}; - for (const player of state.playerPositions) { - positions[player.playerId] = { - playerName: player.playerName, - boardPosition: player.boardPosition, - isOnline: player.isOnline - }; - - // Restore extra turns - if (player.extraTurns > 0) { - const extraTurnsKey = `extra_turns:${gameId}:${player.playerId}`; - await this.redisService.setWithExpiry(extraTurnsKey, player.extraTurns.toString(), 3600); - } - - // Restore turns to lose - if (player.turnsToLose > 0) { - const turnsToLoseKey = `turns_to_lose:${gameId}:${player.playerId}`; - await this.redisService.setWithExpiry(turnsToLoseKey, player.turnsToLose.toString(), 3600); - } - } - await this.redisService.setWithExpiry(positionsKey, JSON.stringify(positions), 3600); - - // Restore board data if available - if (state.boardFields) { - const boardKey = `board_data:${gameId}`; - await this.redisService.setWithExpiry(boardKey, JSON.stringify({ fields: state.boardFields }), 3600); - } - } - - /** - * Restore Redis state (partial - pending actions may need re-triggering) - */ - private async restoreRedisState(gameId: string, redisState: any): Promise { - // Note: Pending cards and timers should be recreated by game logic - // This is just for reference/debugging - logOther('Redis state reference saved (timers/pending actions need manual restart)'); - } - - /** - * Cleanup snapshots for finished game - */ - async cleanupGameSnapshots(gameId: string): Promise { - try { - await this.snapshotRepository.deleteByGameId(gameId); - logOther(`Game snapshots cleaned up for game ${gameId}`); - } catch (error) { - logError('Failed to cleanup game snapshots', error as Error); - } - } - - /** - * Get snapshot history for debugging - */ - async getSnapshotHistory(gameId: string): Promise { - return await this.snapshotRepository.findByGameId(gameId); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/GameTokenService.ts b/SerpentRace_Backend/src/Application/Services/GameTokenService.ts deleted file mode 100644 index c4fca106..00000000 --- a/SerpentRace_Backend/src/Application/Services/GameTokenService.ts +++ /dev/null @@ -1,205 +0,0 @@ -import jwt, { SignOptions } from 'jsonwebtoken'; -import { Request } from 'express'; - -export interface GameTokenPayload { - gameId: string; - gameCode: string; - playerName: string; - isAuthenticated: boolean; - userId?: string; // Optional - only for authenticated players - tokenType: 'game_session'; - iat?: number; - exp?: number; -} - -export class GameTokenService { - private readonly secretKey: string; - private readonly gameTokenExpiry: number; - - constructor() { - this.secretKey = process.env.JWT_SECRET || 'your-secret-key'; - - // Game tokens expire after 24 hours (or configured duration) - // This should be longer than typical game duration - this.gameTokenExpiry = parseInt(process.env.GAME_TOKEN_EXPIRY || '86400'); // 24 hours default - - if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) { - throw new Error('JWT_SECRET environment variable must be set in production'); - } - } - - /** - * Create a game session token for a player - * @param gameId - The database ID of the game - * @param gameCode - The public game code (e.g., ABC123) - * @param playerName - The player's name in the game - * @param userId - Optional user ID for authenticated players - * @returns Game session JWT token - */ - createGameToken(gameId: string, gameCode: string, playerName: string, userId?: string): string { - const now = Math.floor(Date.now() / 1000); - - const payload: GameTokenPayload = { - gameId, - gameCode, - playerName, - isAuthenticated: !!userId, - userId, - tokenType: 'game_session', - iat: now, - exp: now + this.gameTokenExpiry - }; - - const options: SignOptions = {}; - const token = jwt.sign(payload, this.secretKey, options); - - return token; - } - - /** - * Verify and decode a game session token - * @param token - The game session JWT token - * @returns Decoded payload or null if invalid - */ - verifyGameToken(token: string): GameTokenPayload | null { - try { - const decoded = jwt.verify(token, this.secretKey) as GameTokenPayload; - - // Verify it's actually a game token - if (decoded.tokenType !== 'game_session') { - return null; - } - - return decoded; - } catch (error) { - return null; - } - } - - /** - * Extract game token from request headers or query params - * @param req - Express request object - * @returns Game token string or null - */ - extractGameTokenFromRequest(req: Request): string | null { - // Check Authorization header - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - return authHeader.substring(7); - } - - // Check query parameter (for WebSocket handshake) - if (req.query && req.query.gameToken && typeof req.query.gameToken === 'string') { - return req.query.gameToken; - } - - // Check game_token cookie - if (req.cookies && req.cookies.game_token) { - return req.cookies.game_token; - } - - return null; - } - - /** - * Verify game token from request and return payload - * @param req - Express request object - * @returns Decoded game token payload or null - */ - verifyGameTokenFromRequest(req: Request): GameTokenPayload | null { - const token = this.extractGameTokenFromRequest(req); - if (!token) { - return null; - } - - return this.verifyGameToken(token); - } - - /** - * Check if a game token is valid for a specific game - * @param token - The game session token - * @param gameCode - The game code to validate against - * @param playerName - Optional player name to validate - * @returns True if token is valid for the game - */ - isValidForGame(token: string, gameCode: string, playerName?: string): boolean { - const payload = this.verifyGameToken(token); - if (!payload) { - return false; - } - - // Check game code matches - if (payload.gameCode !== gameCode) { - return false; - } - - // Check player name if provided - if (playerName && payload.playerName !== playerName) { - return false; - } - - return true; - } - - /** - * Refresh a game token (extend expiry) - * @param currentToken - The current game token - * @returns New token with extended expiry or null if invalid - */ - refreshGameToken(currentToken: string): string | null { - const payload = this.verifyGameToken(currentToken); - if (!payload) { - return null; - } - - // Create new token with same data but fresh expiry - return this.createGameToken( - payload.gameId, - payload.gameCode, - payload.playerName, - payload.userId - ); - } - - /** - * Get remaining time before token expires - * @param token - The game session token - * @returns Seconds until expiry or -1 if invalid/expired - */ - getTimeUntilExpiry(token: string): number { - const payload = this.verifyGameToken(token); - if (!payload || !payload.exp) { - return -1; - } - - const now = Math.floor(Date.now() / 1000); - const remaining = payload.exp - now; - - return remaining > 0 ? remaining : -1; - } - - /** - * Create a game token response object for API responses - * @param gameId - The database ID of the game - * @param gameCode - The public game code - * @param playerName - The player's name - * @param userId - Optional user ID for authenticated players - * @returns Object with token and metadata - */ - createGameTokenResponse(gameId: string, gameCode: string, playerName: string, userId?: string) { - const token = this.createGameToken(gameId, gameCode, playerName, userId); - const expiresIn = this.gameTokenExpiry; - - return { - gameToken: token, - gameCode, - playerName, - isAuthenticated: !!userId, - expiresIn, - expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(), - tokenType: 'game_session' - }; - } -} - -export default GameTokenService; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts b/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts deleted file mode 100644 index fd2fcf14..00000000 --- a/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts +++ /dev/null @@ -1,3566 +0,0 @@ -import { Server as SocketIOServer, Socket } from 'socket.io'; -import { GameTokenService, GameTokenPayload } from './GameTokenService'; -import { GameRepository } from '../../Infrastructure/Repository/GameRepository'; -import { UserRepository } from '../../Infrastructure/Repository/UserRepository'; -import { TurnHistoryRepository } from '../../Infrastructure/Repository/TurnHistoryRepository'; -import { GameSnapshotRepository } from '../../Infrastructure/Repository/GameSnapshotRepository'; -import { GameAggregate, GameState, LoginType, GameField } from '../../Domain/Game/GameAggregate'; -import { logAuth, logError, logOther, logWarning } from './Logger'; -import { RedisService } from './RedisService'; -import { FieldEffectService, CardProcessingResult } from './FieldEffectService'; -import { CardDrawingService } from './CardDrawingService'; -import { BoardGenerationService } from '../Game/BoardGenerationService'; -import { GamemasterService, GamemasterDecision } from './GamemasterService'; -import { TurnHistoryService } from './TurnHistoryService'; -import { GameSnapshotService } from './GameSnapshotService'; -import { TurnActionType } from '../../Domain/Game/TurnHistoryAggregate'; -import { SnapshotTrigger } from '../../Domain/Game/GameSnapshotAggregate'; -import { - GameActionData, - PlayerPosition, - GameStateUpdateData, - FieldEffectRequest, - JoinGameData, - LeaveGameData -} from './Interfaces/GameInterfaces'; -import { json } from 'stream/consumers'; - -interface AuthenticatedSocket extends Socket { - userId?: string; - gameCode?: string; - playerName?: string; - isAuthenticated?: boolean; -} - -interface DiceRollData { - gameCode: string; - diceValue: number; // Value from frontend (1-6) -} - -interface GameChatData { - gameCode: string; - message: string; -} - -interface CardAnswerData { - gameCode: string; - answer: any; - cardId?: string; // Optional card ID sent from frontend -} - -interface GamemasterDecisionData { - gameCode: string; - requestId: string; - decision: 'approve' | 'reject'; -} - -interface PendingCardState { - playerId: string; - playerName: string; - card: any; - field: GameField; // Field info - dice: number; // Dice roll - currentPosition: number; // Position before card - landedPosition: number; // Position after dice roll (where they landed) - drawnAt: number; - answerGiven?: boolean; // Track if answer submitted - answerCorrect?: boolean; // Track if answer was correct - requiresGuess?: boolean; // Track if guess is required - guessedPosition?: number; // Store player's guess -} - -interface PendingDecisionState { - playerId: string; - playerName: string; - card: any; - field: GameField; // Field info - dice: number; // Dice roll (always 6 for jokers) - currentPosition: number; // Position before card - drawnAt: number; - recursionDepth: number; - gamemasterDecided?: boolean; // Track if gamemaster decided - gamemasterApproved?: boolean; // Track approval result - guessedPosition?: number; // Store player's guess -} - -export class GameWebSocketService { - private io: SocketIOServer; - private gameTokenService: GameTokenService; - private gameRepository: GameRepository; - private userRepository: UserRepository; - private redisService: RedisService; - private fieldEffectService: FieldEffectService; - private cardDrawingService: CardDrawingService; - private boardGenerationService: BoardGenerationService; - private gamemasterService: GamemasterService; - private turnHistoryService: TurnHistoryService; - private gameSnapshotService: GameSnapshotService; - - constructor( - io: SocketIOServer, - gameRepository: GameRepository, - userRepository: UserRepository, - redisService: RedisService, - turnHistoryRepository: TurnHistoryRepository, - gameSnapshotRepository: GameSnapshotRepository - ) { - this.io = io; - this.gameTokenService = new GameTokenService(); - this.gameRepository = gameRepository; - this.userRepository = userRepository; - this.redisService = redisService; - - // Initialize services in proper dependency order - this.boardGenerationService = new BoardGenerationService(); - this.gamemasterService = new GamemasterService(); - this.cardDrawingService = new CardDrawingService(); - this.fieldEffectService = new FieldEffectService( - this.boardGenerationService, - this.gamemasterService - ); - this.turnHistoryService = new TurnHistoryService(turnHistoryRepository); - this.gameSnapshotService = new GameSnapshotService(gameSnapshotRepository, redisService); - - this.setupGameNamespace(); - } - - private setupGameNamespace(): void { - // Create a namespace specifically for game events - const gameNamespace = this.io.of('/game'); - - gameNamespace.on('connection', (socket: AuthenticatedSocket) => { - logOther(`New game socket connection: ${socket.id}`); - - // For game sockets, authentication is optional (public games) - // Players will authenticate when joining a specific game - this.setupGameEventHandlers(socket); - }); - } - - private setupGameEventHandlers(socket: AuthenticatedSocket): void { - // Join game room - socket.on('game:join', async (data: any) => { - await this.handleJoinGame(socket, data); - }); - - // Leave game room - socket.on('game:leave', async (data: LeaveGameData) => { - await this.handleLeaveGame(socket, data); - }); - - // Game actions (dice roll, move, etc.) - socket.on('game:action', async (data: GameActionData) => { - await this.handleGameAction(socket, data); - }); - - // Game chat within a specific game - socket.on('game:chat', async (data: GameChatData) => { - await this.handleGameChat(socket, data); - }); - - // Player ready status - socket.on('game:ready', async (data: { gameCode: string; ready: boolean }) => { - await this.handlePlayerReady(socket, data); - }); - - // Gamemaster approve player (private games only) - socket.on('game:approve-player', async (data: { gameCode: string; playerName: string }) => { - await this.handleApprovePlayer(socket, data); - }); - - // Gamemaster reject player (private games only) - socket.on('game:reject-player', async (data: { gameCode: string; playerName: string; reason?: string }) => { - await this.handleRejectPlayer(socket, data); - }); - - // Player joining after approval (private games) - socket.on('game:join-approved', async (data: JoinGameData) => { - await this.handleJoinApproved(socket, data); - }); - - // Dice roll from frontend - socket.on('game:dice-roll', async (data: DiceRollData) => { - await this.handleDiceRoll(socket, data); - }); - - // Card answer from player - socket.on('game:card-answer', async (data: CardAnswerData) => { - await this.handleCardAnswer(socket, data); - }); - - // Gamemaster decision on joker card - socket.on('game:gamemaster-decision', async (data: GamemasterDecisionData) => { - await this.handleGamemasterDecision(socket, data); - }); - - // Position guess (for question cards) - socket.on('game:position-guess', async (data: { gameCode: string; guessedPosition: number }) => { - await this.handlePositionGuess(socket, data); - }); - - // Joker position guess (for joker cards) - socket.on('game:joker-position-guess', async (data: { gameCode: string; guessedPosition: number }) => { - await this.handleJokerPositionGuess(socket, data); - }); - - // Disconnect handling - socket.on('disconnect', async () => { - await this.handleDisconnect(socket); - }); - } - - private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise { - try { - // Socket.IO automatically deserializes JSON - data is already an object - const gameToken = data?.gameToken; - - if (!gameToken) { - logError('Game join failed: No game token provided'); - socket.emit('game:error', { message: 'Game token is required' }); - return; - } - - // Verify the game token - const gameTokenPayload = this.gameTokenService.verifyGameToken(gameToken); - if (!gameTokenPayload) { - logError('Game join failed: Invalid game token'); - socket.emit('game:error', { message: 'Invalid or expired game token' }); - return; - } - - const { gameId, gameCode, playerName, isAuthenticated, userId } = gameTokenPayload; - - // Validate game still exists - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game || game.id !== gameId) { - logError(`Game join failed: Game not found - Code: ${gameCode}`); - socket.emit('game:error', { message: 'Game not found or token invalid' }); - return; - } - - // Check if player name is already in use by checking connected players - const connectedPlayers = await this.getConnectedPlayers(gameCode); - if (connectedPlayers.includes(playerName)) { - logOther(`Game join failed: Player name "${playerName}" already in use in game ${gameCode}`); - socket.emit('game:error', { message: `Player name "${playerName}" is already in use in this game` }); - return; - } - - // Set socket properties from game token - socket.gameCode = gameCode; - socket.playerName = playerName; - socket.isAuthenticated = isAuthenticated; - socket.userId = userId; - - // Check if this is a private game and player needs gamemaster approval - const isGamemaster = game.createdby === userId; - const needsApproval = game.logintype === LoginType.PRIVATE && !isGamemaster; - - logOther(`Player joining game: ${playerName}`); - logOther(` - userId: ${userId}`); - logOther(` - game.createdby: ${game.createdby}`); - logOther(` - isGamemaster: ${isGamemaster}`); - logOther(` - needsApproval: ${needsApproval}`); - - // Generate dynamic room names (needed for both approval and direct join) - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - if (needsApproval) { - // For private games, non-gamemaster players need approval - // Add to pending players list and notify gamemaster - await this.addToPendingPlayers(gameCode, playerName); - - // Send pending status to the requesting player - socket.emit('game:pending-approval', { - gameCode, - playerName, - message: 'Waiting for gamemaster approval to join the game', - timestamp: new Date().toISOString() - }); - - // Notify gamemaster about the pending player - socket.to(gameRoomName).emit('game:player-requesting-join', { - playerName: playerName, - isAuthenticated, - message: `${playerName} is requesting to join the game`, - timestamp: new Date().toISOString() - }); - - return; // Don't join rooms yet - wait for approval - } - - // Join both the general game room and player-specific room - await socket.join(gameRoomName); - await socket.join(playerRoomName); - - // Update Redis with active player connection FIRST (before getting state) - await this.updatePlayerConnection(gameCode, playerName, true); - - // Send success response to the joining player - socket.emit('game:joined', { - gameCode, - playerName, - isAuthenticated, - gameId, - isGamemaster, - timestamp: new Date().toISOString() - }); - - - // Notify other players in the game (broadcast) - socket.to(gameRoomName).emit('game:player-joined', { - playerName: playerName, - isAuthenticated, - isGamemaster, - timestamp: new Date().toISOString() - }); - - - // Send current game state to the joining player (now includes this player) - const gameState = await this.getGameState(gameCode); - // Add isGamemaster flag for this specific player - const gameStateWithMasterFlag = { ...gameState, isGamemaster }; - socket.emit('game:state', gameStateWithMasterFlag); - - // Broadcast updated game state to all other players so they see the new player - socket.to(gameRoomName).emit('game:state-update', gameState); - - } catch (error) { - socket.emit('game:error', { - message: 'Failed to join game', - error: error instanceof Error ? error.message : 'Unknown error' - }); - } - } - - private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise { - try { - const { gameCode } = data; - const playerName = socket.playerName; - - // Validate we have the required data - if (!playerName) { - logError('Cannot leave game: socket has no playerName'); - socket.emit('game:error', { message: 'Player has no name' }); - return; - } - - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Leave both rooms - await socket.leave(gameRoomName); - await socket.leave(playerRoomName); - - logOther(`Player ${playerName} left game room: ${gameRoomName}`); - - // Notify other players - socket.to(gameRoomName).emit('game:player-left', { - playerName: playerName, - timestamp: new Date().toISOString() - }); - - // Update Redis before clearing socket properties - await this.updatePlayerConnection(gameCode, playerName, false); - - // Clear socket properties - socket.gameCode = undefined; - socket.playerName = undefined; - - } catch (error) { - logError('Error leaving game', error as Error); - socket.emit('game:error', { message: 'Failed to leave game' }); - } - } - - private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise { - try { - const { gameCode, action, data: actionData } = data; - - if (!socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to perform actions' }); - return; - } - - // Validate it's the player's turn (this would need game state logic) - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - // Process the game action based on type - const result = await this.processGameAction(game, socket.userId!, action, actionData); - - if (result.success) { - // Broadcast action to all players in the game - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:action-result', { - action, - playerName: socket.playerName, - result: result.data, - timestamp: new Date().toISOString() - }); // If the action resulted in a game state change, broadcast the new state - if (result.stateChanged) { - const updatedGameState = await this.getGameState(gameCode); - this.io.of('/game').to(gameRoomName).emit('game:state-update', updatedGameState); - } - } else { - socket.emit('game:error', { message: result.error }); - } - - } catch (error) { - logError('Error processing game action', error as Error); - socket.emit('game:error', { message: 'Failed to process action' }); - } - } - - private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise { - try { - const { gameCode, message } = data; - - if (!socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to chat' }); - return; - } - - const gameRoomName = `game_${gameCode}`; - - // Broadcast chat message to all players in the game - this.io.of('/game').to(gameRoomName).emit('game:chat-message', { - playerName: socket.playerName, - message, - timestamp: new Date().toISOString() - }); - - logOther(`Game chat in ${gameCode}: ${socket.playerName || socket.userId}: ${message}`); - - } catch (error) { - logError('Error handling game chat', error as Error); - socket.emit('game:error', { message: 'Failed to send chat message' }); - } - } - - private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise { - try { - const { gameCode, ready } = data; - const gameRoomName = `game_${gameCode}`; - - // Update player ready status in Redis - await this.updatePlayerReadyStatus(gameCode, socket.playerName!, ready); - - // Broadcast ready status to all players - this.io.of('/game').to(gameRoomName).emit('game:player-ready', { - playerName: socket.playerName, - ready, - timestamp: new Date().toISOString() - }); - - // Check if all players are ready and start game if so - const allReady = await this.checkAllPlayersReady(gameCode); - if (allReady) { - this.io.of('/game').to(gameRoomName).emit('game:all-ready', { - message: 'All players are ready! Game can start.', - timestamp: new Date().toISOString() - }); - } - - } catch (error) { - logError('Error handling player ready status', error as Error); - socket.emit('game:error', { message: 'Failed to update ready status' }); - } - } - - private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise { - try { - const { gameCode, playerName } = data; - - // Verify that the requesting socket is the gamemaster - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - const isGamemaster = game.createdby === socket.userId; - if (!isGamemaster) { - socket.emit('game:error', { message: 'Only the gamemaster can approve players' }); - return; - } - - if (game.logintype !== LoginType.PRIVATE) { - socket.emit('game:error', { message: 'Player approval is only for private games' }); - return; - } - - // Check if player is in pending list - const pendingPlayers = await this.getPendingPlayers(gameCode); - if (!pendingPlayers.includes(playerName)) { - socket.emit('game:error', { message: 'Player not found in pending list' }); - return; - } - - // Remove from pending players - await this.removeFromPendingPlayers(gameCode, playerName); - - // Notify the approved player to join the game rooms - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Find the pending player's socket and move them to the game - this.io.of('/game').emit('game:approval-granted', { - gameCode, - playerName, - gameRoomName, - playerRoomName, - message: 'You have been approved to join the game!', - timestamp: new Date().toISOString() - }); - - // Notify all players about the approval - this.io.of('/game').to(gameRoomName).emit('game:player-approved', { - playerName, - approvedBy: socket.playerName, - timestamp: new Date().toISOString() - }); - - // Send updated game state to gamemaster to preserve their status - const gameState = await this.getGameState(gameCode); - const gamemasterState = { ...gameState, isGamemaster: true }; - socket.emit('game:state', gamemasterState); - - logOther(`Player ${playerName} approved by gamemaster in game ${gameCode}`); - - } catch (error) { - logError('Error approving player', error as Error); - socket.emit('game:error', { message: 'Failed to approve player' }); - } - } - - private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise { - try { - const { gameCode, playerName, reason } = data; - - // Verify that the requesting socket is the gamemaster - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - const isGamemaster = game.createdby === socket.userId; - if (!isGamemaster) { - socket.emit('game:error', { message: 'Only the gamemaster can reject players' }); - return; - } - - if (game.logintype !== LoginType.PRIVATE) { - socket.emit('game:error', { message: 'Player rejection is only for private games' }); - return; - } - - // Check if player is in pending list - const pendingPlayers = await this.getPendingPlayers(gameCode); - if (!pendingPlayers.includes(playerName)) { - socket.emit('game:error', { message: 'Player not found in pending list' }); - return; - } - - // Remove from pending players - await this.removeFromPendingPlayers(gameCode, playerName); - - // Notify the rejected player - this.io.of('/game').emit('game:approval-denied', { - gameCode, - playerName, - reason: reason || 'Your request to join the game was denied', - timestamp: new Date().toISOString() - }); - - // Send updated game state to gamemaster to preserve their status - const gameState = await this.getGameState(gameCode); - const gamemasterState = { ...gameState, isGamemaster: true }; - socket.emit('game:state', gamemasterState); - - logOther(`Player ${playerName} rejected by gamemaster in game ${gameCode}${reason ? ': ' + reason : ''}`); - - } catch (error) { - logError('Error rejecting player', error as Error); - socket.emit('game:error', { message: 'Failed to reject player' }); - } - } - - private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise { - try { - const { gameToken } = data; - - if (!gameToken) { - socket.emit('game:error', { message: 'Game token is required' }); - return; - } - - // Verify the game token - const gameTokenPayload = this.gameTokenService.verifyGameToken(gameToken); - if (!gameTokenPayload) { - socket.emit('game:error', { message: 'Invalid or expired game token' }); - return; - } - - const { gameId, gameCode, playerName, isAuthenticated, userId } = gameTokenPayload; - - // Validate game still exists - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game || game.id !== gameId) { - socket.emit('game:error', { message: 'Game not found or token invalid' }); - return; - } - - // Check if player was actually approved (not in pending list anymore) - const pendingPlayers = await this.getPendingPlayers(gameCode); - if (pendingPlayers.includes(playerName)) { - socket.emit('game:error', { message: 'Player still pending approval' }); - return; - } - - // Set socket properties from game token - socket.gameCode = gameCode; - socket.playerName = playerName; - socket.isAuthenticated = isAuthenticated; - socket.userId = userId; - - // Generate dynamic room names and join - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - await socket.join(gameRoomName); - await socket.join(playerRoomName); - - logOther(`Approved player ${playerName} joined game room: ${gameRoomName}`); - - // Update Redis with active player connection FIRST (before getting state) - await this.updatePlayerConnection(gameCode, playerName, true); - - // Send success response to the joining player - socket.emit('game:joined', { - gameCode, - playerName, - isAuthenticated, - gameId, - isGamemaster: false, - timestamp: new Date().toISOString() - }); - - // Notify other players in the game (broadcast) - socket.to(gameRoomName).emit('game:player-joined', { - playerName: playerName, - isAuthenticated, - isGamemaster: false, - timestamp: new Date().toISOString() - }); - - // Send current game state to the joining player (after approval) - const gameState = await this.getGameState(gameCode); - // Check if this player is gamemaster (shouldn't be, since they were just approved) - const gameForMasterCheck = await this.gameRepository.findByGameCode(gameCode); - const playerIsGamemaster = gameForMasterCheck?.createdby === socket.userId; - const gameStateWithMasterFlag = { ...gameState, isGamemaster: playerIsGamemaster }; - socket.emit('game:state', gameStateWithMasterFlag); - - // Broadcast updated game state to all other players so they see the new player - socket.to(gameRoomName).emit('game:state-update', gameState); - - } catch (error) { - logError('Error handling approved join', error as Error); - socket.emit('game:error', { message: 'Failed to join after approval' }); - } - } - - private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise { - try { - const { gameCode, diceValue } = data; - - // Validate input - if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to roll dice' }); - return; - } - - if (!diceValue || diceValue < 1 || diceValue > 6) { - socket.emit('game:error', { message: 'Invalid dice value. Must be between 1 and 6' }); - return; - } - - // Get current game state - const gameState = await this.getCurrentGameState(gameCode); - if (!gameState) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - // Check if it's the player's turn - // Use userId for authenticated players, playerName for guests - const playerIdentifier = socket.userId || socket.playerName; - if (!playerIdentifier) { - socket.emit('game:error', { message: 'Player identification failed' }); - return; - } - - if (gameState.currentPlayer !== playerIdentifier) { - socket.emit('game:error', { message: 'It is not your turn' }); - return; - } - - // Get player's current position - const playerPositions = await this.getPlayerPositions(gameCode); - const currentPlayer = playerPositions.find(p => p.playerId === playerIdentifier); - - if (!currentPlayer) { - socket.emit('game:error', { message: 'Player not found in game' }); - return; - } - - // Calculate new position after dice roll - let newPosition = Math.min(currentPlayer.boardPosition + diceValue, 100); // Win at 100 - - const gameRoomName = `game_${gameCode}`; - - // Emit dice-rolled event FIRST (for frontend animation) - this.io.of('/game').to(gameRoomName).emit('game:dice-rolled', { - playerId: playerIdentifier, - playerName: socket.playerName, - diceValue: diceValue, - calculatedDestination: newPosition, - timestamp: new Date().toISOString() - }); - - // Emit player-moving event (token starts animation) - this.io.of('/game').to(gameRoomName).emit('game:player-moving', { - playerId: playerIdentifier, - playerName: socket.playerName, - fromPosition: currentPlayer.boardPosition, - toPosition: newPosition, - timestamp: new Date().toISOString() - }); - - // Check if player won (reached position 100) - if (newPosition >= 100) { - // Update position BEFORE ending game - await this.updatePlayerPosition(gameCode, playerIdentifier, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: playerIdentifier, - playerName: socket.playerName, - position: newPosition, - fieldType: 'finish', - timestamp: new Date().toISOString() - }); - - await this.endGame(gameCode, playerIdentifier, socket.playerName!); - return; - } - - // Check if player landed on special field (positive, negative, or luck) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === newPosition); - - if (landedField && this.isSpecialField(landedField)) { - // Wait 2 seconds for frontend animation to complete - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Process special field - draw card - // Position will be updated AFTER card/consequence logic in handleSpecialFieldLanding - await this.handleSpecialFieldLanding( - gameCode, - playerIdentifier, - socket.playerName!, - landedField, - newPosition, - diceValue, - currentPlayer.boardPosition - ); - return; // Don't advance turn yet, waiting for card answer - } - } - - // No special field - update position now and advance turn - await this.updatePlayerPosition(gameCode, playerIdentifier, newPosition); - - // Log turn history - await this.turnHistoryService.logTurnAction( - gameCode, - playerIdentifier, - socket.playerName!, - gameState.turnNumber || 1, - TurnActionType.DICE_ROLL, - currentPlayer.boardPosition, - newPosition, - { diceValue } - ); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: playerIdentifier, - playerName: socket.playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - await this.advanceTurn(gameCode); - - logOther(`Player ${socket.playerName} rolled ${diceValue}, moved from ${currentPlayer.boardPosition} to ${newPosition}`, { - gameCode, - playerId: socket.userId - }); - - } catch (error) { - logError('Error handling dice roll', error as Error); - socket.emit('game:error', { message: 'Failed to process dice roll' }); - } - } - - private async handleCardAnswer(socket: AuthenticatedSocket, data: CardAnswerData): Promise { - try { - const { gameCode, answer, cardId } = data; - - logOther(`Card answer received`, { - gameCode, - playerId: socket.userId, - playerName: socket.playerName, - cardId, - answerType: typeof answer - }); - - // Validate input - if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to answer cards' }); - return; - } - - if (!socket.userId) { - socket.emit('game:error', { message: 'Player not authenticated' }); - return; - } - - // Get pending card from Redis - const pendingCard = await this.getPendingCard(gameCode, socket.userId); - if (!pendingCard) { - logError(`No pending card found for player ${socket.playerName} (${socket.userId}) in game ${gameCode}`); - socket.emit('game:error', { message: 'No pending card answer found' }); - return; - } - - const pendingState = pendingCard as PendingCardState; - - // Clear the timeout by clearing from CardDrawingService - const answerKey = `${gameCode}:${socket.userId}`; - this.cardDrawingService.clearAnswerTimeout(answerKey); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast player's answer to all players BEFORE validation - this.io.of('/game').to(gameRoomName).emit('game:answer-submitted', { - playerName: socket.playerName, - playerId: socket.userId, - answer: answer, - message: `${socket.playerName} answered: ${JSON.stringify(answer)}`, - timestamp: new Date().toISOString() - }); - - // Add dramatic pause before showing result - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Process the answer - const result = this.cardDrawingService.processAnswer(pendingState.card, answer); - - // Update pending state - pendingState.answerGiven = true; - pendingState.answerCorrect = result.correct; - - // Broadcast validation result - this.io.of('/game').to(gameRoomName).emit('game:answer-validated', { - playerName: socket.playerName, - playerId: socket.userId, - isCorrect: result.correct, - correctAnswer: pendingState.card.answer, - message: result.correct - ? `✅ ${socket.playerName} answered correctly!` - : `❌ ${socket.playerName} answered incorrectly. Correct answer: ${JSON.stringify(pendingState.card.answer)}`, - timestamp: new Date().toISOString() - }); - - // Log answer submission - const gameState = await this.getCurrentGameState(gameCode); - if (gameState) { - await this.turnHistoryService.logTurnAction( - gameCode, - socket.userId!, - socket.playerName!, - gameState.turnNumber || 1, - TurnActionType.ANSWER_SUBMITTED, - pendingState.currentPosition, - pendingState.currentPosition, - { - cardId: pendingState.card.cardid, - answer: answer, - isCorrect: result.correct - } - ); - } - - // ========================================== - // NEW: Determine if position guess is required - // ========================================== - const requiresGuess = this.determineGuessRequirement( - pendingState.field.type, - result.correct - ); - - if (requiresGuess) { - // Request position guess - try { - await this.requestPositionGuess(gameCode, socket.userId, socket.playerName!, pendingState); - } catch (error) { - logError('Error requesting position guess, advancing turn', error as Error); - await this.clearPendingCard(gameCode, socket.userId); - await this.advanceTurn(gameCode); - } - } else { - // No guess required, handle based on field type - if (pendingState.field.type === 'positive' && !result.correct) { - // Positive field + wrong answer = stay at landed position (no movement back) - this.io.of('/game').to(gameRoomName).emit('game:no-movement', { - playerId: socket.userId, - playerName: socket.playerName, - reason: 'Wrong answer on positive field', - message: `${socket.playerName} stays at position ${pendingState.landedPosition}`, - timestamp: new Date().toISOString() - }); - } else if (pendingState.field.type === 'negative' && result.correct) { - // Negative field + correct answer = stay at landed position (avoided penalty) - this.io.of('/game').to(gameRoomName).emit('game:penalty-avoided', { - playerId: socket.userId, - playerName: socket.playerName, - message: `${socket.playerName} avoided the penalty! Stays at position ${pendingState.landedPosition}`, - timestamp: new Date().toISOString() - }); - } - - // Emit player-arrived event (stay at landed position, not reverting to pre-dice position) - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: socket.userId, - playerName: socket.playerName, - position: pendingState.landedPosition || pendingState.currentPosition, // Use landed position - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Clean up and advance turn - await this.clearPendingCard(gameCode, socket.userId); - await this.advanceTurn(gameCode); - } - - logOther(`Player ${socket.playerName} answered card: ${result.correct ? 'correct' : 'wrong'}`, { - gameCode, - playerId: socket.userId, - requiresGuess - }); - - } catch (error) { - logError('Error handling card answer', error as Error); - socket.emit('game:error', { message: 'Failed to process card answer' }); - // Ensure turn advances even on error - try { - if (socket.userId && data.gameCode) { - const pendingCard = await this.getPendingCard(data.gameCode, socket.userId); - if (pendingCard) { - await this.clearPendingCard(data.gameCode, socket.userId); - } - await this.advanceTurn(data.gameCode); - } - } catch (advanceError) { - logError('Error advancing turn after card answer error', advanceError as Error); - } - } - } - - private async handleGamemasterDecision(socket: AuthenticatedSocket, data: GamemasterDecisionData): Promise { - try { - const { gameCode, requestId, decision } = data; - - // Validate input - if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to make decisions' }); - return; - } - - if (!socket.userId) { - socket.emit('game:error', { message: 'Gamemaster not authenticated' }); - return; - } - - // Verify this is the gamemaster - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game || game.createdby !== socket.userId) { - socket.emit('game:error', { message: 'Only the gamemaster can make this decision' }); - return; - } - - // Get pending decision from Redis - const pendingDecision = await this.getPendingDecision(gameCode, requestId); - if (!pendingDecision) { - socket.emit('game:error', { message: 'Decision request not found or expired' }); - return; - } - - const pendingState = pendingDecision as PendingDecisionState; - - // Process decision through GamemasterService - const result = this.gamemasterService.processGamemasterDecision( - requestId, - decision === 'approve' ? GamemasterDecision.APPROVE : GamemasterDecision.REJECT - ); - - if (!result) { - socket.emit('game:error', { message: 'Failed to process gamemaster decision' }); - // Clean up - await this.clearPendingDecision(gameCode, requestId); - return; - } - - const gameRoomName = `game_${gameCode}`; - const approved = decision === 'approve'; - - // Update pending state with decision - pendingState.gamemasterDecided = true; - pendingState.gamemasterApproved = approved; - - // Broadcast decision result to all players - this.io.of('/game').to(gameRoomName).emit('game:gamemaster-decision-result', { - playerName: pendingState.playerName, - playerId: pendingState.playerId, - gamemasterName: socket.playerName, - decision: decision, - approved: approved, - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - - // ========================================== - // NEW: Determine if position guess is required - // ========================================== - const requiresGuess = ( - (pendingState.field.type === 'positive' && approved) || - (pendingState.field.type === 'negative' && !approved) - ); - - if (requiresGuess) { - // Request position guess - await this.requestJokerPositionGuess( - gameCode, - pendingState.playerId, - pendingState.playerName, - pendingState - ); - } else { - // No guess required - if (pendingState.field.type === 'positive' && !approved) { - // Positive field + rejected = no movement (stay at currentPosition) - this.io.of('/game').to(gameRoomName).emit('game:no-movement', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - reason: 'Gamemaster rejected on positive field', - message: `${pendingState.playerName} stays at position ${pendingState.currentPosition}`, - timestamp: new Date().toISOString() - }); - } else if (pendingState.field.type === 'negative' && approved) { - // Negative field + approved = no movement (avoided penalty, stay at currentPosition) - this.io.of('/game').to(gameRoomName).emit('game:penalty-avoided', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - message: `${pendingState.playerName} avoided the penalty! Stays at position ${pendingState.currentPosition}`, - timestamp: new Date().toISOString() - }); - } - - // Emit player-arrived event (stayed at original position) - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - position: pendingState.currentPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Clean up and advance turn - await this.clearPendingDecision(gameCode, requestId); - await this.advanceTurn(gameCode); - } - - logOther(`Gamemaster ${socket.playerName} made decision: ${decision} for player ${pendingState.playerName}`, { - gameCode, - requestId, - requiresGuess - }); - - } catch (error) { - logError('Error handling gamemaster decision', error as Error); - socket.emit('game:error', { message: 'Failed to process gamemaster decision' }); - } - } - - /** - * Handle player landing on special field (positive, negative, luck) - * Draws card and initiates appropriate flow based on card type - */ - private async handleSpecialFieldLanding( - gameCode: string, - playerId: string, - playerName: string, - field: GameField, - position: number, - dice: number, - currentPosition: number - ): Promise { - try { - const gameRoomName = `game_${gameCode}`; - - // Get game data for card drawing - const gameData = await this.gameRepository.findByGameCode(gameCode); - if (!gameData) { - logError('Game not found when handling special field landing'); - await this.advanceTurn(gameCode); - return; - } - - // Draw a card based on field type - const cardDrawResult = this.cardDrawingService.drawCard( - gameData, - field.type as 'positive' | 'negative' | 'luck', - playerId - ); - - if (!cardDrawResult.success || !cardDrawResult.card) { - // No more cards available or error - this.io.of('/game').to(gameRoomName).emit('game:card-error', { - playerName, - playerId, - error: cardDrawResult.error || 'Failed to draw card', - timestamp: new Date().toISOString() - }); - await this.advanceTurn(gameCode); - return; - } - - const card = cardDrawResult.card; - - // Check if card has consequence (joker/luck card) even without type field - const hasConsequence = card.consequence !== undefined && card.consequence !== null; - const isLuckType = this.isLuckCard(card.type); - - // Get game state for turn number - const gameState = await this.getCurrentGameState(gameCode); - - // Broadcast card drawn to all players (everyone sees the question) - this.io.of('/game').to(gameRoomName).emit('game:card-drawn', { - playerName, - playerId, - cardType: hasConsequence ? 'joker' : this.getCardTypeName(card.type), - question: card.question, - fieldType: field.type, - timestamp: new Date().toISOString() - }); - - logOther('Card drawn event broadcasted', { - playerName, - playerId, - cardType: hasConsequence ? 'joker' : this.getCardTypeName(card.type), - hasConsequence, - cardTypeNumber: card.type, - question: card.question?.substring(0, 50), - fieldType: field.type - }); - - // Log card drawn - if (gameState) { - await this.turnHistoryService.logTurnAction( - gameCode, - playerId, - playerName, - gameState.turnNumber || 1, - TurnActionType.CARD_DRAWN, - currentPosition, - currentPosition, // Position unchanged at this point - { - cardId: card.cardid, - cardType: hasConsequence ? 'joker' : this.getCardTypeName(card.type), - question: card.question, - fieldType: field.type - } - ); - } - - // Check card type and handle accordingly - if (isLuckType || hasConsequence) { - // Update position to destination FIRST (player has landed on the luck field) - await this.updatePlayerPosition(gameCode, playerId, position); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: position, - fieldType: field.type, - timestamp: new Date().toISOString() - }); - - // Luck card - process immediately (no answer required) - const result = this.cardDrawingService.processLuckCard(card); - - // Broadcast luck result - this.io.of('/game').to(gameRoomName).emit('game:card-result', { - playerName, - playerId, - correct: true, - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - - // Process luck card with multi-turn support - // Note: processLuckCard will update position again if consequence moves player - if (card.consequence) { - await this.processLuckCard(gameCode, playerId, playerName, card.consequence, position); - } else { - // Fallback to old method if no consequence object - await this.applyCardConsequence(gameCode, playerId, playerName, result.consequence); - } - } else { - // Question card - send to player for answer - if (!cardDrawResult.clientData) { - logError('Client data missing for question card'); - logOther('Card details for missing clientData', { - cardType: card.type, - cardId: card.cardid, - hasCard: !!card, - cardKeys: card ? Object.keys(card) : [] - }); - await this.advanceTurn(gameCode); - return; - } - - // Update position to destination FIRST (player has landed on the field) - await this.updatePlayerPosition(gameCode, playerId, position); - - // Emit player-arrived event so frontend shows the position - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: position, - fieldType: field.type, - timestamp: new Date().toISOString() - }); - - // Send interactive card to the player who drew it - const playerRoomName = `game_${gameCode}:${playerName}`; - this.io.of('/game').to(playerRoomName).emit('game:card-drawn-self', { - cardData: cardDrawResult.clientData, - timeLimit: 60, // 60 seconds to answer - timestamp: new Date().toISOString() - }); - - logOther('Card sent to player for answer', { - playerName, - playerId, - cardId: card.cardid, - cardType: this.getCardTypeName(card.type), - hasClientData: !!cardDrawResult.clientData, - clientDataKeys: cardDrawResult.clientData ? Object.keys(cardDrawResult.clientData) : [] - }); - - // Start answer timeout - const answerKey = this.cardDrawingService.startAnswerTimeout( - gameCode, - playerId, - card, - this.handleCardAnswerTimeout.bind(this) - ); - - // Store pending card in Redis - await this.storePendingCard(gameCode, playerId, { - playerId: playerId, - playerName: playerName, - card: card, - field: field, - dice: dice, - currentPosition: currentPosition, - landedPosition: position, // Store where they landed - drawnAt: Date.now() - }); - - logOther(`Stored pending card for player ${playerName}`, { - gameCode, - playerId, - cardType: this.getCardTypeName(card.type), - cardId: card.cardid, - redisKey: `game_pending_card:${gameCode}:${playerId}` - }); - } - - } catch (error) { - logError('Error handling special field landing', error as Error); - await this.advanceTurn(gameCode); - } - } - - /** - * Handle card answer timeout (player didn't answer in time) - */ - private async handleCardAnswerTimeout(gameCode: string, playerId: string, card: any): Promise { - try { - // Clear from Redis - await this.clearPendingCard(gameCode, playerId); - - const gameRoomName = `game_${gameCode}`; - const pendingCard = await this.getPendingCard(gameCode, playerId); - const playerName = pendingCard?.playerName || 'Player'; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:card-timeout', { - playerName, - playerId, - message: '⏰ Time\'s up!', - timestamp: new Date().toISOString() - }); - - // Process as timeout (automatic wrong answer) - const result = this.cardDrawingService.processTimeoutAnswer(card); - - // Broadcast result - this.io.of('/game').to(gameRoomName).emit('game:card-result', { - playerName, - playerId, - correct: false, - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - - // Apply penalty - await this.applyCardConsequence(gameCode, playerId, playerName, result.consequence); - - } catch (error) { - logError('Error handling card answer timeout', error as Error); - } - } - - /** - * Get human-readable card type name - */ - private getCardTypeName(cardType?: number): string { - if (cardType === undefined) return 'unknown'; - - const typeNames = ['quiz', 'sentence_pairing', 'own_answer', 'true_false', 'closer', 'joker', 'luck']; - return typeNames[cardType] || 'unknown'; - } - - private async handleDisconnect(socket: AuthenticatedSocket): Promise { - logOther(`Game socket disconnected: ${socket.id} (player: ${socket.playerName})`); - - // If the socket was in a game, handle cleanup - if (socket.gameCode && socket.playerName) { - try { - // Check if this player is the gamemaster - const game = await this.gameRepository.findByGameCode(socket.gameCode); - const isGamemaster = game && socket.userId && game.createdby === socket.userId; - - // If gamemaster leaves, end the game immediately - if (isGamemaster && game) { - logOther(`Gamemaster ${socket.playerName} left game ${socket.gameCode}, ending game`); - - const gameRoomName = `game_${socket.gameCode}`; - - // Notify all players - this.io.of('/game').to(gameRoomName).emit('game:ended', { - reason: 'gamemaster_left', - gamemasterName: socket.playerName, - message: `🎭 Gamemaster ${socket.playerName} left. Game has ended.`, - timestamp: new Date().toISOString() - }); - - // Update database - await this.gameRepository.update(game.id, { - state: GameState.CANCELLED, - enddate: new Date() - }); - - // Clean up all game data - await this.cleanupGameData(socket.gameCode, game.id); - - return; // Exit early, no need for further cleanup - } - - // Clean up any pending card answer - if (socket.userId) { - const pendingCard = await this.getPendingCard(socket.gameCode, socket.userId); - if (pendingCard) { - // Clear timeout - const answerKey = `${socket.gameCode}:${socket.userId}`; - this.cardDrawingService.clearAnswerTimeout(answerKey); - await this.clearPendingCard(socket.gameCode, socket.userId); - - // Notify others - const gameRoomName = `game_${socket.gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:player-disconnected-during-card', { - playerName: socket.playerName, - playerId: socket.userId, - timestamp: new Date().toISOString() - }); - } - } - - // Update player connection status - await this.updatePlayerConnection(socket.gameCode, socket.playerName, false); - - // Create snapshot on player disconnect during active game - const gameState = await this.getCurrentGameState(socket.gameCode); - if (gameState) { - await this.gameSnapshotService.createSnapshot( - socket.gameCode, - (gameState.currentTurn || 0) + 1, - SnapshotTrigger.PLAYER_DISCONNECT, - `Player ${socket.playerName} disconnected` - ).catch(err => { - logError('Failed to create disconnect snapshot', err as Error); - logOther('Disconnect snapshot context', { - gameCode: socket.gameCode, - playerName: socket.playerName - }); - }); - } - - // Check if disconnected player was current player - const playerIdentifier = socket.userId || socket.playerName; - if (gameState && gameState.currentPlayer === playerIdentifier) { - logOther(`Current player ${socket.playerName} disconnected, advancing turn`, { gameCode: socket.gameCode }); - - // Broadcast disconnect during turn - const gameRoomName = `game_${socket.gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:player-disconnected-during-turn', { - playerName: socket.playerName, - playerId: playerIdentifier, - message: `${socket.playerName} disconnected during their turn`, - timestamp: new Date().toISOString() - }); - - // Advance to next player - await this.advanceTurn(socket.gameCode); - } - - // Clean up player-specific Redis data - await this.cleanupPlayerData(socket.gameCode, socket.playerName, socket.userId); - - // Notify other players about disconnection - const gameRoomName = `game_${socket.gameCode}`; - socket.to(gameRoomName).emit('game:player-disconnected', { - playerName: socket.playerName, - playerId: socket.userId, - timestamp: new Date().toISOString() - }); - - // Check if this was the last player - if so, consider ending/cleaning the game - const connectedPlayers = await this.getConnectedPlayers(socket.gameCode); - if (connectedPlayers.length === 0) { - logOther(`All players disconnected from game ${socket.gameCode}, scheduling cleanup`); - // Schedule cleanup after a delay to allow for reconnections - setTimeout(async () => { - const stillConnected = await this.getConnectedPlayers(socket.gameCode!); - if (stillConnected.length === 0) { - await this.handleAbandonedGame(socket.gameCode!); - } - }, 60000); // 1 minute delay - } - - } catch (error) { - logError('Error updating player connection on disconnect', error as Error); - logOther('Disconnect error context', { gameCode: socket.gameCode, playerName: socket.playerName }); - } - } - } - - /** - * Clean up player-specific data when they disconnect - * @param gameCode Game code - * @param playerName Player name - * @param playerId Player ID - */ - private async cleanupPlayerData(gameCode: string, playerName: string, playerId?: string): Promise { - try { - // Remove from ready players - await this.redisService.setRemove(`game_ready:${gameCode}`, playerName); - - // Remove from pending players if they were pending - await this.redisService.setRemove(`game_pending:${gameCode}`, playerName); - - logOther(`Cleaned up player data for ${playerName} in game ${gameCode}`); - - } catch (error) { - logError('Error cleaning up player data', error as Error); - } - } - - /** - * Handle games that have been abandoned by all players - * @param gameCode Game code - */ - private async handleAbandonedGame(gameCode: string): Promise { - try { - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) return; - - // Only clean up games that haven't finished yet - if (game.state !== GameState.FINISHED && game.state !== GameState.CANCELLED) { - logOther(`Handling abandoned game ${gameCode}`, { gameId: game.id }); - - // Mark game as cancelled in database - await this.gameRepository.update(game.id, { - state: GameState.CANCELLED, - enddate: new Date(), - }); - - // Clean up all Redis data for this abandoned game - await this.cleanupGameData(gameCode, game.id); - - logOther(`Abandoned game ${gameCode} has been cleaned up`); - } - - } catch (error) { - logError('Error handling abandoned game', error as Error); - } - } - - // Helper methods for game state management - - private async getGameState(gameCode: string): Promise { - try { - // Try gameplay first (game started/in-progress) - const gameplayState = await this.getCurrentGameState(gameCode); - if (gameplayState) { - return gameplayState; - } - - // Fallback to game: key for pre-game lobby - const gameKey = `game:${gameCode}`; - const gameStr = await this.redisService.get(gameKey); - if (gameStr) { - const gameData = JSON.parse(gameStr); - // Add pending players for private games - if (gameData.logintype === LoginType.PRIVATE) { - gameData.pendingPlayers = await this.getPendingPlayers(gameCode); - } - return gameData; - } - - return null; - } catch (error) { - logError('Error getting game state', error as Error); - return null; - } - } - - private async processGameAction(game: GameAggregate, playerId: string, action: string, actionData: any): Promise<{ success: boolean; data?: any; error?: string; stateChanged?: boolean }> { - // This would contain the actual game logic - // For now, returning a placeholder - - switch (action) { - case 'roll-dice': - // Handle dice rolling logic - const diceResult = Math.floor(Math.random() * 6) + 1; - return { - success: true, - data: { dice: diceResult }, - stateChanged: true - }; - - case 'move': - // Handle player movement logic - return { - success: true, - data: { newPosition: actionData.position }, - stateChanged: true - }; - - case 'use-field': - // Handle special field usage - return { - success: true, - data: { fieldUsed: actionData.fieldType }, - stateChanged: true - }; - - case 'end-turn': - // Handle turn ending logic - return { - success: true, - data: { nextPlayer: 'next-player-id' }, - stateChanged: true - }; - - default: - return { - success: false, - error: 'Unknown action type' - }; - } - } - - private async updatePlayerConnection(gameCode: string, playerName: string, connected: boolean): Promise { - const key = `game_connections:${gameCode}`; - if (connected) { - await this.redisService.setAdd(key, playerName); - } else { - await this.redisService.setRemove(key, playerName); - } - // Note: RedisService doesn't have expire method, we'll handle expiration differently - } - - private async updatePlayerReadyStatus(gameCode: string, playerName: string, ready: boolean): Promise { - const key = `game_ready:${gameCode}`; - if (ready) { - await this.redisService.setAdd(key, playerName); - } else { - await this.redisService.setRemove(key, playerName); - } - // Note: RedisService doesn't have expire method, we'll handle expiration differently - } - - private async addToPendingPlayers(gameCode: string, playerName: string): Promise { - const key = `game_pending:${gameCode}`; - await this.redisService.setAdd(key, playerName); - } - - private async removeFromPendingPlayers(gameCode: string, playerName: string): Promise { - const key = `game_pending:${gameCode}`; - await this.redisService.setRemove(key, playerName); - } - - private async getPendingPlayers(gameCode: string): Promise { - const key = `game_pending:${gameCode}`; - return await this.redisService.setMembers(key); - } - - // Redis methods for pending card answers - private async storePendingCard(gameCode: string, playerId: string, cardState: PendingCardState): Promise { - const key = `game_pending_card:${gameCode}:${playerId}`; - await this.redisService.setWithExpiry(key, JSON.stringify(cardState), 90); // 90 seconds (30 seconds buffer after timeout) - } - - private async getPendingCard(gameCode: string, playerId: string): Promise { - const key = `game_pending_card:${gameCode}:${playerId}`; - const dataStr = await this.redisService.get(key); - return dataStr ? JSON.parse(dataStr) : null; - } - - private async clearPendingCard(gameCode: string, playerId: string): Promise { - const key = `game_pending_card:${gameCode}:${playerId}`; - await this.redisService.del(key); - } - - // Redis methods for pending gamemaster decisions - private async storePendingDecision(gameCode: string, requestId: string, decisionState: PendingDecisionState): Promise { - const key = `game_pending_decision:${gameCode}:${requestId}`; - await this.redisService.setWithExpiry(key, JSON.stringify(decisionState), 150); // 150 seconds (30 seconds buffer after timeout) - } - - private async getPendingDecision(gameCode: string, requestId: string): Promise { - const key = `game_pending_decision:${gameCode}:${requestId}`; - const dataStr = await this.redisService.get(key); - return dataStr ? JSON.parse(dataStr) : null; - } - - private async clearPendingDecision(gameCode: string, requestId: string): Promise { - const key = `game_pending_decision:${gameCode}:${requestId}`; - await this.redisService.del(key); - } - - // Helper to get all pending decision keys for a game - private async getAllPendingDecisionKeys(gameCode: string): Promise { - // Note: This is a simplified version. In production, you might want to maintain a set of request IDs - // For now, we'll rely on the GamemasterService's in-memory tracking - const pendingDecisions = this.gamemasterService.getPendingDecisionsForGame(gameCode); - return pendingDecisions.map(d => d.requestId); - } - - private async getCurrentGameState(gameCode: string): Promise { - try { - const gamePlayKey = `gameplay:${gameCode}`; - const gameStateStr = await this.redisService.get(gamePlayKey); - - if (gameStateStr) { - return JSON.parse(gameStateStr); - } - return null; - } catch (error) { - logError('Error getting current game state', error as Error); - return null; - } - } - - private async getPlayerPositions(gameCode: string): Promise { - try { - // Get positions from gameplay (single source of truth) - const gameState = await this.getCurrentGameState(gameCode); - if (gameState && gameState.players) { - return gameState.players.map((player: any) => ({ - playerId: player.playerId, - playerName: player.playerName || player.playerId, - boardPosition: player.position || 0, - turnOrder: player.turnOrder - })); - } - - return []; - } catch (error) { - logError('Error getting player positions', error as Error); - return []; - } - } - - private async updatePlayerPosition(gameCode: string, playerId: string, newPosition: number): Promise { - try { - // Update position in gameplay (single source of truth) - const gameState = await this.getCurrentGameState(gameCode); - if (gameState && gameState.players) { - const player = gameState.players.find((p: any) => p.playerId === playerId); - if (player) { - player.position = newPosition; - - // Save updated gameplay state - const gamePlayKey = `gameplay:${gameCode}`; - await this.redisService.set(gamePlayKey, JSON.stringify(gameState)); - } - } - } catch (error) { - logError('Error updating player position', error as Error); - } - } - - // ============================================ - // TURN TRACKING REDIS METHODS - // ============================================ - - /** - * Set the number of extra turns for a player - */ - private async setPlayerExtraTurns( - gameCode: string, - playerId: string, - count: number - ): Promise { - const key = `player_extra_turns:${gameCode}:${playerId}`; - await this.redisService.set(key, count.toString()); - logOther(`Set extra turns for player ${playerId}`, { gameCode, count }); - } - - /** - * Get the number of extra turns for a player - */ - private async getPlayerExtraTurns( - gameCode: string, - playerId: string - ): Promise { - const key = `player_extra_turns:${gameCode}:${playerId}`; - const value = await this.redisService.get(key); - return value ? parseInt(value, 10) : 0; - } - - /** - * Decrement extra turns by 1, delete key if reaches 0 - */ - private async decrementPlayerExtraTurns( - gameCode: string, - playerId: string - ): Promise { - const current = await this.getPlayerExtraTurns(gameCode, playerId); - if (current > 1) { - await this.setPlayerExtraTurns(gameCode, playerId, current - 1); - } else { - const key = `player_extra_turns:${gameCode}:${playerId}`; - await this.redisService.del(key); - } - } - - /** - * Set the number of turns to lose for a player - */ - private async setPlayerTurnsToLose( - gameCode: string, - playerId: string, - count: number - ): Promise { - const key = `player_turns_to_lose:${gameCode}:${playerId}`; - await this.redisService.set(key, count.toString()); - logOther(`Set turns to lose for player ${playerId}`, { gameCode, count }); - } - - /** - * Get the number of turns to lose for a player - */ - private async getPlayerTurnsToLose( - gameCode: string, - playerId: string - ): Promise { - const key = `player_turns_to_lose:${gameCode}:${playerId}`; - const value = await this.redisService.get(key); - return value ? parseInt(value, 10) : 0; - } - - /** - * Decrement turns to lose by 1, delete key if reaches 0 - */ - private async decrementPlayerTurnsToLose( - gameCode: string, - playerId: string - ): Promise { - const current = await this.getPlayerTurnsToLose(gameCode, playerId); - if (current > 1) { - await this.setPlayerTurnsToLose(gameCode, playerId, current - 1); - } else { - const key = `player_turns_to_lose:${gameCode}:${playerId}`; - await this.redisService.del(key); - } - } - - /** - * Clear all turn tracking data for a player - */ - private async clearPlayerTurnData( - gameCode: string, - playerId: string - ): Promise { - await this.redisService.del(`player_extra_turns:${gameCode}:${playerId}`); - await this.redisService.del(`player_turns_to_lose:${gameCode}:${playerId}`); - } - - // ============================================ - // POSITION GUESSING MECHANICS - // ============================================ - - /** - * Determine if position guess is required based on field type and answer correctness - * - * Logic: - * - Positive field + correct answer = GUESS (reward scenario) - * - Positive field + wrong answer = NO GUESS (no movement) - * - Negative field + correct answer = NO GUESS (avoid penalty) - * - Negative field + wrong answer = GUESS (penalty scenario) - * - Regular field = NO GUESS (never guess on regular fields) - */ - private determineGuessRequirement( - fieldType: 'regular' | 'positive' | 'negative' | 'luck', - answerCorrect: boolean - ): boolean { - if (fieldType === 'positive') { - return answerCorrect; // Correct = guess for reward - } else if (fieldType === 'negative') { - return !answerCorrect; // Wrong = guess for penalty - } - return false; // Regular and luck fields never require guess - } - - /** - * Request position guess from player with stepping calculation info - */ - private async requestPositionGuess( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingCardState - ): Promise { - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Calculate what the actual position would be (without showing to player yet) - // Use the LANDED field position for pattern modifier calculation - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - pendingState.dice - ); - - // Calculate the ACTUAL pattern modifier based on the LANDED field position - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Store stepping info for later validation - pendingState.requiresGuess = true; - const cardKey = `game_pending_card:${gameCode}:${playerId}`; - await this.redisService.setWithExpiry(cardKey, JSON.stringify(pendingState), 30); - - // Start timeout for position guess (30 seconds) - setTimeout(() => { - this.handlePositionGuessTimeout(gameCode, playerId, playerName, pendingState); - }, 30000); - - // Notify player to guess - send the ACTUAL pattern modifier and LANDED field position - this.io.of('/game').to(playerRoomName).emit('game:position-guess-request', { - message: 'Guess your final position!', - currentPosition: landedFieldPosition, - diceRoll: pendingState.dice, - fieldStepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - timeLimit: 30, - timestamp: new Date().toISOString() - }); - - // Notify others that player is guessing - this.io.of('/game').to(gameRoomName).emit('game:player-guessing', { - playerId, - playerName, - message: `${playerName} is guessing their final position...`, - timestamp: new Date().toISOString() - }); - - logOther(`Position guess requested from ${playerName}`, { gameCode }); - } - - /** - * Handle position guess timeout (player didn't guess in time) - */ - private async handlePositionGuessTimeout( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingCardState - ): Promise { - try { - // Check if pending state still exists (player might have already guessed) - const cardKey = `game_pending_card:${gameCode}:${playerId}`; - const stateJson = await this.redisService.get(cardKey); - if (!stateJson) { - // Already processed, nothing to do - return; - } - - // Clear from Redis - await this.clearPendingCard(gameCode, playerId); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:guess-timeout', { - playerId, - playerName, - message: `⏰ ${playerName} didn't guess in time!`, - timestamp: new Date().toISOString() - }); - - // Calculate actual position using LANDED field position - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - pendingState.dice - ); - - // Apply -2 penalty for timeout (treated as wrong guess) - const finalPosition = Math.max(1, actualPosition - 2); - - // Update player position - await this.updatePlayerPosition(gameCode, playerId, finalPosition); - - // Emit player-arrived event FIRST (before guess-result) - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: finalPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Calculate the actual pattern modifier used (based on LANDED field) - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Broadcast result - this.io.of('/game').to(gameRoomName).emit('game:guess-result', { - playerId, - playerName, - guessedPosition: null, - actualPosition: actualPosition, - finalPosition: finalPosition, - guessCorrect: false, - penaltyApplied: true, - calculation: { - startPosition: landedFieldPosition, - diceRoll: pendingState.dice, - stepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - calculatedPosition: actualPosition, - penalty: -2 - }, - message: `❌ ${playerName} timed out! Penalty applied. Final position: ${finalPosition}`, - timestamp: new Date().toISOString() - }); - - // Check for win condition - if (finalPosition >= 100) { - await this.endGame(gameCode, playerId, playerName); - return; - } - - // Check if landed on special field (secondary landing) - // For positive/negative fields, this will draw joker card - // For luck fields, this will return false and we'll handle them below - const secondaryLandingHandled = await this.checkSecondaryLanding( - gameCode, - playerId, - playerName, - finalPosition, - 0 // recursion depth - ); - - if (secondaryLandingHandled) { - // Joker card flow initiated, don't advance turn - return; - } - - // Check if landed on luck field (non-joker secondary landing) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === finalPosition); - - if (landedField && landedField.type === 'luck') { - // Handle luck field normally - await this.handleSpecialFieldLanding( - gameCode, - playerId, - playerName, - landedField, - finalPosition, - 6, // Secondary landing uses dice = 6 - finalPosition // Use finalPosition as currentPosition for next card draw - ); - return; - } - } - - // No special field, advance turn - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling position guess timeout', error as Error); - // Don't call advanceTurn here - already called above if successful - } - } - - /** - * Handle position guess submission from player - */ - private async handlePositionGuess(socket: AuthenticatedSocket, data: { - gameCode: string; - guessedPosition: number; - }): Promise { - try { - const { gameCode, guessedPosition } = data; - const playerId = socket.userId || socket.playerName; - const playerName = socket.playerName; - - if (!playerId || !playerName) { - socket.emit('error', { message: 'Player identification failed' }); - return; - } - - // Get pending card state - const cardKey = `game_pending_card:${gameCode}:${playerId}`; - const stateJson = await this.redisService.get(cardKey); - if (!stateJson) { - socket.emit('error', { message: 'No pending guess found' }); - return; - } - - // Clear from Redis immediately to prevent timeout handler from processing - await this.clearPendingCard(gameCode, playerId); - - const pendingState: PendingCardState = JSON.parse(stateJson); - pendingState.guessedPosition = guessedPosition; - - // Broadcast the guess to everyone - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:position-guess-broadcast', { - playerId, - playerName, - guessedPosition, - message: `${playerName} guessed position ${guessedPosition}`, - timestamp: new Date().toISOString() - }); - - // Process the guess with FieldEffectService - await this.processQuestionCardWithGuess(gameCode, pendingState); - - // Clean up pending state - await this.redisService.del(cardKey); - - } catch (error) { - logError('Error handling position guess', error as Error); - socket.emit('error', { message: 'Failed to process guess' }); - } - } - - /** - * Process question card with guess using FieldEffectService - */ - private async processQuestionCardWithGuess( - gameCode: string, - pendingState: PendingCardState - ): Promise { - // Calculate actual position using BoardGenerationService - // Use the LANDED field position for calculation - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - pendingState.dice - ); - - let finalPosition = actualPosition; - let guessCorrect = false; - let penaltyApplied = false; - - // Check if guess was correct - if (pendingState.guessedPosition === actualPosition) { - guessCorrect = true; - } else { - // Wrong guess: apply -2 penalty - finalPosition = Math.max(1, actualPosition - 2); - penaltyApplied = true; - } - - // Update player position - await this.updatePlayerPosition(gameCode, pendingState.playerId, finalPosition); - - // Calculate the actual pattern modifier used (based on LANDED field) - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Emit player-arrived event FIRST (before guess-result) - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - position: finalPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Broadcast result - this.io.of('/game').to(gameRoomName).emit('game:guess-result', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - guessedPosition: pendingState.guessedPosition, - actualPosition: actualPosition, - finalPosition: finalPosition, - guessCorrect, - penaltyApplied, - calculation: { - startPosition: pendingState.field.position, - diceRoll: pendingState.dice, - stepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - calculatedPosition: actualPosition, - penalty: penaltyApplied ? -2 : 0 - }, - message: guessCorrect - ? `✅ ${pendingState.playerName} guessed correctly! Moved to ${finalPosition}` - : `❌ ${pendingState.playerName} guessed wrong! Penalty applied. Final position: ${finalPosition}`, - timestamp: new Date().toISOString() - }); - - // Check for win condition (position 100) - if (finalPosition >= 100) { - await this.endGame(gameCode, pendingState.playerId, pendingState.playerName); - return; - } - - // Check if landed on special field (secondary landing) - // For positive/negative fields, this will draw joker card - // For luck fields, this will return false and we'll handle them below - const secondaryLandingHandled = await this.checkSecondaryLanding( - gameCode, - pendingState.playerId, - pendingState.playerName, - finalPosition, - 0 // recursion depth - ); - - if (secondaryLandingHandled) { - // Joker card flow initiated, don't advance turn - return; - } - - // Check if landed on luck field (non-joker secondary landing) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === finalPosition); - - if (landedField && landedField.type === 'luck') { - // Handle luck field normally - await this.handleSpecialFieldLanding( - gameCode, - pendingState.playerId, - pendingState.playerName, - landedField, - finalPosition, - 6, // Secondary landing uses dice = 6 - finalPosition // Use finalPosition as currentPosition for next card draw - ); - return; - } - } - - // No special field, advance turn - await this.advanceTurn(gameCode); - } - - /** - * Process luck card consequence with multi-turn support - */ - private async processLuckCard( - gameCode: string, - playerId: string, - playerName: string, - consequence: { type: number; value?: number }, - currentPosition: number - ): Promise { - const gameRoomName = `game_${gameCode}`; - let newPosition = currentPosition; - let shouldAdvanceTurn = true; - const consequenceValue = consequence.value || 1; - - // ConsequenceType enum: 0=MOVE_FORWARD, 1=MOVE_BACKWARD, 2=LOSE_TURN, 3=EXTRA_TURN, 5=GO_TO_START - switch (consequence.type) { - case 0: // MOVE_FORWARD - newPosition = Math.min(currentPosition + consequenceValue, 100); - await this.updatePlayerPosition(gameCode, playerId, newPosition); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'MOVE_FORWARD', - value: consequenceValue, - newPosition, - message: `${playerName} moves forward ${consequenceValue} steps to position ${newPosition}!`, - timestamp: new Date().toISOString() - }); - // Emit player-arrived so frontend visualizes the movement - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'luck', - timestamp: new Date().toISOString() - }); - break; - - case 1: // MOVE_BACKWARD - newPosition = Math.max(1, currentPosition - consequenceValue); - await this.updatePlayerPosition(gameCode, playerId, newPosition); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'MOVE_BACKWARD', - value: consequenceValue, - newPosition, - message: `${playerName} moves backward ${consequenceValue} steps to position ${newPosition}!`, - timestamp: new Date().toISOString() - }); - // Emit player-arrived so frontend visualizes the movement - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'luck', - timestamp: new Date().toISOString() - }); - break; - - case 2: // LOSE_TURN - // Store turns to lose in Redis - await this.setPlayerTurnsToLose(gameCode, playerId, consequenceValue); - - // Emit turn-lost event - this.io.of('/game').to(gameRoomName).emit('game:turn-lost', { - playerId, - playerName, - turnsToLose: consequenceValue, - message: `${playerName} will lose ${consequenceValue} turn(s)!`, - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'LOSE_TURN', - value: consequenceValue, - turnsToLose: consequenceValue, - message: `${playerName} will lose ${consequenceValue} turn(s)!`, - timestamp: new Date().toISOString() - }); - shouldAdvanceTurn = true; // Skip to next player immediately - break; - - case 3: // EXTRA_TURN - // Store extra turns in Redis - await this.setPlayerExtraTurns(gameCode, playerId, consequenceValue); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'EXTRA_TURN', - value: consequenceValue, - extraTurns: consequenceValue, - message: `${playerName} gets ${consequenceValue} extra turn(s)!`, - timestamp: new Date().toISOString() - }); - shouldAdvanceTurn = true; // Let advanceTurn() handle extra turns - break; - - case 5: // GO_TO_START - newPosition = 1; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'GO_TO_START', - value: consequenceValue, - newPosition: 1, - message: `${playerName} goes back to START!`, - timestamp: new Date().toISOString() - }); - break; - } - - // Check for win condition (position 100) - if (newPosition >= 100) { - await this.endGame(gameCode, playerId, playerName); - return; - } - - // Advance turn if needed - if (shouldAdvanceTurn) { - await this.advanceTurn(gameCode); - } - } - - // ============================================ - // JOKER CARD POSITION GUESSING - // ============================================ - - /** - * Request position guess from player AFTER gamemaster decision (for jokers) - */ - private async requestJokerPositionGuess( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingDecisionState - ): Promise { - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Calculate stepping info with dice = 6, using LANDED field position - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - 6 // Joker cards always use dice value of 6 - ); - - // Calculate the ACTUAL pattern modifier based on LANDED field position - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Update pending state - const decisionKey = `pending_decision:${gameCode}:${pendingState.playerId}`; - await this.redisService.setWithExpiry(decisionKey, JSON.stringify(pendingState), 30); - - // Start timeout for joker position guess (30 seconds) - setTimeout(() => { - this.handleJokerPositionGuessTimeout(gameCode, playerId, playerName, pendingState); - }, 30000); - - // Notify player to guess - send the ACTUAL pattern modifier and LANDED field position - this.io.of('/game').to(playerRoomName).emit('game:joker-position-guess-request', { - message: 'Guess your final position after joker!', - currentPosition: landedFieldPosition, - diceRoll: 6, - fieldStepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - timeLimit: 30, - timestamp: new Date().toISOString() - }); - - // Notify others - this.io.of('/game').to(gameRoomName).emit('game:player-guessing', { - playerId, - playerName, - message: `${playerName} is guessing their position after joker...`, - timestamp: new Date().toISOString() - }); - - logOther(`Joker position guess requested from ${playerName}`, { gameCode }); - } - - /** - * Handle joker position guess timeout (player didn't guess in time) - */ - private async handleJokerPositionGuessTimeout( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingDecisionState - ): Promise { - try { - // Check if pending state still exists (player might have already guessed) - const decisionKey = `pending_decision:${gameCode}:${playerId}`; - const stateJson = await this.redisService.get(decisionKey); - if (!stateJson) { - // Already processed, nothing to do - return; - } - - // Clear from Redis - await this.clearPendingDecision(gameCode, playerId); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:guess-timeout', { - playerId, - playerName, - message: `⏰ ${playerName} didn't guess in time after joker!`, - timestamp: new Date().toISOString() - }); - - // Default behavior on timeout: no movement (player stays at current position) - this.io.of('/game').to(gameRoomName).emit('game:joker-complete', { - playerId, - playerName, - guessedPosition: null, - actualPosition: pendingState.currentPosition, - finalPosition: pendingState.currentPosition, - guessCorrect: false, - penaltyApplied: false, - moved: false, - calculation: { - startPosition: pendingState.field.position, - diceRoll: 6, - stepValue: pendingState.field.stepValue || 0, - patternModifier: this.boardGenerationService.getPatternModifier(pendingState.field.position, (pendingState.field.stepValue || 0) > 0), - calculatedPosition: pendingState.currentPosition, - penalty: 0 - }, - message: `${playerName} timed out on joker guess (no movement)`, - timestamp: new Date().toISOString() - }); - - // Advance turn - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling joker position guess timeout', error as Error); - // Ensure turn advances even on error - await this.advanceTurn(gameCode); - } - } - - /** - * Handle joker position guess submission - */ - private async handleJokerPositionGuess(socket: AuthenticatedSocket, data: { - gameCode: string; - guessedPosition: number; - }): Promise { - try { - const { gameCode, guessedPosition } = data; - const playerId = socket.userId || socket.playerName; - const playerName = socket.playerName; - - if (!playerId || !playerName) { - socket.emit('error', { message: 'Player identification failed' }); - return; - } - - // Get pending decision state - try with playerId as key - let decisionKey = `pending_decision:${gameCode}:${playerId}`; - let stateJson = await this.redisService.get(decisionKey); - - if (!stateJson) { - socket.emit('error', { message: 'No pending joker guess found' }); - return; - } - - // Clear from Redis immediately to prevent timeout handler from processing - await this.clearPendingDecision(gameCode, playerId); - - const pendingState: PendingDecisionState = JSON.parse(stateJson); - pendingState.guessedPosition = guessedPosition; - - // Broadcast the guess - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:position-guess-broadcast', { - playerId, - playerName, - guessedPosition, - message: `${playerName} guessed position ${guessedPosition}`, - timestamp: new Date().toISOString() - }); - - // Calculate actual position using BoardGenerationService - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - pendingState.currentPosition, - pendingState.field.stepValue || 0, - 6 // Joker always uses dice = 6 - ); - - let finalPosition = actualPosition; - let guessCorrect = false; - let penaltyApplied = false; - - // Check guess - if (guessedPosition === actualPosition) { - guessCorrect = true; - } else { - finalPosition = Math.max(1, actualPosition - 2); - penaltyApplied = true; - } - - // Apply movement based on field type and gamemaster decision - const shouldMove = (pendingState.field.type === 'positive' && pendingState.gamemasterApproved) || - (pendingState.field.type === 'negative' && !pendingState.gamemasterApproved); - - if (shouldMove) { - await this.updatePlayerPosition(gameCode, playerId, finalPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: finalPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - } - - // Calculate the actual pattern modifier used - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(pendingState.currentPosition, positiveField); - - // Broadcast joker complete - this.io.of('/game').to(gameRoomName).emit('game:joker-complete', { - playerId, - playerName, - guessedPosition, - actualPosition: actualPosition, - finalPosition: shouldMove ? finalPosition : pendingState.currentPosition, - guessCorrect, - penaltyApplied, - moved: shouldMove, - calculation: { - startPosition: pendingState.field.position, - diceRoll: 6, - stepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - calculatedPosition: actualPosition, - penalty: penaltyApplied ? -2 : 0 - }, - message: shouldMove - ? (guessCorrect - ? `✅ ${playerName} guessed correctly! Moved to ${finalPosition}` - : `❌ ${playerName} guessed wrong! Penalty applied. Final position: ${finalPosition}`) - : `${playerName} did not move (gamemaster decision)`, - timestamp: new Date().toISOString() - }); - - // Clean up - await this.redisService.del(decisionKey); - - // Check for win condition - const movedPosition = shouldMove ? finalPosition : pendingState.currentPosition; - if (movedPosition >= 100) { - await this.endGame(gameCode, playerId, playerName); - return; - } - - // Check if landed on special field (secondary landing) if moved - if (shouldMove) { - // For positive/negative fields, this will draw joker card - // For luck fields, this will return false and we'll handle them below - const secondaryLandingHandled = await this.checkSecondaryLanding( - gameCode, - playerId, - playerName, - finalPosition, - 0 // recursion depth - ); - - if (secondaryLandingHandled) { - // Joker card flow initiated, don't advance turn - return; - } - - // Check if landed on luck field (non-joker secondary landing) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === finalPosition); - - if (landedField && landedField.type === 'luck') { - // Handle luck field normally - await this.handleSpecialFieldLanding( - gameCode, - playerId, - playerName, - landedField, - finalPosition, - 6, // Secondary landing uses dice = 6 - finalPosition // Use finalPosition as currentPosition for next card draw - ); - return; - } - } - } - - // No special field or didn't move, advance turn - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling joker position guess', error as Error); - socket.emit('error', { message: 'Failed to process joker guess' }); - } - } - - private async advanceTurn(gameCode: string): Promise { - try { - const gameState = await this.getCurrentGameState(gameCode); - if (!gameState) return; - - const currentTurnIndex = gameState.currentTurn || 0; - const currentPlayerId = gameState.turnSequence[currentTurnIndex]; - - // ========================================== - // PHASE 1: Check if current player has extra turns - // ========================================== - const extraTurns = await this.getPlayerExtraTurns(gameCode, currentPlayerId); - if (extraTurns > 0) { - // Current player gets another turn - await this.decrementPlayerExtraTurns(gameCode, currentPlayerId); - - const playerPositions = await this.getPlayerPositions(gameCode); - const currentPlayer = playerPositions.find(p => p.playerId === currentPlayerId); - const currentPlayerName = currentPlayer?.playerName || currentPlayerId; - - // Notify about extra turn - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:extra-turn-remaining', { - playerId: currentPlayerId, - playerName: currentPlayerName, - remainingExtraTurns: extraTurns - 1, - message: `${currentPlayerName} has ${extraTurns - 1} extra turn(s) remaining!`, - timestamp: new Date().toISOString() - }); - - // Notify player they can roll again - const playerRoomName = `game_${gameCode}:${currentPlayerName}`; - this.io.of('/game').to(playerRoomName).emit('game:your-turn', { - message: 'Extra turn! Roll the dice again!', - canRoll: true, - isExtraTurn: true, - timestamp: new Date().toISOString() - }); - - logOther(`Player ${currentPlayerName} using extra turn`, { - gameCode, - remainingExtraTurns: extraTurns - 1 - }); - - return; // Same player continues, don't advance - } - - // ========================================== - // PHASE 2: Find next player, skipping those with lost turns - // ========================================== - let nextTurnIndex = (currentTurnIndex + 1) % gameState.turnSequence.length; - const skippedPlayers: Array<{ - playerId: string; - playerName: string; - remainingTurnsToLose: number; - }> = []; - let loopGuard = 0; - const maxLoops = gameState.turnSequence.length; - - while (loopGuard < maxLoops) { - const candidatePlayerId = gameState.turnSequence[nextTurnIndex]; - const turnsToLose = await this.getPlayerTurnsToLose(gameCode, candidatePlayerId); - - if (turnsToLose > 0) { - // This player loses their turn - await this.decrementPlayerTurnsToLose(gameCode, candidatePlayerId); - - const playerPositions = await this.getPlayerPositions(gameCode); - const skippedPlayer = playerPositions.find(p => p.playerId === candidatePlayerId); - const skippedPlayerName = skippedPlayer?.playerName || candidatePlayerId; - - skippedPlayers.push({ - playerId: candidatePlayerId, - playerName: skippedPlayerName, - remainingTurnsToLose: turnsToLose - 1 - }); - - logOther(`Player ${skippedPlayerName} turn skipped`, { - gameCode, - remainingTurnsToLose: turnsToLose - 1 - }); - - // Move to next player in sequence - nextTurnIndex = (nextTurnIndex + 1) % gameState.turnSequence.length; - loopGuard++; - } else { - // Found a player who can play - break; - } - } - - // ========================================== - // PHASE 3: Update game state with valid next player - // ========================================== - const nextPlayerId = gameState.turnSequence[nextTurnIndex]; - gameState.currentTurn = nextTurnIndex; - gameState.currentPlayer = nextPlayerId; - - // Save updated state - const gamePlayKey = `gameplay:${gameCode}`; - await this.redisService.set(gamePlayKey, JSON.stringify(gameState)); - - // Create snapshot every 5 turns - const newTurnNumber = nextTurnIndex + 1; - if (this.gameSnapshotService.shouldCreateSnapshot(newTurnNumber)) { - await this.gameSnapshotService.createSnapshot( - gameCode, - newTurnNumber, - SnapshotTrigger.TURN_INTERVAL, - `Automatic snapshot at turn ${newTurnNumber}` - ).catch(err => { - logError('Failed to create turn snapshot', err as Error); - logOther('Turn snapshot context', { gameCode, turnNumber: newTurnNumber }); - }); - } - - // Get next player info - const playerPositions = await this.getPlayerPositions(gameCode); - const nextPlayer = playerPositions.find(p => p.playerId === nextPlayerId); - const nextPlayerName = nextPlayer?.playerName || nextPlayerId; - - // ========================================== - // PHASE 4: Notify about skipped players (if any) - // ========================================== - const gameRoomName = `game_${gameCode}`; - if (skippedPlayers.length > 0) { - this.io.of('/game').to(gameRoomName).emit('game:players-skipped', { - skippedPlayers, - message: `${skippedPlayers.map(p => p.playerName).join(', ')} skipped due to lost turn(s)`, - timestamp: new Date().toISOString() - }); - } - - // ========================================== - // PHASE 5: Notify about turn change - // ========================================== - this.io.of('/game').to(gameRoomName).emit('game:turn-changed', { - currentPlayer: nextPlayerId, - currentPlayerName: nextPlayerName, - turnNumber: nextTurnIndex + 1, - message: `It's ${nextPlayerName}'s turn!`, - timestamp: new Date().toISOString() - }); - - // Send special notification to the current player - const playerRoomName = `game_${gameCode}:${nextPlayerName}`; - this.io.of('/game').to(playerRoomName).emit('game:your-turn', { - message: 'It\'s your turn! Roll the dice!', - canRoll: true, - timestamp: new Date().toISOString() - }); - - logOther(`Turn advanced in game ${gameCode}`, { - previousTurn: currentTurnIndex, - newTurn: nextTurnIndex, - nextPlayer: nextPlayerName, - skippedCount: skippedPlayers.length - }); - - } catch (error) { - logError('Error advancing turn', error as Error); - } - } - - private async endGame(gameCode: string, winnerId: string, winnerName: string): Promise { - try { - // Update game state to finished - const gameState = await this.getCurrentGameState(gameCode); - if (gameState) { - gameState.gamePhase = 'finished'; - gameState.winner = winnerId; - gameState.winnerName = winnerName; - gameState.endedAt = new Date().toISOString(); - - const gamePlayKey = `gameplay:${gameCode}`; - await this.redisService.set(gamePlayKey, JSON.stringify(gameState)); - } - - // Update database game record - const game = await this.gameRepository.findByGameCode(gameCode); - if (game) { - await this.gameRepository.update(game.id, { - state: GameState.FINISHED, - winnerId: winnerId, - enddate: new Date() - }); - } - - // Broadcast game end to all players - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:ended', { - winner: winnerId, - winnerName: winnerName, - message: `🎉 ${winnerName} won the game! Congratulations!`, - finalPositions: await this.getPlayerPositions(gameCode), - timestamp: new Date().toISOString() - }); - - // Clean up all game-related Redis data and socket connections - await this.cleanupGameData(gameCode, game?.id); - - logOther(`Game ${gameCode} ended and cleaned up`, { - winner: winnerName, - winnerId, - gameId: game?.id - }); - - } catch (error) { - logError('Error ending game', error as Error); - } - } - - private async checkAllPlayersReady(gameCode: string): Promise { - try { - // Get connected players from Redis - const connectedPlayers = await this.getConnectedPlayers(gameCode); - const readyPlayers = await this.getReadyPlayers(gameCode); - - // All connected players must be ready for the game to start - return readyPlayers.length === connectedPlayers.length && connectedPlayers.length > 1; - } catch (error) { - logError('Error checking if all players are ready', error as Error); - return false; - } - } - - /** - * Apply card consequence (movement, turn effects) to a player - */ - private async applyCardConsequence(gameCode: string, playerId: string, playerName: string, consequence: number, recursionDepth: number = 0): Promise { - try { - // Safety check: prevent infinite loops - const MAX_RECURSION_DEPTH = 5; - if (recursionDepth >= MAX_RECURSION_DEPTH) { - logWarning(`Max recursion depth reached for consequence application in game ${gameCode}`); - await this.advanceTurn(gameCode); - return; - } - - // ConsequenceType enum: - // 0: MOVE_FORWARD, 1: MOVE_BACKWARD, 2: LOSE_TURN, 3: EXTRA_TURN, 5: GO_TO_START - - const positions = await this.getPlayerPositions(gameCode); - const currentPlayer = positions.find(p => p.playerId === playerId); - - if (!currentPlayer) { - logWarning(`Player ${playerId} not found when applying consequence`); - return; - } - - const gameRoomName = `game_${gameCode}`; - let newPosition = currentPlayer.boardPosition; - let positionChanged = false; - - switch (consequence) { - case 0: // MOVE_FORWARD - newPosition = Math.min(currentPlayer.boardPosition + 3, 101); // Move forward 3 steps - positionChanged = newPosition !== currentPlayer.boardPosition; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'move_forward', - oldPosition: currentPlayer.boardPosition, - newPosition, - timestamp: new Date().toISOString() - }); - break; - - case 1: // MOVE_BACKWARD - newPosition = Math.max(currentPlayer.boardPosition - 3, 0); // Move backward 3 steps - positionChanged = newPosition !== currentPlayer.boardPosition; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'move_backward', - oldPosition: currentPlayer.boardPosition, - newPosition, - timestamp: new Date().toISOString() - }); - break; - - case 2: // LOSE_TURN - // Immediately advance to next player - await this.advanceTurn(gameCode); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'lose_turn', - timestamp: new Date().toISOString() - }); - return; // Early return, turn already advanced - break; - - case 3: // EXTRA_TURN - // Don't advance turn, player gets to go again - const playerRoomName = `game_${gameCode}:${playerName}`; - this.io.of('/game').to(playerRoomName).emit('game:extra-turn', { - message: 'You get an extra turn!', - canRoll: true, - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'extra_turn', - timestamp: new Date().toISOString() - }); - return; // Early return, no turn advance needed - break; - - case 5: // GO_TO_START - newPosition = 0; - positionChanged = newPosition !== currentPlayer.boardPosition; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'go_to_start', - oldPosition: currentPlayer.boardPosition, - newPosition, - timestamp: new Date().toISOString() - }); - break; - - default: - logWarning(`Unknown consequence type: ${consequence}`); - } - - // Check for secondary special field landing (only if position changed) - if (positionChanged && newPosition > 0 && newPosition < 101) { - const secondaryLanding = await this.checkSecondaryLanding(gameCode, playerId, playerName, newPosition, recursionDepth); - if (secondaryLanding) { - // Secondary landing detected, joker flow initiated, don't advance turn yet - return; - } - } - - // If no secondary landing, advance to next player - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error applying card consequence', error as Error); - } - } - - /** - * Check if player landed on special field as result of consequence - * Returns true if joker card flow was initiated, false otherwise - */ - private async checkSecondaryLanding(gameCode: string, playerId: string, playerName: string, position: number, recursionDepth: number): Promise { - try { - logOther(`🔍 Checking secondary landing for ${playerName} at position ${position}`, { - gameCode, - playerId, - playerName, - position, - recursionDepth - }); - - const boardData = await this.getBoardData(gameCode); - if (!boardData || !boardData.fields) { - logOther('❌ No board data found for secondary landing check'); - return false; - } - - const landedField = boardData.fields.find((f: GameField) => f.position === position); - - // Check if field is special (positive or negative only for joker) - if (!landedField || !this.isSpecialField(landedField)) { - logOther(`❌ Position ${position} is not a special field`, { - hasField: !!landedField, - fieldType: landedField?.type - }); - return false; - } - - // Only positive and negative fields trigger joker on secondary landing - if (landedField.type !== 'positive' && landedField.type !== 'negative') { - logOther(`❌ Field type ${landedField.type} does not trigger joker (only positive/negative do)`); - return false; - } - - logOther(`✅ Secondary landing detected on ${landedField.type} field at position ${position} - Drawing joker card!`, { - fieldType: landedField.type, - position - }); - - const gameRoomName = `game_${gameCode}`; - - // Notify players about secondary landing - this.io.of('/game').to(gameRoomName).emit('game:secondary-landing', { - playerName, - playerId, - position, - fieldType: landedField.type, - message: `${playerName} landed on a ${landedField.type} field! Drawing joker card...`, - timestamp: new Date().toISOString() - }); - - // Wait 2 seconds for animation - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Draw joker card - await this.handleJokerCardDrawing(gameCode, playerId, playerName, landedField, position, recursionDepth); - - return true; // Joker flow initiated - - } catch (error) { - logError('Error checking secondary landing', error as Error); - return false; - } - } - - /** - * Handle joker card drawing and gamemaster decision flow - */ - private async handleJokerCardDrawing(gameCode: string, playerId: string, playerName: string, field: GameField, position: number, recursionDepth: number): Promise { - try { - const gameRoomName = `game_${gameCode}`; - - // Get game data - const gameData = await this.gameRepository.findByGameCode(gameCode); - if (!gameData) { - logError('Game not found when drawing joker card'); - await this.advanceTurn(gameCode); - return; - } - - // Draw joker card - const jokerResult = this.cardDrawingService.drawJokerCard(gameData, playerId); - - if (!jokerResult.success || !jokerResult.card) { - // No more joker cards available - this.io.of('/game').to(gameRoomName).emit('game:joker-error', { - playerName, - playerId, - error: jokerResult.error || 'No joker cards available', - timestamp: new Date().toISOString() - }); - await this.advanceTurn(gameCode); - return; - } - - const jokerCard = jokerResult.card; - - // Emit joker-activated event - this.io.of('/game').to(gameRoomName).emit('game:joker-activated', { - playerName, - playerId, - message: `${playerName} activated a Joker card!`, - timestamp: new Date().toISOString() - }); - - // Broadcast joker drawn to all players - this.io.of('/game').to(gameRoomName).emit('game:joker-drawn', { - playerName, - playerId, - jokerCard: { - question: jokerCard.question, - consequence: jokerCard.consequence - }, - waitingForGamemaster: true, - timestamp: new Date().toISOString() - }); - - // Request gamemaster decision - const requestId = this.gamemasterService.requestGamemasterDecision( - gameCode, - playerId, - playerName, - jokerCard, - (reqId: string) => this.handleGamemasterDecisionTimeout(gameCode, reqId, playerId, playerName, recursionDepth) - ); - - // Store pending decision in Redis - await this.storePendingDecision(gameCode, requestId, { - playerId, - playerName, - card: jokerCard, - field: field, - dice: 6, // Joker cards always use dice value of 6 - currentPosition: position, // Current position is where they landed - drawnAt: Date.now(), - recursionDepth - }); - - // Find gamemaster - const gamemaster = gameData.createdby; - const gamemasterUser = await this.userRepository.findById(gamemaster); - const gamemasterName = gamemasterUser?.username || 'Gamemaster'; - - // Send decision request to gamemaster only - const gamemasterRoomName = `game_${gameCode}:${gamemasterName}`; - this.io.of('/game').to(gamemasterRoomName).emit('game:gamemaster-decision-request', { - requestId, - playerName, - playerId, - jokerCard: { - question: jokerCard.question, - consequence: jokerCard.consequence - }, - timeLimit: 120, // 120 seconds - recursionDepth, // Send to frontend for context - timestamp: new Date().toISOString() - }); - - logOther(`Joker card drawn for ${playerName}, waiting for gamemaster decision`, { - gameCode, - requestId, - recursionDepth - }); - - } catch (error) { - logError('Error handling joker card drawing', error as Error); - await this.advanceTurn(gameCode); - } - } - - /** - * Handle gamemaster decision timeout (120 seconds elapsed) - */ - private async handleGamemasterDecisionTimeout(gameCode: string, requestId: string, playerId: string, playerName: string, recursionDepth: number): Promise { - try { - // Clear from Redis - await this.clearPendingDecision(gameCode, requestId); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:gamemaster-timeout', { - playerName, - playerId, - message: '🎭 Gamemaster didn\'t respond in time. No effect applied.', - timestamp: new Date().toISOString() - }); - - // Process timeout through GamemasterService - const result = this.gamemasterService.processTimeoutDecision(requestId); - - if (result) { - // Broadcast final result - this.io.of('/game').to(gameRoomName).emit('game:gamemaster-decision-result', { - playerName, - playerId, - gamemasterName: 'System (timeout)', - decision: 'timeout', - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - } - - // Advance turn (no consequence applied on timeout) - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling gamemaster decision timeout', error as Error); - await this.advanceTurn(gameCode); - } - } - - // Public method to broadcast game state updates from external services - public async broadcastGameStateUpdate(gameCode: string, gameState: any): Promise { - const roomName = `game_${gameCode}`; - this.io.of('/game').to(roomName).emit('game:state-update', gameState); - } - - // Public method to broadcast game events from external services - public async broadcastGameEvent(gameCode: string, event: string, data: any): Promise { - const roomName = `game_${gameCode}`; - this.io.of('/game').to(roomName).emit(event, data); - } - - // Public method to send events to a specific player - public async sendToPlayer(gameCode: string, playerName: string, event: string, data: any): Promise { - const playerRoomName = `game_${gameCode}:${playerName}`; - this.io.of('/game').to(playerRoomName).emit(event, data); - logOther(`Sent event '${event}' to player ${playerName} in game ${gameCode}`); - } - - // Public method to send events to multiple specific players - public async sendToPlayers(gameCode: string, playerNames: string[], event: string, data: any): Promise { - for (const playerName of playerNames) { - await this.sendToPlayer(gameCode, playerName, event, data); - } - } - - // Public method to get connected players in a game - public async getConnectedPlayers(gameCode: string): Promise { - const key = `game_connections:${gameCode}`; - return await this.redisService.setMembers(key); - } - - // Public method to get ready players in a game - public async getReadyPlayers(gameCode: string): Promise { - const key = `game_ready:${gameCode}`; - return await this.redisService.setMembers(key); - } - - // Public method to broadcast game start with board data and player order - public async broadcastGameStart(gameCode: string, boardData: any, playerOrder: string[], gameData: any): Promise { - try { - const roomName = `game_${gameCode}`; - - // Create comprehensive game start data - const gameStartData = { - gameCode, - gameId: gameData.id, - status: 'started', - boardData, - playerOrder, - currentPlayer: playerOrder[0], // First player starts - currentTurn: 0, - maxPlayers: gameData.maxplayers, - players: gameData.players, - startedAt: new Date().toISOString(), - message: 'Game has started! Good luck to all players!' - }; - - // Broadcast to all players in the game - this.io.of('/game').to(roomName).emit('game:start', gameStartData); - - // Note: Game state is already stored in gameplay:{gameCode} - no need for duplicate game_state: entry - - // Initialize player positions (all start at 0) - const playerPositions = await this.getPlayerPositions(gameCode); - - // Notify the first player that it's their turn - const firstPlayerName = playerPositions.find(p => p.playerId === playerOrder[0])?.playerName || playerOrder[0]; - const firstPlayerRoomName = `game_${gameCode}:${firstPlayerName}`; - - // Send turn-changed event for initial turn with player name - this.io.of('/game').to(roomName).emit('game:turn-changed', { - currentPlayer: playerOrder[0], - currentPlayerName: firstPlayerName, - turnNumber: 1, - message: `It's ${firstPlayerName}'s turn!`, - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(firstPlayerRoomName).emit('game:your-turn', { - message: 'You go first! Roll the dice to start the game!', - canRoll: true, - timestamp: new Date().toISOString() - }); - - logOther(`Game start broadcasted to all players in room: ${roomName}`, { - gameCode, - gameId: gameData.id, - playerCount: gameData.players.length, - boardFields: boardData?.fields?.length || 0, - firstPlayer: playerOrder[0], - firstPlayerName - }); - - } catch (error) { - logError('Error broadcasting game start', error as Error); - throw error; // Re-throw so the caller knows the broadcast failed - } - } - - /** - * Comprehensive cleanup of all game-related data when game ends - * @param gameCode Game code - * @param gameId Game ID from database - */ - private async cleanupGameData(gameCode: string, gameId?: string): Promise { - try { - logOther(`Starting cleanup for game ${gameCode}`, { gameId }); - - // 1. Force disconnect all players from game rooms - const gameRoomName = `game_${gameCode}`; - const gameRoom = this.io.of('/game').adapter.rooms.get(gameRoomName); - - if (gameRoom) { - // Get all socket IDs in the room - const socketIds = Array.from(gameRoom); - - for (const socketId of socketIds) { - const socket = this.io.of('/game').sockets.get(socketId); - if (socket) { - // Leave game rooms - await socket.leave(gameRoomName); - await socket.leave(`game_${gameCode}:${(socket as any).playerName}`); - - // Clear game-related socket data - (socket as any).gameCode = undefined; - (socket as any).playerName = undefined; - - // Notify player that game has ended - socket.emit('game:cleanup-complete', { - gameCode, - message: 'Game session has ended and been cleaned up', - timestamp: new Date().toISOString() - }); - } - } - } - - // 2. Clean up all Redis game data - const keysToClean = [ - `gameplay:${gameCode}`, // Game play state (contains everything) - `game_connections:${gameCode}`, // Connected players - `game_ready:${gameCode}`, // Ready players - `game_pending:${gameCode}`, // Pending players (for private games) - `game_room:${gameCode}` // Game room mapping - ]; - - // Clean up legacy keys if they exist - if (gameId) { - keysToClean.push(`game_board_${gameId}`); // Legacy board storage - keysToClean.push(`game_state:${gameCode}`); // Legacy game state - } - - // Clean up game-specific keys - for (const key of keysToClean) { - await this.redisService.del(key); - } - - // Clean up all pending card answers for this game - const connectedPlayers = await this.getConnectedPlayers(gameCode); - for (const playerId of connectedPlayers) { - await this.clearPendingCard(gameCode, playerId); - } - - // Clean up all pending gamemaster decisions - const pendingDecisionIds = await this.getAllPendingDecisionKeys(gameCode); - for (const requestId of pendingDecisionIds) { - await this.clearPendingDecision(gameCode, requestId); - // Also cancel in GamemasterService to clear timeouts - this.gamemasterService.cancelDecision(requestId); - } - - // Clean up turn tracking for all players - const gameState = await this.getCurrentGameState(gameCode); - if (gameState?.turnSequence) { - for (const playerId of gameState.turnSequence) { - await this.clearPlayerTurnData(gameCode, playerId); - } - } - - // Clean up additional game keys - const additionalKeys = [ - `game:${gameCode}` // Pre-game lobby data (uses gameCode not gameId) - ]; - - for (const key of additionalKeys) { - await this.redisService.del(key); - } - - logOther(`Game cleanup completed for ${gameCode}`, { - gameId, - keysCleanedCount: keysToClean.length + (gameId ? 3 : 0) - }); - - } catch (error) { - logError('Error during game cleanup', error as Error); - logOther('Game cleanup failed', { gameCode, gameId, errorMessage: error instanceof Error ? error.message : String(error) }); - } - } - - /** - * Public method to manually trigger game cleanup (for external services) - * @param gameCode Game code to clean up - * @param gameId Optional game ID - */ - public async triggerGameCleanup(gameCode: string, gameId?: string): Promise { - logOther(`Manual cleanup triggered for game ${gameCode}`, { gameId }); - await this.cleanupGameData(gameCode, gameId); - } - - /** - * Get board data for a game from Redis - * Board data is stored in gameplay:{gameCode} - */ - private async getBoardData(gameCode: string): Promise { - try { - // Get from gameplay (single source of truth) - const gameState = await this.getCurrentGameState(gameCode); - return gameState?.boardData || null; - - } catch (error) { - logError('Error getting board data', error as Error); - return null; - } - } - - /** - * Check if field is special (requires card drawing) - * @param field Game field to check - * @returns True if field is special - */ - private isSpecialField(field: GameField): boolean { - return field.type === 'positive' || field.type === 'negative' || field.type === 'luck'; - } - - /** - * Check if card is a luck card - * @param cardType Card type - * @returns True if luck card - */ - private isLuckCard(cardType?: number): boolean { - return cardType === 6; // Luck cards have type 6 - } - - /** - * Create snapshots for all active games (called on server shutdown) - * @returns Number of snapshots created - */ - public async snapshotAllActiveGames(): Promise { - try { - logOther('Creating snapshots for all active games before shutdown'); - - // Find all active games from database - const activeGames = await this.gameRepository.findActiveGames(); - let snapshotCount = 0; - - for (const game of activeGames) { - try { - // Get current game state from Redis - const gameState = await this.getCurrentGameState(game.gamecode); - if (!gameState) { - logOther(`Skipping game ${game.gamecode} - no Redis state found`); - continue; - } - - const turnNumber = (gameState.currentTurn || 0) + 1; - await this.gameSnapshotService.createSnapshot( - game.gamecode, - turnNumber, - SnapshotTrigger.SERVER_SHUTDOWN, - `Server shutdown snapshot` - ); - snapshotCount++; - logOther(`Created shutdown snapshot for game ${game.gamecode}`, { turnNumber }); - } catch (gameError) { - logError(`Failed to snapshot game ${game.gamecode}`, gameError as Error); - } - } - - logOther(`Completed shutdown snapshots`, { totalGames: activeGames.length, successfulSnapshots: snapshotCount }); - return snapshotCount; - } catch (error) { - logError('Error creating shutdown snapshots', error as Error); - return 0; - } - } - - /** - * Restore all active games from latest snapshots (called on server startup) - * @returns Number of games restored - */ - public async restoreAllActiveGames(): Promise { - try { - logOther('Attempting to restore active games from snapshots'); - - // Find all active games from database - const activeGames = await this.gameRepository.findActiveGames(); - let restoredCount = 0; - - for (const game of activeGames) { - try { - // Try to restore from latest snapshot - const restored = await this.gameSnapshotService.restoreFromSnapshot(game.gamecode); - if (restored) { - restoredCount++; - logOther(`Restored game ${game.gamecode} from snapshot`); - } else { - logOther(`No snapshot found for game ${game.gamecode}, skipping`); - } - } catch (gameError) { - logError(`Failed to restore game ${game.gamecode}`, gameError as Error); - } - } - - logOther(`Completed game restoration`, { totalGames: activeGames.length, restoredGames: restoredCount }); - return restoredCount; - } catch (error) { - logError('Error restoring games from snapshots', error as Error); - return 0; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/GamemasterService.ts b/SerpentRace_Backend/src/Application/Services/GamemasterService.ts deleted file mode 100644 index 896ea45b..00000000 --- a/SerpentRace_Backend/src/Application/Services/GamemasterService.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { GameAggregate, GameCard } from '../../Domain/Game/GameAggregate'; - -export interface GamemasterDecisionRequest { - gameId: string; - playerId: string; - playerName: string; - card: GameCard; - requestId: string; - timeoutId: NodeJS.Timeout; - startTime: Date; -} - -export enum GamemasterDecision { - APPROVE = 'approve', - REJECT = 'reject' -} - -export interface GamemasterDecisionResult { - decision: GamemasterDecision; - consequence: boolean; // true = apply consequence, false = don't apply - description: string; -} - -/** - * Service responsible for handling gamemaster decisions on joker cards - * Integrates with existing gamemaster role identification system - */ -export class GamemasterService { - private pendingDecisions: Map = new Map(); - private readonly DECISION_TIMEOUT_MS = 120000; // 2 minutes for gamemaster to decide - - /** - * Request gamemaster decision for a joker card - * @param gameId Game ID - * @param playerId Player ID who drew the joker card - * @param playerName Player name for display - * @param card The joker card that needs decision - * @param onTimeout Callback when gamemaster doesn't respond in time - * @returns Request ID for tracking this decision - */ - requestGamemasterDecision( - gameId: string, - playerId: string, - playerName: string, - card: GameCard, - onTimeout: (requestId: string) => void - ): string { - const requestId = `${gameId}:${playerId}:${Date.now()}`; - - // Clear any existing decision for this player - this.clearExistingDecision(gameId, playerId); - - // Set timeout for gamemaster decision - const timeoutId = setTimeout(() => { - onTimeout(requestId); - this.pendingDecisions.delete(requestId); - }, this.DECISION_TIMEOUT_MS); - - // Store pending decision - this.pendingDecisions.set(requestId, { - gameId, - playerId, - playerName, - card, - requestId, - timeoutId, - startTime: new Date() - }); - - return requestId; - } - - /** - * Process gamemaster's decision on a joker card - * @param requestId The request ID returned from requestGamemasterDecision - * @param decision The gamemaster's decision - * @returns Result with consequence application info - */ - processGamemasterDecision(requestId: string, decision: GamemasterDecision): GamemasterDecisionResult | null { - const pendingRequest = this.pendingDecisions.get(requestId); - - if (!pendingRequest) { - return null; // Request not found or already processed - } - - // Clear the timeout since decision was made - clearTimeout(pendingRequest.timeoutId); - this.pendingDecisions.delete(requestId); - - // Determine if consequence should be applied based on its nature and decision - const consequence = pendingRequest.card.consequence; - const isNegativeConsequence = this.isNegativeConsequence(consequence?.type); - - let applyConsequence: boolean; - if (isNegativeConsequence) { - // Negative consequences applied when gamemaster REJECTS - applyConsequence = decision === GamemasterDecision.REJECT; - } else { - // Positive consequences applied when gamemaster APPROVES - applyConsequence = decision === GamemasterDecision.APPROVE; - } - - return { - decision, - consequence: applyConsequence, - description: this.getDecisionDescription(decision, applyConsequence, pendingRequest.card, isNegativeConsequence) - }; - } - - /** - * Process automatic decision when gamemaster times out - * @param requestId The request ID that timed out - * @returns Result with default rejection applied - */ - processTimeoutDecision(requestId: string): GamemasterDecisionResult | null { - const pendingRequest = this.pendingDecisions.get(requestId); - - if (!pendingRequest) { - return null; - } - - this.pendingDecisions.delete(requestId); - - return { - decision: GamemasterDecision.REJECT, - consequence: false, - description: `🎭 Gamemaster didn't respond in time. No effect applied.` - }; - } - - /** - * Get pending decision by request ID - * @param requestId The request ID - * @returns Pending decision request or undefined - */ - getPendingDecision(requestId: string): GamemasterDecisionRequest | undefined { - return this.pendingDecisions.get(requestId); - } - - /** - * Get all pending decisions for a game - * @param gameId Game ID - * @returns Array of pending decisions for the game - */ - getPendingDecisionsForGame(gameId: string): GamemasterDecisionRequest[] { - return Array.from(this.pendingDecisions.values()) - .filter(request => request.gameId === gameId); - } - - /** - * Check if gamemaster is the correct user for a game - * @param game Game aggregate - * @param userId User ID to check - * @returns True if user is the gamemaster - */ - isGamemaster(game: GameAggregate, userId: string): boolean { - return game.createdby === userId; - } - - /** - * Cancel a pending decision (e.g., if player leaves game) - * @param requestId Request ID to cancel - * @returns True if decision was cancelled - */ - cancelDecision(requestId: string): boolean { - const pendingRequest = this.pendingDecisions.get(requestId); - - if (!pendingRequest) { - return false; - } - - clearTimeout(pendingRequest.timeoutId); - this.pendingDecisions.delete(requestId); - return true; - } - - /** - * Clear any existing pending decision for a player in a game - * @param gameId Game ID - * @param playerId Player ID - */ - private clearExistingDecision(gameId: string, playerId: string): void { - for (const [requestId, request] of this.pendingDecisions.entries()) { - if (request.gameId === gameId && request.playerId === playerId) { - clearTimeout(request.timeoutId); - this.pendingDecisions.delete(requestId); - break; - } - } - } - - /** - * Get human-readable description for joker card effect - * @param card The joker card - * @param applied Whether the effect will be applied - * @returns Description string - */ - private getJokerDescription(card: GameCard, applied: boolean): string { - if (!applied) { - return 'No effect applied.'; - } - - if (!card.consequence) { - return 'Apply joker effect!'; - } - - switch (card.consequence.type) { - case 0: // MOVE_FORWARD - return `Move forward ${card.consequence.value || 1} steps!`; - case 1: // MOVE_BACKWARD - return `Move backward ${card.consequence.value || 1} steps!`; - case 2: // LOSE_TURN - return 'Lose your next turn!'; - case 3: // EXTRA_TURN - return 'Get an extra turn!'; - case 5: // GO_TO_START - return 'Go back to start!'; - default: - return 'Apply joker effect!'; - } - } - - /** - * Get remaining time for a pending decision - * @param requestId Request ID - * @returns Remaining time in seconds, or -1 if not found - */ - getRemainingTime(requestId: string): number { - const pending = this.pendingDecisions.get(requestId); - if (!pending) { - return -1; - } - - const elapsed = Date.now() - pending.startTime.getTime(); - const remaining = Math.max(0, this.DECISION_TIMEOUT_MS - elapsed); - return Math.ceil(remaining / 1000); // Return in seconds - } - - /** - * Get count of pending decisions for a game - * @param gameId Game ID - * @returns Number of pending decisions - */ - getPendingDecisionCount(gameId: string): number { - return Array.from(this.pendingDecisions.values()) - .filter(request => request.gameId === gameId).length; - } - - /** - * Determine if a consequence type is negative - * @param consequenceType The consequence type to check - * @returns True if consequence is negative - */ - private isNegativeConsequence(consequenceType?: number): boolean { - if (consequenceType === undefined) return false; - - // Negative consequences: MOVE_BACKWARD, LOSE_TURN, GO_TO_START - return [1, 2, 5].includes(consequenceType); // MOVE_BACKWARD=1, LOSE_TURN=2, GO_TO_START=5 - } - - /** - * Get description for gamemaster decision result - * @param decision Gamemaster's decision - * @param applyConsequence Whether consequence will be applied - * @param card The joker card - * @param isNegative Whether the consequence is negative - * @returns Description string - */ - private getDecisionDescription(decision: GamemasterDecision, applyConsequence: boolean, card: GameCard, isNegative: boolean): string { - if (decision === GamemasterDecision.APPROVE) { - if (isNegative) { - return '🎭 Gamemaster approved - no penalty applied!'; - } else { - return `🎭 Gamemaster approved! ${this.getJokerDescription(card, true)}`; - } - } else { - if (isNegative) { - return `🎭 Gamemaster rejected! ${this.getJokerDescription(card, true)}`; - } else { - return '🎭 Gamemaster rejected - no bonus applied.'; - } - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/Interfaces/GameInterfaces.ts b/SerpentRace_Backend/src/Application/Services/Interfaces/GameInterfaces.ts deleted file mode 100644 index 30a3e640..00000000 --- a/SerpentRace_Backend/src/Application/Services/Interfaces/GameInterfaces.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Shared interfaces for game-related WebSocket communications - * Used by both WebSocketService and GameWebSocketService - */ - -export interface JoinGameRoomData { - gameCode: string; -} - -export interface LeaveGameRoomData { - gameCode: string; -} - -export interface GameStateUpdateData { - gameId: string; - gameCode: string; - players: string[]; - state: string; - currentTurn?: string; -} - -export interface GameActionData { - gameId: string; - gameCode: string; - playerId: string; - action: 'pick_card' | 'play_card' | 'end_turn' | 'leave_game' | 'roll-dice' | 'move' | 'use-field'; - data?: any; -} - -// Field Effect Service WebSocket interfaces -export interface FieldEffectCalculationData { - gameId: string; - gameCode: string; - playerId: string; - currentPosition: number; - card: any; // GameCard - field: any; // GameField - dice: number; - guessedPosition?: number; -} - -export interface FieldEffectResultData { - gameId: string; - gameCode: string; - playerId: string; - result: { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: any; - gamemasterResult?: any; - description: string; - effects: string[]; - turnEffect?: { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - value: number; - playerId: string; - }; - }; -} - -export interface GamemasterDecisionRequestData { - gameId: string; - gameCode: string; - requestId: string; - playerId: string; - playerName: string; - card: any; // GameCard - timeRemaining: number; -} - -export interface GamemasterDecisionResponseData { - requestId: string; - decision: 'approve' | 'reject'; -} - -// Game-specific interfaces for GameWebSocketService -export interface JoinGameData { - gameToken: string; // Required game session token -} - -export interface LeaveGameData { - gameCode: string; -} - -export interface DiceRollData { - gameCode: string; - diceValue: number; // Value from frontend (1-6) -} - -export interface PlayerPosition { - playerId: string; - playerName: string; - boardPosition: number; - turnOrder: number; -} - -export interface GameChatData { - gameCode: string; - message: string; -} - -// Field Effect related types -export interface FieldEffectRequest { - gameId: string; - playerId: string; - playerName: string; - currentPosition: number; - card: any; - field: any; - dice: number; - guessedPosition?: number; -} - -export interface FieldEffectResult { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: any; - gamemasterResult?: any; - description: string; - effects: string[]; - turnEffect?: { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - value: number; - playerId: string; - }; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/JWTService.ts b/SerpentRace_Backend/src/Application/Services/JWTService.ts deleted file mode 100644 index dc2061af..00000000 --- a/SerpentRace_Backend/src/Application/Services/JWTService.ts +++ /dev/null @@ -1,315 +0,0 @@ -import jwt, { SignOptions } from 'jsonwebtoken'; -import { Request, Response } from 'express'; -import { UserState } from '../../Domain/User/UserAggregate'; - -export interface TokenPayload { - userId: string; - authLevel: 0 | 1; - userStatus: UserState; - orgId: string; - type?: 'access'; - iat?: number; - exp?: number; -} - -export interface RefreshTokenPayload { - userId: string; - type: 'refresh'; - orgId?: string; - tokenId?: string; // For token rotation/revocation - iat?: number; - exp?: number; -} - -export interface TokenPair { - accessToken: string; - refreshToken: string; -} - -export class JWTService { - private readonly secretKey: string; - private readonly refreshSecretKey: string; - private readonly tokenExpiry: number; - private readonly refreshTokenExpiry: number; - private readonly cookieName: string; - private readonly refreshCookieName: string; - - constructor() { - this.secretKey = process.env.JWT_SECRET || 'your-secret-key'; - this.refreshSecretKey = process.env.JWT_REFRESH_SECRET || this.secretKey + '_refresh'; - - // Access token expiry (short-lived) - let expiry = 1800; // Default 30 minutes for better security - if (process.env.JWT_EXPIRY) { - expiry = parseInt(process.env.JWT_EXPIRY); - } else if (process.env.JWT_EXPIRATION) { - expiry = this.parseDuration(process.env.JWT_EXPIRATION); - } - - // Refresh token expiry (long-lived) - let refreshExpiry = 604800; // Default 7 days - if (process.env.JWT_REFRESH_EXPIRATION) { - refreshExpiry = this.parseDuration(process.env.JWT_REFRESH_EXPIRATION); - } - - this.tokenExpiry = expiry; - this.refreshTokenExpiry = refreshExpiry; - this.cookieName = 'auth_token'; - this.refreshCookieName = 'refresh_token'; - - if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) { - throw new Error('JWT_SECRET environment variable must be set in production'); - } - } - - /** - * Create a pair of access and refresh tokens - */ - public createTokenPair(payload: Omit): TokenPair { - const now = Math.floor(Date.now() / 1000); - - // Create access token - const accessTokenPayload: TokenPayload = { - ...payload, - type: 'access', - iat: now, - exp: now + this.tokenExpiry - }; - const accessToken = jwt.sign(accessTokenPayload, this.secretKey); - - // Create refresh token - const refreshTokenPayload: RefreshTokenPayload = { - userId: payload.userId, - type: 'refresh', - orgId: payload.orgId, - iat: now, - exp: now + this.refreshTokenExpiry - }; - const refreshToken = jwt.sign(refreshTokenPayload, this.refreshSecretKey); - - return { accessToken, refreshToken }; - } - - /** - * Create access and refresh tokens and set cookies (for cookie-based auth) - */ - create(payload: Omit, res: Response): TokenPair { - const tokenPair = this.createTokenPair(payload); - this.setTokenCookies(res, tokenPair); - return tokenPair; - } - - /** - * Check if the request is using Bearer token authentication - */ - private isUsingBearerAuth(req: Request): boolean { - // No cookie but has Authorization header - return !req.cookies?.[this.cookieName] && - !!req.headers.authorization && - req.headers.authorization.startsWith('Bearer '); - } - - /** - * Verify a refresh token - */ - public verifyRefreshToken(token: string): RefreshTokenPayload | null { - try { - const decoded = jwt.verify(token, this.refreshSecretKey) as RefreshTokenPayload; - if (decoded.type !== 'refresh') { - return null; - } - return decoded; - } catch (error) { - return null; - } - } - - /** - * Attempt to refresh tokens using refresh token from cookies or headers - */ - public attemptTokenRefresh(req: Request, res: Response): TokenPair | null { - try { - // Try to get refresh token from cookie first - let refreshToken = req.cookies[this.refreshCookieName]; - - // If no cookie, try X-Refresh-Token header - if (!refreshToken) { - refreshToken = req.headers['x-refresh-token'] as string; - } - - if (!refreshToken) { - return null; - } - - const refreshPayload = this.verifyRefreshToken(refreshToken); - if (!refreshPayload) { - return null; - } - - // Create new token pair - const newTokenPair = this.createTokenPair({ - userId: refreshPayload.userId, - authLevel: 0, // Default auth level, should be fetched from user data - userStatus: UserState.VERIFIED_REGULAR, // Default status, should be fetched from user data - orgId: refreshPayload.orgId || '' - }); - - // Set new tokens based on authentication method - if (req.cookies[this.cookieName] || req.cookies[this.refreshCookieName]) { - // Cookie-based auth: set new cookies - this.setTokenCookies(res, newTokenPair); - } else { - // Header-based auth: send tokens in response headers - res.setHeader('X-New-Access-Token', newTokenPair.accessToken); - res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken); - res.setHeader('X-Token-Refreshed', 'true'); - } - - return newTokenPair; - } catch (error) { - return null; - } - } - - /** - * Set token cookies for cookie-based authentication - */ - private setTokenCookies(res: Response, tokenPair: TokenPair): void { - // Set access token cookie - res.cookie(this.cookieName, tokenPair.accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: this.tokenExpiry * 1000, - }); - - // Set refresh token cookie - res.cookie(this.refreshCookieName, tokenPair.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: this.refreshTokenExpiry * 1000, - }); - } - - verify(req: Request): TokenPayload | null { - try { - // First try to get token from cookie - let token = req.cookies[this.cookieName]; - - // If no cookie token, try Authorization header - if (!token) { - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - token = authHeader.substring(7); - } - } - - if (!token) return null; - - const decoded = jwt.verify(token, this.secretKey) as TokenPayload; - return decoded; - } catch (error) { - return null; - } - } - - /** - * Logout user by clearing tokens - */ - public logout(req: Request, res: Response): void { - // Clear cookies if they exist - if (req.cookies[this.cookieName]) { - res.clearCookie(this.cookieName, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict' - }); - } - - if (req.cookies[this.refreshCookieName]) { - res.clearCookie(this.refreshCookieName, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict' - }); - } - - // For bearer token auth, set headers to indicate logout - res.setHeader('X-Auth-Logout', 'true'); - res.setHeader('X-Clear-Tokens', 'true'); - } - - // Check if token needs refresh (within 25% of expiry time) - shouldRefreshToken(payload: TokenPayload): boolean { - if (!payload.exp || !payload.iat) return false; - - const now = Math.floor(Date.now() / 1000); - const tokenAge = now - payload.iat; - const tokenLifetime = payload.exp - payload.iat; - const refreshThreshold = tokenLifetime * 0.75; // Refresh when 75% of lifetime has passed - - return tokenAge >= refreshThreshold; - } - - // Conditionally refresh token only if needed - refreshIfNeeded(payload: TokenPayload, res: Response, req?: Request): boolean { - if (this.shouldRefreshToken(payload)) { - if (req) { - // Try to use the new refresh token system - const newTokenPair = this.attemptTokenRefresh(req, res); - if (newTokenPair) { - return true; - } - } - - // Fallback: create new token pair - const freshPayload: Omit = { - userId: payload.userId, - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId - }; - - // Check if using Bearer authentication - if (req && this.isUsingBearerAuth(req)) { - // For Bearer auth, create token pair and add to headers - const newTokenPair = this.createTokenPair(freshPayload); - res.setHeader('X-New-Access-Token', newTokenPair.accessToken); - res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken); - res.setHeader('X-Token-Refreshed', 'true'); - } else { - // For cookie auth, create token pair and set cookies - const newTokenPair = this.create(freshPayload, res); - this.setTokenCookies(res, newTokenPair); - } - - return true; - } - return false; - } - - /** - * Parse duration string to seconds (e.g., "24h", "7d", "30m") - * @param duration Duration string - * @returns Duration in seconds - */ - private parseDuration(duration: string): number { - const match = duration.match(/^(\d+)([smhd])$/); - if (!match) { - throw new Error(`Invalid duration format: ${duration}. Use format like '24h', '7d', '30m'`); - } - - const [, value, unit] = match; - const num = parseInt(value); - - switch (unit) { - case 's': return num; // seconds - case 'm': return num * 60; // minutes - case 'h': return num * 60 * 60; // hours - case 'd': return num * 60 * 60 * 24; // days - default: - throw new Error(`Unsupported duration unit: ${unit}`); - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/Logger.ts b/SerpentRace_Backend/src/Application/Services/Logger.ts deleted file mode 100644 index 74521339..00000000 --- a/SerpentRace_Backend/src/Application/Services/Logger.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { LoggingService, LogLevel } from './LoggingService'; -import { Request, Response } from 'express'; - -// Singleton instance -const logger = LoggingService.getInstance(); - -// Convenience functions for each log level -export const logRequest = (message: string, req?: Request, res?: Response, metadata?: any) => { - logger.log(LogLevel.REQUEST, message, metadata, req, res); -}; - -export const logError = (message: string, error?: Error, req?: Request, res?: Response) => { - const metadata = error ? { - name: error.name, - message: error.message, - stack: error.stack - } : undefined; - logger.log(LogLevel.ERROR, message, metadata, req, res); -}; - -export const logWarning = (message: string, metadata?: any, req?: Request, res?: Response) => { - logger.log(LogLevel.WARNING, message, metadata, req, res); -}; - -export const logAuth = (message: string, userId?: string, metadata?: any, req?: Request, res?: Response) => { - const authMetadata = { - userId, - ...metadata - }; - logger.log(LogLevel.AUTH, message, authMetadata, req, res); -}; - -export const logDatabase = (message: string, query?: string, executionTime?: number, metadata?: any) => { - const dbMetadata = { - query: query ? query.substring(0, 200) : undefined, - executionTime, - ...metadata - }; - logger.log(LogLevel.DATABASE, message, dbMetadata); -}; - -export const logStartup = (message: string, metadata?: any) => { - logger.log(LogLevel.STARTUP, message, metadata); -}; - -export const logConnection = (message: string, type: string, status: 'success' | 'failure' | 'attempt', metadata?: any) => { - const connectionMetadata = { - connectionType: type, - status, - ...metadata - }; - logger.log(LogLevel.CONNECTION, message, connectionMetadata); -}; - -export const logOther = (message: string, metadata?: any, req?: Request, res?: Response) => { - logger.log(LogLevel.OTHER, message, metadata, req, res); -}; - -// Export the main service -export { LoggingService, LogLevel }; -export default logger; diff --git a/SerpentRace_Backend/src/Application/Services/LoggingService.ts b/SerpentRace_Backend/src/Application/Services/LoggingService.ts deleted file mode 100644 index 125ab2a6..00000000 --- a/SerpentRace_Backend/src/Application/Services/LoggingService.ts +++ /dev/null @@ -1,419 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Request, Response, NextFunction } from 'express'; -import * as Minio from 'minio'; - -export enum LogLevel { - REQUEST = 'REQUEST', - ERROR = 'ERROR', - WARNING = 'WARNING', - AUTH = 'AUTH', - DATABASE = 'DATABASE', - STARTUP = 'STARTUP', - CONNECTION = 'CONNECTION', - OTHER = 'OTHER' -} - -export interface LogEntry { - timestamp: string; - level: LogLevel; - message: string; - metadata?: any; - requestId?: string; - userId?: string; - ip?: string; - userAgent?: string; - method?: string; - url?: string; - statusCode?: number; - responseTime?: number; -} - -export class LoggingService { - private static instance: LoggingService; - private minioClient: Minio.Client | null = null; - private logBuffer: LogEntry[] = []; - private currentLogFile: string | null = null; - private logCount = 0; - private readonly maxLogsPerFile = parseInt(process.env.MAX_LOGS_PER_FILE || '10000'); - private readonly logsDir = path.join(process.cwd(), 'logs'); - private readonly bucketName = process.env.MINIO_BUCKET_NAME || 'serpentrace-logs'; - private uploadInterval: NodeJS.Timeout | null = null; - - private constructor() { - this.initializeLogsDirectory(); - this.initializeMinioClient(); - this.createNewLogFile(); - - if (process.env.NODE_ENV !== 'test') { - this.startPeriodicUpload(); - } - - process.on('SIGTERM', () => this.shutdown()); - process.on('SIGINT', () => this.shutdown()); - process.on('beforeExit', () => this.shutdown()); - } - - static getInstance(): LoggingService { - if (!LoggingService.instance) { - LoggingService.instance = new LoggingService(); - } - return LoggingService.instance; - } - - private initializeLogsDirectory(): void { - try { - if (!fs.existsSync(this.logsDir)) { - fs.mkdirSync(this.logsDir, { recursive: true }); - } - - // Create monthly subdirectory - const monthlyDir = this.getMonthlyDirectory(); - if (!fs.existsSync(monthlyDir)) { - fs.mkdirSync(monthlyDir, { recursive: true }); - } - } catch (error) { - console.error('Failed to initialize logs directory:', error); - } - } - - private initializeMinioClient(): void { - try { - // Check if in production or development - if (process.env.NODE_ENV === 'production') { - if (process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY) { - this.minioClient = new Minio.Client({ - endPoint: process.env.MINIO_ENDPOINT, - port: parseInt(process.env.MINIO_PORT || '9000'), - useSSL: process.env.MINIO_USE_SSL === 'true', - accessKey: process.env.MINIO_ACCESS_KEY, - secretKey: process.env.MINIO_SECRET_KEY - }); - - this.ensureBucketExists(); - } else { - console.warn('Minio configuration not found. Logs will only be stored locally and in console.'); - } - } else { - // Development mode - only use MinIO if explicitly configured - if (process.env.MINIO_ENDPOINT || process.env.ENABLE_MINIO === 'true') { - this.minioClient = new Minio.Client({ - endPoint: process.env.MINIO_ENDPOINT || 'localhost', - port: parseInt(process.env.MINIO_PORT || '9000'), - useSSL: false, - accessKey: process.env.MINIO_ACCESS_KEY || 'serpentrace', - secretKey: process.env.MINIO_SECRET_KEY || 'serpentrace123!' - }); - - this.ensureBucketExists(); - } else { - console.log('Development mode: MinIO disabled. Set ENABLE_MINIO=true to enable MinIO logging.'); - this.minioClient = null; - } - } - - - } catch (error) { - console.error('Failed to initialize Minio client:', error); - this.minioClient = null; - } - } - - private async ensureBucketExists(): Promise { - if (!this.minioClient) return; - - try { - const exists = await this.minioClient.bucketExists(this.bucketName); - if (!exists) { - await this.minioClient.makeBucket(this.bucketName); - this.log(LogLevel.STARTUP, `Created Minio bucket: ${this.bucketName}`); - } - } catch (error) { - console.warn('MinIO connection failed - disabling MinIO logging:', (error as Error).message); - // Disable MinIO client if connection fails - this.minioClient = null; - } - } - - private startPeriodicUpload(): void { - // Upload current log file to Minio every 2 minutes - this.uploadInterval = setInterval(async () => { - if (this.currentLogFile && this.minioClient) { - await this.uploadToMinio(this.currentLogFile); - } - }, 2 * 60 * 1000); // 2 minutes - } - - private getMonthlyDirectory(): string { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - return path.join(this.logsDir, `${year}-${month}`); - } - - private getMonthlyMinioPrefix(): string { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - return `${year}-${month}/`; - } - - private createNewLogFile(): void { - const now = new Date(); - const timestamp = now.toISOString().replace(/[:.]/g, '-'); - const fileName = `serpentrace-${timestamp}.log`; - - this.currentLogFile = path.join(this.getMonthlyDirectory(), fileName); - this.logCount = 0; - - // Write log file header - const header = `# SerpentRace Backend Logs\n# Started: ${now.toISOString()}\n# Max entries per file: ${this.maxLogsPerFile}\n\n`; - try { - fs.writeFileSync(this.currentLogFile, header); - } catch (error) { - console.error('Failed to create log file:', error); - } - } - - private formatLogEntry(entry: LogEntry): string { - const parts = [ - entry.timestamp, - `[${entry.level}]`, - entry.message - ]; - - if (entry.requestId) parts.push(`ReqId:${entry.requestId}`); - if (entry.userId) parts.push(`UserId:${entry.userId}`); - if (entry.ip) parts.push(`IP:${entry.ip}`); - if (entry.method && entry.url) parts.push(`${entry.method} ${entry.url}`); - if (entry.statusCode) parts.push(`Status:${entry.statusCode}`); - if (entry.responseTime) parts.push(`Time:${entry.responseTime}ms`); - if (entry.userAgent) parts.push(`UA:${entry.userAgent.substring(0, 50)}`); - if (entry.metadata) parts.push(`Meta:${JSON.stringify(entry.metadata)}`); - - return parts.join(' | '); - } - - private async writeToLocalFile(entry: LogEntry): Promise { - if (!this.currentLogFile) return; - - try { - const logLine = this.formatLogEntry(entry) + '\n'; - fs.appendFileSync(this.currentLogFile, logLine); - - this.logCount++; - - // Check if we need to rotate the log file - if (this.logCount >= this.maxLogsPerFile) { - await this.rotateLogFile(); - } - } catch (error) { - console.error('Failed to write to log file:', error); - } - } - - private async rotateLogFile(): Promise { - if (!this.currentLogFile) return; - - try { - // Upload current file to Minio before rotating - await this.uploadToMinio(this.currentLogFile); - - // Create new log file - this.createNewLogFile(); - - this.log(LogLevel.OTHER, 'Log file rotated due to size limit'); - } catch (error) { - console.error('Failed to rotate log file:', error); - } - } - - private async uploadToMinio(filePath: string): Promise { - if (!this.minioClient) { - console.warn('Minio client not initialized, skipping upload'); - return; - } - - if (!fs.existsSync(filePath)) { - console.warn(`Log file does not exist: ${filePath}`); - return; - } - - try { - const fileName = path.basename(filePath); - const objectName = this.getMonthlyMinioPrefix() + fileName; - - console.log(`Attempting to upload log file to Minio: ${objectName}`); - await this.minioClient.fPutObject(this.bucketName, objectName, filePath); - console.log(`Successfully uploaded log file to Minio: ${objectName}`); - } catch (error) { - console.error('Failed to upload to Minio:', error); - console.error('Minio config:', { - endpoint: this.minioClient ? 'configured' : 'not configured', - bucket: this.bucketName - }); - } - } - - private logToConsole(entry: LogEntry): void { - // In production, skip OTHER, CONNECTION, and REQUEST logs - if (process.env.NODE_ENV === 'production') { - if (entry.level === LogLevel.OTHER || - entry.level === LogLevel.CONNECTION || - entry.level === LogLevel.REQUEST) { - return; - } - } - - const formattedEntry = this.formatLogEntry(entry); - - switch (entry.level) { - case LogLevel.ERROR: - console.error(formattedEntry); - break; - case LogLevel.WARNING: - console.warn(formattedEntry); - break; - case LogLevel.REQUEST: - case LogLevel.AUTH: - case LogLevel.DATABASE: - case LogLevel.CONNECTION: - console.info(formattedEntry); - break; - case LogLevel.STARTUP: - console.log(formattedEntry); - break; - default: - console.log(formattedEntry); - } - } - - public log( - level: LogLevel, - message: string, - metadata?: any, - req?: Request, - res?: Response, - responseTime?: number - ): void { - // In production, skip OTHER, CONNECTION, and REQUEST logs entirely - if (process.env.NODE_ENV === 'production') { - if (level === LogLevel.OTHER || - level === LogLevel.CONNECTION || - level === LogLevel.REQUEST) { - return; - } - } - - const entry: LogEntry = { - timestamp: new Date().toISOString(), - level, - message, - metadata - }; - - // Add request context if available - if (req) { - entry.requestId = (req as any).requestId || this.generateRequestId(); - entry.userId = (req as any).user?.userId; - entry.ip = req.ip || req.socket?.remoteAddress || 'unknown'; - entry.userAgent = req.get ? req.get('User-Agent') : 'unknown'; - entry.method = req.method; - entry.url = req.originalUrl || req.url; - } - - if (res) { - entry.statusCode = res.statusCode; - } - - if (responseTime !== undefined) { - entry.responseTime = responseTime; - } - - // Log to all three destinations - this.logToConsole(entry); - this.writeToLocalFile(entry); - - // Add to buffer for potential batch processing - this.logBuffer.push(entry); - - // Limit buffer size - if (this.logBuffer.length > 1000) { - this.logBuffer = this.logBuffer.slice(-500); - } - } - - private generateRequestId(): string { - return Math.random().toString(36).substr(2, 9); - } - - public async shutdown(): Promise { - try { - // Clear the upload interval - if (this.uploadInterval) { - clearInterval(this.uploadInterval); - this.uploadInterval = null; - } - - // Upload current log file to Minio - if (this.currentLogFile) { - await this.uploadToMinio(this.currentLogFile); - } - - this.log(LogLevel.STARTUP, 'Logging service shutting down gracefully'); - - // Give time for final logs to be written - await new Promise(resolve => setTimeout(resolve, 1000)); - } catch (error) { - console.error('Error during logging service shutdown:', error); - } - } - - // Middleware factory methods - public requestLoggingMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const startTime = Date.now(); - - // Generate request ID - (req as any).requestId = this.generateRequestId(); - - // Log request start - this.log(LogLevel.REQUEST, `Incoming request`, undefined, req); - - // Override res.end to log response - const originalEnd = res.end.bind(res); - res.end = (...args: any[]): Response => { - const responseTime = Date.now() - startTime; - LoggingService.getInstance().log( - LogLevel.REQUEST, - `Request completed`, - undefined, - req, - res, - responseTime - ); - return originalEnd(...args); - }; - - next(); - }; - } - - public errorLoggingMiddleware() { - return (error: Error, req: Request, res: Response, next: NextFunction) => { - this.log( - LogLevel.ERROR, - `Unhandled error: ${error.message}`, - { - stack: error.stack, - name: error.name - }, - req, - res - ); - next(error); - }; - } -} - -export default LoggingService; diff --git a/SerpentRace_Backend/src/Application/Services/PasswordService.ts b/SerpentRace_Backend/src/Application/Services/PasswordService.ts deleted file mode 100644 index 56f3ec08..00000000 --- a/SerpentRace_Backend/src/Application/Services/PasswordService.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as bcrypt from 'bcrypt'; -import { logError } from './Logger'; - -export class PasswordService { - private static readonly SALT_ROUNDS = 12; - - /** - * Hashes a plain text password using bcrypt - * @param password - The plain text password to hash - * @returns Promise - The hashed password - */ - static async hashPassword(password: string): Promise { - try { - if (!password || typeof password !== 'string') { - throw new Error('Password must be a non-empty string'); - } - - return await bcrypt.hash(password, this.SALT_ROUNDS); - } catch (error) { - logError('PasswordService.hashPassword error', error instanceof Error ? error : new Error(String(error))); - - if (error instanceof Error && error.message === 'Password must be a non-empty string') { - throw error; // Re-throw validation errors as-is - } - - throw new Error('Failed to hash password'); - } - } - - /** - * Verifies a plain text password against a hashed password - * @param password - The plain text password to verify - * @param hashedPassword - The hashed password to compare against - * @returns Promise - True if password matches, false otherwise - */ - static async verifyPassword(password: string, hashedPassword: string): Promise { - try { - if (!password || typeof password !== 'string') { - return false; // Invalid input should return false, not throw - } - - if (!hashedPassword || typeof hashedPassword !== 'string') { - return false; // Invalid input should return false, not throw - } - - return await bcrypt.compare(password, hashedPassword); - } catch (error) { - logError('PasswordService.verifyPassword error', error instanceof Error ? error : new Error(String(error))); - return false; // Return false on error instead of throwing - } - } - - /** - * Validates password strength requirements - * @param password - The password to validate - * @returns object - Object containing isValid boolean and error messages - */ - static validatePasswordStrength(password: string): { isValid: boolean; errors: string[] } { - try { - const errors: string[] = []; - - if (!password || typeof password !== 'string') { - errors.push('Password must be provided as a string'); - return { isValid: false, errors }; - } - - if (password.length < 8) { - errors.push('Password must be at least 8 characters long'); - } - - if (!/[A-Z]/.test(password)) { - errors.push('Password must contain at least one uppercase letter'); - } - - if (!/[a-z]/.test(password)) { - errors.push('Password must contain at least one lowercase letter'); - } - - if (!/\d/.test(password)) { - errors.push('Password must contain at least one number'); - } - - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.push('Password must contain at least one special character'); - } - - return { - isValid: errors.length === 0, - errors - }; - } catch (error) { - logError('PasswordService.validatePasswordStrength error', error instanceof Error ? error : new Error(String(error))); - return { - isValid: false, - errors: ['Password validation failed due to internal error'] - }; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/RedisService.ts b/SerpentRace_Backend/src/Application/Services/RedisService.ts deleted file mode 100644 index de0b38bb..00000000 --- a/SerpentRace_Backend/src/Application/Services/RedisService.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { createClient, RedisClientType } from 'redis'; -import { logError, logStartup, logWarning } from './Logger'; - -export interface ActiveChatData { - chatId: string; - participants: string[]; - lastActivity: Date; - messageCount: number; - chatType: 'direct' | 'group' | 'game'; - gameId?: string; - name?: string; -} - -export interface ActiveUserData { - userId: string; - activeChatIds: string[]; - lastActivity: Date; - isOnline: boolean; -} - -export class RedisService { - private static instance: RedisService; - private client: RedisClientType; - private isConnected: boolean = false; - - private constructor() { - const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; - this.client = createClient({ - url: redisUrl, - socket: { - reconnectStrategy: (retries) => Math.min(retries * 50, 500) - } - }); - - this.client.on('error', (err) => { - logError('Redis connection error', err); - this.isConnected = false; - }); - - this.client.on('connect', () => { - logStartup('Redis client connected successfully'); - this.isConnected = true; - }); - - this.client.on('disconnect', () => { - logWarning('Redis client disconnected'); - this.isConnected = false; - }); - } - - public static getInstance(): RedisService { - if (!RedisService.instance) { - RedisService.instance = new RedisService(); - } - return RedisService.instance; - } - - public async connect(): Promise { - try { - if (!this.isConnected) { - await this.client.connect(); - } - } catch (error) { - logError('Failed to connect to Redis', error as Error); - throw error; - } - } - - public async disconnect(): Promise { - try { - if (this.isConnected) { - await this.client.disconnect(); - } - } catch (error) { - logError('Failed to disconnect from Redis', error as Error); - } - } - - public async setActiveChat(chatId: string, chatData: ActiveChatData): Promise { - try { - const key = `active_chat:${chatId}`; - await this.client.hSet(key, { - chatId: chatData.chatId, - participants: JSON.stringify(chatData.participants), - lastActivity: chatData.lastActivity.toISOString(), - messageCount: chatData.messageCount.toString(), - chatType: chatData.chatType, - gameId: chatData.gameId || '', - name: chatData.name || '' - }); - - // Set expiration for 1 hour of inactivity - await this.client.expire(key, 3600); - } catch (error) { - logError(`Failed to set active chat ${chatId}`, error as Error); - } - } - - public async getActiveChat(chatId: string): Promise { - try { - const key = `active_chat:${chatId}`; - const data = await this.client.hGetAll(key); - - if (!data.chatId) { - return null; - } - - return { - chatId: data.chatId, - participants: JSON.parse(data.participants), - lastActivity: new Date(data.lastActivity), - messageCount: parseInt(data.messageCount, 10), - chatType: data.chatType as 'direct' | 'group' | 'game', - gameId: data.gameId || undefined, - name: data.name || undefined - }; - } catch (error) { - logError(`Failed to get active chat ${chatId}`, error as Error); - return null; - } - } - - public async removeActiveChat(chatId: string): Promise { - try { - const key = `active_chat:${chatId}`; - await this.client.del(key); - } catch (error) { - logError(`Failed to remove active chat ${chatId}`, error as Error); - } - } - - public async getAllActiveChats(): Promise { - try { - const pattern = 'active_chat:*'; - const keys = await this.client.keys(pattern); - const chats: ActiveChatData[] = []; - - for (const key of keys) { - const data = await this.client.hGetAll(key); - if (data.chatId) { - chats.push({ - chatId: data.chatId, - participants: JSON.parse(data.participants), - lastActivity: new Date(data.lastActivity), - messageCount: parseInt(data.messageCount, 10), - chatType: data.chatType as 'direct' | 'group' | 'game', - gameId: data.gameId || undefined, - name: data.name || undefined - }); - } - } - - return chats; - } catch (error) { - logError('Failed to get all active chats', error as Error); - return []; - } - } - - public async setActiveUser(userId: string, userData: ActiveUserData): Promise { - try { - const key = `active_user:${userId}`; - await this.client.hSet(key, { - userId: userData.userId, - activeChatIds: JSON.stringify(userData.activeChatIds), - lastActivity: userData.lastActivity.toISOString(), - isOnline: userData.isOnline.toString() - }); - - // Set expiration for 2 hours - await this.client.expire(key, 7200); - } catch (error) { - logError(`Failed to set active user ${userId}`, error as Error); - } - } - - public async getActiveUser(userId: string): Promise { - try { - const key = `active_user:${userId}`; - const data = await this.client.hGetAll(key); - - if (!data.userId) { - return null; - } - - return { - userId: data.userId, - activeChatIds: JSON.parse(data.activeChatIds), - lastActivity: new Date(data.lastActivity), - isOnline: data.isOnline === 'true' - }; - } catch (error) { - logError(`Failed to get active user ${userId}`, error as Error); - return null; - } - } - - public async removeActiveUser(userId: string): Promise { - try { - const key = `active_user:${userId}`; - await this.client.del(key); - } catch (error) { - logError(`Failed to remove active user ${userId}`, error as Error); - } - } - - public async addUserToChat(userId: string, chatId: string): Promise { - try { - const userData = await this.getActiveUser(userId) || { - userId, - activeChatIds: [], - lastActivity: new Date(), - isOnline: true - }; - - if (!userData.activeChatIds.includes(chatId)) { - userData.activeChatIds.push(chatId); - userData.lastActivity = new Date(); - await this.setActiveUser(userId, userData); - } - } catch (error) { - logError(`Failed to add user ${userId} to chat ${chatId}`, error as Error); - } - } - - public async removeUserFromChat(userId: string, chatId: string): Promise { - try { - const userData = await this.getActiveUser(userId); - if (userData) { - userData.activeChatIds = userData.activeChatIds.filter(id => id !== chatId); - userData.lastActivity = new Date(); - await this.setActiveUser(userId, userData); - } - } catch (error) { - logError(`Failed to remove user ${userId} from chat ${chatId}`, error as Error); - } - } - - public async getUserActiveChats(userId: string): Promise { - try { - const userData = await this.getActiveUser(userId); - return userData?.activeChatIds || []; - } catch (error) { - logError(`Failed to get active chats for user ${userId}`, error as Error); - return []; - } - } - - public async updateChatActivity(chatId: string, messageCount?: number): Promise { - try { - const chatData = await this.getActiveChat(chatId); - if (chatData) { - chatData.lastActivity = new Date(); - if (messageCount !== undefined) { - chatData.messageCount = messageCount; - } - await this.setActiveChat(chatId, chatData); - } - } catch (error) { - logError(`Failed to update chat activity ${chatId}`, error as Error); - } - } - - public async getInactiveChats(inactivityMinutes: number): Promise { - try { - const cutoffTime = new Date(Date.now() - inactivityMinutes * 60 * 1000); - const allChats = await this.getAllActiveChats(); - - return allChats - .filter(chat => chat.lastActivity < cutoffTime) - .map(chat => chat.chatId); - } catch (error) { - logError('Failed to get inactive chats', error as Error); - return []; - } - } - - public async cleanupInactiveChats(inactivityMinutes: number): Promise { - try { - const inactiveChats = await this.getInactiveChats(inactivityMinutes); - - for (const chatId of inactiveChats) { - await this.removeActiveChat(chatId); - } - - return inactiveChats; - } catch (error) { - logError('Failed to cleanup inactive chats', error as Error); - return []; - } - } - - public async ping(): Promise { - try { - const result = await this.client.ping(); - return result === 'PONG'; - } catch (error) { - logError('Redis ping failed', error as Error); - return false; - } - } - - public isRedisConnected(): boolean { - return this.isConnected; - } - - // Generic Redis methods for game data - public async get(key: string): Promise { - try { - const value = await this.client.get(key); - // Refresh TTL on access for game-related keys - if (value && this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - return value; - } catch (error) { - logError(`Failed to get key ${key}`, error as Error); - return null; - } - } - - public async set(key: string, value: string): Promise { - try { - await this.client.set(key, value); - // Auto-expire game-related keys after 30 minutes - if (this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // 30 minutes - } - } catch (error) { - logError(`Failed to set key ${key}`, error as Error); - } - } - - public async setWithExpiry(key: string, value: string, expirySeconds: number): Promise { - try { - await this.client.setEx(key, expirySeconds, value); - } catch (error) { - logError(`Failed to set key ${key} with expiry`, error as Error); - } - } - - public async del(key: string): Promise { - try { - await this.client.del(key); - } catch (error) { - logError(`Failed to delete key ${key}`, error as Error); - } - } - - public async setAdd(key: string, member: string): Promise { - try { - await this.client.sAdd(key, member); - // Refresh TTL for game-related keys - if (this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - } catch (error) { - logError(`Failed to add member to set ${key}`, error as Error); - } - } - - public async setRemove(key: string, member: string): Promise { - try { - await this.client.sRem(key, member); - // Refresh TTL for game-related keys - if (this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - } catch (error) { - logError(`Failed to remove member from set ${key}`, error as Error); - } - } - - public async setMembers(key: string): Promise { - try { - const members = await this.client.sMembers(key); - // Refresh TTL on access for game-related keys - if (members.length > 0 && this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - return members; - } catch (error) { - logError(`Failed to get members of set ${key}`, error as Error); - return []; - } - } - - public async exists(key: string): Promise { - try { - const result = await this.client.exists(key); - // Refresh TTL on access for game-related keys - if (result === 1 && this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - return result === 1; - } catch (error) { - logError(`Failed to check existence of key ${key}`, error as Error); - return false; - } - } - - /** - * Check if a key is game-related and should have auto-expiration - * Game-related patterns: gameplay:*, game:*, game_*, board:*, game_pending_card:*, etc. - */ - private isGameRelatedKey(key: string): boolean { - const gamePatterns = [ - 'gameplay:', - 'game:', - 'game_', - 'board:', - 'game_pending_card:', - 'game_pending_decision:', - 'game_player_extra_turns:', - 'game_player_turns_to_lose:', - 'game_positions:', - 'game_ready:', - 'game_room:', - 'active_game:' - ]; - return gamePatterns.some(pattern => key.startsWith(pattern)); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/TokenService.ts b/SerpentRace_Backend/src/Application/Services/TokenService.ts deleted file mode 100644 index 48ff12fd..00000000 --- a/SerpentRace_Backend/src/Application/Services/TokenService.ts +++ /dev/null @@ -1,229 +0,0 @@ -import * as crypto from 'crypto'; -import { logError } from './Logger'; - -export interface VerificationToken { - token: string; - expiresAt: Date; - createdAt: Date; -} - -export interface PasswordResetToken { - token: string; - expiresAt: Date; - createdAt: Date; -} - -export class TokenService { - private static readonly VERIFICATION_TOKEN_EXPIRES_HOURS = 24; - private static readonly PASSWORD_RESET_TOKEN_EXPIRES_HOURS = 1; - private static readonly TOKEN_LENGTH = 32; - - /** - * Generate a secure random token - * @param length - Length of the token in bytes (default: 32) - * @returns Hexadecimal string token - */ - static generateSecureToken(length: number = TokenService.TOKEN_LENGTH): string { - try { - return crypto.randomBytes(length).toString('hex'); - } catch (error) { - logError('TokenService.generateSecureToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate secure token'); - } - } - - /** - * Generate email verification token with expiration - * @returns VerificationToken object with token and expiration info - */ - static generateVerificationToken(): VerificationToken { - try { - const token = this.generateSecureToken(); - const createdAt = new Date(); - const expiresAt = new Date(createdAt.getTime() + (this.VERIFICATION_TOKEN_EXPIRES_HOURS * 60 * 60 * 1000)); - - return { - token, - createdAt, - expiresAt - }; - } catch (error) { - logError('TokenService.generateVerificationToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate verification token'); - } - } - - /** - * Generate password reset token with expiration - * @returns PasswordResetToken object with token and expiration info - */ - static generatePasswordResetToken(): PasswordResetToken { - try { - const token = this.generateSecureToken(); - const createdAt = new Date(); - const expiresAt = new Date(createdAt.getTime() + (this.PASSWORD_RESET_TOKEN_EXPIRES_HOURS * 60 * 60 * 1000)); - - return { - token, - createdAt, - expiresAt - }; - } catch (error) { - logError('TokenService.generatePasswordResetToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate password reset token'); - } - } - - /** - * Check if a token has expired - * @param expiresAt - Expiration date of the token - * @returns True if token has expired, false otherwise - */ - static isTokenExpired(expiresAt: Date): boolean { - try { - return new Date() > expiresAt; - } catch (error) { - logError('TokenService.isTokenExpired error', error instanceof Error ? error : new Error(String(error))); - return true; // Assume expired on error for security - } - } - - /** - * Validate token format (basic validation) - * @param token - Token to validate - * @returns True if token format is valid, false otherwise - */ - static isValidTokenFormat(token: string): boolean { - try { - if (!token || typeof token !== 'string') { - return false; - } - - // Check if token is hexadecimal and has expected length - const hexRegex = /^[a-f0-9]+$/i; - const expectedLength = this.TOKEN_LENGTH * 2; // Each byte becomes 2 hex characters - - return hexRegex.test(token) && token.length === expectedLength; - } catch (error) { - logError('TokenService.isValidTokenFormat error', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Generate a verification URL with token - * @param baseUrl - Base URL of the application - * @param token - Verification token - * @returns Complete verification URL - */ - static generateVerificationUrl(baseUrl: string, token: string): string { - try { - // Remove trailing slash from baseUrl if present - const cleanBaseUrl = baseUrl.replace(/\/$/, ''); - return `${cleanBaseUrl}/verify-email?token=${encodeURIComponent(token)}`; - } catch (error) { - logError('TokenService.generateVerificationUrl error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate verification URL'); - } - } - - /** - * Generate a password reset URL with token - * @param baseUrl - Base URL of the application - * @param token - Password reset token - * @returns Complete password reset URL - */ - static generatePasswordResetUrl(baseUrl: string, token: string): string { - try { - // Remove trailing slash from baseUrl if present - const cleanBaseUrl = baseUrl.replace(/\/$/, ''); - return `${cleanBaseUrl}/reset-password?token=${encodeURIComponent(token)}`; - } catch (error) { - logError('TokenService.generatePasswordResetUrl error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate password reset URL'); - } - } - - /** - * Hash a token for secure storage in database - * @param token - Plain text token to hash - * @returns Hashed token - */ - static async hashToken(token: string): Promise { - try { - if (!token || typeof token !== 'string') { - throw new Error('Token must be a non-empty string'); - } - - return crypto.createHash('sha256').update(token).digest('hex'); - } catch (error) { - logError('TokenService.hashToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to hash token'); - } - } - - /** - * Verify a plain text token against a hashed token - * @param plainToken - Plain text token to verify - * @param hashedToken - Hashed token to compare against - * @returns True if tokens match, false otherwise - */ - static async verifyToken(plainToken: string, hashedToken: string): Promise { - try { - if (!plainToken || !hashedToken) { - return false; - } - - const hashedPlainToken = await this.hashToken(plainToken); - return hashedPlainToken === hashedToken; - } catch (error) { - logError('TokenService.verifyToken error', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Get token expiration info in human-readable format - * @param expiresAt - Expiration date - * @returns Human-readable expiration info - */ - static getExpirationInfo(expiresAt: Date): { expired: boolean; timeLeft: string } { - try { - const now = new Date(); - const expired = now > expiresAt; - - if (expired) { - const timeAgo = Math.floor((now.getTime() - expiresAt.getTime()) / (1000 * 60)); - return { - expired: true, - timeLeft: `Expired ${timeAgo} minute(s) ago` - }; - } - - const timeLeft = Math.floor((expiresAt.getTime() - now.getTime()) / (1000 * 60)); - const hours = Math.floor(timeLeft / 60); - const minutes = timeLeft % 60; - - let timeString = ''; - if (hours > 0) { - timeString = `${hours} hour(s)`; - if (minutes > 0) { - timeString += ` and ${minutes} minute(s)`; - } - } else { - timeString = `${minutes} minute(s)`; - } - - return { - expired: false, - timeLeft: `Expires in ${timeString}` - }; - } catch (error) { - logError('TokenService.getExpirationInfo error', error instanceof Error ? error : new Error(String(error))); - return { - expired: true, - timeLeft: 'Unable to determine expiration' - }; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/TurnHistoryService.ts b/SerpentRace_Backend/src/Application/Services/TurnHistoryService.ts deleted file mode 100644 index 28d332fa..00000000 --- a/SerpentRace_Backend/src/Application/Services/TurnHistoryService.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ITurnHistoryRepository } from '../../Domain/IRepository/ITurnHistoryRepository'; -import { TurnHistoryAggregate, TurnActionType, TurnActionData } from '../../Domain/Game/TurnHistoryAggregate'; -import { logOther, logError } from './Logger'; - -export class TurnHistoryService { - constructor(private turnHistoryRepository: ITurnHistoryRepository) {} - - /** - * Log a turn action - */ - async logTurnAction( - gameId: string, - playerId: string, - playerName: string, - turnNumber: number, - actionType: TurnActionType, - positionBefore: number, - positionAfter: number, - actionData?: TurnActionData - ): Promise { - try { - const turnHistory = new TurnHistoryAggregate(); - turnHistory.gameid = gameId; - turnHistory.playerid = playerId; - turnHistory.playername = playerName; - turnHistory.turnNumber = turnNumber; - turnHistory.actionType = actionType; - turnHistory.positionBefore = positionBefore; - turnHistory.positionAfter = positionAfter; - turnHistory.actionData = actionData || null; - - await this.turnHistoryRepository.save(turnHistory); - - logOther(`Turn history logged: ${actionType}`, { - gameId, - playerId, - playerName, - turnNumber, - positionBefore, - positionAfter - }); - } catch (error) { - logError('Failed to log turn history', error as Error); - // Don't throw - logging shouldn't break game flow - } - } - - /** - * Get game replay data - */ - async getGameReplay(gameId: string): Promise { - return await this.turnHistoryRepository.findByGameId(gameId); - } - - /** - * Get player's turn history in a game - */ - async getPlayerHistory(gameId: string, playerId: string): Promise { - return await this.turnHistoryRepository.findByGameAndPlayer(gameId, playerId); - } - - /** - * Get recent turns for a game - */ - async getRecentTurns(gameId: string, limit: number = 10): Promise { - return await this.turnHistoryRepository.findLastNTurns(gameId, limit); - } - - /** - * Clean up turn history for a finished game - */ - async cleanupGameHistory(gameId: string): Promise { - try { - await this.turnHistoryRepository.deleteByGameId(gameId); - logOther(`Turn history cleaned up for game ${gameId}`); - } catch (error) { - logError('Failed to cleanup turn history', error as Error); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/ValidationMiddleware.ts b/SerpentRace_Backend/src/Application/Services/ValidationMiddleware.ts deleted file mode 100644 index cabe01b9..00000000 --- a/SerpentRace_Backend/src/Application/Services/ValidationMiddleware.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { ErrorResponseService } from './ErrorResponseService'; -import { logError, logWarning } from './Logger'; - -/** - * Common validation middleware functions for request validation - */ -export class ValidationMiddleware { - - /** - * Validates required fields in request body - * @param requiredFields Array of required field names - */ - static validateRequiredFields(requiredFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const missingFields: string[] = []; - - for (const field of requiredFields) { - if (!req.body || req.body[field] === undefined || req.body[field] === null || req.body[field] === '') { - missingFields.push(field); - } - } - - if (missingFields.length > 0) { - logWarning('Validation failed - missing required fields', { - missingFields, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Missing required fields', - { missingFields } - ); - } - - next(); - }; - } - - /** - * Validates field types in request body - * @param fieldTypes Object mapping field names to expected types - */ - static validateFieldTypes(fieldTypes: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const typeErrors: string[] = []; - - for (const [field, expectedType] of Object.entries(fieldTypes)) { - if (req.body && req.body[field] !== undefined) { - const actualType = Array.isArray(req.body[field]) ? 'array' : typeof req.body[field]; - - if (actualType !== expectedType) { - typeErrors.push(`Field '${field}' should be ${expectedType}, got ${actualType}`); - } - } - } - - if (typeErrors.length > 0) { - logWarning('Validation failed - invalid field types', { - typeErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Invalid field types', - { errors: typeErrors } - ); - } - - next(); - }; - } - - /** - * Validates string field length constraints - * @param constraints Object mapping field names to min/max length - */ - static validateStringLength(constraints: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const lengthErrors: string[] = []; - - for (const [field, constraint] of Object.entries(constraints)) { - if (req.body && typeof req.body[field] === 'string') { - const value = req.body[field]; - - if (constraint.min !== undefined && value.length < constraint.min) { - lengthErrors.push(`Field '${field}' must be at least ${constraint.min} characters`); - } - - if (constraint.max !== undefined && value.length > constraint.max) { - lengthErrors.push(`Field '${field}' must not exceed ${constraint.max} characters`); - } - } - } - - if (lengthErrors.length > 0) { - logWarning('Validation failed - string length constraints', { - lengthErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'String length validation failed', - { errors: lengthErrors } - ); - } - - next(); - }; - } - - /** - * Validates email format - * @param emailFields Array of field names that should contain valid emails - */ - static validateEmailFormat(emailFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const emailErrors: string[] = []; - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - for (const field of emailFields) { - if (req.body && req.body[field] && typeof req.body[field] === 'string') { - if (!emailRegex.test(req.body[field])) { - emailErrors.push(`Field '${field}' must contain a valid email address`); - } - } - } - - if (emailErrors.length > 0) { - logWarning('Validation failed - invalid email format', { - emailErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Email format validation failed', - { errors: emailErrors } - ); - } - - next(); - }; - } - - /** - * Validates UUIDs format - * @param uuidFields Array of field names that should contain valid UUIDs - */ - static validateUUIDFormat(uuidFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const uuidErrors: string[] = []; - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - - for (const field of uuidFields) { - const value = field.includes('.') - ? this.getNestedValue(req, field) - : req.body?.[field] || req.params?.[field] || req.query?.[field]; - - if (value && typeof value === 'string') { - if (!uuidRegex.test(value)) { - uuidErrors.push(`Field '${field}' must contain a valid UUID`); - } - } - } - - if (uuidErrors.length > 0) { - logWarning('Validation failed - invalid UUID format', { - uuidErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'UUID format validation failed', - { errors: uuidErrors } - ); - } - - next(); - }; - } - - /** - * Validates numeric constraints - * @param constraints Object mapping field names to min/max values - */ - static validateNumericConstraints(constraints: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const numericErrors: string[] = []; - - for (const [field, constraint] of Object.entries(constraints)) { - if (req.body && typeof req.body[field] === 'number') { - const value = req.body[field]; - - if (constraint.min !== undefined && value < constraint.min) { - numericErrors.push(`Field '${field}' must be at least ${constraint.min}`); - } - - if (constraint.max !== undefined && value > constraint.max) { - numericErrors.push(`Field '${field}' must not exceed ${constraint.max}`); - } - } - } - - if (numericErrors.length > 0) { - logWarning('Validation failed - numeric constraints', { - numericErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Numeric validation failed', - { errors: numericErrors } - ); - } - - next(); - }; - } - - /** - * Validates that arrays are not empty - * @param arrayFields Array of field names that should contain non-empty arrays - */ - static validateNonEmptyArrays(arrayFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const arrayErrors: string[] = []; - - for (const field of arrayFields) { - if (req.body && Array.isArray(req.body[field])) { - if (req.body[field].length === 0) { - arrayErrors.push(`Field '${field}' must not be empty`); - } - } else if (req.body && req.body[field] !== undefined) { - arrayErrors.push(`Field '${field}' must be an array`); - } - } - - if (arrayErrors.length > 0) { - logWarning('Validation failed - empty arrays', { - arrayErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Array validation failed', - { errors: arrayErrors } - ); - } - - next(); - }; - } - - /** - * Validates allowed values for enum-like fields - * @param allowedValues Object mapping field names to arrays of allowed values - */ - static validateAllowedValues(allowedValues: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const valueErrors: string[] = []; - - for (const [field, allowed] of Object.entries(allowedValues)) { - if (req.body && req.body[field] !== undefined) { - if (!allowed.includes(req.body[field])) { - valueErrors.push(`Field '${field}' must be one of: ${allowed.join(', ')}`); - } - } - } - - if (valueErrors.length > 0) { - logWarning('Validation failed - disallowed values', { - valueErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Value validation failed', - { errors: valueErrors } - ); - } - - next(); - }; - } - - /** - * Combines multiple validation middlewares - * @param validations Array of validation middleware functions - */ - static combine(validations: Array<(req: Request, res: Response, next: NextFunction) => void>) { - return async (req: Request, res: Response, next: NextFunction) => { - let currentIndex = 0; - - const runNext = (error?: any) => { - if (error) { - return next(error); - } - - if (currentIndex >= validations.length) { - return next(); - } - - const currentValidation = validations[currentIndex++]; - - try { - currentValidation(req, res, (err?: any) => { - if (res.headersSent) { - return; // Response already sent, don't continue - } - runNext(err); - }); - } catch (error) { - logError('Validation middleware error', error as Error, req, res); - ErrorResponseService.sendInternalServerError(res); - } - }; - - runNext(); - }; - } - - /** - * Helper method to get nested values from request - * @param req Request object - * @param path Dot-notation path like 'body.user.id' - */ - private static getNestedValue(req: Request, path: string): any { - const parts = path.split('.'); - let current: any = req; - - for (const part of parts) { - if (current && typeof current === 'object') { - current = current[part]; - } else { - return undefined; - } - } - - return current; - } -} diff --git a/SerpentRace_Backend/src/Application/Services/WebSocketService.ts b/SerpentRace_Backend/src/Application/Services/WebSocketService.ts deleted file mode 100644 index 5ef871f1..00000000 --- a/SerpentRace_Backend/src/Application/Services/WebSocketService.ts +++ /dev/null @@ -1,1408 +0,0 @@ -import { Server as HttpServer } from 'http'; -import { Server as SocketIOServer, Socket } from 'socket.io'; -import { JWTService, TokenPayload } from './JWTService'; -import { ChatRepository } from '../../Infrastructure/Repository/ChatRepository'; -import { ChatArchiveRepository } from '../../Infrastructure/Repository/ChatArchiveRepository'; -import { UserRepository } from '../../Infrastructure/Repository/UserRepository'; -import { ChatAggregate, ChatType, ChatTypeType, Message } from '../../Domain/Chat/ChatAggregate'; -import { UserState } from '../../Domain/User/UserAggregate'; -import { logAuth, logError, logRequest, logWarning } from './Logger'; -import { RedisService, ActiveChatData } from './RedisService'; -import { v4 as uuidv4 } from 'uuid'; - -interface AuthenticatedSocket extends Socket { - userId?: string; - authLevel?: 0 | 1; - userStatus?: UserState; - orgId?: string | null; -} - -interface JoinChatData { - chatId: string; -} - -interface SendMessageData { - chatId: string; - message: string; -} - -interface CreateGroupData { - name: string; - userIds: string[]; -} - -interface CreateDirectChatData { - targetUserId: string; -} - -interface CreateGameChatData { - gameId: string; - gameName: string; - playerIds: string[]; -} - -interface DeleteChatData { - chatId: string; -} - -interface DeleteChatArchiveData { - archiveId: string; -} - -interface DeleteMessageData { - chatId: string; - messageId: string; -} - -// Game-related WebSocket interfaces (prepared for future implementation) -interface JoinGameRoomData { - gameCode: string; -} - -interface LeaveGameRoomData { - gameCode: string; -} - -interface GameStateUpdateData { - gameId: string; - gameCode: string; - players: string[]; - state: string; - currentTurn?: string; -} - -interface GameActionData { - gameId: string; - gameCode: string; - playerId: string; - action: 'pick_card' | 'play_card' | 'end_turn' | 'leave_game'; - data?: any; -} - -export class WebSocketService { - private io: SocketIOServer; - private jwtService: JWTService; - private chatRepository: ChatRepository; - private chatArchiveRepository: ChatArchiveRepository; - private userRepository: UserRepository; - private redisService: RedisService; - private connectedUsers: Map = new Map(); - private chatTimeout: number; - private maxMessagesPerUser: number; - private messageCleanupWeeks: number; - private userMessageCounts: Map = new Map(); - - constructor(httpServer: HttpServer) { - this.io = new SocketIOServer(httpServer, { - cors: { - origin: ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080'], - methods: ['GET', 'POST'], - credentials: true - } - }); - - this.jwtService = new JWTService(); - this.chatRepository = new ChatRepository(); - this.chatArchiveRepository = new ChatArchiveRepository(); - this.userRepository = new UserRepository(); - this.redisService = RedisService.getInstance(); - this.chatTimeout = parseInt(process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'); - this.maxMessagesPerUser = parseInt(process.env.CHAT_MAX_MESSAGES_PER_USER || '100'); - this.messageCleanupWeeks = parseInt(process.env.CHAT_MESSAGE_CLEANUP_WEEKS || '4'); - - // Initialize Redis connection - this.initializeRedis(); - - this.setupSocketHandlers(); - this.setupArchivingScheduler(); - - logRequest('WebSocket service initialized', undefined, undefined, { - chatTimeoutMinutes: this.chatTimeout - }); - } - - private async initializeRedis(): Promise { - try { - await this.redisService.connect(); - } catch (error) { - logError('Failed to initialize Redis connection', error as Error); - } - } - - private setupSocketHandlers() { - this.io.use(async (socket: AuthenticatedSocket, next) => { - try { - const token = socket.handshake.auth.token || socket.handshake.headers.cookie - ?.split(';') - .find(c => c.trim().startsWith('auth_token=')) - ?.split('=')[1]; - - if (!token) { - logWarning('WebSocket connection rejected - No token provided', { - socketId: socket.id, - ip: socket.handshake.address - }); - return next(new Error('Authentication required')); - } - - // Create a mock request object for JWT verification - const mockRequest = { - headers: { - authorization: `Bearer ${token}`, - cookie: `auth_token=${token}` - }, - cookies: { - auth_token: token - } - } as any; - - const payload = this.jwtService.verify(mockRequest); - if (!payload) { - logWarning('WebSocket connection rejected - Invalid token', { - socketId: socket.id, - ip: socket.handshake.address - }); - return next(new Error('Invalid token')); - } - - socket.userId = payload.userId; - socket.authLevel = payload.authLevel; - socket.userStatus = payload.userStatus; - socket.orgId = payload.orgId; - - logAuth('WebSocket connection authenticated', payload.userId, { - socketId: socket.id, - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId - }); - - next(); - } catch (error) { - logError('WebSocket authentication error', error as Error); - next(new Error('Authentication failed')); - } - }); - - this.io.on('connection', (socket: AuthenticatedSocket) => { - this.handleConnection(socket); - }); - } - - private async handleConnection(socket: AuthenticatedSocket) { - const userId = socket.userId!; - - // Store connected user - this.connectedUsers.set(userId, socket); - - // Load user's active chats and join rooms - try { - const userChats = await this.chatRepository.findActiveChatsForUser(userId); - const chatIds = userChats.map(chat => chat.id); - - // Join all chat rooms - chatIds.forEach(chatId => { - socket.join(chatId); - }); - - // Store user's chat memberships in Redis - await this.redisService.setActiveUser(userId, { - userId, - activeChatIds: chatIds, - lastActivity: new Date(), - isOnline: true - }); - - // Also store each active chat in Redis - for (const chat of userChats) { - await this.redisService.setActiveChat(chat.id, { - chatId: chat.id, - participants: chat.users, - lastActivity: chat.lastActivity || new Date(), - messageCount: chat.messages.length, - chatType: chat.type as 'direct' | 'group' | 'game', - gameId: chat.gameId || undefined, - name: chat.name || undefined - }); - } - - logAuth('User connected to WebSocket', userId, { - socketId: socket.id, - activeChats: chatIds.length - }); - - // Send user their active chats with unread counts - const chatsWithUnread = await Promise.all(userChats.map(async (chat) => ({ - id: chat.id, - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users, - lastActivity: chat.lastActivity, - unreadCount: this.calculateUnreadMessages(chat, userId), - isArchived: false - }))); - - socket.emit('chats:list', chatsWithUnread); - - } catch (error) { - logError('Error loading user chats on connection', error as Error, undefined, undefined); - socket.emit('error', { message: 'Failed to load chats' }); - } - - // Setup event handlers - socket.on('chat:join', (data: JoinChatData) => this.handleJoinChat(socket, data)); - socket.on('chat:leave', (data: JoinChatData) => this.handleLeaveChat(socket, data)); - socket.on('message:send', (data: SendMessageData) => this.handleSendMessage(socket, data)); - socket.on('group:create', (data: CreateGroupData) => this.handleCreateGroup(socket, data)); - socket.on('chat:direct', (data: CreateDirectChatData) => this.handleCreateDirectChat(socket, data)); - socket.on('game:chat:create', (data: CreateGameChatData) => this.handleCreateGameChat(socket, data)); - socket.on('chat:history', (data: JoinChatData) => this.handleGetChatHistory(socket, data)); - socket.on('chat:delete', (data: DeleteChatData) => this.handleDeleteChat(socket, data)); - socket.on('chat:archive:delete', (data: DeleteChatArchiveData) => this.handleDeleteChatArchive(socket, data)); - socket.on('message:delete', (data: DeleteMessageData) => this.handleDeleteMessage(socket, data)); - - socket.on('disconnect', () => this.handleDisconnection(socket)); - } - - private async handleJoinChat(socket: AuthenticatedSocket, data: JoinChatData) { - try { - const userId = socket.userId!; - const chat = await this.chatRepository.findById(data.chatId); - - if (!chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Check if user is member of this chat - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to join this chat' }); - return; - } - - // Join the chat room - socket.join(data.chatId); - - // Add to user's active chats in Redis - await this.redisService.addUserToChat(userId, data.chatId); - - // Update chat activity in Redis - await this.redisService.updateChatActivity(data.chatId); - - // Update last activity in database - await this.chatRepository.update(data.chatId, { lastActivity: new Date() }); - - logAuth('User joined chat', userId, { - chatId: data.chatId, - chatType: chat.type - }); - - socket.emit('chat:joined', { - chatId: data.chatId, - messages: chat.messages.slice(-10) // Last 10 messages - }); - - } catch (error) { - logError('Error joining chat', error as Error); - socket.emit('error', { message: 'Failed to join chat' }); - } - } - - private async handleLeaveChat(socket: AuthenticatedSocket, data: JoinChatData) { - try { - const userId = socket.userId!; - - // Leave the chat room - socket.leave(data.chatId); - - // Remove from user's active chats in Redis - await this.redisService.removeUserFromChat(userId, data.chatId); - - logAuth('User left chat', userId, { - chatId: data.chatId - }); - - socket.emit('chat:left', { chatId: data.chatId }); - - } catch (error) { - logError('Error leaving chat', error as Error); - socket.emit('error', { message: 'Failed to leave chat' }); - } - } - - private async handleSendMessage(socket: AuthenticatedSocket, data: SendMessageData) { - try { - const userId = socket.userId!; - - // Rate limiting check - if (!this.checkMessageRateLimit(userId)) { - socket.emit('error', { message: `Rate limit exceeded. Maximum ${this.maxMessagesPerUser} messages per minute allowed.` }); - return; - } - - // Validate message is string and not empty - if (typeof data.message !== 'string' || !data.message.trim()) { - socket.emit('error', { message: 'Message must be a non-empty string' }); - return; - } - - const chat = await this.chatRepository.findById(data.chatId); - if (!chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Check if user is member of this chat - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to send message to this chat' }); - return; - } - - // Create message - const message: Message = { - id: uuidv4(), - date: new Date(), - userid: userId, - text: data.message.trim() - }; - - // Manage message history based on chat type - let updatedMessages = [...chat.messages, message]; - updatedMessages = this.pruneMessages(updatedMessages, chat.type); - - // Update chat - await this.chatRepository.update(data.chatId, { - messages: updatedMessages, - lastActivity: new Date() - }); - - // Update chat activity in Redis with new message count - await this.redisService.updateChatActivity(data.chatId, updatedMessages.length); - - // Broadcast to all users in the chat room - this.io.to(data.chatId).emit('message:received', { - chatId: data.chatId, - message: message - }); - - // Send notifications to offline users - await this.notifyOfflineUsers(chat, message); - - logAuth('Message sent', userId, { - chatId: data.chatId, - messageLength: data.message.length, - chatType: chat.type - }); - - } catch (error) { - logError('Error sending message', error as Error); - socket.emit('error', { message: 'Failed to send message' }); - } - } - - private async handleCreateGroup(socket: AuthenticatedSocket, data: CreateGroupData) { - try { - const userId = socket.userId!; - - // Check if user is premium (required to create groups) - const user = await this.userRepository.findById(userId); - if (!user || user.state !== UserState.VERIFIED_PREMIUM) { - socket.emit('error', { message: 'Premium subscription required to create groups' }); - return; - } - - // Validate group data - if (!data.name?.trim()) { - socket.emit('error', { message: 'Group name is required' }); - return; - } - - if (!data.userIds || data.userIds.length === 0) { - socket.emit('error', { message: 'At least one member is required' }); - return; - } - - // Verify all users exist - const members = await Promise.all( - data.userIds.map(id => this.userRepository.findById(id)) - ); - - if (members.some(member => !member)) { - socket.emit('error', { message: 'One or more users not found' }); - return; - } - - // Create group chat - const groupChat = await this.chatRepository.create({ - type: ChatType.GROUP, - name: data.name.trim(), - createdBy: userId, - users: [userId, ...data.userIds], // Include creator - messages: [], - lastActivity: new Date() - }); - - // Add all members to the group room and store in Redis - const allMemberIds = data.userIds.concat(userId); - for (const memberId of allMemberIds) { - const memberSocket = this.connectedUsers.get(memberId); - if (memberSocket) { - memberSocket.join(groupChat.id); - } - - // Update user's chat list in Redis - await this.redisService.addUserToChat(memberId, groupChat.id); - } - - // Store the group chat in Redis - await this.redisService.setActiveChat(groupChat.id, { - chatId: groupChat.id, - participants: allMemberIds, - lastActivity: new Date(), - messageCount: 0, - chatType: 'group', - name: groupChat.name || undefined - }); - - // Notify all members - this.io.to(groupChat.id).emit('group:created', { - chat: { - id: groupChat.id, - type: groupChat.type, - name: groupChat.name, - createdBy: groupChat.createdBy, - users: groupChat.users, - messages: [] - } - }); - - logAuth('Group created', userId, { - groupId: groupChat.id, - groupName: data.name, - memberCount: groupChat.users.length - }); - - } catch (error) { - logError('Error creating group', error as Error); - socket.emit('error', { message: 'Failed to create group' }); - } - } - - private async handleCreateDirectChat(socket: AuthenticatedSocket, data: CreateDirectChatData) { - try { - const userId = socket.userId!; - - // Validate target user exists - const targetUser = await this.userRepository.findById(data.targetUserId); - if (!targetUser) { - socket.emit('error', { message: 'Target user not found' }); - return; - } - - // Check if direct chat already exists - const existingChats = await this.chatRepository.findByUserId(userId); - const existingDirectChat = existingChats.find(chat => - chat.type === ChatType.DIRECT && - chat.users.length === 2 && - chat.users.includes(data.targetUserId) - ); - - if (existingDirectChat) { - socket.emit('chat:direct:exists', { - chatId: existingDirectChat.id - }); - return; - } - - // Create direct chat - const directChat = await this.chatRepository.create({ - type: ChatType.DIRECT, - users: [userId, data.targetUserId], - messages: [], - lastActivity: new Date() - }); - - // Add both users to the chat room if they're online and store in Redis - const memberIds = [userId, data.targetUserId]; - for (const memberId of memberIds) { - const memberSocket = this.connectedUsers.get(memberId); - if (memberSocket) { - memberSocket.join(directChat.id); - } - - // Update user's chat list in Redis - await this.redisService.addUserToChat(memberId, directChat.id); - } - - // Store the direct chat in Redis - await this.redisService.setActiveChat(directChat.id, { - chatId: directChat.id, - participants: memberIds, - lastActivity: new Date(), - messageCount: 0, - chatType: 'direct' - }); - - // Notify both users - this.io.to(directChat.id).emit('chat:direct:created', { - chat: { - id: directChat.id, - type: directChat.type, - users: directChat.users, - messages: [] - } - }); - - logAuth('Direct chat created', userId, { - chatId: directChat.id, - targetUserId: data.targetUserId - }); - - } catch (error) { - logError('Error creating direct chat', error as Error); - socket.emit('error', { message: 'Failed to create direct chat' }); - } - } - - private async handleCreateGameChat(socket: AuthenticatedSocket, data: CreateGameChatData) { - try { - const userId = socket.userId!; - - // Check if game chat already exists - const existingGameChat = await this.chatRepository.findByGameId(data.gameId); - if (existingGameChat) { - socket.emit('game:chat:exists', { - chatId: existingGameChat.id - }); - return; - } - - // Create game chat - const gameChat = await this.chatRepository.create({ - type: ChatType.GAME, - name: data.gameName, - gameId: data.gameId, - users: data.playerIds, - messages: [], - lastActivity: new Date() - }); - - // Add all players to the game chat room if they're online and store in Redis - for (const playerId of data.playerIds) { - const playerSocket = this.connectedUsers.get(playerId); - if (playerSocket) { - playerSocket.join(gameChat.id); - } - - // Update user's chat list in Redis - await this.redisService.addUserToChat(playerId, gameChat.id); - } - - // Store the game chat in Redis - await this.redisService.setActiveChat(gameChat.id, { - chatId: gameChat.id, - participants: data.playerIds, - lastActivity: new Date(), - messageCount: 0, - chatType: 'game', - gameId: gameChat.gameId || undefined, - name: gameChat.name || undefined - }); - - // Notify all players - this.io.to(gameChat.id).emit('game:chat:created', { - chat: { - id: gameChat.id, - type: gameChat.type, - name: gameChat.name, - gameId: gameChat.gameId, - users: gameChat.users, - messages: [] - } - }); - - logAuth('Game chat created', userId, { - chatId: gameChat.id, - gameId: data.gameId, - gameName: data.gameName, - playerCount: data.playerIds.length - }); - - } catch (error) { - logError('Error creating game chat', error as Error); - socket.emit('error', { message: 'Failed to create game chat' }); - } - } - - private async handleGetChatHistory(socket: AuthenticatedSocket, data: JoinChatData) { - try { - const userId = socket.userId!; - const chat = await this.chatRepository.findById(data.chatId); - - if (!chat) { - // Check if it's archived - const archived = await this.chatRepository.getArchivedChat(data.chatId); - if (archived) { - socket.emit('chat:history:archived', { - chatId: data.chatId, - messages: archived.archivedMessages, - chatType: archived.chatType, - isGameChat: archived.chatType === ChatType.GAME - }); - } else { - socket.emit('error', { message: 'Chat not found' }); - } - return; - } - - // Check if user has access - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to view this chat' }); - return; - } - - socket.emit('chat:history', { - chatId: data.chatId, - messages: chat.messages, - chatInfo: { - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users - } - }); - - } catch (error) { - logError('Error getting chat history', error as Error); - socket.emit('error', { message: 'Failed to get chat history' }); - } - } - - private async handleDeleteChat(socket: AuthenticatedSocket, data: DeleteChatData) { - try { - const userId = socket.userId!; - const chat = await this.chatRepository.findById(data.chatId); - - if (!chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Check if user is member of this chat - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to delete this chat' }); - return; - } - - // Perform soft delete - const deletedChat = await this.chatRepository.softDelete(data.chatId); - if (!deletedChat) { - socket.emit('error', { message: 'Failed to delete chat' }); - return; - } - - // Remove from Redis active chats - await this.redisService.removeActiveChat(data.chatId); - - // Notify all participants that the chat has been deleted - this.io.to(data.chatId).emit('chat:deleted', { - chatId: data.chatId, - deletedBy: userId - }); - - // Remove all users from the chat room - for (const participantId of chat.users) { - const participantSocket = this.connectedUsers.get(participantId); - if (participantSocket) { - participantSocket.leave(data.chatId); - } - // Remove from user's active chats in Redis - await this.redisService.removeUserFromChat(participantId, data.chatId); - } - - logAuth('Chat deleted', userId, { - chatId: data.chatId, - chatType: chat.type, - participantCount: chat.users.length - }); - - socket.emit('chat:delete:success', { - chatId: data.chatId, - message: 'Chat deleted successfully' - }); - - } catch (error) { - logError('Error deleting chat', error as Error); - socket.emit('error', { message: 'Failed to delete chat' }); - } - } - - private async handleDeleteChatArchive(socket: AuthenticatedSocket, data: DeleteChatArchiveData) { - try { - const userId = socket.userId!; - const archive = await this.chatArchiveRepository.findById(data.archiveId); - - if (!archive) { - socket.emit('error', { message: 'Chat archive not found' }); - return; - } - - // Check if user was a participant in the archived chat - if (!archive.participants.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to delete this chat archive' }); - return; - } - - // Hard delete the archive (since it's already archived) - await this.chatArchiveRepository.delete(data.archiveId); - - logAuth('Chat archive deleted', userId, { - archiveId: data.archiveId, - originalChatId: archive.chatId, - chatType: archive.chatType, - participantCount: archive.participants.length - }); - - socket.emit('chat:archive:delete:success', { - archiveId: data.archiveId, - message: 'Chat archive deleted successfully' - }); - - } catch (error) { - logError('Error deleting chat archive', error as Error); - socket.emit('error', { message: 'Failed to delete chat archive' }); - } - } - - private async handleDeleteMessage(socket: AuthenticatedSocket, data: DeleteMessageData) { - try { - const userId = socket.userId!; - - // Check if user has admin/moderator privileges - const user = await this.userRepository.findById(userId); - if (!user || user.state !== UserState.ADMIN) { // Check if user is admin - socket.emit('error', { message: 'Insufficient permissions to delete messages' }); - return; - } - - const success = await this.deleteMessage(data.chatId, data.messageId, userId); - if (success) { - socket.emit('message:delete:success', { - chatId: data.chatId, - messageId: data.messageId, - message: 'Message deleted successfully' - }); - } else { - socket.emit('error', { message: 'Failed to delete message or message not found' }); - } - - } catch (error) { - logError('Error handling delete message request', error as Error); - socket.emit('error', { message: 'Failed to delete message' }); - } - } - - private async handleDisconnection(socket: AuthenticatedSocket) { - const userId = socket.userId; - if (userId) { - this.connectedUsers.delete(userId); - - // Update user status in Redis - const userData = await this.redisService.getActiveUser(userId); - if (userData) { - userData.isOnline = false; - userData.lastActivity = new Date(); - await this.redisService.setActiveUser(userId, userData); - } - - logAuth('User disconnected from WebSocket', userId, { - socketId: socket.id - }); - } - } - - // Utility methods - private calculateUnreadMessages(chat: ChatAggregate, userId: string): number { - // Simple implementation - count messages after user's last seen - // In production, you'd store lastSeen timestamp per user per chat - return chat.messages.filter(msg => msg.userid !== userId).length; - } - - private pruneMessages(messages: Message[], chatType: ChatTypeType): Message[] { - const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); - - // Remove messages older than 2 weeks - let prunedMessages = messages.filter(msg => new Date(msg.date) > twoWeeksAgo); - - // For group chats, only apply the 2-week time limit (unlimited messages per user) - if (chatType === ChatType.GROUP) { - return prunedMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - } - - // For direct and game chats, apply both time limit and per-user message limit - // Group by user and keep last 10 messages per user - const messagesByUser = new Map(); - prunedMessages.forEach(msg => { - if (!messagesByUser.has(msg.userid)) { - messagesByUser.set(msg.userid, []); - } - messagesByUser.get(msg.userid)!.push(msg); - }); - - // Keep only last 10 messages per user - const finalMessages: Message[] = []; - messagesByUser.forEach((userMessages, userId) => { - const last10 = userMessages.slice(-10); - finalMessages.push(...last10); - }); - - // Sort by date - return finalMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - } - - private async notifyOfflineUsers(chat: ChatAggregate, message: Message) { - // Find users who are not currently connected - const offlineUsers = chat.users.filter(userId => - userId !== message.userid && !this.connectedUsers.has(userId) - ); - - // In a real implementation, you would send push notifications or emails here - if (offlineUsers.length > 0) { - logRequest('Offline users to notify', undefined, undefined, { - chatId: chat.id, - offlineUserCount: offlineUsers.length, - messageFrom: message.userid - }); - } - } - - private setupArchivingScheduler() { - // Run every hour to check for inactive chats - setInterval(async () => { - try { - // First, cleanup inactive chats from Redis and get their IDs - const inactiveChatIds = await this.redisService.cleanupInactiveChats(this.chatTimeout); - - // Archive the inactive chats in the database - for (const chatId of inactiveChatIds) { - const chat = await this.chatRepository.findById(chatId); - if (chat) { - await this.chatRepository.archiveChat(chat); - logRequest('Chat archived due to inactivity', undefined, undefined, { - chatId: chat.id, - chatType: chat.type, - lastActivity: chat.lastActivity, - messageCount: chat.messages.length - }); - } - } - - // Also find inactive chats from database that might not be in Redis - const dbInactiveChats = await this.chatRepository.findInactiveChats(this.chatTimeout); - const additionalInactiveChats = dbInactiveChats.filter(chat => - !inactiveChatIds.includes(chat.id) - ); - - for (const chat of additionalInactiveChats) { - await this.chatRepository.archiveChat(chat); - logRequest('Chat archived due to inactivity (from DB)', undefined, undefined, { - chatId: chat.id, - chatType: chat.type, - lastActivity: chat.lastActivity, - messageCount: chat.messages.length - }); - } - - const totalArchived = inactiveChatIds.length + additionalInactiveChats.length; - if (totalArchived > 0) { - logRequest('Chat archiving completed', undefined, undefined, { - archivedCount: totalArchived, - redisCleanedUp: inactiveChatIds.length, - databaseCleanedUp: additionalInactiveChats.length, - timeoutMinutes: this.chatTimeout - }); - } - - // Cleanup old messages from archived chats based on messageCleanupWeeks - await this.cleanupOldMessages(); - - } catch (error) { - logError('Error in chat archiving scheduler', error as Error); - } - }, 60 * 60 * 1000); // 1 hour - - // Also run message count cleanup every 5 minutes - setInterval(() => { - this.cleanupMessageCounts(); - }, 5 * 60 * 1000); // 5 minutes - } - - // Public methods for game integration - public async createGameChat(gameId: string, gameName: string, playerIds: string[]): Promise { - try { - const existingGameChat = await this.chatRepository.findByGameId(gameId); - if (existingGameChat) { - return existingGameChat; - } - - const gameChat = await this.chatRepository.create({ - type: ChatType.GAME, - name: gameName, - gameId: gameId, - users: playerIds, - messages: [], - lastActivity: new Date() - }); - - // Notify connected players - playerIds.forEach(playerId => { - const playerSocket = this.connectedUsers.get(playerId); - if (playerSocket) { - playerSocket.join(gameChat.id); - playerSocket.emit('game:chat:created', { - chat: { - id: gameChat.id, - type: gameChat.type, - name: gameChat.name, - gameId: gameChat.gameId, - users: gameChat.users, - messages: [] - } - }); - } - }); - - return gameChat; - } catch (error) { - logError('Error creating game chat programmatically', error as Error); - return null; - } - } - - public getConnectedUserCount(): number { - return this.connectedUsers.size; - } - - public isUserConnected(userId: string): boolean { - return this.connectedUsers.has(userId); - } - - public async cleanup(): Promise { - try { - await this.redisService.disconnect(); - } catch (error) { - logError('Error during WebSocket service cleanup', error as Error); - } - } - - /** - * Manually trigger cleanup of old messages and chats - * This can be called by admin endpoints for maintenance - */ - public async triggerManualCleanup(): Promise<{ deletedArchives: number; deletedChats: number }> { - try { - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - (this.messageCleanupWeeks * 7)); - - // Clean up old archived messages - const deletedArchivesCount = await this.chatArchiveRepository.cleanup(this.messageCleanupWeeks * 7); - - // Clean up soft-deleted chats - const softDeletedChats = await this.chatRepository.findByPageIncludingDeleted(0, 1000); - let deletedChatsCount = 0; - - for (const chat of softDeletedChats.chats) { - if (chat.state === 2 && chat.updateDate < cutoffDate) { // SOFT_DELETE state = 2 - await this.chatRepository.delete(chat.id); // Hard delete - deletedChatsCount++; - } - } - - logRequest('Manual cleanup triggered', undefined, undefined, { - cutoffDate: cutoffDate.toISOString(), - cleanupWeeks: this.messageCleanupWeeks, - deletedArchives: deletedArchivesCount, - deletedChats: deletedChatsCount, - triggeredBy: 'manual' - }); - - return { deletedArchives: deletedArchivesCount, deletedChats: deletedChatsCount }; - - } catch (error) { - logError('Error during manual cleanup', error as Error); - throw error; - } - } - - /** - * Clean up old messages from archived chats based on messageCleanupWeeks setting - */ - private async cleanupOldMessages(): Promise { - try { - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - (this.messageCleanupWeeks * 7)); - - // Clean up old archived messages using ChatArchiveRepository - const deletedArchivesCount = await this.chatArchiveRepository.cleanup(this.messageCleanupWeeks * 7); - - // Also clean up soft-deleted chats from the main repository - // Get all soft-deleted chats that are older than the cleanup period - const softDeletedChats = await this.chatRepository.findByPageIncludingDeleted(0, 1000); - let deletedChatsCount = 0; - - for (const chat of softDeletedChats.chats) { - if (chat.state === 2 && chat.updateDate < cutoffDate) { // SOFT_DELETE state = 2 - await this.chatRepository.delete(chat.id); // Hard delete - deletedChatsCount++; - } - } - - logRequest('Old message cleanup completed', undefined, undefined, { - cutoffDate: cutoffDate.toISOString(), - cleanupWeeks: this.messageCleanupWeeks, - deletedArchives: deletedArchivesCount, - deletedChats: deletedChatsCount, - note: 'Cleanup completed using both ChatRepository and ChatArchiveRepository' - }); - - } catch (error) { - logError('Error cleaning up old messages', error as Error); - } - } - - /** - * Check if user has exceeded message rate limit - * @param userId User ID to check - * @returns true if within limit, false if exceeded - */ - private checkMessageRateLimit(userId: string): boolean { - const now = Date.now(); - const minute = 60 * 1000; // 1 minute in milliseconds - - const userStats = this.userMessageCounts.get(userId) || { count: 0, lastReset: now }; - - // Reset counter if more than a minute has passed - if (now - userStats.lastReset >= minute) { - userStats.count = 0; - userStats.lastReset = now; - } - - // Check if user is within limits - if (userStats.count >= this.maxMessagesPerUser) { - return false; - } - - // Increment counter - userStats.count++; - this.userMessageCounts.set(userId, userStats); - - return true; - } - - /** - * Delete a specific message from chat history - * This can be used for moderation purposes - */ - public async deleteMessage(chatId: string, messageId: string, moderatorUserId: string): Promise { - try { - // Get the chat - const chat = await this.chatRepository.findById(chatId); - if (!chat) { - // Check archived chats - const archivedChat = await this.chatRepository.getArchivedChat(chatId); - if (!archivedChat) { - logWarning('Chat not found for message deletion', { - chatId, - messageId, - moderatorUserId - }); - return false; - } - - // Remove message from archived chat - const updatedMessages = archivedChat.archivedMessages.filter(msg => msg.id !== messageId); - if (updatedMessages.length === archivedChat.archivedMessages.length) { - logWarning('Message not found in archived chat', { - chatId, - messageId, - moderatorUserId - }); - return false; - } - - // Update archived chat - await this.chatArchiveRepository.create({ - ...archivedChat, - archivedMessages: updatedMessages - }); - - logAuth('Message deleted from archived chat', moderatorUserId, { - chatId, - messageId, - originalMessageCount: archivedChat.archivedMessages.length, - newMessageCount: updatedMessages.length - }); - - return true; - } - - // Remove message from active chat - const updatedMessages = chat.messages.filter(msg => msg.id !== messageId); - if (updatedMessages.length === chat.messages.length) { - logWarning('Message not found in active chat', { - chatId, - messageId, - moderatorUserId - }); - return false; - } - - // Update active chat - await this.chatRepository.update(chatId, { - messages: updatedMessages - }); - - // Notify all users in the chat about message deletion - this.io.to(chatId).emit('message:deleted', { - chatId, - messageId, - deletedBy: moderatorUserId - }); - - logAuth('Message deleted from active chat', moderatorUserId, { - chatId, - messageId, - originalMessageCount: chat.messages.length, - newMessageCount: updatedMessages.length - }); - - return true; - - } catch (error) { - logError('Error deleting message', error as Error); - return false; - } - } - - /** - * Clean up old user message count entries (called periodically) - */ - private cleanupMessageCounts(): void { - const now = Date.now(); - const minute = 60 * 1000; - - for (const [userId, stats] of this.userMessageCounts.entries()) { - if (now - stats.lastReset >= minute * 5) { // Keep for 5 minutes - this.userMessageCounts.delete(userId); - } - } - } - - // Game-related WebSocket handlers (prepared for future implementation) - - /** - * Handle player joining a game room for real-time updates - * @param socket The authenticated socket - * @param data Game room data containing game code - */ - private async handleJoinGameRoom(socket: AuthenticatedSocket, data: JoinGameRoomData) { - try { - const userId = socket.userId!; - const gameRoom = `game_${data.gameCode}`; - - logAuth('Player joining game room', userId, { - gameCode: data.gameCode, - gameRoom, - socketId: socket.id - }); - - // Join the WebSocket room for this game - await socket.join(gameRoom); - - // Emit confirmation to the player - socket.emit('game:joined', { - gameCode: data.gameCode, - room: gameRoom, - message: 'Successfully joined game room' - }); - - // Notify other players in the game room - socket.to(gameRoom).emit('game:player_joined', { - playerId: userId, - gameCode: data.gameCode, - timestamp: new Date().toISOString() - }); - - logAuth('Player joined game room successfully', userId, { - gameCode: data.gameCode, - gameRoom - }); - - } catch (error) { - logError('Error joining game room', error as Error); - socket.emit('game:error', { - message: 'Failed to join game room', - gameCode: data.gameCode - }); - } - } - - /** - * Handle player leaving a game room - * @param socket The authenticated socket - * @param data Game room data containing game code - */ - private async handleLeaveGameRoom(socket: AuthenticatedSocket, data: LeaveGameRoomData) { - try { - const userId = socket.userId!; - const gameRoom = `game_${data.gameCode}`; - - logAuth('Player leaving game room', userId, { - gameCode: data.gameCode, - gameRoom, - socketId: socket.id - }); - - // Leave the WebSocket room - await socket.leave(gameRoom); - - // Notify other players in the game room - socket.to(gameRoom).emit('game:player_left', { - playerId: userId, - gameCode: data.gameCode, - timestamp: new Date().toISOString() - }); - - // Confirm to the leaving player - socket.emit('game:left', { - gameCode: data.gameCode, - message: 'Successfully left game room' - }); - - logAuth('Player left game room successfully', userId, { - gameCode: data.gameCode, - gameRoom - }); - - } catch (error) { - logError('Error leaving game room', error as Error); - socket.emit('game:error', { - message: 'Failed to leave game room', - gameCode: data.gameCode - }); - } - } - - /** - * Handle game actions (cards, turns, etc.) - prepared for future implementation - * @param socket The authenticated socket - * @param data Game action data - */ - private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData) { - try { - const userId = socket.userId!; - const gameRoom = `game_${data.gameCode}`; - - logAuth('Game action received', userId, { - gameId: data.gameId, - gameCode: data.gameCode, - action: data.action, - socketId: socket.id - }); - - // Validate that the player is authorized to perform this action - if (data.playerId !== userId) { - socket.emit('game:error', { - message: 'Unauthorized action', - gameCode: data.gameCode - }); - return; - } - - // TODO: Implement specific game logic here - // This will be implemented when the game flow is discussed - - // For now, just broadcast the action to other players - socket.to(gameRoom).emit('game:action_performed', { - playerId: userId, - gameCode: data.gameCode, - action: data.action, - data: data.data, - timestamp: new Date().toISOString() - }); - - // Confirm action to the acting player - socket.emit('game:action_confirmed', { - gameCode: data.gameCode, - action: data.action, - message: 'Action processed successfully' - }); - - logAuth('Game action processed', userId, { - gameId: data.gameId, - gameCode: data.gameCode, - action: data.action - }); - - } catch (error) { - logError('Error processing game action', error as Error); - socket.emit('game:error', { - message: 'Failed to process game action', - gameCode: data.gameCode, - action: data.action - }); - } - } - - /** - * Broadcast game state updates to all players in a game - * @param gameCode The game code - * @param gameState The updated game state - */ - public broadcastGameStateUpdate(gameCode: string, gameState: GameStateUpdateData): void { - try { - const gameRoom = `game_${gameCode}`; - - this.io.to(gameRoom).emit('game:state_updated', { - ...gameState, - timestamp: new Date().toISOString() - }); - - logRequest('Game state broadcasted', undefined, undefined, { - gameCode, - gameRoom, - playerCount: gameState.players.length - }); - - } catch (error) { - logError('Error broadcasting game state', error as Error); - } - } - - /** - * Notify players when a game starts - * @param gameCode The game code - * @param players Array of player IDs - */ - public notifyGameStart(gameCode: string, players: string[]): void { - try { - const gameRoom = `game_${gameCode}`; - - this.io.to(gameRoom).emit('game:started', { - gameCode, - players, - message: 'Game has started!', - timestamp: new Date().toISOString() - }); - - logRequest('Game start notification sent', undefined, undefined, { - gameCode, - playerCount: players.length - }); - - } catch (error) { - logError('Error notifying game start', error as Error); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommand.ts deleted file mode 100644 index 0cad88e6..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ActivateUserCommand { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommandHandler.ts deleted file mode 100644 index b4a22ab0..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommandHandler.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { ActivateUserCommand } from './ActivateUserCommand'; - - -export class ActivateUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: ActivateUserCommand): Promise { - await this.userRepo.activate(cmd.id); - return true; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/CreateUserCommand.ts deleted file mode 100644 index 25b11ae5..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface CreateUserCommand { - username: string; - password: string; - email: string; - fname: string; - lname: string; - code?: string; - orgid?: string; - phone?: string; - language: 'hu' | 'de' | 'en'; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts deleted file mode 100644 index 6c9a2cf5..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { CreateUserCommand } from './CreateUserCommand'; -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserAggregate, UserState } from '../../../Domain/User/UserAggregate'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { PasswordService } from '../../Services/PasswordService'; -import { EmailService } from '../../Services/EmailService'; -import { TokenService } from '../../Services/TokenService'; -import { logDatabase, logError, logAuth, logWarning } from '../../Services/Logger'; - -export class CreateUserCommandHandler { - constructor( - private readonly userRepo: IUserRepository, - private readonly emailService: EmailService - ) {} - - async execute(cmd: CreateUserCommand): Promise { - try { - // Validate password strength - const passwordValidation = PasswordService.validatePasswordStrength(cmd.password); - if (!passwordValidation.isValid) { - throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`); - } - - const user = new UserAggregate(); - user.username = cmd.username; - - // Hash the password before storing - user.password = await PasswordService.hashPassword(cmd.password); - - // Generate verification token - const verificationTokenData = TokenService.generateVerificationToken(); - user.token = await TokenService.hashToken(verificationTokenData.token); - user.TokenExpires = verificationTokenData.expiresAt; - - user.email = cmd.email; - user.fname = cmd.fname; - user.lname = cmd.lname; - user.orgid = cmd.orgid || null; - user.phone = cmd.phone || null; - user.state = UserState.REGISTERED_NOT_VERIFIED; - - const created = await this.userRepo.create(user); - - // Send verification email (non-blocking) - this.sendVerificationEmailAsync(cmd.language, created, verificationTokenData.token); - - return UserMapper.toShortDto(created); - } catch (error) { - // Only log the error once here, don't log again in router - const errorMessage = (error as Error).message; - - // Re-throw validation errors as-is (don't log as these are user input errors) - if (errorMessage.includes('Password validation failed')) { - throw error; - } - - // Handle database constraint errors - if (errorMessage.includes('duplicate') || errorMessage.includes('unique') || - errorMessage.includes('UNIQUE constraint') || errorMessage.includes('already exists')) { - throw new Error('User with this username or email already exists'); - } - - // Log database/system errors but throw user-friendly message - logError('CreateUserCommandHandler error', error as Error); - throw new Error('Failed to create user'); - } - } - - private async sendVerificationEmailAsync(language: 'hu' | 'de' | 'en', user: UserAggregate, token: string): Promise { - try { - const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173'; - const verificationUrl = TokenService.generateVerificationUrl(baseUrl, token); - - const emailSent = await this.emailService.sendVerificationEmail( - user.email, - `${user.fname} ${user.lname}`, - token, - verificationUrl, - language - ); - - if (!emailSent) { - logWarning('Failed to send verification email', { email: user.email, userId: user.id }); - } else { - logAuth('Verification email sent successfully', user.id, { email: user.email }); - } - } catch (emailError) { - logError('Error sending verification email', emailError as Error); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommand.ts deleted file mode 100644 index 3d24d35c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DeactivateUserCommand { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommandHandler.ts deleted file mode 100644 index 08880f0c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommandHandler.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { DeactivateUserCommand } from './DeactivateUserCommand'; - - -export class DeactivateUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: DeactivateUserCommand): Promise { - await this.userRepo.deactivate(cmd.id); - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommand.ts deleted file mode 100644 index 22de9f40..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteUserCommand { - id: string; - soft?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommandHandler.ts deleted file mode 100644 index eb1af0ae..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommandHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { DeleteUserCommand } from './DeleteUserCommand'; - - -export class DeleteUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: DeleteUserCommand): Promise { - if (cmd.soft) { - await this.userRepo.softDelete(cmd.id); - } else { - await this.userRepo.delete(cmd.id); - } - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/LoginCommand.ts b/SerpentRace_Backend/src/Application/User/commands/LoginCommand.ts deleted file mode 100644 index b4861329..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/LoginCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface LoginCommand { - username: string; - password: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts deleted file mode 100644 index a7df6f72..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { LoginCommand } from './LoginCommand'; -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { PasswordService } from '../../Services/PasswordService'; -import { JWTService } from '../../Services/JWTService'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { logAuth, logDatabase, logError, logWarning } from '../../Services/Logger'; -import { Response } from 'express'; - -export interface LoginResponse { - user: ShortUserDto; - token?: string; - refreshToken?: string; - requiresOrgReauth?: boolean; - orgLoginUrl?: string; - organizationName?: string; -} - -export class LoginCommandHandler { - constructor( - private readonly userRepo: IUserRepository, - private readonly jwtService: JWTService, - private readonly orgRepo: IOrganizationRepository - ) {} - - async execute(cmd: LoginCommand, res?: Response): Promise { - const startTime = Date.now(); - - try { - logAuth('Login attempt', undefined, { username: cmd.username }); - - const user = await this.userRepo.findByUsername(cmd.username) || - await this.userRepo.findByEmail(cmd.username); - - logDatabase('User lookup completed', undefined, Date.now() - startTime, { - found: !!user, - searchBy: cmd.username.includes('@') ? 'email' : 'username' - }); - - if (!user) { - logAuth('Login failed - User not found', undefined, { username: cmd.username }); - throw new Error('Invalid username'); - } - - // Check if user account state allows login - const restrictedStates = [ - UserState.REGISTERED_NOT_VERIFIED, - UserState.SOFT_DELETE, - UserState.DEACTIVATED - ]; - - if (restrictedStates.includes(user.state)) { - let stateDescription = ''; - let errorMessage = ''; - switch (user.state) { - case UserState.REGISTERED_NOT_VERIFIED: - stateDescription = 'Email not verified'; - errorMessage = 'User account not verified'; - break; - case UserState.SOFT_DELETE: - stateDescription = 'Account deleted'; - errorMessage = 'User account deactivated'; - break; - case UserState.DEACTIVATED: - stateDescription = 'Account deactivated'; - errorMessage = 'User account deactivated'; - break; - } - - logAuth('Login failed - Account state restriction', user.id, { - username: cmd.username, - userState: user.state, - stateDescription - }); - throw new Error(errorMessage); - } - - try { - const passwordStartTime = Date.now(); - const isPasswordValid = await PasswordService.verifyPassword(cmd.password, user.password); - - logAuth('Password verification completed', user.id, { - valid: isPasswordValid, - verificationTime: Date.now() - passwordStartTime - }); - - if (!isPasswordValid) { - logWarning('Login failed - Invalid password', { - userId: user.id, - username: cmd.username - }); - throw new Error('Invalid password'); - } - } catch (error) { - logError('Password verification error', error as Error); - throw new Error('Invalid password'); - } - - const mockRes = { - cookie: () => {} - } as any; - - const tokenPayload = { - userId: user.id, - authLevel: (user.state === UserState.ADMIN ? 1 : 0) as 0 | 1, - userStatus: user.state, - orgId: user.orgid || '' - }; - - try { - // Use the real response object if provided, otherwise use mock - const responseObj = res || mockRes; - - // Check if client prefers Bearer token authentication - const isWebClient = res?.req?.headers['origin'] || res?.req?.headers['referer']; - const explicitBearerRequest = res?.req?.headers['x-auth-method'] === 'bearer'; - - const prefersBearerAuth = res && !isWebClient && ( - res.req?.headers['authorization'] !== undefined || - explicitBearerRequest - ); - - let tokenPair: any; - - if (prefersBearerAuth && res) { - // Create token pair for Bearer authentication (no cookies) - tokenPair = this.jwtService.createTokenPair(tokenPayload); - } else { - // Cookie-based authentication (sets cookies automatically) - tokenPair = this.jwtService.create(tokenPayload, responseObj); - } - - // Check if user belongs to an organization and needs reauthentication - let requiresOrgReauth = false; - let orgLoginUrl: string | undefined; - let organizationName: string | undefined; - - if (user.orgid) { - const organization = await this.orgRepo.findById(user.orgid); - if (organization) { - organizationName = organization.name; - - // Check if user has logged in to organization within the last month - const oneMonthAgo = new Date(); - oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); - - const needsReauth = !user.Orglogindate || user.Orglogindate < oneMonthAgo; - - if (needsReauth && organization.url) { - requiresOrgReauth = true; - orgLoginUrl = organization.url; - - logAuth('User requires organization reauthentication', user.id, { - organizationId: user.orgid, - organizationName: organization.name, - lastOrgLogin: user.Orglogindate?.toISOString() || 'never', - orgLoginUrl: organization.url - }); - } - } - } - - logAuth('Login successful', user.id, { - authLevel: tokenPayload.authLevel, - userStatus: tokenPayload.userStatus, - orgId: tokenPayload.orgId, - requiresOrgReauth, - organizationName, - totalLoginTime: Date.now() - startTime - }); - let response: LoginResponse; - if (prefersBearerAuth){ - response = { - user: UserMapper.toShortDto(user), - token: tokenPair.accessToken, - refreshToken: tokenPair.refreshToken - }; - } - else { - response = { - user: UserMapper.toShortDto(user) - }; - } - - if (requiresOrgReauth) { - response.requiresOrgReauth = true; - response.orgLoginUrl = orgLoginUrl; - response.organizationName = organizationName; - } - - return response; - } catch (error) { - logError('Token creation failed during login', error as Error); - throw new Error('Login failed due to internal error'); - } - } catch (error) { - if (error instanceof Error) { - logError('Login handler error', error); - - // Handle database connection errors - if (error.message.includes('database connection')) { - logDatabase('Database connection error during login', undefined, Date.now() - startTime); - throw new Error('Database connection error'); - } - - // Re-throw authentication/validation errors as-is - if (error.message.includes('Invalid username') || - error.message.includes('Invalid password') || - error.message.includes('not verified') || - error.message.includes('deactivated') || - error.message === 'Login failed due to internal error' || - error.message === 'Database connection error') { - throw error; - } - } - // Default database error handling - logDatabase('Unexpected database error during login', undefined, Date.now() - startTime); - throw new Error('Database connection error'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/LogoutCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/LogoutCommandHandler.ts deleted file mode 100644 index ccbda81c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/LogoutCommandHandler.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Request, Response } from 'express'; -import { logAuth, logError, logWarning } from '../../Services/Logger'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { JWTService } from '../../Services/JWTService'; -import { RedisService } from '../../Services/RedisService'; - -export class LogoutCommandHandler { - private jwtService: JWTService; - private redisService: RedisService; - - constructor(private readonly userRepo: IUserRepository) { - this.jwtService = new JWTService(); - this.redisService = RedisService.getInstance(); - } - - async execute(userId: string, res: Response, req?: Request): Promise { - try { - logAuth('Logout process started', userId); - - // 1. Get tokens from request to blacklist them - let accessTokenToBlacklist: string | null = null; - let refreshTokenToBlacklist: string | null = null; - - if (req) { - // Extract access token from cookie or Authorization header - accessTokenToBlacklist = req.cookies['auth_token']; - if (!accessTokenToBlacklist && req.headers.authorization) { - const authHeader = req.headers.authorization; - if (authHeader.startsWith('Bearer ')) { - accessTokenToBlacklist = authHeader.substring(7); - } - } - - // Extract refresh token from cookie or header - refreshTokenToBlacklist = req.cookies['refresh_token']; - if (!refreshTokenToBlacklist) { - refreshTokenToBlacklist = req.headers['x-refresh-token'] as string; - } - } - - // 2. Blacklist both access and refresh tokens in Redis - if (accessTokenToBlacklist && req) { - try { - const decoded = this.jwtService.verify(req); - if (decoded && decoded.exp) { - const ttl = decoded.exp - Math.floor(Date.now() / 1000); - if (ttl > 0) { - await this.redisService.setWithExpiry(`blacklist:${accessTokenToBlacklist}`, 'true', ttl); - logAuth('Access token blacklisted', userId, { tokenExpiry: ttl }); - } - } - } catch (error) { - logWarning('Failed to blacklist access token', { userId, error: (error as Error).message }); - } - } - - // Blacklist refresh token if present - if (refreshTokenToBlacklist) { - try { - const refreshDecoded = this.jwtService.verifyRefreshToken(refreshTokenToBlacklist); - if (refreshDecoded && refreshDecoded.exp) { - const ttl = refreshDecoded.exp - Math.floor(Date.now() / 1000); - if (ttl > 0) { - await this.redisService.setWithExpiry(`blacklist:${refreshTokenToBlacklist}`, 'true', ttl); - logAuth('Refresh token blacklisted', userId, { tokenExpiry: ttl }); - } - } - } catch (error) { - logWarning('Failed to blacklist refresh token', { userId, error: (error as Error).message }); - } - } - - // 3. Use JWT service to clear cookies and set logout headers - if (req) { - this.jwtService.logout(req, res); - } - - // 4. Remove user from active sessions in Redis - try { - await this.redisService.removeActiveUser(userId); - logAuth('User removed from active sessions', userId); - } catch (error) { - logWarning('Failed to remove user from active sessions', { userId, error: (error as Error).message }); - // Continue even if this fails - } - - // 5. Update user's last logout timestamp in database - try { - const updateResult = await this.userRepo.update(userId, { updateDate: new Date() }); - if (updateResult) { - logAuth('User last logout timestamp updated', userId); - } - } catch (error) { - logWarning('Failed to update user logout timestamp', { userId, error: (error as Error).message }); - // Continue even if this fails - } - - // 6. Clear any user-specific cache entries - try { - // Clear user session data - await this.redisService.del(`user:${userId}:session`); - await this.redisService.del(`user:${userId}:active_chats`); - logAuth('User cache cleared', userId); - } catch (error) { - logWarning('Failed to clear user cache', { userId, error: (error as Error).message }); - // Continue even if this fails - } - - logAuth('User logout completed successfully', userId); - return true; - - } catch (error) { - logError('LogoutCommandHandler error', error as Error); - return false; - } - } - - /** - * Check if a token is blacklisted - */ - async isTokenBlacklisted(token: string): Promise { - try { - const result = await this.redisService.get(`blacklist:${token}`); - return result === 'true'; - } catch (error) { - logError('Error checking token blacklist', error as Error); - return false; - } - } - - /** - * Logout user from all devices by blacklisting all their active tokens - * This is a simplified version - in a real implementation you'd track active tokens per user - */ - async logoutFromAllDevices(userId: string): Promise { - try { - // Clear all user-related Redis keys - const userKeys = [ - `user:${userId}:session`, - `user:${userId}:active_chats`, - `user:${userId}:active_tokens`, - `user:${userId}:websocket_connections` - ]; - - for (const key of userKeys) { - try { - await this.redisService.del(key); - } catch (error) { - logWarning(`Failed to delete Redis key: ${key}`, { userId, error: (error as Error).message }); - } - } - - // Update user logout timestamp - await this.userRepo.update(userId, { updateDate: new Date() }); - - logAuth('User logged out from all devices', userId); - return true; - } catch (error) { - logError('Error logging out user from all devices', error as Error); - return false; - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommand.ts b/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommand.ts deleted file mode 100644 index b4c7b99c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RequestPasswordResetCommand { - language: 'hu' | 'de' | 'en'; - email: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommandHandler.ts deleted file mode 100644 index faba0fcd..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommandHandler.ts +++ /dev/null @@ -1,70 +0,0 @@ - -import { RequestPasswordResetCommand } from './RequestPasswordResetCommand'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { EmailService } from '../../Services/EmailService'; -import { TokenService } from '../../Services/TokenService'; -import { logAuth, logWarning, logError } from '../../Services/Logger'; - -export class RequestPasswordResetCommandHandler { - constructor( - private userRepo: IUserRepository, - private emailService: EmailService - ) {} - - async execute(cmd: RequestPasswordResetCommand): Promise { - try { - if (!cmd.email) { - throw new Error('Email is required'); - } - - // Find user by email - const user = await this.userRepo.findByEmail(cmd.email); - - if (!user) { - // Don't reveal if user exists or not for security reasons - // Still return true but don't send email - logAuth(`Password reset requested for non-existent email: ${cmd.email}`); - return true; - } - - // Generate password reset token - const resetTokenData = TokenService.generatePasswordResetToken(); - - // Update user with reset token - user.token = await TokenService.hashToken(resetTokenData.token); - user.TokenExpires = resetTokenData.expiresAt; - - await this.userRepo.update(user.id, user); - - // Send password reset email - try { - const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173'; - const resetUrl = TokenService.generatePasswordResetUrl(baseUrl, resetTokenData.token); - - - const emailSent = await this.emailService.sendPasswordResetEmail( - user.email, - `${user.fname} ${user.lname}`, - resetTokenData.token, - resetUrl, - cmd.language - ); - - if (!emailSent) { - logWarning(`Failed to send password reset email to ${user.email}`); - // Don't throw error - request should still succeed even if email fails - } else { - logAuth(`Password reset email sent successfully to ${user.email}`); - } - } catch (emailError) { - logError('Error sending password reset email', emailError instanceof Error ? emailError : new Error(String(emailError))); - // Don't throw error - request should still succeed even if email fails - } - - return true; - } catch (error) { - logError('Password reset request error', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommand.ts b/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommand.ts deleted file mode 100644 index 31736ee1..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ResetPasswordCommand { - token: string; - newPassword: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommandHandler.ts deleted file mode 100644 index dad3b20e..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommandHandler.ts +++ /dev/null @@ -1,58 +0,0 @@ - -import { ResetPasswordCommand } from './ResetPasswordCommand'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { TokenService } from '../../Services/TokenService'; -import { PasswordService } from '../../Services/PasswordService'; -import { logError } from '../../Services/Logger'; - -export class ResetPasswordCommandHandler { - constructor(private userRepo: IUserRepository) {} - - async execute(cmd: ResetPasswordCommand): Promise { - try { - if (!cmd.token) { - throw new Error('Reset token is required'); - } - - if (!cmd.newPassword) { - throw new Error('New password is required'); - } - - // Validate password strength - const validation = PasswordService.validatePasswordStrength(cmd.newPassword); - if (!validation.isValid) { - throw new Error(`Password validation failed: ${validation.errors.join(', ')}`); - } - - // Hash the token to compare with stored value - const hashedToken = await TokenService.hashToken(cmd.token); - - // Find user with this password reset token - const user = await this.userRepo.findByToken(hashedToken); - - if (!user) { - throw new Error('Invalid or expired reset token'); - } - - // Check if token is expired - if (user.TokenExpires && user.TokenExpires < new Date()) { - throw new Error('Reset token has expired'); - } - - // Hash the new password - const hashedPassword = await PasswordService.hashPassword(cmd.newPassword); - - // Update user password and clear reset token - user.password = hashedPassword; - user.token = null; - user.TokenExpires = null; - - await this.userRepo.update(user.id, user); - - return true; - } catch (error) { - logError('Password reset error', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommand.ts deleted file mode 100644 index 76c5f2ca..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommand.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface UpdateUserCommand { - id: string; - orgid?: string; - username?: string; - password?: string; - email?: string; - fname?: string; - lname?: string; - code?: string; - phone?: string; - state?: number; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommandHandler.ts deleted file mode 100644 index 9f348592..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommandHandler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { UpdateUserCommand } from './UpdateUserCommand'; - -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { PasswordService } from '../../Services/PasswordService'; - -export class UpdateUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: UpdateUserCommand): Promise { - const updateData = { ...cmd }; - - // Hash the password if it's being updated - if (cmd.password) { - // Validate password strength - const passwordValidation = PasswordService.validatePasswordStrength(cmd.password); - if (!passwordValidation.isValid) { - throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`); - } - - updateData.password = await PasswordService.hashPassword(cmd.password); - } - - const updated = await this.userRepo.update(cmd.id, updateData); - if (!updated) return null; - return UserMapper.toShortDto(updated); - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommand.ts b/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommand.ts deleted file mode 100644 index dd3e7e1f..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface VerifyEmailCommand { - token: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommandHandler.ts deleted file mode 100644 index 44c3f95a..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommandHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - -import { VerifyEmailCommand } from './VerifyEmailCommand'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { TokenService } from '../../Services/TokenService'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { logError } from '../../Services/Logger'; - -export class VerifyEmailCommandHandler { - constructor(private userRepo: IUserRepository) {} - - async execute(cmd: VerifyEmailCommand): Promise { - try { - if (!cmd.token) { - throw new Error('Verification token is required'); - } - - // Hash the token to compare with stored value - const hashedToken = await TokenService.hashToken(cmd.token); - - // Find user with this verification token - const user = await this.userRepo.findByToken(hashedToken); - - if (!user) { - throw new Error('Invalid or expired verification token'); - } - - // Check if token is expired - if (user.TokenExpires && user.TokenExpires < new Date()) { - throw new Error('Verification token has expired'); - } - - // Update user verification status - user.token = null; - user.TokenExpires = null; - user.state = UserState.VERIFIED_REGULAR; - - await this.userRepo.update(user.id, user); - - return true; - } catch (error) { - logError('Email verification error', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQuery.ts b/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQuery.ts deleted file mode 100644 index 21b92e4c..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetUserByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQueryHandler.ts deleted file mode 100644 index ae8ff109..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQueryHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { GetUserByIdQuery } from './GetUserByIdQuery'; -import { DetailUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { logError } from '../../Services/Logger'; - -export class GetUserByIdQueryHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(query: GetUserByIdQuery): Promise { - try { - const user = await this.userRepo.findById(query.id); - if (!user) return null; - return UserMapper.toDetailDto(user); - } catch (error) { - logError('GetUserByIdQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Handle invalid ID format - if (error instanceof Error && error.message.includes('invalid') && error.message.includes('uuid')) { - return null; // Treat invalid UUID as not found - } - - // Handle database errors - if (error instanceof Error && error.message.includes('database')) { - throw new Error('Database connection error'); - } - - // Generic error for other cases - throw new Error('Failed to retrieve user'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQuery.ts b/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQuery.ts deleted file mode 100644 index b1fc4346..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetUsersByPageQuery { - from: number; - to: number; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQueryHandler.ts deleted file mode 100644 index 0c8c33ae..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQueryHandler.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { GetUsersByPageQuery } from './GetUsersByPageQuery'; -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { logError, logRequest } from '../../Services/Logger'; - -export class GetUsersByPageQueryHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(query: GetUsersByPageQuery): Promise<{ users: ShortUserDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - logRequest('Get users by page query started', undefined, undefined, { - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - const result = query.includeDeleted - ? await this.userRepo.findByPageIncludingDeleted(query.from, query.to) - : await this.userRepo.findByPage(query.from, query.to); - - logRequest('Get users by page query completed', undefined, undefined, { - from: query.from, - to: query.to, - returned: result.users.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - users: UserMapper.toShortDtoList(result.users), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetUsersByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Handle database errors - if (error instanceof Error && error.message.includes('database')) { - throw new Error('Database connection error'); - } - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve users'); - } - } -} diff --git a/SerpentRace_Backend/src/Domain/Chat/ChatAggregate.ts b/SerpentRace_Backend/src/Domain/Chat/ChatAggregate.ts deleted file mode 100644 index 664a743a..00000000 --- a/SerpentRace_Backend/src/Domain/Chat/ChatAggregate.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, UpdateDateColumn, CreateDateColumn } from 'typeorm'; - -export interface Message { - id: string; // UUID for each message - date: Date; - userid: string; // UUID reference to UserAggregate - text: string; -} - -export const ChatState = { - ACTIVE: 0, - ARCHIVE: 1, - SOFT_DELETE: 2 -} as const; - -export type ChatStateType = typeof ChatState[keyof typeof ChatState]; - -export const ChatType = { - DIRECT: 'direct', - GROUP: 'group', - GAME: 'game' -} as const; - -export type ChatTypeType = typeof ChatType[keyof typeof ChatType]; - -@Entity('Chats') -export class ChatAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 50, default: ChatType.DIRECT }) - type!: ChatTypeType; - - @Column({ type: 'varchar', length: 255, nullable: true }) - name!: string | null; // Group name or Game name - - @Column({ type: 'uuid', nullable: true }) - gameId!: string | null; // Game UUID reference for game chats - - @Column({ type: 'uuid', nullable: true }) - createdBy!: string | null; // User who created the group/chat - - @Column('uuid', { array: true }) - users!: string[]; // Active participants - - @Column('json', { default: [] }) - messages!: Message[]; // Active messages (last 10 per user, max 2 weeks) - - @Column({ type: 'timestamp', nullable: true }) - lastActivity!: Date | null; - - @CreateDateColumn() - createDate!: Date; - - @UpdateDateColumn() - updateDate!: Date; - - @Column({ type: 'int', default: ChatState.ACTIVE }) - state!: ChatStateType; - - // Archive when inactive for specified period - @Column({ type: 'timestamp', nullable: true }) - archiveDate!: Date | null; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/Chat/ChatArchiveAggregate.ts b/SerpentRace_Backend/src/Domain/Chat/ChatArchiveAggregate.ts deleted file mode 100644 index 518042a1..00000000 --- a/SerpentRace_Backend/src/Domain/Chat/ChatArchiveAggregate.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; -import { Message } from './ChatAggregate'; - -@Entity('ChatArchives') -export class ChatArchiveAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid' }) - chatId!: string; // Reference to original chat - - @Column('json') - archivedMessages!: Message[]; // All archived messages - - @Column({ type: 'timestamp' }) - archivedAt!: Date; - - @CreateDateColumn() - createDate!: Date; - - // Metadata for context - @Column({ type: 'varchar', length: 50 }) - chatType!: string; // direct, group, game - - @Column({ type: 'varchar', length: 255, nullable: true }) - chatName!: string | null; - - @Column({ type: 'uuid', nullable: true }) - gameId!: string | null; - - @Column('uuid', { array: true }) - participants!: string[]; // Users who participated -} diff --git a/SerpentRace_Backend/src/Domain/Contact/ContactAggregate.ts b/SerpentRace_Backend/src/Domain/Contact/ContactAggregate.ts deleted file mode 100644 index a7e79581..00000000 --- a/SerpentRace_Backend/src/Domain/Contact/ContactAggregate.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -export enum ContactType { - BUG = 0, - PROBLEM = 1, - QUESTION = 2, - SALES = 3, - OTHER = 4 -} - -export enum ContactState { - ACTIVE = 0, - RESOLVED = 1, - SOFT_DELETE = 2 -} - -@Entity('Contacts') -export class ContactAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 255 }) - name!: string; - - @Column({ type: 'varchar', length: 255 }) - email!: string; - - @Column({ type: 'uuid', nullable: true }) - userid!: string | null; // If logged in user - - @Column({ type: 'int' }) - type!: ContactType; - - @Column({ type: 'text' }) - txt!: string; - - @Column({ type: 'int', default: ContactState.ACTIVE }) - state!: ContactState; - - @CreateDateColumn() - createDate!: Date; - - @UpdateDateColumn() - updateDate!: Date; - - // Admin response field for email response feature - @Column({ type: 'text', nullable: true }) - adminResponse!: string | null; - - @Column({ type: 'timestamp', nullable: true }) - responseDate!: Date | null; - - @Column({ type: 'uuid', nullable: true }) - respondedBy!: string | null; // Admin user id who responded -} diff --git a/SerpentRace_Backend/src/Domain/Deck/DeckAggregate.ts b/SerpentRace_Backend/src/Domain/Deck/DeckAggregate.ts deleted file mode 100644 index a9b7faec..00000000 --- a/SerpentRace_Backend/src/Domain/Deck/DeckAggregate.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; -import { UserAggregate } from '../User/UserAggregate'; -import { logError } from '../../Application/Services/Logger'; - -export enum Type { - LUCK = 0, - JOKER = 1, - QUESTION = 2 -} - -export enum CType { - PUBLIC = 0, - PRIVATE = 1, - ORGANIZATION = 2 -} - -export enum State { - ACTIVE = 0, - SOFT_DELETE = 1 -} - -export enum CardType { - QUIZ = 0, - SENTENCE_PAIRING = 1, - OWN_ANSWER = 2, - TRUE_FALSE = 3, - CLOSER = 4 -} - -export enum ConsequenceType { - MOVE_FORWARD = 0, - MOVE_BACKWARD = 1, - LOSE_TURN = 2, - EXTRA_TURN = 3, - GO_TO_START = 5 -} - -export interface Consequence { - type: ConsequenceType; - value?: number; -} - -export interface Card { - text: string; - type?: CardType; - answer?: string | null; - consequence?: Consequence | null; -} - -@Entity('Decks') -export class DeckAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 255 }) - name!: string; - - @Column({ type: 'int'}) - type!: Type; - - @Column({ type: 'uuid', name: 'user_id' }) - userid!: string; - - @CreateDateColumn({ name: 'creation_date' }) - creationdate!: Date; - - @Column({ type: 'json' }) - cards!: Card[]; - - @Column({ type: 'int', default: 0, name: 'played_number' }) - playedNumber!: number; - - @Column({ type: 'int', default: CType.PUBLIC }) - ctype!: CType; - - @UpdateDateColumn() - updateDate!: Date; - - @Column({ type: 'int', default: State.ACTIVE }) - state!: State; - - @ManyToOne(() => OrganizationAggregate, { nullable: true }) - @JoinColumn({ name: 'organization_id' }) - organization!: OrganizationAggregate | null; - - @ManyToOne(() => UserAggregate, { eager: false }) - @JoinColumn({ name: 'user_id' }) - user!: UserAggregate | null; - - isEditable(userId:string): boolean{ - // A deck is editable if the user is the creator - if (!this.user) { - logError(`DeckAggregate.isEditable: User is null for deck id ${this.id}`); - return false; - } - //if admin, always editable - if (this.user?.isAdmin) { - return true; - } - return this.user?.id.toString() === userId;; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/Game/GameAggregate.ts b/SerpentRace_Backend/src/Domain/Game/GameAggregate.ts deleted file mode 100644 index 46ddf85e..00000000 --- a/SerpentRace_Backend/src/Domain/Game/GameAggregate.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { Consequence, CardType } from '../Deck/DeckAggregate'; -import { UserAggregate } from '../User/UserAggregate'; -import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; - -export enum GameState { - WAITING = 0, - ACTIVE = 1, - FINISHED = 2, - CANCELLED = 3 -} - -export enum LoginType { - PUBLIC = 0, - PRIVATE = 1, - ORGANIZATION = 2 -} - -export enum DeckType { - JOCKER = 0, - LUCK = 1, - QUEST = 2 -} - -export interface GameCard { - cardid: string; - question?: string; - answer?: any; // Support complex answer structures (string, object, array) - type?: CardType; // Card type for validation logic - consequence?: Consequence | null; - played?: boolean; - playerid?: string; -} - -export interface GameDeck { - deckid: string; - decktype: DeckType; - cards: GameCard[]; -} - -@Entity('Games') -export class GameAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 10, unique: true }) - gamecode!: string; - - @Column({ type: 'int' }) - maxplayers!: number; - - @Column({ type: 'int', default: LoginType.PUBLIC }) - logintype!: LoginType; - - @Column({ type: 'int', default: 50 }) - boardsize!: number; - - @Column({ type: 'uuid', nullable: false, name: 'createdBy' }) - createdby!: string; - - @Column({ type: 'uuid', nullable: true, name: 'organizationid' }) - orgid!: string | null; - - @Column({ type: 'jsonb', default: () => "'[]'", name: 'decks' }) - gamedecks!: GameDeck[]; - - @Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' }) - players!: string[]; - - @Column({ type: 'uuid', nullable: true, name: 'winnerId' }) - winnerId!: string | null; - - @Column({ type: 'int', default: GameState.WAITING }) - state!: GameState; - - @CreateDateColumn({ name: 'createDate' }) - createdate!: Date; - - @Column({ type: 'timestamp', nullable: true, name: 'start_date' }) - startdate!: Date | null; - - @Column({ type: 'timestamp', nullable: true, name: 'finishDate' }) - enddate!: Date | null; - - @UpdateDateColumn({ name: 'updateDate' }) - updateDate!: Date; - - @ManyToOne(() => UserAggregate, { eager: false }) - @JoinColumn({ name: 'createdBy' }) - user!: UserAggregate | null; - - @ManyToOne(() => UserAggregate, { eager: false }) - @JoinColumn({ name: 'winnerId' }) - winner!: UserAggregate | null; - - @ManyToOne(() => OrganizationAggregate, { eager: false }) - @JoinColumn({ name: 'organizationId' }) - organization!: OrganizationAggregate | null; -} - -// Board Generation Types -export interface GameField { - position: number; - type: 'regular' | 'positive' | 'negative' | 'luck'; - stepValue?: number; -} - -export interface BoardData { - gameId?: string; - fields: GameField[]; - generationComplete?: boolean; - generatedAt?: Date; - error?: string; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/Game/GameSnapshotAggregate.ts b/SerpentRace_Backend/src/Domain/Game/GameSnapshotAggregate.ts deleted file mode 100644 index 0f83f29b..00000000 --- a/SerpentRace_Backend/src/Domain/Game/GameSnapshotAggregate.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; -import { GameAggregate } from './GameAggregate'; - -export interface PlayerSnapshot { - playerId: string; - playerName: string; - boardPosition: number; - extraTurns: number; - turnsToLose: number; - isOnline: boolean; -} - -export interface GameStateSnapshot { - currentPlayer: string; - currentPlayerName: string; - turnNumber: number; - turnOrder: string[]; - playerPositions: PlayerSnapshot[]; - boardFields?: any[]; - deckStates?: any; - pendingActions?: any; -} - -export enum SnapshotTrigger { - TURN_INTERVAL = 'turn_interval', // Every N turns - PLAYER_DISCONNECT = 'player_disconnect', // When player disconnects - CRITICAL_EVENT = 'critical_event', // Important game events - MANUAL = 'manual', // Manual checkpoint - SERVER_SHUTDOWN = 'server_shutdown' // Before server shutdown -} - -@Entity('GameSnapshots') -@Index(['gameid', 'createdat']) -@Index(['gameid', 'trigger']) -export class GameSnapshotAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid', name: 'gameid' }) - gameid!: string; - - @Column({ type: 'int', name: 'turn_number' }) - turnNumber!: number; - - @Column({ - type: 'enum', - enum: SnapshotTrigger, - name: 'trigger' - }) - trigger!: SnapshotTrigger; - - @Column({ type: 'jsonb', name: 'game_state' }) - gameState!: GameStateSnapshot; - - @Column({ type: 'jsonb', name: 'redis_state', nullable: true }) - redisState!: any | null; - - @Column({ type: 'text', name: 'notes', nullable: true }) - notes!: string | null; - - @CreateDateColumn({ name: 'createdat' }) - createdat!: Date; - - @ManyToOne(() => GameAggregate, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'gameid' }) - game?: GameAggregate; -} diff --git a/SerpentRace_Backend/src/Domain/Game/TurnHistoryAggregate.ts b/SerpentRace_Backend/src/Domain/Game/TurnHistoryAggregate.ts deleted file mode 100644 index d9c62804..00000000 --- a/SerpentRace_Backend/src/Domain/Game/TurnHistoryAggregate.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; -import { GameAggregate } from './GameAggregate'; - -export enum TurnActionType { - DICE_ROLL = 'dice_roll', - CARD_DRAWN = 'card_drawn', - ANSWER_SUBMITTED = 'answer_submitted', - POSITION_GUESS = 'position_guess', - GAMEMASTER_DECISION = 'gamemaster_decision', - LUCK_CONSEQUENCE = 'luck_consequence', - EXTRA_TURN = 'extra_turn', - TURN_LOST = 'turn_lost', - PLAYER_DISCONNECTED = 'player_disconnected', - TIMEOUT = 'timeout' -} - -export interface TurnActionData { - diceValue?: number; - cardId?: string; - cardType?: string; - question?: string; - answer?: any; - isCorrect?: boolean; - guessedPosition?: number; - actualPosition?: number; - consequenceType?: string; - consequenceValue?: number; - decision?: string; - reason?: string; - [key: string]: any; // Allow additional properties -} - -@Entity('TurnHistory') -@Index(['gameid', 'turnNumber']) -@Index(['gameid', 'playerid']) -@Index(['createdat']) -export class TurnHistoryAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid', name: 'gameid' }) - gameid!: string; - - @Column({ type: 'uuid', name: 'playerid' }) - playerid!: string; - - @Column({ type: 'varchar', length: 255, name: 'playername' }) - playername!: string; - - @Column({ type: 'int', name: 'turn_number' }) - turnNumber!: number; - - @Column({ - type: 'enum', - enum: TurnActionType, - name: 'action_type' - }) - actionType!: TurnActionType; - - @Column({ type: 'jsonb', name: 'action_data', nullable: true }) - actionData!: TurnActionData | null; - - @Column({ type: 'int', name: 'position_before' }) - positionBefore!: number; - - @Column({ type: 'int', name: 'position_after' }) - positionAfter!: number; - - @CreateDateColumn({ name: 'createdat' }) - createdat!: Date; - - @ManyToOne(() => GameAggregate, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'gameid' }) - game?: GameAggregate; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IBaseRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IBaseRepository.ts deleted file mode 100644 index 4a06282d..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IBaseRepository.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Base Repository Interface - * Contains common repository methods that all repositories should implement - * Reduces code duplication across repository interfaces - */ -export interface IBaseRepository { - // Core CRUD operations - create(entity: Partial): Promise; - findById(id: string): Promise; - findByIdIncludingDeleted(id: string): Promise; - update(id: string, update: Partial): Promise; - delete(id: string): Promise; - softDelete(id: string): Promise; -} - -/** - * Paginated Repository Interface - * For repositories that support pagination and search operations - * This allows typed responses for each repository type - */ -export interface IPaginatedRepository extends IBaseRepository { - // Pagination operations - findByPage(from: number, to: number): Promise; - findByPageIncludingDeleted(from: number, to: number): Promise; - - // Search operations - search(query: string, limit?: number, offset?: number): Promise; - searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/IRepository/IChatArchiveRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IChatArchiveRepository.ts deleted file mode 100644 index 494197d4..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IChatArchiveRepository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ChatArchiveAggregate } from '../Chat/ChatArchiveAggregate'; - -export interface IChatArchiveRepository { - create(archive: Partial): Promise; - findAll(): Promise; - findById(id: string): Promise; - findByChatId(chatId: string): Promise; - findByGameId(gameId: string): Promise; - delete(id: string): Promise; - cleanup(olderThanDays: number): Promise; // Clean up old archives -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IChatRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IChatRepository.ts deleted file mode 100644 index 95f3c6f9..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IChatRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChatAggregate } from '../Chat/ChatAggregate'; -import { ChatArchiveAggregate } from '../Chat/ChatArchiveAggregate'; -import { IBaseRepository } from './IBaseRepository'; - -export interface IChatRepository extends IBaseRepository { - // Pagination operations with proper typing - findByPage(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }>; - findByPageIncludingDeleted(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }>; - - // Chat-specific methods - findByUserId(userId: string): Promise; - findByUserIdIncludingDeleted(userId: string): Promise; - findByGameId(gameId: string): Promise; - findActiveChatsForUser(userId: string): Promise; - findInactiveChats(inactivityMinutes: number): Promise; - archiveChat(chat: ChatAggregate): Promise; - getArchivedChat(chatId: string): Promise; - restoreFromArchive(chatId: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IContactRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IContactRepository.ts deleted file mode 100644 index a055d2f6..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IContactRepository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ContactAggregate } from '../Contact/ContactAggregate'; -import { IBaseRepository } from './IBaseRepository'; - -export interface IContactRepository extends IBaseRepository { - // Pagination operations with proper typing - findByPage(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }>; - findByPageIncludingDeleted(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }>; - - // Contact-specific search methods (different signature than base) - search(searchTerm: string): Promise; - searchIncludingDeleted(searchTerm: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IDeckRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IDeckRepository.ts deleted file mode 100644 index a858f5fe..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IDeckRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DeckAggregate } from '../Deck/DeckAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IDeckRepository extends IPaginatedRepository { - // Deck-specific methods for restrictions and filtering - countActiveByUserId(userId: string): Promise; - countOrganizationalByUserId(userId: string): Promise; - findFilteredDecks(userId: string, userOrgId?: string | null, isAdmin?: boolean, from?: number, to?: number): Promise<{ decks: DeckAggregate[], totalCount: number }>; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IGameRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IGameRepository.ts deleted file mode 100644 index 73d9b958..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IGameRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { GameAggregate, GameState } from '../Game/GameAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IGameRepository extends IPaginatedRepository { - // Game-specific methods - findByGameCode(gamecode: string): Promise; - findActiveGames(): Promise; - findGamesByPlayer(playerId: string): Promise; - findWaitingGames(): Promise; - findFinishedGames(from?: number, to?: number): Promise<{ games: GameAggregate[], totalCount: number }>; - addPlayerToGame(gameId: string, playerId: string): Promise; - removePlayerFromGame(gameId: string, playerId: string): Promise; - updateGameState(gameId: string, state: GameState, winner?: string): Promise; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/IRepository/IGameSnapshotRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IGameSnapshotRepository.ts deleted file mode 100644 index 0a97c08a..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IGameSnapshotRepository.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { GameSnapshotAggregate, SnapshotTrigger } from '../Game/GameSnapshotAggregate'; - -export interface IGameSnapshotRepository { - /** - * Save a game state snapshot - */ - save(snapshot: GameSnapshotAggregate): Promise; - - /** - * Get the most recent snapshot for a game - */ - findLatestByGameId(gameId: string): Promise; - - /** - * Get all snapshots for a game - */ - findByGameId(gameId: string): Promise; - - /** - * Get snapshots by trigger type - */ - findByGameAndTrigger(gameId: string, trigger: SnapshotTrigger): Promise; - - /** - * Get snapshot at specific turn - */ - findByGameAndTurn(gameId: string, turnNumber: number): Promise; - - /** - * Delete old snapshots (keep only last N) - */ - deleteOldSnapshots(gameId: string, keepCount: number): Promise; - - /** - * Delete all snapshots for a game - */ - deleteByGameId(gameId: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IOrganizationRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IOrganizationRepository.ts deleted file mode 100644 index 373d3fb6..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IOrganizationRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IOrganizationRepository extends IPaginatedRepository { - // Organization-specific methods can be added here if needed -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/ITurnHistoryRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/ITurnHistoryRepository.ts deleted file mode 100644 index 653b3679..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/ITurnHistoryRepository.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TurnHistoryAggregate, TurnActionType, TurnActionData } from '../Game/TurnHistoryAggregate'; - -export interface ITurnHistoryRepository { - /** - * Save a turn history entry - */ - save(turnHistory: TurnHistoryAggregate): Promise; - - /** - * Get all turn history for a game - */ - findByGameId(gameId: string): Promise; - - /** - * Get turn history for a specific player in a game - */ - findByGameAndPlayer(gameId: string, playerId: string): Promise; - - /** - * Get the last N turns for a game - */ - findLastNTurns(gameId: string, limit: number): Promise; - - /** - * Get turn count for a game - */ - countTurnsByGame(gameId: string): Promise; - - /** - * Delete all turn history for a game - */ - deleteByGameId(gameId: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IUserRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IUserRepository.ts deleted file mode 100644 index 4db923b0..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IUserRepository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { UserAggregate } from '../User/UserAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IUserRepository extends IPaginatedRepository { - // User-specific methods - findByUsername(username: string): Promise; - findByEmail(email: string): Promise; - findByToken(token: string): Promise; - deactivate(id: string): Promise; - activate(id: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/Organization/OrganizationAggregate.ts b/SerpentRace_Backend/src/Domain/Organization/OrganizationAggregate.ts deleted file mode 100644 index 5a3be365..00000000 --- a/SerpentRace_Backend/src/Domain/Organization/OrganizationAggregate.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; -import { UserAggregate } from '../User/UserAggregate'; - -export const OrganizationState = { - REGISTERED: 0, - ACTIVE: 1, - SOFT_DELETE: 2 -} as const; - -export type OrganizationStateType = typeof OrganizationState[keyof typeof OrganizationState]; - -@Entity('Organizations') -export class OrganizationAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 255 }) - name!: string; - - @Column({ type: 'varchar', length: 100 }) - contactfname!: string; - - @Column({ type: 'varchar', length: 100 }) - contactlname!: string; - - @Column({ type: 'varchar', length: 20 }) - contactphone!: string; - - @Column({ type: 'varchar', length: 255 }) - contactemail!: string; - - @Column({ type: 'int', default: OrganizationState.REGISTERED }) - state!: OrganizationStateType; - - @CreateDateColumn() - regdate!: Date; - - @UpdateDateColumn({ name: 'updateDate' }) - updateDate!: Date; - - @Column({ type: 'varchar', length: 500, nullable: true }) - url!: string | null; - - @Column({ type: 'int', default: 0 }) - userinorg!: number; - - @Column({ type: 'int', nullable: true }) - maxOrganizationalDecks!: number | null; - - @OneToMany(() => UserAggregate, user => user.orgid) - users!: UserAggregate[]; - } \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/User/UserAggregate.ts b/SerpentRace_Backend/src/Domain/User/UserAggregate.ts deleted file mode 100644 index f67fefcf..00000000 --- a/SerpentRace_Backend/src/Domain/User/UserAggregate.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -export enum UserState { - REGISTERED_NOT_VERIFIED = 0, - VERIFIED_REGULAR = 1, - VERIFIED_PREMIUM = 2, - SOFT_DELETE = 3, - DEACTIVATED = 4, - ADMIN = 5 -} - -@Entity('Users') -export class UserAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid', nullable: true }) - orgid!: string | null; - - @Column({ type: 'varchar', length: 100, unique: true }) - username!: string; - - @Column({ type: 'varchar', length: 255 }) - password!: string; - - @Column({ type: 'varchar', length: 255, unique: true }) - email!: string; - - @Column({ type: 'varchar', length: 100 }) - fname!: string; - - @Column({ type: 'varchar', length: 100 }) - lname!: string; - - @Column({ type: 'varchar', length: 255, nullable: true }) - token!: string | null; - - @Column({ type: 'timestamp', nullable: true }) - TokenExpires!: Date | null; - - @Column({ type: 'varchar', length: 20, nullable: true }) - phone!: string | null; - - @Column({ - type: 'int', - default: UserState.REGISTERED_NOT_VERIFIED - }) - state!: UserState; - - @CreateDateColumn() - regdate!: Date; - - @UpdateDateColumn() - updateDate!: Date; - - @Column({ type: 'timestamp', nullable: true }) - Orglogindate!: Date | null; - - get isAdmin(): boolean { - return this.state === UserState.ADMIN; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Infrastructure/Migrations/1762370334693-full.ts b/SerpentRace_Backend/src/Infrastructure/Migrations/1762370334693-full.ts deleted file mode 100644 index 610a6821..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Migrations/1762370334693-full.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Full1762370334693 implements MigrationInterface { - name = 'Full1762370334693' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "Games" RENAME COLUMN "winnerid" TO "winnerId"`); - await queryRunner.query(`ALTER TABLE "Games" ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId") REFERENCES "Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "Games" DROP CONSTRAINT "FK_330362bff8b25bb573f31fb4023"`); - await queryRunner.query(`ALTER TABLE "Games" RENAME COLUMN "winnerId" TO "winnerid"`); - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/Migrationsettings/1762370333970-full.ts b/SerpentRace_Backend/src/Infrastructure/Migrationsettings/1762370333970-full.ts deleted file mode 100644 index aeb7547a..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Migrationsettings/1762370333970-full.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Full1762370333970 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - } - - public async down(queryRunner: QueryRunner): Promise { - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/ChatArchiveRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/ChatArchiveRepository.ts deleted file mode 100644 index 3ec25839..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/ChatArchiveRepository.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Repository } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ChatArchiveAggregate } from '../../Domain/Chat/ChatArchiveAggregate'; -import { IChatArchiveRepository } from '../../Domain/IRepository/IChatArchiveRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; -import { ChatState } from '../../Domain/Chat/ChatAggregate'; - -export class ChatArchiveRepository implements IChatArchiveRepository { - private repo: Repository; - - constructor() { - this.repo = AppDataSource.getRepository(ChatArchiveAggregate); - } - - async create(archive: Partial) { - const startTime = Date.now(); - try { - const result = await this.repo.save(archive); - logDatabase('Chat archive created successfully', undefined, Date.now() - startTime, { - archiveId: result.id, - chatId: result.chatId, - messageCount: result.archivedMessages?.length || 0 - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.create error', error as Error); - throw new Error('Failed to create chat archive in database'); - } - } - - async findAll() { - const startTime = Date.now(); - try { - const result = await this.repo.find(); - logDatabase('All chat archives retrieved', undefined, Date.now() - startTime, { - count: result.length - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findAll error', error as Error); - throw new Error('Failed to retrieve chat archives from database'); - } - } - - async findById(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ id }); - logDatabase('Chat archive retrieved by id', `findById(${id})`, Date.now() - startTime, { - archiveId: id, - found: !!result - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findById error', error as Error); - throw new Error('Failed to find chat archive by id'); - } - } - - async findByChatId(chatId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .find({ - where: { chatId }, - order: { archivedAt: 'DESC' } - }); - - logDatabase('Chat archives retrieved by chat id', `findByChatId(${chatId})`, Date.now() - startTime, { - chatId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findByChatId error', error as Error); - throw new Error('Failed to find chat archives by chat id'); - } - } - - async findByGameId(gameId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .find({ - where: { gameId }, - order: { archivedAt: 'DESC' } - }); - - logDatabase('Chat archives retrieved by game id', `findByGameId(${gameId})`, Date.now() - startTime, { - gameId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findByGameId error', error as Error); - throw new Error('Failed to find chat archives by game id'); - } - } - - async delete(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.delete(id); - logDatabase('Chat archive deleted', `delete(${id})`, Date.now() - startTime, { - archiveId: id, - affected: result.affected - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.delete error', error as Error); - throw new Error('Failed to delete chat archive'); - } - } - - async cleanup(olderThanDays: number) { - const startTime = Date.now(); - try { - const cutoffDate = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000); - - const result = await this.repo - .createQueryBuilder() - .delete() - .where('archivedAt < :cutoffDate', { cutoffDate }) - .execute(); - - logDatabase('Chat archive cleanup completed', `cleanup(${olderThanDays} days)`, Date.now() - startTime, { - olderThanDays, - deleted: result.affected, - cutoffDate - }); - - return result.affected || 0; - } catch (error) { - logError('ChatArchiveRepository.cleanup error', error as Error); - throw new Error('Failed to cleanup old chat archives'); - } - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/ChatRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/ChatRepository.ts deleted file mode 100644 index 1b266e7f..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/ChatRepository.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { Repository, MoreThan, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ChatAggregate, ChatState, ChatType } from '../../Domain/Chat/ChatAggregate'; -import { ChatArchiveAggregate } from '../../Domain/Chat/ChatArchiveAggregate'; -import { IChatRepository } from '../../Domain/IRepository/IChatRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class ChatRepository implements IChatRepository { - private repo: Repository; - private archiveRepo: Repository; - - constructor() { - this.repo = AppDataSource.getRepository(ChatAggregate); - this.archiveRepo = AppDataSource.getRepository(ChatArchiveAggregate); - } - - async create(chat: Partial) { - const startTime = Date.now(); - try { - const result = await this.repo.save(chat); - logDatabase('Chat created successfully', undefined, Date.now() - startTime, { - chatId: result.id, - type: result.type, - participants: result.users?.length || 0 - }); - return result; - } catch (error) { - logError('ChatRepository.create error', error as Error); - throw new Error('Failed to create chat in database'); - } - } - - async findByPage(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const skip = from; - const take = to - from + 1; - - const [chats, totalCount] = await this.repo.findAndCount({ - where: { state: Not(ChatState.SOFT_DELETE) }, - order: { createDate: 'DESC' }, - skip, - take - }); - - logDatabase('Chats page retrieved successfully', undefined, Date.now() - startTime, { - from, - to, - returned: chats.length, - totalCount - }); - - return { chats, totalCount }; - } catch (error) { - logError('ChatRepository.findByPage error', error as Error); - throw new Error('Failed to retrieve chats page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const skip = from; - const take = to - from + 1; - - const [chats, totalCount] = await this.repo.findAndCount({ - order: { createDate: 'DESC' }, - skip, - take - }); - - logDatabase('Chats page retrieved successfully (including deleted)', undefined, Date.now() - startTime, { - from, - to, - returned: chats.length, - totalCount - }); - - return { chats, totalCount }; - } catch (error) { - logError('ChatRepository.findByPageIncludingDeleted error', error as Error); - throw new Error('Failed to retrieve chats page from database'); - } - } - - async findById(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOne({ - where: { - id, - state: Not(ChatState.SOFT_DELETE) - } - }); - logDatabase('Chat findById query completed', undefined, Date.now() - startTime, { - found: !!result, - chatId: id - }); - return result; - } catch (error) { - logError('ChatRepository.findById error', error as Error); - throw new Error('Failed to retrieve chat from database'); - } - } - - async findByIdIncludingDeleted(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ id }); - logDatabase('Chat findByIdIncludingDeleted query completed', undefined, Date.now() - startTime, { - found: !!result, - chatId: id - }); - return result; - } catch (error) { - logError('ChatRepository.findByIdIncludingDeleted error', error as Error); - throw new Error('Failed to retrieve chat from database'); - } - } - - async findByUserId(userId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .createQueryBuilder('chat') - .where(':userId = ANY(chat.users)', { userId }) - .andWhere('chat.state != :softDelete', { softDelete: ChatState.SOFT_DELETE }) - .getMany(); - - logDatabase('Chats retrieved by user id', `findByUserId(${userId})`, Date.now() - startTime, { - userId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatRepository.findByUserId error', error as Error); - throw new Error('Failed to find chats by user id'); - } - } - - async findByUserIdIncludingDeleted(userId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .createQueryBuilder('chat') - .where(':userId = ANY(chat.users)', { userId }) - .getMany(); - - logDatabase('Chats retrieved by user id (including deleted)', `findByUserIdIncludingDeleted(${userId})`, Date.now() - startTime, { - userId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatRepository.findByUserIdIncludingDeleted error', error as Error); - throw new Error('Failed to find all chats by user id'); - } - } - - async findByGameId(gameId: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ - gameId, - type: ChatType.GAME, - state: ChatState.ACTIVE - }); - logDatabase('Chat retrieved by game id', `findByGameId(${gameId})`, Date.now() - startTime, { - gameId, - found: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.findByGameId error', error as Error); - throw new Error('Failed to find chat by game id'); - } - } - - async findActiveChatsForUser(userId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .createQueryBuilder('chat') - .where(':userId = ANY(chat.users)', { userId }) - .andWhere('chat.state = :state', { state: ChatState.ACTIVE }) - .orderBy('chat.lastActivity', 'DESC') - .getMany(); - - logDatabase('Active chats retrieved for user', `findActiveChatsForUser(${userId})`, Date.now() - startTime, { - userId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatRepository.findActiveChatsForUser error', error as Error); - throw new Error('Failed to find active chats for user'); - } - } - - async findInactiveChats(inactivityMinutes: number) { - const startTime = Date.now(); - try { - const cutoffDate = new Date(Date.now() - inactivityMinutes * 60 * 1000); - - const result = await this.repo - .createQueryBuilder('chat') - .where('chat.state = :state', { state: ChatState.ACTIVE }) - .andWhere('(chat.lastActivity < :cutoffDate OR chat.lastActivity IS NULL)', { cutoffDate }) - .getMany(); - - logDatabase('Inactive chats retrieved', `findInactiveChats(${inactivityMinutes}min)`, Date.now() - startTime, { - inactivityMinutes, - count: result.length, - cutoffDate - }); - return result; - } catch (error) { - logError('ChatRepository.findInactiveChats error', error as Error); - throw new Error('Failed to find inactive chats'); - } - } - - async update(id: string, update: Partial) { - const startTime = Date.now(); - try { - await this.repo.update(id, update); - const result = await this.findById(id); - logDatabase('Chat updated successfully', `update(${id})`, Date.now() - startTime, { - chatId: id, - updatedFields: Object.keys(update), - success: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.update error', error as Error); - throw new Error('Failed to update chat in database'); - } - } - - async delete(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.delete(id); - logDatabase('Chat deleted', `delete(${id})`, Date.now() - startTime, { - chatId: id, - affected: result.affected - }); - return result; - } catch (error) { - logError('ChatRepository.delete error', error as Error); - throw new Error('Failed to delete chat'); - } - } - - async softDelete(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: ChatState.SOFT_DELETE }); - const result = await this.findById(id); - logDatabase('Chat soft deleted', `softDelete(${id})`, Date.now() - startTime, { - chatId: id, - success: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.softDelete error', error as Error); - throw new Error('Failed to soft delete chat'); - } - } - - async archiveChat(chat: ChatAggregate) { - const startTime = Date.now(); - try { - const archive = new ChatArchiveAggregate(); - archive.chatId = chat.id; - archive.archivedMessages = chat.messages; - archive.archivedAt = new Date(); - archive.chatType = chat.type; - archive.chatName = chat.name; - archive.gameId = chat.gameId; - archive.participants = chat.users; - - const archivedResult = await this.archiveRepo.save(archive); - - await this.repo.update(chat.id, { - state: ChatState.ARCHIVE, - messages: [], - archiveDate: new Date() - }); - - logDatabase('Chat archived successfully', `archiveChat(${chat.id})`, Date.now() - startTime, { - chatId: chat.id, - messageCount: chat.messages.length, - archiveId: archivedResult.id - }); - - return archivedResult; - } catch (error) { - logError('ChatRepository.archiveChat error', error as Error); - throw new Error('Failed to archive chat'); - } - } - - async getArchivedChat(chatId: string) { - const startTime = Date.now(); - try { - const result = await this.archiveRepo.findOneBy({ chatId }); - logDatabase('Archived chat retrieved', `getArchivedChat(${chatId})`, Date.now() - startTime, { - chatId, - found: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.getArchivedChat error', error as Error); - throw new Error('Failed to retrieve archived chat'); - } - } - - async restoreFromArchive(chatId: string) { - const startTime = Date.now(); - try { - const archive = await this.archiveRepo.findOneBy({ chatId }); - if (!archive) { - return null; - } - - // Game chats cannot be restored, only viewed - if (archive.chatType === ChatType.GAME) { - logDatabase('Game chat restore attempt blocked', `restoreFromArchive(${chatId})`, Date.now() - startTime, { - chatId, - chatType: archive.chatType, - blocked: true - }); - return null; - } - - // Restore messages to the chat - await this.repo.update(chatId, { - state: ChatState.ACTIVE, - messages: archive.archivedMessages, - lastActivity: new Date(), - archiveDate: null - }); - - const result = await this.findById(chatId); - logDatabase('Chat restored from archive', `restoreFromArchive(${chatId})`, Date.now() - startTime, { - chatId, - messageCount: archive.archivedMessages.length, - success: !!result - }); - - return result; - } catch (error) { - logError('ChatRepository.restoreFromArchive error', error as Error); - throw new Error('Failed to restore chat from archive'); - } - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/ContactRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/ContactRepository.ts deleted file mode 100644 index dab30922..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/ContactRepository.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ContactAggregate, ContactState } from '../../Domain/Contact/ContactAggregate'; -import { IContactRepository } from '../../Domain/IRepository/IContactRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class ContactRepository implements IContactRepository { - private repo: Repository; - - constructor() { - this.repo = AppDataSource.getRepository(ContactAggregate); - } - - async create(contact: Partial) { - return this.repo.save(contact); - } - - async findById(id: string) { - return this.repo - .createQueryBuilder('contact') - .where('contact.id = :id', { id }) - .andWhere('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE }) - .getOne(); - } - - async findByPage(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { - state: Not(ContactState.SOFT_DELETE) - } - }); - - // Get paginated results - const contacts = await this.repo - .createQueryBuilder('contact') - .where('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE }) - .orderBy('contact.createDate', 'DESC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Contact page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${contacts.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { contacts, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Contact page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('ContactRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get contacts page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const contacts = await this.repo - .createQueryBuilder('contact') - .orderBy('contact.createDate', 'DESC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Contact page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${contacts.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { contacts, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Contact page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('ContactRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get contacts page from database'); - } - } - - async update(id: string, update: Partial) { - await this.repo.update(id, update); - return this.findById(id); - } - - async delete(id: string) { - return this.repo.delete(id); - } - - async softDelete(id: string) { - await this.repo.update(id, { state: ContactState.SOFT_DELETE }); - return this.findById(id); - } - - async findByIdIncludingDeleted(id: string) { - return this.repo.findOneBy({ id }); // Returns contact regardless of state - } - - async searchIncludingDeleted(searchTerm: string) { - return this.repo - .createQueryBuilder('contact') - .where('contact.name ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.email ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.txt ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .getMany(); - } - - async search(searchTerm: string) { - return this.repo - .createQueryBuilder('contact') - .where('contact.name ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.email ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.txt ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .andWhere('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE }) - .getMany(); - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/DeckRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/DeckRepository.ts deleted file mode 100644 index 3c50b183..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/DeckRepository.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { DeckAggregate, State, CType } from '../../Domain/Deck/DeckAggregate'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; -import { AdminBypassService } from '../../Application/Services/AdminBypassService'; - -export class DeckRepository implements IDeckRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(DeckAggregate); - } - - async create(deck: Partial) { - return this.repo.save(deck); - } - - async findByPage(from: number, to: number): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(State.SOFT_DELETE) } - }); - - // Get paginated results - const decks = await this.repo.find({ - where: { state: Not(State.SOFT_DELETE) }, - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Deck page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('DeckRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get decks page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const decks = await this.repo.find({ - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Deck page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('DeckRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get decks page from database'); - } - } - - async findById(id: string) { - return this.repo.findOne({ - where: { - id, - state: Not(State.SOFT_DELETE) - } - }); - } - - async findByIdIncludingDeleted(id: string) { - return this.repo.findOneBy({ id }); - } - - async update(id: string, update: Partial) { - await this.repo.update(id, update); - return this.findByIdIncludingDeleted(id); - } - - async delete(id: string) { - return this.repo.delete(id); - } - - async softDelete(id: string) { - await this.repo.update(id, { state: State.SOFT_DELETE }); - return this.findById(id); - } - - async search(query: string, limit: number = 20, offset: number = 0): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('deck') - .where('deck.state != :softDelete', { softDelete: State.SOFT_DELETE }) - .andWhere('LOWER(deck.name) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const decks = await queryBuilder - .orderBy('deck.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Deck search completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck search failed', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('DeckRepository.search error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search decks in database'); - } - } - - async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('deck') - .where('LOWER(deck.name) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const decks = await queryBuilder - .orderBy('deck.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Deck search completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck search failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('DeckRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search all decks in database'); - } - } - - /** - * Count active (non-soft-deleted) decks for a specific user - * @param userId - User ID to count decks for - * @returns Number of active decks - */ - async countActiveByUserId(userId: string): Promise { - const startTime = performance.now(); - try { - const count = await this.repo.count({ - where: { - userid: userId, - state: Not(State.SOFT_DELETE) - } - }); - - const endTime = performance.now(); - logDatabase('User active deck count completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, count: ${count}`); - - return count; - } catch (error) { - const endTime = performance.now(); - logDatabase('User active deck count failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}`); - logError('DeckRepository.countActiveByUserId error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to count active decks for user'); - } - } - - /** - * Count organizational decks for a specific user - * @param userId - User ID to count organizational decks for - * @returns Number of organizational decks - */ - async countOrganizationalByUserId(userId: string): Promise { - const startTime = performance.now(); - try { - const count = await this.repo.count({ - where: { - userid: userId, - ctype: CType.ORGANIZATION, - state: Not(State.SOFT_DELETE) - } - }); - - const endTime = performance.now(); - logDatabase('User organizational deck count completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, count: ${count}`); - - return count; - } catch (error) { - const endTime = performance.now(); - logDatabase('User organizational deck count failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}`); - logError('DeckRepository.countOrganizationalByUserId error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to count organizational decks for user'); - } - } - - /** - * Find decks with filtering based on user permissions and mandatory pagination - * @param userId - User ID for filtering - * @param userOrgId - User's organization ID (if any) - * @param isAdmin - Whether user is admin (bypasses filtering) - * @param from - Start index for pagination (default: 0) - * @param to - End index for pagination (default: 49) - * @returns Paginated filtered list of decks with total count - */ - async findFilteredDecks(userId: string, userOrgId?: string | null, isAdmin?: boolean, from: number = 0, to: number = 49): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - // Validate pagination parameters - if (from < 0 || to < from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = to - from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - const skip = from; - const take = limit; - - // Admin gets ALL decks with pagination - if (isAdmin) { - AdminBypassService.logAdminBypass( - 'FIND_FILTERED_DECKS_BYPASS', - userId, - 'all-decks-filtered', - { - bypassType: 'admin-all-decks-filtered', - userOrgId, - from, - to, - operation: 'read' - } - ); - - const [decks, totalCount] = await this.repo.findAndCount({ - where: { state: Not(State.SOFT_DELETE) }, - relations: ['organization', 'user'], - order: { creationdate: 'DESC' }, - skip, - take - }); - - const endTime = performance.now(); - logDatabase('Admin filtered deck query completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, found: ${decks.length}, totalCount: ${totalCount}, isAdmin: true`); - - return { decks, totalCount }; - } - - // Regular user complex filtering - const queryBuilder = this.repo.createQueryBuilder('deck') - .leftJoinAndSelect('deck.organization', 'org') - .leftJoinAndSelect('deck.user', 'user') - .where('deck.state != :deletedState', { deletedState: State.SOFT_DELETE }); - - queryBuilder.andWhere('(' + - // User's private decks - '(deck.userid = :userId AND deck.ctype = :privateType) OR ' + - // All public decks - '(deck.ctype = :publicType)' + - // Organization decks from same org (if user has org) - (userOrgId ? ' OR (deck.ctype = :orgType AND org.id = :orgId)' : '') + - ')', { - userId, - privateType: CType.PRIVATE, - publicType: CType.PUBLIC, - ...(userOrgId && { orgType: CType.ORGANIZATION, orgId: userOrgId }) - }); - - queryBuilder - .orderBy('deck.creationdate', 'DESC') - .skip(skip) - .take(take); - - const [decks, totalCount] = await queryBuilder.getManyAndCount(); - - const endTime = performance.now(); - logDatabase('User filtered deck query completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, userOrgId: ${userOrgId}, found: ${decks.length}, totalCount: ${totalCount}, isAdmin: false`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Filtered deck query failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, isAdmin: ${isAdmin}`); - logError('DeckRepository.findFilteredDecks error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find filtered decks'); - } - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/GameRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/GameRepository.ts deleted file mode 100644 index d16dfbc0..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/GameRepository.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { Repository, Not, In } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { GameAggregate, GameState } from '../../Domain/Game/GameAggregate'; -import { IGameRepository } from '../../Domain/IRepository/IGameRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class GameRepository implements IGameRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(GameAggregate); - } - - async create(game: Partial): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.save(game); - const endTime = performance.now(); - logDatabase('Game created', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${result.id}, gameCode: ${result.gamecode}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.create error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to create game in database'); - } - } - - async findByPage(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(GameState.CANCELLED) } - }); - - // Get paginated results - const games = await this.repo.find({ - where: { state: Not(GameState.CANCELLED) }, - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Game page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('GameRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get games page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination (including deleted) - const totalCount = await this.repo.count(); - - // Get paginated results (including deleted) - const games = await this.repo.find({ - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Game page query (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game page query (including deleted) failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('GameRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get games page (including deleted) from database'); - } - } - - async findById(id: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.findOne({ - where: { id, state: Not(GameState.CANCELLED) } - }); - const endTime = performance.now(); - logDatabase('Game findById completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, found: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game findById failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.findById error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find game by id in database'); - } - } - - async findByIdIncludingDeleted(id: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.findOne({ - where: { id } - }); - const endTime = performance.now(); - logDatabase('Game findByIdIncludingDeleted completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, found: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game findByIdIncludingDeleted failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.findByIdIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find game by id (including deleted) in database'); - } - } - - async findByGameCode(gamecode: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.findOne({ - where: { gamecode, state: Not(GameState.CANCELLED) } - }); - const endTime = performance.now(); - logDatabase('Game findByGameCode completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameCode: ${gamecode}, found: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game findByGameCode failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameCode: ${gamecode}`); - logError('GameRepository.findByGameCode error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find game by game code in database'); - } - } - - async search(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED }) - .andWhere('(game.gamecode ILIKE :query)', { query: `%${query}%` }); - - // Get total count - const totalCount = await queryBuilder.getCount(); - - // Apply pagination if provided - if (limit !== undefined) { - queryBuilder.take(limit); - } - if (offset !== undefined) { - queryBuilder.skip(offset); - } - - const games = await queryBuilder.orderBy('game.updateDate', 'DESC').getMany(); - - const endTime = performance.now(); - logDatabase('Game search completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game search failed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}`); - logError('GameRepository.search error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search games in database'); - } - } - - async searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('(game.gamecode ILIKE :query)', { query: `%${query}%` }); - - // Get total count - const totalCount = await queryBuilder.getCount(); - - // Apply pagination if provided - if (limit !== undefined) { - queryBuilder.take(limit); - } - if (offset !== undefined) { - queryBuilder.skip(offset); - } - - const games = await queryBuilder.orderBy('game.updateDate', 'DESC').getMany(); - - const endTime = performance.now(); - logDatabase('Game search (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game search (including deleted) failed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}`); - logError('GameRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search games (including deleted) in database'); - } - } - - async update(id: string, update: Partial): Promise { - const startTime = performance.now(); - try { - await this.repo.update(id, update); - const result = await this.findById(id); - const endTime = performance.now(); - logDatabase('Game update completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, updated: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game update failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.update error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to update game in database'); - } - } - - async delete(id: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.delete(id); - const endTime = performance.now(); - logDatabase('Game delete completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, affected: ${result.affected}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game delete failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.delete error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to delete game from database'); - } - } - - async softDelete(id: string): Promise { - const startTime = performance.now(); - try { - await this.repo.update(id, { state: GameState.CANCELLED }); - const result = await this.findByIdIncludingDeleted(id); - const endTime = performance.now(); - logDatabase('Game soft delete completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, updated: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game soft delete failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.softDelete error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to soft delete game in database'); - } - } - - // Game-specific methods - async findActiveGames(): Promise { - const startTime = performance.now(); - try { - const games = await this.repo.find({ - where: { state: GameState.ACTIVE }, - order: { updateDate: 'DESC' } - }); - const endTime = performance.now(); - logDatabase('Active games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`); - return games; - } catch (error) { - const endTime = performance.now(); - logDatabase('Active games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.findActiveGames error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find active games in database'); - } - } - - async findGamesByPlayer(playerId: string): Promise { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED }) - .andWhere('JSON_CONTAINS(game.players, :playerId)', { playerId: `"${playerId}"` }) - .orderBy('game.updateDate', 'DESC'); - - const games = await queryBuilder.getMany(); - const endTime = performance.now(); - logDatabase('Games by player query completed', `executionTime: ${Math.round(endTime - startTime)}ms, playerId: ${playerId}, found: ${games.length}`); - return games; - } catch (error) { - const endTime = performance.now(); - logDatabase('Games by player query failed', `executionTime: ${Math.round(endTime - startTime)}ms, playerId: ${playerId}`); - logError('GameRepository.findGamesByPlayer error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find games by player in database'); - } - } - - async findWaitingGames(): Promise { - const startTime = performance.now(); - try { - const games = await this.repo.find({ - where: { state: GameState.WAITING }, - order: { createdate: 'ASC' } - }); - const endTime = performance.now(); - logDatabase('Waiting games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`); - return games; - } catch (error) { - const endTime = performance.now(); - logDatabase('Waiting games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.findWaitingGames error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find waiting games in database'); - } - } - - async findFinishedGames(from?: number, to?: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('game.state = :finishedState', { finishedState: GameState.FINISHED }) - .orderBy('game.enddate', 'DESC'); - - // Get total count - const totalCount = await queryBuilder.getCount(); - - // Apply pagination if provided - if (from !== undefined && to !== undefined) { - const limit = to - from + 1; - const offset = from; - queryBuilder.take(limit).skip(offset); - } - - const games = await queryBuilder.getMany(); - const endTime = performance.now(); - logDatabase('Finished games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}`); - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Finished games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.findFinishedGames error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find finished games in database'); - } - } - - async addPlayerToGame(gameId: string, playerId: string): Promise { - const startTime = performance.now(); - try { - const game = await this.findById(gameId); - if (!game) { - return null; - } - - // Check if player is already in the game - if (game.players.includes(playerId)) { - return game; - } - - // Check if game is full - if (game.players.length >= game.maxplayers) { - throw new Error('Game is full'); - } - - const updatedPlayers = [...game.players, playerId]; - const result = await this.update(gameId, { players: updatedPlayers }); - - const endTime = performance.now(); - logDatabase('Player added to game', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Add player to game failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - logError('GameRepository.addPlayerToGame error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to add player to game in database'); - } - } - - async removePlayerFromGame(gameId: string, playerId: string): Promise { - const startTime = performance.now(); - try { - const game = await this.findById(gameId); - if (!game) { - return null; - } - - const updatedPlayers = game.players.filter(id => id !== playerId); - const result = await this.update(gameId, { players: updatedPlayers }); - - const endTime = performance.now(); - logDatabase('Player removed from game', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Remove player from game failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - logError('GameRepository.removePlayerFromGame error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to remove player from game in database'); - } - } - - async updateGameState(gameId: string, state: GameState, winner?: string): Promise { - const startTime = performance.now(); - try { - const updateData: Partial = { state }; - - if (state === GameState.ACTIVE) { - updateData.startdate = new Date(); - } - - if (state === GameState.FINISHED) { - updateData.enddate = new Date(); - if (winner) { - updateData.winnerId = winner; - } - } - - const result = await this.update(gameId, updateData); - - const endTime = performance.now(); - logDatabase('Game state updated', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, state: ${updateData.state}, winner: ${winner}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game state update failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}`); - logError('GameRepository.updateGameState error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to update game state in database'); - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/GameSnapshotRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/GameSnapshotRepository.ts deleted file mode 100644 index 15107377..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/GameSnapshotRepository.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Repository, LessThan } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { IGameSnapshotRepository } from '../../Domain/IRepository/IGameSnapshotRepository'; -import { GameSnapshotAggregate, SnapshotTrigger } from '../../Domain/Game/GameSnapshotAggregate'; - -export class GameSnapshotRepository implements IGameSnapshotRepository { - private repository: Repository; - - constructor() { - this.repository = AppDataSource.getRepository(GameSnapshotAggregate); - } - - async save(snapshot: GameSnapshotAggregate): Promise { - return await this.repository.save(snapshot); - } - - async findLatestByGameId(gameId: string): Promise { - return await this.repository.findOne({ - where: { gameid: gameId }, - order: { createdat: 'DESC' } - }); - } - - async findByGameId(gameId: string): Promise { - return await this.repository.find({ - where: { gameid: gameId }, - order: { turnNumber: 'ASC', createdat: 'ASC' } - }); - } - - async findByGameAndTrigger(gameId: string, trigger: SnapshotTrigger): Promise { - return await this.repository.find({ - where: { - gameid: gameId, - trigger: trigger - }, - order: { createdat: 'DESC' } - }); - } - - async findByGameAndTurn(gameId: string, turnNumber: number): Promise { - return await this.repository.findOne({ - where: { - gameid: gameId, - turnNumber: turnNumber - }, - order: { createdat: 'DESC' } - }); - } - - async deleteOldSnapshots(gameId: string, keepCount: number): Promise { - const snapshots = await this.repository.find({ - where: { gameid: gameId }, - order: { createdat: 'DESC' }, - select: ['id', 'createdat'] - }); - - if (snapshots.length > keepCount) { - const idsToDelete = snapshots - .slice(keepCount) - .map(s => s.id); - - if (idsToDelete.length > 0) { - await this.repository.delete(idsToDelete); - } - } - } - - async deleteByGameId(gameId: string): Promise { - await this.repository.delete({ gameid: gameId }); - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/OrganizationRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/OrganizationRepository.ts deleted file mode 100644 index f8647e60..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/OrganizationRepository.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { OrganizationAggregate, OrganizationState } from '../../Domain/Organization/OrganizationAggregate'; -import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class OrganizationRepository implements IOrganizationRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(OrganizationAggregate); - } - - async create(org: Partial) { - return this.repo.save(org); - } - - async findByPage(from: number, to: number): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(OrganizationState.SOFT_DELETE) } - }); - - // Get paginated results - const organizations = await this.repo.find({ - where: { state: Not(OrganizationState.SOFT_DELETE) }, - order: { name: 'ASC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Organization page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('OrganizationRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get organizations page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const organizations = await this.repo.find({ - order: { name: 'ASC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Organization page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('OrganizationRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get organizations page from database'); - } - } - - async findById(id: string) { - return this.repo.findOne({ - where: { - id, - state: Not(OrganizationState.SOFT_DELETE) - } - }); - } - - async findByIdIncludingDeleted(id: string) { - return this.repo.findOneBy({ id }); - } - - async update(id: string, update: Partial) { - await this.repo.update(id, update); - return this.findById(id); - } - - async delete(id: string) { - return this.repo.delete(id); - } - - async softDelete(id: string) { - await this.repo.update(id, { state: OrganizationState.SOFT_DELETE }); - return this.findById(id); - } - - async search(query: string, limit: number = 20, offset: number = 0): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('org') - .where('org.state != :softDelete', { softDelete: OrganizationState.SOFT_DELETE }) - .andWhere('(LOWER(org.name) LIKE :pattern OR LOWER(org.contactfname) LIKE :pattern OR LOWER(org.contactlname) LIKE :pattern OR LOWER(org.contactemail) LIKE :pattern OR LOWER(CONCAT(org.contactfname, \' \', org.contactlname)) LIKE :pattern)', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const organizations = await queryBuilder - .orderBy('org.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Organization search completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization search failed', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('OrganizationRepository.search error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search organizations in database'); - } - } - - async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('org') - .where('LOWER(org.name) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(org.contactfname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(org.contactlname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(org.contactemail) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(CONCAT(org.contactfname, \' \', org.contactlname)) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const organizations = await queryBuilder - .orderBy('org.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Organization search completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization search failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('OrganizationRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search all organizations in database'); - } - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/TurnHistoryRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/TurnHistoryRepository.ts deleted file mode 100644 index 5b801210..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/TurnHistoryRepository.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Repository } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ITurnHistoryRepository } from '../../Domain/IRepository/ITurnHistoryRepository'; -import { TurnHistoryAggregate } from '../../Domain/Game/TurnHistoryAggregate'; - -export class TurnHistoryRepository implements ITurnHistoryRepository { - private repository: Repository; - - constructor() { - this.repository = AppDataSource.getRepository(TurnHistoryAggregate); - } - - async save(turnHistory: TurnHistoryAggregate): Promise { - return await this.repository.save(turnHistory); - } - - async findByGameId(gameId: string): Promise { - return await this.repository.find({ - where: { gameid: gameId }, - order: { turnNumber: 'ASC', createdat: 'ASC' } - }); - } - - async findByGameAndPlayer(gameId: string, playerId: string): Promise { - return await this.repository.find({ - where: { - gameid: gameId, - playerid: playerId - }, - order: { turnNumber: 'ASC', createdat: 'ASC' } - }); - } - - async findLastNTurns(gameId: string, limit: number): Promise { - return await this.repository.find({ - where: { gameid: gameId }, - order: { turnNumber: 'DESC', createdat: 'DESC' }, - take: limit - }); - } - - async countTurnsByGame(gameId: string): Promise { - return await this.repository.count({ - where: { gameid: gameId } - }); - } - - async deleteByGameId(gameId: string): Promise { - await this.repository.delete({ gameid: gameId }); - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/UserRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/UserRepository.ts deleted file mode 100644 index 40d772ae..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/UserRepository.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { UserAggregate, UserState } from '../../Domain/User/UserAggregate'; -import { IUserRepository } from '../../Domain/IRepository/IUserRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class UserRepository implements IUserRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(UserAggregate); - } - - async create(user: Partial) { - const startTime = Date.now(); - try { - const result = await this.repo.save(user); - logDatabase('User created successfully', undefined, Date.now() - startTime, { - userId: result.id, - username: user.username, - email: user.email - }); - return result; - } catch (error) { - logError('UserRepository.create error', error as Error); - - // Handle unique constraint violations - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) { - throw new Error('User with this username or email already exists'); - } - - throw new Error('Failed to create user in database'); - } - } - - async findByPage(from: number, to: number): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(UserState.SOFT_DELETE) } - }); - - // Get paginated results - const users = await this.repo.find({ - where: { state: Not(UserState.SOFT_DELETE) }, - order: { regdate: 'DESC' }, - take: limit, - skip: offset - }); - - logDatabase('User page query completed', `from: ${from}, to: ${to}`, Date.now() - startTime, { - found: users.length, - total: totalCount - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.findByPage error', error as Error); - throw new Error('Failed to get users page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const users = await this.repo.find({ - order: { regdate: 'DESC' }, - take: limit, - skip: offset - }); - - logDatabase('User page query completed (including deleted)', `from: ${from}, to: ${to}`, Date.now() - startTime, { - found: users.length, - total: totalCount - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.findByPageIncludingDeleted error', error as Error); - throw new Error('Failed to get users page from database'); - } - } - - async findById(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOne({ - where: { - id, - state: Not(UserState.SOFT_DELETE) - } - }); - logDatabase('User findById query completed', `findOneBy({ id: ${id} })`, Date.now() - startTime, { - found: !!result, - userId: id - }); - return result; - } catch (error) { - logError('UserRepository.findById error', error as Error); - - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - return null; - } - - throw new Error('Failed to retrieve user from database'); - } - } - - async findByIdIncludingDeleted(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ id }); - logDatabase('User findByIdIncludingDeleted query completed', `findOneBy({ id: ${id} })`, Date.now() - startTime, { - found: !!result, - userId: id - }); - return result; - } catch (error) { - logError('UserRepository.findByIdIncludingDeleted error', error as Error); - - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - return null; - } - - throw new Error('Failed to retrieve user from database'); - } - } - - async findByUsername(username: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ username }); - logDatabase('User findByUsername query completed', `findOneBy({ username: ${username} })`, Date.now() - startTime, { - found: !!result, - username - }); - return result; - } catch (error) { - logError('UserRepository.findByUsername error', error as Error); - throw new Error('Failed to retrieve user by username from database'); - } - } - - async findByEmail(email: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ email }); - logDatabase('User findByEmail query completed', `findOneBy({ email: ${email} })`, Date.now() - startTime, { - found: !!result, - email - }); - return result; - } catch (error) { - logError('UserRepository.findByEmail error', error as Error); - throw new Error('Failed to retrieve user by email from database'); - } - } - - async findByToken(token: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ token: token }); - logDatabase('User findByToken query completed', `findOneBy({ token })`, Date.now() - startTime, { - found: !!result, - tokenPrefix: token.substring(0, 8) + '...' - }); - return result; - } catch (error) { - logError('UserRepository.findByToken error', error as Error); - throw new Error('Failed to retrieve user by token from database'); - } - } - - async update(id: string, update: Partial) { - const startTime = Date.now(); - try { - await this.repo.update(id, update); - const result = await this.findById(id); - logDatabase('User updated successfully', `update(${id})`, Date.now() - startTime, { - userId: id, - updatedFields: Object.keys(update), - success: !!result - }); - return result; - } catch (error) { - logError('UserRepository.update error', error as Error); - - // Handle unique constraint violations - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) { - throw new Error('Username or email already exists'); - } - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to update user in database'); - } - } - - async delete(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.delete(id); - logDatabase('User deleted successfully', `delete(${id})`, Date.now() - startTime, { - userId: id, - affected: result.affected - }); - return result; - } catch (error) { - logError('UserRepository.delete error', error as Error); - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to delete user from database'); - } - } - - async softDelete(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: UserState.SOFT_DELETE }); - const result = await this.findById(id); - logDatabase('User soft deleted successfully', `update(${id}, { state: SOFT_DELETE })`, Date.now() - startTime, { - userId: id, - success: !!result - }); - return result; - } catch (error) { - logError('UserRepository.softDelete error', error as Error); - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to soft delete user in database'); - } - } - - async deactivate(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: UserState.DEACTIVATED }); - const result = await this.findById(id); - logDatabase('User deactivated successfully', `update(${id}, { state: DEACTIVATED })`, Date.now() - startTime, { - userId: id, - success: !!result - }); - return result; - } catch (error) { - logError('UserRepository.deactivate error', error as Error); - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to deactivate user in database'); - } - } - - async search(query: string, limit: number = 20, offset: number = 0): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('user') - .where('user.state != :softDelete', { softDelete: UserState.SOFT_DELETE }) - .andWhere('(LOWER(user.username) LIKE :pattern OR LOWER(user.email) LIKE :pattern OR LOWER(user.fname) LIKE :pattern OR LOWER(user.lname) LIKE :pattern OR LOWER(CONCAT(user.fname, \' \', user.lname)) LIKE :pattern)', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const users = await queryBuilder - .orderBy('user.username', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - logDatabase('User search completed', - `search query: ${query.substring(0, 50)}...`, - Date.now() - startTime, { - query, - limit, - offset, - totalCount, - returnedCount: users.length - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.search error', error as Error); - throw new Error('Failed to search users in database'); - } - } - - async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('user') - .where('LOWER(user.username) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(user.email) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(user.fname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(user.lname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(CONCAT(user.fname, \' \', user.lname)) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const users = await queryBuilder - .orderBy('user.username', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - logDatabase('User search completed (including deleted)', - `search query: ${query.substring(0, 50)}...`, - Date.now() - startTime, { - query, - limit, - offset, - totalCount, - returnedCount: users.length - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.searchIncludingDeleted error', error as Error); - throw new Error('Failed to search all users in database'); - } - } - - async activate(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: UserState.VERIFIED_REGULAR }); - const result = await this.findById(id); - logDatabase('User activated successfully', `update(${id}, { state: VERIFIED_REGULAR })`, Date.now() - startTime, { - userId: id, - success: !!result - }); - return result; - } - catch (error) { - logError('UserRepository.activate error', error as Error); - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - throw new Error('Failed to activate user in database'); - } - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/ormconfig.ts b/SerpentRace_Backend/src/Infrastructure/ormconfig.ts deleted file mode 100644 index 939b2117..00000000 --- a/SerpentRace_Backend/src/Infrastructure/ormconfig.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DataSource } from 'typeorm'; -import { join } from 'path'; - -export const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_NAME || 'serpentrace', - synchronize: false, // Set to false when using migrations - logging: process.env.NODE_ENV === 'development', - entities: [join(__dirname, '../Domain/**/*Aggregate.ts')], - migrations: [join(__dirname, './Migrations/*.ts')], - migrationsTableName: 'migrations', - migrationsRun: false // Let migrations run manually -}); \ No newline at end of file diff --git a/SerpentRace_Backend/src/Templates/contact-response-de.html b/SerpentRace_Backend/src/Templates/contact-response-de.html deleted file mode 100644 index bf1a0e0a..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-de.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - {{companyName}} - Antwort auf Ihre {{contactTypeString}} - - - - - - diff --git a/SerpentRace_Backend/src/Templates/contact-response-de.txt b/SerpentRace_Backend/src/Templates/contact-response-de.txt deleted file mode 100644 index b02f1fcb..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-de.txt +++ /dev/null @@ -1,21 +0,0 @@ -{{companyName}} - Antwort auf Ihre {{contactTypeString}} - -Hallo {{contactName}}, - -Vielen Dank, dass Sie uns kontaktiert haben! Wir haben Ihre Nachricht geprĂŒft und unser Team hat die folgende Antwort vorbereitet. - -=== IHRE URSPRÜNGLICHE NACHRICHT ({{contactTypeString}}) === -{{originalMessage}} - -=== UNSERE ANTWORT === -{{adminResponse}} - -Wenn Sie weitere Fragen haben oder zusĂ€tzliche Hilfe benötigen, zögern Sie bitte nicht, uns erneut zu kontaktieren. - -Vielen Dank, dass Sie sich fĂŒr {{companyName}} entschieden haben! - -FĂŒr weitere UnterstĂŒtzung kontaktieren Sie uns unter {{supportEmail}} -Dies ist eine automatische Antwort. Bitte antworten Sie nicht direkt auf diese E-Mail. - ---- -{{companyName}} Support-Team diff --git a/SerpentRace_Backend/src/Templates/contact-response-hu.html b/SerpentRace_Backend/src/Templates/contact-response-hu.html deleted file mode 100644 index cb9bff77..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-hu.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - {{companyName}} - VĂĄlasz az Ön {{contactTypeString}} ĂŒzenetĂ©re - - - - - - diff --git a/SerpentRace_Backend/src/Templates/contact-response-hu.txt b/SerpentRace_Backend/src/Templates/contact-response-hu.txt deleted file mode 100644 index 5433961b..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-hu.txt +++ /dev/null @@ -1,21 +0,0 @@ -{{companyName}} - VĂĄlasz az Ön {{contactTypeString}} ĂŒzenetĂ©re - -Kedves {{contactName}}! - -KöszönjĂŒk, hogy kapcsolatba lĂ©pett velĂŒnk! ÁtnĂ©ztĂŒk az Ön ĂŒzenetĂ©t Ă©s csapatunk az alĂĄbbi vĂĄlaszt kĂ©szĂ­tette. - -=== AZ ÖN EREDETI ÜZENETE ({{contactTypeString}}) === -{{originalMessage}} - -=== VÁLASZUNK === -{{adminResponse}} - -Ha tovĂĄbbi kĂ©rdĂ©se van vagy tovĂĄbbi segĂ­tsĂ©gre van szĂŒksĂ©ge, kĂ©rjĂŒk, ne habozzon kapcsolatba lĂ©pni velĂŒnk Ășjra. - -KöszönjĂŒk, hogy a {{companyName}} szolgĂĄltatĂĄsait vĂĄlasztotta! - -TovĂĄbbi tĂĄmogatĂĄsĂ©rt lĂ©pjen kapcsolatba velĂŒnk a {{supportEmail}} cĂ­men -Ez egy automatikus vĂĄlasz. KĂ©rjĂŒk, ne vĂĄlaszoljon közvetlenĂŒl erre az e-mailre. - ---- -{{companyName}} TĂĄmogatĂĄsi Csapat diff --git a/SerpentRace_Backend/src/Templates/contact-response.html b/SerpentRace_Backend/src/Templates/contact-response.html deleted file mode 100644 index b5f6fc89..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - {{companyName}} - Response to Your {{contactTypeString}} - - - - - - diff --git a/SerpentRace_Backend/src/Templates/contact-response.txt b/SerpentRace_Backend/src/Templates/contact-response.txt deleted file mode 100644 index cdf4a016..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response.txt +++ /dev/null @@ -1,21 +0,0 @@ -{{companyName}} - Response to Your {{contactTypeString}} - -Hello {{contactName}}, - -Thank you for contacting us! We've reviewed your message and our team has provided a response below. - -=== YOUR ORIGINAL MESSAGE ({{contactTypeString}}) === -{{originalMessage}} - -=== OUR RESPONSE === -{{adminResponse}} - -If you have any additional questions or need further assistance, please don't hesitate to contact us again. - -Thank you for choosing {{companyName}}! - -For additional support, contact us at {{supportEmail}} -This is an automated response. Please do not reply directly to this email. - ---- -{{companyName}} Support Team diff --git a/SerpentRace_Backend/src/Templates/password-reset-de.html b/SerpentRace_Backend/src/Templates/password-reset-de.html deleted file mode 100644 index ff5ee585..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-de.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - SerpentRace - Passwort zurĂŒcksetzen - - - - - - diff --git a/SerpentRace_Backend/src/Templates/password-reset-de.txt b/SerpentRace_Backend/src/Templates/password-reset-de.txt deleted file mode 100644 index 86210ee6..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-de.txt +++ /dev/null @@ -1,44 +0,0 @@ -🐍 {{ companyName }} - Passwort zurĂŒcksetzen -=============================================== - -Hallo {{ userName }}! - -Wir haben eine Anfrage zum ZurĂŒcksetzen Ihres Passworts fĂŒr Ihr {{ companyName }} Konto erhalten. - -Wenn Sie diese Anfrage gestellt haben, verwenden Sie den folgenden Link, um Ihr Passwort zurĂŒckzusetzen: - -PASSWORT-RESET-LINK: -{{ resetUrl }} - -RESET-TOKEN: -{{ resetToken }} - -Sie können entweder den obigen Link oder das Reset-Token verwenden, um Ihr Passwort zurĂŒckzusetzen. - -WICHTIGE SICHERHEITSINFORMATIONEN: -🚹 Dieser Passwort-Reset-Link lĂ€uft aus SicherheitsgrĂŒnden in 1 Stunde ab -🚹 Falls Sie keine Passwort-ZurĂŒcksetzung angefordert haben, ignorieren Sie diese E-Mail bitte und Ihr Passwort bleibt unverĂ€ndert -🚹 Teilen Sie Ihr Reset-Token niemals mit anderen -🚹 {{ companyName }} wird Sie niemals per E-Mail nach Ihrem Passwort fragen - -SICHERHEITSTIPPS FÜR IHR NEUES PASSWORT: -💡 Verwenden Sie mindestens 8 Zeichen -💡 Verwenden Sie Groß- und Kleinbuchstaben -💡 FĂŒgen Sie Zahlen und Sonderzeichen hinzu -💡 Verwenden Sie keine Passwörter von anderen Konten wieder -💡 ErwĂ€gen Sie die Verwendung eines Passwort-Managers - -DIESE ZURÜCKSETZUNG NICHT ANGEFORDERT? -Falls Sie keine Passwort-ZurĂŒcksetzung angefordert haben, ist Ihr Konto weiterhin sicher. Sie können diese E-Mail getrost ignorieren. -Falls Sie jedoch Bedenken bezĂŒglich unbefugten Zugriffs haben, kontaktieren Sie bitte umgehend unser Support-Team. - -BENÖTIGEN SIE HILFE? -Falls Sie Sicherheitsbedenken haben oder UnterstĂŒtzung benötigen, kontaktieren Sie unser Support-Team unter {{ supportEmail }} - -Zu Ihrer Sicherheit können wir Sie bitten, Ihre IdentitĂ€t zu verifizieren, wenn Sie den Support kontaktieren. - ---- -Diese E-Mail wurde vom {{ companyName }} Sicherheitsteam gesendet -Dies ist eine automatische Nachricht, bitte antworten Sie nicht auf diese E-Mail. - -© 2025 {{ companyName }}. Alle Rechte vorbehalten. diff --git a/SerpentRace_Backend/src/Templates/password-reset-hu.html b/SerpentRace_Backend/src/Templates/password-reset-hu.html deleted file mode 100644 index e2fef7a2..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-hu.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - SerpentRace - JelszĂł visszaĂĄllĂ­tĂĄs kĂ©rĂ©se - - - - - - diff --git a/SerpentRace_Backend/src/Templates/password-reset-hu.txt b/SerpentRace_Backend/src/Templates/password-reset-hu.txt deleted file mode 100644 index f4e8002c..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-hu.txt +++ /dev/null @@ -1,44 +0,0 @@ -🐍 {{ companyName }} - JelszĂł visszaĂĄllĂ­tĂĄs kĂ©rĂ©se -=============================================== - -ÜdvözöljĂŒk {{ userName }}! - -KĂ©rĂ©st kaptunk a {{ companyName }} fiĂłkjĂĄhoz tartozĂł jelszĂł visszaĂĄllĂ­tĂĄsĂĄra. - -Ha Ön kĂŒldte ezt a kĂ©rĂ©st, hasznĂĄlja az alĂĄbbi linket a jelszĂł visszaĂĄllĂ­tĂĄsĂĄhoz: - -JELSZÓ VISSZAÁLLÍTÁSI LINK: -{{ resetUrl }} - -VISSZAÁLLÍTÁSI TOKEN: -{{ resetToken }} - -HasznĂĄlhatja a fenti linket vagy a visszaĂĄllĂ­tĂĄsi tokent a jelszava visszaĂĄllĂ­tĂĄsĂĄhoz. - -FONTOS BIZTONSÁGI INFORMÁCIÓK: -🚹 Ez a jelszĂł-visszaĂĄllĂ­tĂĄsi link biztonsĂĄgi okokbĂłl 1 Ăłra mĂșlva lejĂĄr -🚹 Ha Ön nem kĂ©rt jelszĂł visszaĂĄllĂ­tĂĄst, kĂ©rjĂŒk, hagyja figyelmen kĂ­vĂŒl ezt az e-mailt, Ă©s jelszava vĂĄltozatlan marad -🚹 Soha ne ossza meg a visszaĂĄllĂ­tĂĄsi tokenjĂ©t senkivel -🚹 A {{ companyName }} soha nem fogja e-mailben kĂ©rni az Ön jelszavĂĄt - -BIZTONSÁGI TIPPEK AZ ÚJ JELSZAVÁHOZ: -💡 HasznĂĄljon legalĂĄbb 8 karaktert -💡 HasznĂĄljon kis- Ă©s nagybetƱket -💡 Adjon hozzĂĄ szĂĄmokat Ă©s speciĂĄlis karaktereket -💡 Ne hasznĂĄlja Ășjra mĂĄs fiĂłkok jelszavait -💡 Fontolja meg egy jelszĂłkezelƑ hasznĂĄlatĂĄt - -NEM ÖN KÉRTE EZT A VISSZAÁLLÍTÁST? -Ha Ön nem kĂ©rt jelszĂł visszaĂĄllĂ­tĂĄst, fiĂłkja tovĂĄbbra is biztonsĂĄgos. Nyugodtan figyelmen kĂ­vĂŒl hagyhatja ezt az e-mailt. -Azonban, ha aggodalmai vannak a jogosulatlan hozzĂĄfĂ©rĂ©ssel kapcsolatban, kĂ©rjĂŒk, azonnal lĂ©pjen kapcsolatba ĂŒgyfĂ©lszolgĂĄlatunkkal. - -SEGÍTSÉGRE VAN SZÜKSÉGE? -Ha biztonsĂĄgi aggĂĄlyai vannak vagy segĂ­tsĂ©gre van szĂŒksĂ©ge, lĂ©pjen kapcsolatba ĂŒgyfĂ©lszolgĂĄlatunkkal: {{ supportEmail }} - -BiztonsĂĄga Ă©rdekĂ©ben megkĂ©rhetjĂŒk, hogy igazolja szemĂ©lyazonossĂĄgĂĄt, amikor kapcsolatba lĂ©p ĂŒgyfĂ©lszolgĂĄlatunkkal. - ---- -Ezt az e-mailt a {{ companyName }} BiztonsĂĄgi Csapata kĂŒldte -Ez egy automatikus ĂŒzenet, kĂ©rjĂŒk, ne vĂĄlaszoljon erre az e-mailre. - -© 2025 {{ companyName }}. Minden jog fenntartva. diff --git a/SerpentRace_Backend/src/Templates/password-reset.html b/SerpentRace_Backend/src/Templates/password-reset.html deleted file mode 100644 index 2b734b4b..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - SerpentRace - Password Reset Request - - - - - - diff --git a/SerpentRace_Backend/src/Templates/password-reset.txt b/SerpentRace_Backend/src/Templates/password-reset.txt deleted file mode 100644 index 1f28794a..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset.txt +++ /dev/null @@ -1,44 +0,0 @@ -🐍 {{ companyName }} - Password Reset Request -=============================================== - -Hello {{ userName }}! - -We received a request to reset your password for your {{ companyName }} account. - -If you made this request, use the link below to reset your password: - -PASSWORD RESET LINK: -{{ resetUrl }} - -RESET TOKEN: -{{ resetToken }} - -You can use either the link above or the reset token to reset your password. - -IMPORTANT SECURITY INFORMATION: -🚹 This password reset link will expire in 1 hour for your security -🚹 If you didn't request a password reset, please ignore this email and your password will remain unchanged -🚹 Never share your reset token with anyone -🚹 {{ companyName }} will never ask for your password via email - -SECURITY TIPS FOR YOUR NEW PASSWORD: -💡 Use at least 8 characters -💡 Include uppercase and lowercase letters -💡 Add numbers and special characters -💡 Don't reuse passwords from other accounts -💡 Consider using a password manager - -DIDN'T REQUEST THIS RESET? -If you didn't request a password reset, your account is still secure. You can safely ignore this email. -However, if you're concerned about unauthorized access, please contact our support team immediately. - -NEED HELP? -If you have security concerns or need assistance, contact our support team at {{ supportEmail }} - -For your security, we may ask you to verify your identity when contacting support. - ---- -This email was sent by {{ companyName }} Security Team -This is an automated message, please do not reply to this email. - -© 2025 {{ companyName }}. All rights reserved. diff --git a/SerpentRace_Backend/src/Templates/verification-de.html b/SerpentRace_Backend/src/Templates/verification-de.html deleted file mode 100644 index 32459a20..00000000 --- a/SerpentRace_Backend/src/Templates/verification-de.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - SerpentRace - Konto verifizieren - - - - - - diff --git a/SerpentRace_Backend/src/Templates/verification-de.txt b/SerpentRace_Backend/src/Templates/verification-de.txt deleted file mode 100644 index ea50afb8..00000000 --- a/SerpentRace_Backend/src/Templates/verification-de.txt +++ /dev/null @@ -1,36 +0,0 @@ -🐍 {{ companyName }} - Konto verifizieren -=============================================== - -Hallo {{ userName }}! - -Willkommen bei {{ companyName }}! Wir freuen uns, dass Sie unserer Gaming-Community beigetreten sind. - -Um Ihre Registrierung abzuschließen und Ihr Konto zu nutzen, verifizieren Sie bitte Ihre E-Mail-Adresse. - -VERIFIZIERUNGSLINK: -{{ verificationUrl }} - -VERIFIZIERUNGSTOKEN: -{{ verificationToken }} - -Sie können entweder den obigen Link oder das Verifizierungstoken verwenden, um Ihr Konto zu verifizieren. - -SICHERHEITSHINWEIS: -⚠ Dieser Verifizierungslink lĂ€uft in 24 Stunden ab -⚠ Falls Sie kein Konto bei {{ companyName }} erstellt haben, ignorieren Sie diese E-Mail bitte -⚠ Teilen Sie Ihren Verifizierungstoken niemals mit anderen - -Nach der Verifizierung können Sie: -✹ Ihre Spieldecks erstellen und verwalten -🎼 An Gaming-Turnieren und Wettbewerben teilnehmen -đŸ‘„ Sich mit anderen Spielern in Ihrer Organisation verbinden -📊 Ihre Gaming-Statistiken und Fortschritte verfolgen - -BENÖTIGEN SIE HILFE? -Falls Sie Fragen haben oder auf Probleme stoßen, kontaktieren Sie bitte unser Support-Team unter {{ supportEmail }} - ---- -Diese E-Mail wurde von {{ companyName }} gesendet -Dies ist eine automatische Nachricht, bitte antworten Sie nicht auf diese E-Mail. - -© 2025 {{ companyName }}. Alle Rechte vorbehalten. diff --git a/SerpentRace_Backend/src/Templates/verification-hu.html b/SerpentRace_Backend/src/Templates/verification-hu.html deleted file mode 100644 index e6cc0214..00000000 --- a/SerpentRace_Backend/src/Templates/verification-hu.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - SerpentRace - FiĂłk megerƑsĂ­tĂ©se - - - - - - diff --git a/SerpentRace_Backend/src/Templates/verification-hu.txt b/SerpentRace_Backend/src/Templates/verification-hu.txt deleted file mode 100644 index 4519a90a..00000000 --- a/SerpentRace_Backend/src/Templates/verification-hu.txt +++ /dev/null @@ -1,36 +0,0 @@ -🐍 {{ companyName }} - FiĂłk megerƑsĂ­tĂ©se -=============================================== - -ÜdvözöljĂŒk {{ userName }}! - -ÜdvözöljĂŒk a {{ companyName }} közössĂ©gĂ©ben! ÖrĂŒlĂŒnk, hogy csatlakozott hozzĂĄnk jĂĄtĂ©kosközössĂ©gĂŒnkhöz. - -A regisztrĂĄciĂł befejezĂ©sĂ©hez Ă©s fiĂłkja hasznĂĄlatbavĂ©telĂ©hez kĂ©rjĂŒk, erƑsĂ­tse meg e-mail cĂ­mĂ©t. - -MEGERƐSÍTÉSI LINK: -{{ verificationUrl }} - -MEGERƐSÍTÉSI TOKEN: -{{ verificationToken }} - -HasznĂĄlhatja a fenti linket vagy a megerƑsĂ­tĂ©si tokent fiĂłkja megerƑsĂ­tĂ©sĂ©hez. - -BIZTONSÁGI FIGYELMEZTETÉS: -⚠ Ez a megerƑsĂ­tĂ©si link 24 Ăłra mĂșlva lejĂĄr -⚠ Ha Ön nem hozott lĂ©tre fiĂłkot a {{ companyName }}-nĂĄl, kĂ©rjĂŒk, hagyja figyelmen kĂ­vĂŒl ezt az e-mailt -⚠ Soha ne ossza meg a megerƑsĂ­tĂ©si tokenjĂ©t senkivel - -A megerƑsĂ­tĂ©s utĂĄn lehetƑsĂ©ge lesz: -✹ JĂĄtĂ©kcsomagok lĂ©trehozĂĄsĂĄra Ă©s kezelĂ©sĂ©re -🎼 JĂĄtĂ©kversenyeken Ă©s bajnoksĂĄgokon valĂł rĂ©szvĂ©telre -đŸ‘„ KapcsolatfelvĂ©telre szervezetĂ©ben lĂ©vƑ mĂĄs jĂĄtĂ©kosokkal -📊 JĂĄtĂ©kstatisztikĂĄk Ă©s fejlƑdĂ©s nyomon követĂ©sĂ©re - -SEGÍTSÉGRE VAN SZÜKSÉGE? -Ha kĂ©rdĂ©sei vannak vagy problĂ©mĂĄkba ĂŒtközik, kĂ©rjĂŒk, lĂ©pjen kapcsolatba ĂŒgyfĂ©lszolgĂĄlatunkkal: {{ supportEmail }} - ---- -Ezt az e-mailt a {{ companyName }} kĂŒldte -Ez egy automatikus ĂŒzenet, kĂ©rjĂŒk, ne vĂĄlaszoljon erre az e-mailre. - -© 2025 {{ companyName }}. Minden jog fenntartva. diff --git a/SerpentRace_Backend/src/Templates/verification.html b/SerpentRace_Backend/src/Templates/verification.html deleted file mode 100644 index f5cb0500..00000000 --- a/SerpentRace_Backend/src/Templates/verification.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - SerpentRace - Verify Your Account - - - - - - diff --git a/SerpentRace_Backend/src/Templates/verification.txt b/SerpentRace_Backend/src/Templates/verification.txt deleted file mode 100644 index 3986b3a6..00000000 --- a/SerpentRace_Backend/src/Templates/verification.txt +++ /dev/null @@ -1,36 +0,0 @@ -🐍 {{ companyName }} - Account Verification -=============================================== - -Hello {{ userName }}! - -Welcome to {{ companyName }}! We're excited to have you join our gaming community. - -To complete your registration and start using your account, please verify your email address. - -VERIFICATION LINK: -{{ verificationUrl }} - -VERIFICATION TOKEN: -{{ verificationToken }} - -You can use either the link above or the verification token to verify your account. - -SECURITY NOTICE: -⚠ This verification link will expire in 24 hours -⚠ If you didn't create an account with {{ companyName }}, please ignore this email -⚠ Never share your verification token with anyone - -Once verified, you'll be able to: -✹ Create and manage your game decks -🎼 Join gaming tournaments and competitions -đŸ‘„ Connect with other players in your organization -📊 Track your gaming statistics and progress - -NEED HELP? -If you have any questions or encounter issues, please contact our support team at {{ supportEmail }} - ---- -This email was sent by {{ companyName }} -This is an automated message, please do not reply to this email. - -© 2025 {{ companyName }}. All rights reserved. diff --git a/SerpentRace_Backend/test-org-auth.js b/SerpentRace_Backend/test-org-auth.js deleted file mode 100644 index 4e627ad2..00000000 --- a/SerpentRace_Backend/test-org-auth.js +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script for Organization Authentication functionality - * This script tests the new organization authentication features: - * 1. Get organization login URL - * 2. Process third-party authentication callback - * 3. Login with organization reauthentication check - */ - -const { container } = require('./dist/Application/Services/DIContainer.js'); - -async function testOrganizationAuth() { - console.log('đŸ§Ș Testing Organization Authentication Functionality\n'); - - try { - // Test 1: Get Organization Login URL - console.log('1ïžâƒŁ Testing Get Organization Login URL Query Handler'); - const getUrlHandler = container.getOrganizationLoginUrlQueryHandler; - console.log('✅ Handler instantiated successfully'); - - // Test 2: Process Organization Auth Callback - console.log('2ïžâƒŁ Testing Process Organization Auth Callback Command Handler'); - const callbackHandler = container.processOrgAuthCallbackCommandHandler; - console.log('✅ Handler instantiated successfully'); - - // Test 3: Enhanced Login Handler with Organization Repository - console.log('3ïžâƒŁ Testing Enhanced Login Handler'); - const loginHandler = container.loginCommandHandler; - console.log('✅ Enhanced login handler instantiated successfully'); - - console.log('\n🎉 All Organization Authentication components initialized successfully!'); - console.log('\n📋 Summary of new functionality:'); - console.log(' ‱ GET /api/organizations/:orgId/login-url - Get organization third-party login URL'); - console.log(' ‱ POST /api/organizations/auth-callback - Process third-party authentication result'); - console.log(' ‱ Enhanced login response includes organization reauthentication requirements'); - console.log(' ‱ Users must reauthenticate with organization if last login > 1 month ago'); - - } catch (error) { - console.error('❌ Error testing organization authentication:', error.message); - process.exit(1); - } -} - -// Run the test -testOrganizationAuth(); diff --git a/SerpentRace_Backend/tests/Application/Chat/ChatMessagingSystem.test.ts b/SerpentRace_Backend/tests/Application/Chat/ChatMessagingSystem.test.ts deleted file mode 100644 index 1f7ee565..00000000 --- a/SerpentRace_Backend/tests/Application/Chat/ChatMessagingSystem.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; -import { AppDataSource } from '../../../src/Infrastructure/ormconfig'; -import { ChatRepository } from '../../../src/Infrastructure/Repository/ChatRepository'; -import { ChatArchiveRepository } from '../../../src/Infrastructure/Repository/ChatArchiveRepository'; -import { UserRepository } from '../../../src/Infrastructure/Repository/UserRepository'; -import { ChatType } from '../../../src/Domain/Chat/ChatAggregate'; -import { UserState } from '../../../src/Domain/User/UserAggregate'; -import { v4 as uuidv4 } from 'uuid'; - -describe('Chat Messaging System', () => { - let chatRepository: ChatRepository; - let chatArchiveRepository: ChatArchiveRepository; - let userRepository: UserRepository; - - let testUser1: any; - let testUser2: any; - let testPremiumUser: any; - - beforeAll(async () => { - if (!AppDataSource.isInitialized) { - await AppDataSource.initialize(); - } - - chatRepository = new ChatRepository(); - chatArchiveRepository = new ChatArchiveRepository(); - userRepository = new UserRepository(); - }); - - beforeEach(async () => { - // Create test users - testUser1 = await userRepository.create({ - username: `testuser1_${Date.now()}`, - email: `test1_${Date.now()}@example.com`, - password: 'hashedpassword', - fname: 'Test', - lname: 'User1', - type: 'regular', - state: UserState.VERIFIED_REGULAR - }); - - testUser2 = await userRepository.create({ - username: `testuser2_${Date.now()}`, - email: `test2_${Date.now()}@example.com`, - password: 'hashedpassword', - fname: 'Test', - lname: 'User2', - type: 'regular', - state: UserState.VERIFIED_REGULAR - }); - - testPremiumUser = await userRepository.create({ - username: `premiumuser_${Date.now()}`, - email: `premium_${Date.now()}@example.com`, - password: 'hashedpassword', - fname: 'Premium', - lname: 'User', - type: 'premium', - state: UserState.VERIFIED_PREMIUM - }); - }); - - afterAll(async () => { - if (AppDataSource.isInitialized) { - await AppDataSource.destroy(); - } - }); - - describe('Direct Chat Creation', () => { - it('should create a direct chat between two users', async () => { - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - expect(chat).toBeDefined(); - expect(chat.type).toBe(ChatType.DIRECT); - expect(chat.users).toEqual([testUser1.id, testUser2.id]); - expect(chat.messages).toEqual([]); - }); - }); - - describe('Group Chat Creation', () => { - it('should create a group chat', async () => { - const chat = await chatRepository.create({ - type: ChatType.GROUP, - name: 'Test Group', - createdBy: testPremiumUser.id, - users: [testPremiumUser.id, testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - expect(chat).toBeDefined(); - expect(chat.type).toBe(ChatType.GROUP); - expect(chat.name).toBe('Test Group'); - expect(chat.createdBy).toBe(testPremiumUser.id); - expect(chat.users.length).toBe(3); - }); - }); - - describe('Game Chat Creation', () => { - it('should create a game chat', async () => { - const gameId = uuidv4(); - - const chat = await chatRepository.create({ - type: ChatType.GAME, - name: 'Test Game Chat', - gameId: gameId, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - expect(chat).toBeDefined(); - expect(chat.type).toBe(ChatType.GAME); - expect(chat.gameId).toBe(gameId); - expect(chat.name).toBe('Test Game Chat'); - }); - - it('should find game chat by game id', async () => { - const gameId = uuidv4(); - - await chatRepository.create({ - type: ChatType.GAME, - name: 'Test Game Chat', - gameId: gameId, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const foundChat = await chatRepository.findByGameId(gameId); - expect(foundChat).toBeDefined(); - expect(foundChat!.gameId).toBe(gameId); - }); - }); - - describe('Message Management', () => { - it('should add and retrieve messages', async () => { - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const message = { - id: uuidv4(), - date: new Date(), - userid: testUser1.id, - text: 'Hello, this is a test message!' - }; - - await chatRepository.update(chat.id, { - messages: [message], - lastActivity: new Date() - }); - - const updatedChat = await chatRepository.findById(chat.id); - expect(updatedChat!.messages).toHaveLength(1); - expect(updatedChat!.messages[0].text).toBe('Hello, this is a test message!'); - expect(updatedChat!.messages[0].userid).toBe(testUser1.id); - }); - }); - - describe('Chat Archiving', () => { - it('should archive a chat with messages', async () => { - const message = { - id: uuidv4(), - date: new Date(), - userid: testUser1.id, - text: 'Message to be archived' - }; - - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [message], - lastActivity: new Date() - }); - - const archive = await chatRepository.archiveChat(chat); - - expect(archive).toBeDefined(); - expect(archive.chatId).toBe(chat.id); - expect(archive.archivedMessages).toHaveLength(1); - expect(archive.archivedMessages[0].text).toBe('Message to be archived'); - - // Check that chat messages were cleared - const archivedChat = await chatRepository.findById(chat.id); - expect(archivedChat!.messages).toEqual([]); - expect(archivedChat!.archiveDate).toBeDefined(); - }); - - it('should retrieve archived chat', async () => { - const message = { - id: uuidv4(), - date: new Date(), - userid: testUser1.id, - text: 'Archived message' - }; - - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [message], - lastActivity: new Date() - }); - - await chatRepository.archiveChat(chat); - - const archive = await chatRepository.getArchivedChat(chat.id); - expect(archive).toBeDefined(); - expect(archive!.archivedMessages).toHaveLength(1); - expect(archive!.archivedMessages[0].text).toBe('Archived message'); - }); - }); - - describe('Chat Queries', () => { - it('should find chats by user id', async () => { - const chat1 = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const chat2 = await chatRepository.create({ - type: ChatType.GROUP, - name: 'Test Group', - createdBy: testPremiumUser.id, - users: [testPremiumUser.id, testUser1.id], - messages: [], - lastActivity: new Date() - }); - - const userChats = await chatRepository.findByUserId(testUser1.id); - expect(userChats.length).toBeGreaterThanOrEqual(2); - - const chatIds = userChats.map(c => c.id); - expect(chatIds).toContain(chat1.id); - expect(chatIds).toContain(chat2.id); - }); - - it('should find active chats for user', async () => { - await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const activeChats = await chatRepository.findActiveChatsForUser(testUser1.id); - expect(activeChats.length).toBeGreaterThanOrEqual(1); - - // All returned chats should be active - activeChats.forEach(chat => { - expect(chat.users).toContain(testUser1.id); - }); - }); - - it('should find inactive chats', async () => { - const oldDate = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago - - await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: oldDate - }); - - const inactiveChats = await chatRepository.findInactiveChats(60); // 60 minutes - expect(inactiveChats.length).toBeGreaterThanOrEqual(1); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Contact/commands/ContactCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Contact/commands/ContactCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 734298e8..00000000 --- a/SerpentRace_Backend/tests/Application/Contact/commands/ContactCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { CreateContactCommandHandler } from '../../../../src/Application/Contact/commands/CreateContactCommandHandler'; -import { UpdateContactCommandHandler } from '../../../../src/Application/Contact/commands/UpdateContactCommandHandler'; -import { DeleteContactCommandHandler } from '../../../../src/Application/Contact/commands/DeleteContactCommandHandler'; -import { CreateContactCommand } from '../../../../src/Application/Contact/commands/CreateContactCommand'; -import { UpdateContactCommand } from '../../../../src/Application/Contact/commands/UpdateContactCommand'; -import { DeleteContactCommand } from '../../../../src/Application/Contact/commands/DeleteContactCommand'; -import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate'; -import { createMockContactRepository, createMockContact } from '../../../testUtils'; - -describe('Contact Command Handlers - Comprehensive', () => { - let mockContactRepository: ReturnType; - - beforeEach(() => { - mockContactRepository = createMockContactRepository(); - }); - - describe('CreateContactCommandHandler', () => { - let handler: CreateContactCommandHandler; - - beforeEach(() => { - handler = new CreateContactCommandHandler(mockContactRepository); - }); - - it('should create contact successfully with all fields', async () => { - // Arrange - const mockContactData = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'John Doe', - email: 'john@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'Test question', - state: ContactState.ACTIVE - }); - - mockContactRepository.create.mockResolvedValue(mockContactData); - - const command: CreateContactCommand = { - name: 'John Doe', - email: 'john@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'Test question' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns ShortContactDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'John Doe', - email: 'john@example.com', - type: ContactType.QUESTION, - state: ContactState.ACTIVE, - createDate: expect.any(Date) - }); - expect(mockContactRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'John Doe', - email: 'john@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'Test question', - state: ContactState.ACTIVE - }) - ); - }); - - it('should create contact without userid (anonymous)', async () => { - // Arrange - const mockContactData = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Anonymous User', - email: 'anon@example.com', - userid: null, - type: ContactType.BUG, - txt: 'Bug report', - state: ContactState.ACTIVE - }); - - mockContactRepository.create.mockResolvedValue(mockContactData); - - const command: CreateContactCommand = { - name: 'Anonymous User', - email: 'anon@example.com', - type: ContactType.BUG, - txt: 'Bug report' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Anonymous User', - email: 'anon@example.com', - type: ContactType.BUG, - state: ContactState.ACTIVE, - createDate: expect.any(Date) - }); - expect(mockContactRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - userid: null - }) - ); - }); - - it('should create contact with different contact types', async () => { - const testCases = [ - { type: ContactType.BUG, description: 'Bug report' }, - { type: ContactType.PROBLEM, description: 'Problem report' }, - { type: ContactType.QUESTION, description: 'Question' }, - { type: ContactType.SALES, description: 'Sales inquiry' }, - { type: ContactType.OTHER, description: 'Other inquiry' } - ]; - - for (const testCase of testCases) { - // Arrange - const mockContactData = createMockContact({ - type: testCase.type, - txt: testCase.description - }); - - mockContactRepository.create.mockResolvedValue(mockContactData); - - const command: CreateContactCommand = { - name: 'Test User', - email: 'test@example.com', - type: testCase.type, - txt: testCase.description - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result.type).toBe(testCase.type); - expect(mockContactRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - txt: testCase.description - }) - ); - } - }); - - it('should handle database errors', async () => { - // Arrange - const command: CreateContactCommand = { - name: 'Error User', - email: 'error@example.com', - type: ContactType.QUESTION, - txt: 'This will cause an error' - }; - - mockContactRepository.create.mockRejectedValue(new Error('Database error')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create contact'); - }); - - it('should handle non-Error exceptions', async () => { - // Arrange - const command: CreateContactCommand = { - name: 'Exception User', - email: 'exception@example.com', - type: ContactType.QUESTION, - txt: 'This will cause an exception' - }; - - mockContactRepository.create.mockRejectedValue('String error'); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create contact'); - }); - }); - - describe('UpdateContactCommandHandler', () => { - let handler: UpdateContactCommandHandler; - - beforeEach(() => { - handler = new UpdateContactCommandHandler(mockContactRepository); - }); - - it('should update contact with admin response', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000', - adminResponse: null, - state: ContactState.ACTIVE - }); - - const updatedContact = createMockContact({ - ...existingContact, - adminResponse: 'Thank you for your inquiry', - state: ContactState.RESOLVED, - responseDate: new Date(), - respondedBy: 'admin123' - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.update.mockResolvedValue(updatedContact); - - const command: UpdateContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - adminResponse: 'Thank you for your inquiry' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns DetailContactDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: expect.any(String), - email: expect.any(String), - userid: expect.any(String), - type: expect.any(Number), - txt: expect.any(String), - state: ContactState.RESOLVED, - createDate: expect.any(Date), - updateDate: expect.any(Date), - adminResponse: 'Thank you for your inquiry', - responseDate: expect.any(Date), - respondedBy: 'admin123' - }); - }); - - it('should update contact state', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000', - state: ContactState.ACTIVE - }); - - const updatedContact = createMockContact({ - ...existingContact, - state: ContactState.RESOLVED - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.update.mockResolvedValue(updatedContact); - - const command: UpdateContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - state: ContactState.RESOLVED - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result.state).toBe(ContactState.RESOLVED); - }); - - it('should throw error when contact not found', async () => { - // Arrange - mockContactRepository.findById.mockResolvedValue(null); - - const command: UpdateContactCommand = { - id: 'non-existent-id', - adminResponse: 'Response' - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Contact not found'); - }); - - it('should handle repository errors during update', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.update.mockRejectedValue(new Error('Database error')); - - const command: UpdateContactCommand = { - id: 'existing-id', - adminResponse: 'Response' - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to update contact'); - }); - }); - - describe('DeleteContactCommandHandler', () => { - let handler: DeleteContactCommandHandler; - - beforeEach(() => { - handler = new DeleteContactCommandHandler(mockContactRepository); - }); - - it('should perform soft delete successfully', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000' - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.softDelete.mockResolvedValue(null); - - const command: DeleteContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - hard: false - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockContactRepository.findById).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockContactRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockContactRepository.delete).not.toHaveBeenCalled(); - }); - - it('should perform hard delete successfully', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000' - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.delete.mockResolvedValue(true); - - const command: DeleteContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - hard: true - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockContactRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockContactRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should default to soft delete when hard flag not specified', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.softDelete.mockResolvedValue(null); - - const command: DeleteContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockContactRepository.softDelete).toHaveBeenCalled(); - expect(mockContactRepository.delete).not.toHaveBeenCalled(); - }); - - it('should throw error when contact not found', async () => { - // Arrange - mockContactRepository.findById.mockResolvedValue(null); - - const command: DeleteContactCommand = { - id: 'non-existent-id', - hard: false - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Contact not found'); - }); - - it('should handle repository errors during deletion', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.softDelete.mockRejectedValue(new Error('Database error')); - - const command: DeleteContactCommand = { - id: 'existing-id', - hard: false - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact'); - }); - - it('should handle hard delete repository errors', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.delete.mockRejectedValue(new Error('Database error')); - - const command: DeleteContactCommand = { - id: 'existing-id', - hard: true - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/ContactMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/ContactMapper.test.ts deleted file mode 100644 index 0c8110c9..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/ContactMapper.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ContactMapper } from '../../../../src/Application/DTOs/Mappers/ContactMapper'; -import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate'; - -describe('ContactMapper', () => { - const createMockContact = (overrides: any = {}) => ({ - id: 'contact-123', - name: 'John Doe', - email: 'john.doe@example.com', - userid: 'user-456', - type: ContactType.QUESTION, - txt: 'This is a test contact message.', - state: ContactState.ACTIVE, - createDate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - adminResponse: null, - responseDate: null, - respondedBy: null, - ...overrides - }); - - describe('toShortDto', () => { - it('should map ContactAggregate to ShortContactDto correctly', () => { - // Arrange - const contact = createMockContact(); - - // Act - const result = ContactMapper.toShortDto(contact); - - // Assert - expect(result).toEqual({ - id: 'contact-123', - name: 'John Doe', - email: 'john.doe@example.com', - type: ContactType.QUESTION, - createDate: new Date('2024-01-01'), - state: ContactState.ACTIVE, - }); - }); - - it('should handle different contact types', () => { - // Arrange - const bugContact = createMockContact({ - id: 'bug-contact', - type: ContactType.BUG, - name: 'Bug Reporter' - }); - - // Act - const result = ContactMapper.toShortDto(bugContact); - - // Assert - expect(result.type).toBe(ContactType.BUG); - expect(result.name).toBe('Bug Reporter'); - }); - }); - - describe('toDetailDto', () => { - it('should map ContactAggregate to DetailContactDto correctly', () => { - // Arrange - const contact = createMockContact(); - - // Act - const result = ContactMapper.toDetailDto(contact); - - // Assert - expect(result).toEqual({ - id: 'contact-123', - name: 'John Doe', - email: 'john.doe@example.com', - userid: 'user-456', - type: ContactType.QUESTION, - txt: 'This is a test contact message.', - state: ContactState.ACTIVE, - createDate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - adminResponse: null, - responseDate: null, - respondedBy: null, - }); - }); - - it('should handle contact with admin response', () => { - // Arrange - const respondedContact = createMockContact({ - adminResponse: 'Thank you for your question. Here is the answer...', - responseDate: new Date('2024-01-03'), - respondedBy: 'admin-789' - }); - - // Act - const result = ContactMapper.toDetailDto(respondedContact); - - // Assert - expect(result.adminResponse).toBe('Thank you for your question. Here is the answer...'); - expect(result.responseDate).toEqual(new Date('2024-01-03')); - expect(result.respondedBy).toBe('admin-789'); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of ContactAggregate to array of ShortContactDto', () => { - // Arrange - const contacts = [ - createMockContact({ id: 'contact-1', name: 'First Contact' }), - createMockContact({ id: 'contact-2', name: 'Second Contact', type: ContactType.BUG }), - createMockContact({ id: 'contact-3', name: 'Third Contact', type: ContactType.SALES }) - ]; - - // Act - const result = ContactMapper.toShortDtoList(contacts); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'contact-1', - name: 'First Contact', - email: 'john.doe@example.com', - type: ContactType.QUESTION, - createDate: new Date('2024-01-01'), - state: ContactState.ACTIVE, - }); - expect(result[1].type).toBe(ContactType.BUG); - expect(result[2].type).toBe(ContactType.SALES); - }); - - it('should handle empty array', () => { - // Arrange - const contacts: any[] = []; - - // Act - const result = ContactMapper.toShortDtoList(contacts); - - // Assert - expect(result).toEqual([]); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/DeckMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/DeckMapper.test.ts deleted file mode 100644 index 35a98110..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/DeckMapper.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { DeckMapper } from '../../../../src/Application/DTOs/Mappers/DeckMapper'; -import { Type, CType, State } from '../../../../src/Domain/Deck/DeckAggregate'; - -describe('DeckMapper', () => { - const createMockDeck = (overrides: any = {}) => ({ - id: 'deck-123', - name: 'Test Deck', - type: Type.LUCK, - userid: 'user-123', - creationdate: new Date('2024-01-01'), - cards: [ - { text: 'Test card 1', answer: 'Answer 1' }, - { text: 'Test card 2' } - ], - playedNumber: 5, - ctype: CType.PUBLIC, - updateDate: new Date('2024-01-02'), - state: State.ACTIVE, - organization: null, - user: { username: 'testuser', id: 'user-123', isAdmin: false }, - isEditable: jest.fn().mockReturnValue(true), - ...overrides - }); - - describe('toShortDto', () => { - it('should map DeckAggregate to ShortDeckDto correctly', () => { - // Arrange - const deck = createMockDeck(); - - // Act - const result = DeckMapper.toShortDto(deck); - - // Assert - expect(result).toEqual({ - id: 'deck-123', - name: 'Test Deck', - type: Type.LUCK, - playedNumber: 5, - ctype: CType.PUBLIC - }); - }); - - it('should handle different deck types', () => { - // Arrange - const jokeDeck = createMockDeck({ - id: 'joker-deck', - name: 'Joker Deck', - type: Type.JOKER, - playedNumber: 10 - }); - - // Act - const result = DeckMapper.toShortDto(jokeDeck); - - // Assert - expect(result.type).toBe(Type.JOKER); - expect(result.playedNumber).toBe(10); - }); - - it('should handle private decks', () => { - // Arrange - const privateDeck = createMockDeck({ - ctype: CType.PRIVATE, - playedNumber: 0 - }); - - // Act - const result = DeckMapper.toShortDto(privateDeck); - - // Assert - expect(result.ctype).toBe(CType.PRIVATE); - expect(result.playedNumber).toBe(0); - }); - }); - - describe('toDetailDto', () => { - it('should map DeckAggregate to DetailDeckDto correctly', () => { - // Arrange - const deck = createMockDeck(); - - // Act - const result = DeckMapper.toDetailDto(deck); - - // Assert - expect(result).toEqual({ - id: 'deck-123', - name: 'Test Deck', - type: Type.LUCK, - userid: 'user-123', - creationdate: new Date('2024-01-01'), - cards: [ - { text: 'Test card 1', answer: 'Answer 1' }, - { text: 'Test card 2' } - ], - playedNumber: 5, - ctype: CType.PUBLIC - }); - }); - - it('should handle empty cards array', () => { - // Arrange - const deckWithNoCards = createMockDeck({ - cards: [] - }); - - // Act - const result = DeckMapper.toDetailDto(deckWithNoCards); - - // Assert - expect(result.cards).toEqual([]); - }); - - it('should handle question type deck', () => { - // Arrange - const questionDeck = createMockDeck({ - type: Type.QUESTION, - cards: [ - { text: 'Question 1?', answer: 'Answer 1' }, - { text: 'Question 2?', answer: null } - ] - }); - - // Act - const result = DeckMapper.toDetailDto(questionDeck); - - // Assert - expect(result.type).toBe(Type.QUESTION); - expect(result.cards).toHaveLength(2); - expect(result.cards[1].answer).toBeNull(); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of DeckAggregate to array of ShortDeckDto', () => { - // Arrange - const decks = [ - createMockDeck({ id: 'deck-1', name: 'First Deck' }), - createMockDeck({ id: 'deck-2', name: 'Second Deck', type: Type.JOKER }), - createMockDeck({ id: 'deck-3', name: 'Third Deck', ctype: CType.PRIVATE }) - ]; - - // Act - const result = DeckMapper.toShortDtoList(decks); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'deck-1', - name: 'First Deck', - type: Type.LUCK, - playedNumber: 5, - ctype: CType.PUBLIC - }); - expect(result[1].type).toBe(Type.JOKER); - expect(result[2].ctype).toBe(CType.PRIVATE); - }); - - it('should handle empty array', () => { - // Arrange - const decks: any[] = []; - - // Act - const result = DeckMapper.toShortDtoList(decks); - - // Assert - expect(result).toEqual([]); - expect(result).toHaveLength(0); - }); - - it('should handle large arrays', () => { - // Arrange - const decks = Array.from({ length: 50 }, (_, i) => - createMockDeck({ - id: `deck-${i + 1}`, - name: `Deck ${i + 1}`, - playedNumber: i - }) - ); - - // Act - const result = DeckMapper.toShortDtoList(decks); - - // Assert - expect(result).toHaveLength(50); - expect(result[0].playedNumber).toBe(0); - expect(result[49].playedNumber).toBe(49); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/OrganizationMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/OrganizationMapper.test.ts deleted file mode 100644 index 5dad953d..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/OrganizationMapper.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { OrganizationMapper } from '../../../../src/Application/DTOs/Mappers/OrganizationMapper'; -import { OrganizationState, OrganizationStateType } from '../../../../src/Domain/Organization/OrganizationAggregate'; - -describe('OrganizationMapper', () => { - const createMockOrganization = (overrides: any = {}) => ({ - id: 'org-123', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'john@test.org', - state: OrganizationState.ACTIVE as OrganizationStateType, - regdate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - url: 'https://test.org', - userinorg: 5, - maxOrganizationalDecks: 10, - users: [ - { id: 'user-1', name: 'User One' }, - { id: 'user-2', name: 'User Two' } - ], - ...overrides - }); - - describe('toShortDto', () => { - it('should map OrganizationAggregate to ShortOrganizationDto correctly', () => { - // Arrange - const org = createMockOrganization(); - - // Act - const result = OrganizationMapper.toShortDto(org); - - // Assert - expect(result).toEqual({ - id: 'org-123', - name: 'Test Organization', - state: OrganizationState.ACTIVE, - userinorg: 5 - }); - }); - - it('should handle different organization states', () => { - // Arrange - const registeredOrg = createMockOrganization({ - state: OrganizationState.REGISTERED, - userinorg: 0 - }); - - // Act - const result = OrganizationMapper.toShortDto(registeredOrg); - - // Assert - expect(result.state).toBe(OrganizationState.REGISTERED); - expect(result.userinorg).toBe(0); - }); - - it('should handle organization with many users', () => { - // Arrange - const orgWithManyUsers = createMockOrganization({ - userinorg: 100 - }); - - // Act - const result = OrganizationMapper.toShortDto(orgWithManyUsers); - - // Assert - expect(result.userinorg).toBe(100); - }); - }); - - describe('toDetailDto', () => { - it('should map OrganizationAggregate to DetailOrganizationDto correctly', () => { - // Arrange - const org = createMockOrganization(); - - // Act - const result = OrganizationMapper.toDetailDto(org); - - // Assert - expect(result).toEqual({ - id: 'org-123', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'john@test.org', - state: OrganizationState.ACTIVE, - regdate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - url: 'https://test.org', - userinorg: 5, - maxOrganizationalDecks: 10, - users: ['user-1', 'user-2'] - }); - }); - - it('should handle organization without URL', () => { - // Arrange - const orgWithoutUrl = createMockOrganization({ - url: null - }); - - // Act - const result = OrganizationMapper.toDetailDto(orgWithoutUrl); - - // Assert - expect(result.url).toBeNull(); - }); - - it('should handle organization without users', () => { - // Arrange - const orgWithoutUsers = createMockOrganization({ - users: null, - userinorg: 0 - }); - - // Act - const result = OrganizationMapper.toDetailDto(orgWithoutUsers); - - // Assert - expect(result.users).toEqual([]); - expect(result.userinorg).toBe(0); - }); - - it('should handle empty users array', () => { - // Arrange - const orgWithEmptyUsers = createMockOrganization({ - users: [], - userinorg: 0 - }); - - // Act - const result = OrganizationMapper.toDetailDto(orgWithEmptyUsers); - - // Assert - expect(result.users).toEqual([]); - }); - - it('should handle soft deleted organization', () => { - // Arrange - const softDeletedOrg = createMockOrganization({ - state: OrganizationState.SOFT_DELETE - }); - - // Act - const result = OrganizationMapper.toDetailDto(softDeletedOrg); - - // Assert - expect(result.state).toBe(OrganizationState.SOFT_DELETE); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of OrganizationAggregate to array of ShortOrganizationDto', () => { - // Arrange - const orgs = [ - createMockOrganization({ id: 'org-1', name: 'First Org', userinorg: 10 }), - createMockOrganization({ id: 'org-2', name: 'Second Org', state: OrganizationState.REGISTERED }), - createMockOrganization({ id: 'org-3', name: 'Third Org', userinorg: 0 }) - ]; - - // Act - const result = OrganizationMapper.toShortDtoList(orgs); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'org-1', - name: 'First Org', - state: OrganizationState.ACTIVE, - userinorg: 10 - }); - expect(result[1].state).toBe(OrganizationState.REGISTERED); - expect(result[2].userinorg).toBe(0); - }); - - it('should handle empty array', () => { - // Arrange - const orgs: any[] = []; - - // Act - const result = OrganizationMapper.toShortDtoList(orgs); - - // Assert - expect(result).toEqual([]); - expect(result).toHaveLength(0); - }); - - it('should handle large arrays', () => { - // Arrange - const orgs = Array.from({ length: 25 }, (_, i) => - createMockOrganization({ - id: `org-${i + 1}`, - name: `Organization ${i + 1}`, - userinorg: i * 2 - }) - ); - - // Act - const result = OrganizationMapper.toShortDtoList(orgs); - - // Assert - expect(result).toHaveLength(25); - expect(result[0].userinorg).toBe(0); - expect(result[24].userinorg).toBe(48); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/UserMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/UserMapper.test.ts deleted file mode 100644 index f9b4789b..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/UserMapper.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { UserMapper } from '../../../../src/Application/DTOs/Mappers/UserMapper'; -import { UserAggregate, UserState } from '../../../../src/Domain/User/UserAggregate'; -import { createMockUser } from '../../../testUtils'; - -describe('UserMapper', () => { - describe('toShortDto', () => { - it('should map UserAggregate to ShortUserDto correctly', () => { - // Arrange - const user = createMockUser({ - id: 'user-123', - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - state: UserState.VERIFIED_REGULAR - }); - - // Act - const result = UserMapper.toShortDto(user); - - // Assert - expect(result).toEqual({ - id: 'user-123', - username: 'testuser', - state: UserState.VERIFIED_REGULAR, - authLevel: 0 - }); - // Should not contain sensitive information - expect(result).not.toHaveProperty('email'); - expect(result).not.toHaveProperty('password'); - expect(result).not.toHaveProperty('token'); - }); - - it('should map admin user with authLevel 1', () => { - // Arrange - const adminUser = createMockUser({ - id: 'admin-123', - username: 'admin', - email: 'admin@example.com', - fname: 'Admin', - lname: 'User', - state: UserState.ADMIN - }); - - // Act - const result = UserMapper.toShortDto(adminUser); - - // Assert - expect(result).toEqual({ - id: 'admin-123', - username: 'admin', - state: UserState.ADMIN, - authLevel: 1 - }); - }); - }); - - describe('toDetailDto', () => { - it('should map UserAggregate to DetailUserDto correctly', () => { - // Arrange - const user = createMockUser({ - id: 'user-123', - orgid: 'org-456', - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - token: 'verification-token', - type: 'admin', - phone: '+1234567890', - state: UserState.ADMIN - }); - - // Act - const result = UserMapper.toDetailDto(user); - - // Assert - expect(result).toEqual({ - id: 'user-123', - orgid: 'org-456', - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - code: 'verification-token', - type: 'admin', - phone: '+1234567890', - state: UserState.ADMIN - }); - // Should not contain password - expect(result).not.toHaveProperty('password'); - }); - - it('should handle null values correctly', () => { - // Arrange - const user = createMockUser({ - id: 'user-123', - orgid: null, - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - token: null, - type: 'regular', - phone: null, - state: UserState.VERIFIED_REGULAR - }); - - // Act - const result = UserMapper.toDetailDto(user); - - // Assert - expect(result.orgid).toBeNull(); - expect(result.code).toBeNull(); - expect(result.phone).toBeNull(); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of UserAggregate to ShortUserDto array', () => { - // Arrange - const users = [ - createMockUser({ id: 'user-1', username: 'user1', state: UserState.VERIFIED_REGULAR }), - createMockUser({ id: 'user-2', username: 'user2', state: UserState.REGISTERED_NOT_VERIFIED }), - createMockUser({ id: 'user-3', username: 'user3', state: UserState.DEACTIVATED }) - ]; - - // Act - const result = UserMapper.toShortDtoList(users); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'user-1', - username: 'user1', - state: UserState.VERIFIED_REGULAR, - authLevel: 0 - }); - expect(result[1]).toEqual({ - id: 'user-2', - username: 'user2', - state: UserState.REGISTERED_NOT_VERIFIED, - authLevel: 0 - }); - expect(result[2]).toEqual({ - id: 'user-3', - username: 'user3', - state: UserState.DEACTIVATED, - authLevel: 0 - }); - }); - - it('should handle empty array', () => { - // Arrange - const users: UserAggregate[] = []; - - // Act - const result = UserMapper.toShortDtoList(users); - - // Assert - expect(result).toEqual([]); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Deck/commands/CreateDeckCommandHandler.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Deck/commands/CreateDeckCommandHandler.comprehensive.test.ts deleted file mode 100644 index de0111b7..00000000 --- a/SerpentRace_Backend/tests/Application/Deck/commands/CreateDeckCommandHandler.comprehensive.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { CreateDeckCommandHandler } from '../../../../src/Application/Deck/commands/CreateDeckCommandHandler'; -import { CreateDeckCommand } from '../../../../src/Application/Deck/commands/CreateDeckCommand'; -import { IDeckRepository } from '../../../../src/Domain/IRepository/IDeckRepository'; -import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository'; -import { UserState } from '../../../../src/Domain/User/UserAggregate'; -import { Type as DeckType } from '../../../../src/Domain/Deck/DeckAggregate'; -import { createMockDeck, createMockDeckRepository, createMockUserRepository, createMockOrganizationRepository, createMockUser } from '../../../testUtils'; - -describe('CreateDeckCommandHandler', () => { - let handler: CreateDeckCommandHandler; - let mockDeckRepository: jest.Mocked; - let mockUserRepository: jest.Mocked; - let mockOrganizationRepository: jest.Mocked; - - beforeEach(() => { - jest.clearAllMocks(); - - mockDeckRepository = createMockDeckRepository(); - mockUserRepository = createMockUserRepository(); - mockOrganizationRepository = createMockOrganizationRepository(); - - handler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository); - }); - - describe('execute', () => { - it('should successfully create a new deck with valid user', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [{ id: 'card-1', name: 'Test Card' }], - }; - - const mockUser = createMockUser({ - id: command.userid, - state: UserState.VERIFIED_REGULAR, - type: 'user' - }); - - const mockDeck = createMockDeck({ - name: command.name, - type: command.type, - userid: command.userid - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockUserRepository.findById).toHaveBeenCalledWith(command.userid); - expect(mockDeckRepository.create).toHaveBeenCalled(); - }); - - it('should throw error when user not found', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'nonexistent-user', - cards: [], - }; - - mockUserRepository.findById.mockResolvedValue(null); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User not found'); - expect(mockUserRepository.findById).toHaveBeenCalledWith(command.userid); - expect(mockDeckRepository.create).not.toHaveBeenCalled(); - }); - - it('should handle admin user creating unlimited decks', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Admin Deck', - type: DeckType.JOKER, - userid: 'admin-123', - cards: [], - }; - - const mockAdminUser = createMockUser({ - id: command.userid, - state: UserState.VERIFIED_REGULAR, - type: 'admin' - }); - - const mockDeck = createMockDeck({ - name: command.name, - type: command.type, - userid: command.userid - }); - - mockUserRepository.findById.mockResolvedValue(mockAdminUser); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockDeckRepository.countActiveByUserId).toHaveBeenCalled(); // Admin still checks but bypasses limits - }); - - it('should handle repository creation errors', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ id: command.userid }); - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockRejectedValue(new Error('Database error')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Database error'); - }); - - it('should create deck with different types', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Question Deck', - type: DeckType.QUESTION, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ id: command.userid }); - const mockDeck = createMockDeck({ - name: command.name, - type: command.type, - userid: command.userid - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockDeckRepository.create).toHaveBeenCalledWith(expect.objectContaining({ - type: DeckType.QUESTION - })); - }); - - it('should handle empty cards array', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Empty Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ id: command.userid }); - const mockDeck = createMockDeck(command); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - }); - - it('should check deck limits for regular users', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ - id: command.userid, - type: 'user' - }); - const mockDeck = createMockDeck({ userid: command.userid }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - await handler.execute(command); - - // Assert - expect(mockDeckRepository.countActiveByUserId).toHaveBeenCalledWith(command.userid); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 31dd1249..00000000 --- a/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { CreateDeckCommandHandler } from '../../../../src/Application/Deck/commands/CreateDeckCommandHandler'; -import { UpdateDeckCommandHandler } from '../../../../src/Application/Deck/commands/UpdateDeckCommandHandler'; -import { DeleteDeckCommandHandler } from '../../../../src/Application/Deck/commands/DeleteDeckCommandHandler'; -import { CreateDeckCommand } from '../../../../src/Application/Deck/commands/CreateDeckCommand'; -import { UpdateDeckCommand } from '../../../../src/Application/Deck/commands/UpdateDeckCommand'; -import { DeleteDeckCommand } from '../../../../src/Application/Deck/commands/DeleteDeckCommand'; -import { DeckAggregate, State as DeckState, Type as DeckType, CType } from '../../../../src/Domain/Deck/DeckAggregate'; -import { UserAggregate, UserState } from '../../../../src/Domain/User/UserAggregate'; -import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository'; -import { IDeckRepository } from '../../../../src/Domain/IRepository/IDeckRepository'; -import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository'; -import { - createMockUser, - createMockDeck, - createMockUserRepository, - createMockDeckRepository, - createMockOrganizationRepository, - createMockDate -} from '../../../testUtils'; - -describe('Deck Command Handlers - Comprehensive Coverage', () => { - let mockUserRepository: jest.Mocked; - let mockDeckRepository: jest.Mocked; - let mockOrganizationRepository: jest.Mocked; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - mockDeckRepository = createMockDeckRepository(); - mockOrganizationRepository = createMockOrganizationRepository(); - jest.clearAllMocks(); - }); - - describe('CreateDeckCommandHandler', () => { - let handler: CreateDeckCommandHandler; - - beforeEach(() => { - handler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository); - }); - - it('should create a new deck successfully', async () => { - // Arrange - const mockUser = createMockUser({ - id: 'user-123', - state: UserState.VERIFIED_REGULAR - }); - const expectedDeck = createMockDeck({ - id: 'deck-123', - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - ctype: CType.PUBLIC, - state: DeckState.ACTIVE, - cards: [] - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.create.mockResolvedValue(expectedDeck); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeTruthy(); - expect(mockUserRepository.findById).toHaveBeenCalledWith('user-123'); - expect(mockDeckRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123' - }) - ); - }); - - it('should throw error when user not found', async () => { - // Arrange - mockUserRepository.findById.mockResolvedValue(null); - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'nonexistent-user', - cards: [] - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User not found'); - expect(mockUserRepository.findById).toHaveBeenCalledWith('nonexistent-user'); - expect(mockDeckRepository.create).not.toHaveBeenCalled(); - }); - - it('should handle admin users bypassing restrictions', async () => { - // Arrange - const adminUser = createMockUser({ - id: 'admin-123', - type: 'admin', - state: UserState.ADMIN - }); - const expectedDeck = createMockDeck({ - name: 'Admin Deck', - userid: 'admin-123' - }); - - mockUserRepository.findById.mockResolvedValue(adminUser); - mockDeckRepository.create.mockResolvedValue(expectedDeck); - // Don't mock countActiveByUserId - admin should bypass this check - - const command: CreateDeckCommand = { - name: 'Admin Deck', - type: DeckType.JOKER, - userid: 'admin-123', - cards: [] - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeTruthy(); - expect(mockDeckRepository.countActiveByUserId).not.toHaveBeenCalled(); - }); - - it('should handle different deck types', async () => { - // Arrange - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - const expectedDeck = createMockDeck({ - name: 'Question Deck', - type: DeckType.QUESTION, - userid: 'user-123' - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.create.mockResolvedValue(expectedDeck); - mockDeckRepository.countActiveByUserId.mockResolvedValue(2); - - const command: CreateDeckCommand = { - name: 'Question Deck', - type: DeckType.QUESTION, - userid: 'user-123', - cards: [] - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeTruthy(); - expect(mockDeckRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - type: DeckType.QUESTION - }) - ); - }); - - it('should handle repository creation errors', async () => { - // Arrange - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockRejectedValue(new Error('Database connection failed')); - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Database connection failed'); - expect(mockDeckRepository.create).toHaveBeenCalled(); - }); - - it('should handle deck limit restrictions for regular users', async () => { - // Arrange - const mockUser = createMockUser({ - id: 'user-123', - state: UserState.VERIFIED_REGULAR, - type: 'regular' - }); - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(10); // Assuming limit is 10 - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - - // Act & Assert - This should succeed if the limit allows, or fail if over limit - // The exact behavior depends on the business rules in CreateDeckCommandHandler - try { - await handler.execute(command); - // If it succeeds, verify the deck was created - expect(mockDeckRepository.create).toHaveBeenCalled(); - } catch (error) { - // If it fails, verify it's a limit error - expect((error as Error).message).toContain('limit'); - } - }); - }); - - describe('UpdateDeckCommandHandler', () => { - let handler: UpdateDeckCommandHandler; - - beforeEach(() => { - handler = new UpdateDeckCommandHandler(mockDeckRepository); - }); - - it('should update deck successfully', async () => { - // Arrange - const updatedDeck = createMockDeck({ - id: 'deck-123', - name: 'New Name', - ctype: CType.PUBLIC - }); - - mockDeckRepository.update.mockResolvedValue(updatedDeck); - - const command: UpdateDeckCommand = { - id: 'deck-123', - name: 'New Name' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Should return ShortDeckDto format - expect(result).toEqual({ - id: 'deck-123', - name: 'New Name', - type: updatedDeck.type, - playedNumber: updatedDeck.playedNumber, - ctype: updatedDeck.ctype, - }); - expect(mockDeckRepository.update).toHaveBeenCalledWith('deck-123', expect.objectContaining({ - id: 'deck-123', - name: 'New Name' - })); - }); - - it('should return null when deck not found (repository returns null)', async () => { - // Arrange - mockDeckRepository.update.mockResolvedValue(null); - - const command: UpdateDeckCommand = { - id: 'nonexistent-deck', - name: 'New Name' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeNull(); - expect(mockDeckRepository.update).toHaveBeenCalledWith('nonexistent-deck', expect.objectContaining({ - id: 'nonexistent-deck', - name: 'New Name' - })); - }); - - it('should handle partial updates', async () => { - // Arrange - const updatedDeck = createMockDeck({ - id: 'deck-123', - name: 'Original Name', // Name stays the same - ctype: CType.PRIVATE // Only ctype changes - }); - - mockDeckRepository.update.mockResolvedValue(updatedDeck); - - const command: UpdateDeckCommand = { - id: 'deck-123', - ctype: CType.PRIVATE - // Note: name is not provided, should remain unchanged - }; - - // Act - const result = await handler.execute(command); - - // Assert - Should return ShortDeckDto format - expect(result).toEqual({ - id: 'deck-123', - name: 'Original Name', - type: updatedDeck.type, - playedNumber: updatedDeck.playedNumber, - ctype: CType.PRIVATE, - }); - expect(mockDeckRepository.update).toHaveBeenCalledWith('deck-123', expect.objectContaining({ - id: 'deck-123', - ctype: CType.PRIVATE - })); - }); - - it('should handle repository update errors', async () => { - // Arrange - const existingDeck = createMockDeck({ id: 'deck-123' }); - mockDeckRepository.findById.mockResolvedValue(existingDeck); - mockDeckRepository.update.mockRejectedValue(new Error('Update failed')); - - const command: UpdateDeckCommand = { - id: 'deck-123', - name: 'New Name' - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Update failed'); - expect(mockDeckRepository.update).toHaveBeenCalled(); - }); - }); - - describe('DeleteDeckCommandHandler', () => { - let handler: DeleteDeckCommandHandler; - - beforeEach(() => { - handler = new DeleteDeckCommandHandler(mockDeckRepository); - }); - - it('should delete deck successfully (soft delete)', async () => { - // Arrange - mockDeckRepository.softDelete.mockResolvedValue(null); // Soft delete returns void - - const command: DeleteDeckCommand = { - id: 'deck-123', - soft: true // Specify soft delete - }; - - // Act - const result = await handler.execute(command); - - // Assert - DeleteDeckCommandHandler always returns true - expect(result).toBe(true); - expect(mockDeckRepository.softDelete).toHaveBeenCalledWith('deck-123'); - }); - - it('should delete deck successfully (hard delete)', async () => { - // Arrange - mockDeckRepository.delete.mockResolvedValue(null); // Delete returns void - - const command: DeleteDeckCommand = { - id: 'deck-123', - soft: false // Specify hard delete - }; - - // Act - const result = await handler.execute(command); - - // Assert - DeleteDeckCommandHandler always returns true - expect(result).toBe(true); - expect(mockDeckRepository.delete).toHaveBeenCalledWith('deck-123'); - }); - - it('should default to hard delete when soft flag not specified', async () => { - // Arrange - mockDeckRepository.delete.mockResolvedValue(null); - - const command: DeleteDeckCommand = { - id: 'deck-123' - // Note: soft flag not specified, defaults to undefined which is falsy - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockDeckRepository.delete).toHaveBeenCalledWith('deck-123'); - expect(mockDeckRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should handle repository deletion errors', async () => { - // Arrange - mockDeckRepository.softDelete.mockRejectedValue(new Error('Deletion failed')); - - const command: DeleteDeckCommand = { - id: 'deck-123', - soft: true - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Deletion failed'); - expect(mockDeckRepository.softDelete).toHaveBeenCalledWith('deck-123'); - }); - }); - - describe('Cross-Command Integration Tests', () => { - let createHandler: CreateDeckCommandHandler; - let updateHandler: UpdateDeckCommandHandler; - let deleteHandler: DeleteDeckCommandHandler; - - beforeEach(() => { - createHandler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository); - updateHandler = new UpdateDeckCommandHandler(mockDeckRepository); - deleteHandler = new DeleteDeckCommandHandler(mockDeckRepository); - }); - - it('should create deck and then update it', async () => { - // Arrange - Create - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - const createdDeck = createMockDeck({ - id: 'deck-123', - name: 'Initial Name', - userid: 'user-123' - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(createdDeck); - - // Arrange - Update - const updatedDeck = createMockDeck({ - id: 'deck-123', - name: 'Updated Name', - userid: 'user-123' - }); - mockDeckRepository.findById.mockResolvedValue(createdDeck); - mockDeckRepository.update.mockResolvedValue(updatedDeck); - - // Act - Create - const createCommand: CreateDeckCommand = { - name: 'Initial Name', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - const createResult = await createHandler.execute(createCommand); - - // Act - Update - const updateCommand: UpdateDeckCommand = { - id: 'deck-123', - name: 'Updated Name' - }; - const updateResult = await updateHandler.execute(updateCommand); - - // Assert - expect(createResult).toBeTruthy(); - expect(updateResult?.name).toBe('Updated Name'); - expect(mockDeckRepository.create).toHaveBeenCalled(); - expect(mockDeckRepository.update).toHaveBeenCalled(); - }); - - it('should handle full lifecycle: create, update, delete', async () => { - // This tests the complete lifecycle of a deck - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - const deck = createMockDeck({ id: 'deck-123', userid: 'user-123' }); - - // Setup all mocks - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(deck); - mockDeckRepository.update.mockResolvedValue(deck); - mockDeckRepository.softDelete.mockResolvedValue(null); - - // Execute lifecycle - const createResult = await createHandler.execute({ - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }); - - const updateResult = await updateHandler.execute({ - id: 'deck-123', - name: 'Updated Deck' - }); - - const deleteResult = await deleteHandler.execute({ - id: 'deck-123', - soft: true - }); - - // Assert all operations succeeded - expect(createResult).toBeTruthy(); - expect(updateResult).toBeTruthy(); - expect(deleteResult).toBe(true); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Game/BoardGenerationService.test.ts b/SerpentRace_Backend/tests/Application/Game/BoardGenerationService.test.ts deleted file mode 100644 index c3f10169..00000000 --- a/SerpentRace_Backend/tests/Application/Game/BoardGenerationService.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { BoardGenerationService } from '../../../src/Application/Game/BoardGenerationService'; - -// Mock dependencies -jest.mock('../../../src/Application/Services/LoggingService'); - -describe('BoardGenerationService', () => { - let boardGenerationService: BoardGenerationService; - - beforeEach(() => { - boardGenerationService = new BoardGenerationService(); - }); - - describe('generateBoard', () => { - it('should generate a board with the correct number of special fields', async () => { - const positiveFields = 10; - const negativeFields = 8; - const luckFields = 5; - - const result = await boardGenerationService.generateBoard( - positiveFields, - negativeFields, - luckFields - ); - - expect(result).toBeDefined(); - expect(result.fields).toHaveLength(100); - - // Count special fields - const actualPositive = result.fields.filter(f => f.type === 'positive').length; - const actualNegative = result.fields.filter(f => f.type === 'negative').length; - const actualLuck = result.fields.filter(f => f.type === 'luck').length; - - expect(actualPositive).toBe(positiveFields); - expect(actualNegative).toBe(negativeFields); - expect(actualLuck).toBe(luckFields); - }); - - it('should ensure positive fields have positive step values', async () => { - const result = await boardGenerationService.generateBoard(5, 5, 2); - - const positiveFields = result.fields.filter(f => f.type === 'positive'); - positiveFields.forEach(field => { - expect(field.stepValue).toBeGreaterThan(0); - }); - }); - - it('should ensure negative fields have negative step values', async () => { - const result = await boardGenerationService.generateBoard(5, 5, 2); - - const negativeFields = result.fields.filter(f => f.type === 'negative'); - negativeFields.forEach(field => { - expect(field.stepValue).toBeLessThan(0); - }); - }); - - it('should ensure luck fields do not have step values', async () => { - const result = await boardGenerationService.generateBoard(5, 5, 2); - - const luckFields = result.fields.filter(f => f.type === 'luck'); - luckFields.forEach(field => { - expect(field.stepValue).toBeUndefined(); - }); - }); - - it('should produce validation results without -1 values', async () => { - const result = await boardGenerationService.generateBoard(10, 8, 5); - - // Check validation results for invalid moves (-1 values) - let invalidMoves = 0; - let totalMoves = 0; - - Object.values(result.validationResults).forEach(diceOutcomes => { - diceOutcomes.forEach(outcome => { - totalMoves++; - if (outcome === -1) { - invalidMoves++; - } - }); - }); - - const errorRate = totalMoves > 0 ? (invalidMoves / totalMoves) * 100 : 0; - - // Log the results for analysis - console.log(`Error rate: ${errorRate}%`); - console.log(`Invalid moves: ${invalidMoves}/${totalMoves}`); - - // The new algorithm should produce much fewer invalid moves - expect(errorRate).toBeLessThan(50); // Allow some errors but much better than before - }); - - it('should respect the 20-30 movement rule in validation', async () => { - const result = await boardGenerationService.generateBoard(10, 8, 5); - - // Check each validation result to ensure it respects distance rules - Object.entries(result.validationResults).forEach(([fieldPosition, diceOutcomes]) => { - const currentPos = parseInt(fieldPosition); - - diceOutcomes.forEach((outcome, diceIndex) => { - if (outcome !== -1) { // Only check valid moves - const distance = Math.abs(outcome - currentPos); - - if (currentPos <= 85) { - // Fields 1-85: max 20 in any direction - expect(distance).toBeLessThanOrEqual(20); - } else { - // Fields 86-100: max 30 backward, max 20 forward - if (outcome > currentPos) { - expect(distance).toBeLessThanOrEqual(20); // forward - } else { - expect(distance).toBeLessThanOrEqual(30); // backward - } - } - } - }); - }); - }); - - it('should position special fields safely within the safe range', async () => { - const result = await boardGenerationService.generateBoard(10, 8, 5); - - const specialFields = result.fields.filter(f => f.type !== 'regular'); - - // Most special fields should be in the safe range (11-90) for the new algorithm - const safeFields = specialFields.filter(f => f.position >= 11 && f.position <= 90); - const safePercentage = (safeFields.length / specialFields.length) * 100; - - console.log(`Safe field percentage: ${safePercentage}%`); - - // Expect most fields to be positioned safely - expect(safePercentage).toBeGreaterThan(70); - }); - }); -}); \ No newline at end of file diff --git a/SerpentRace_Backend/tests/Application/Organization/commands/OrganizationCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Organization/commands/OrganizationCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 80deda87..00000000 --- a/SerpentRace_Backend/tests/Application/Organization/commands/OrganizationCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { CreateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/CreateOrganizationCommandHandler'; -import { UpdateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommandHandler'; -import { DeleteOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommandHandler'; -import { CreateOrganizationCommand } from '../../../../src/Application/Organization/commands/CreateOrganizationCommand'; -import { UpdateOrganizationCommand } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommand'; -import { DeleteOrganizationCommand } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommand'; -import { OrganizationState } from '../../../../src/Domain/Organization/OrganizationAggregate'; -import { createMockOrganizationRepository, createMockOrganization } from '../../../testUtils'; - -describe('Organization Command Handlers - Comprehensive', () => { - let mockOrganizationRepository: ReturnType; - - beforeEach(() => { - mockOrganizationRepository = createMockOrganizationRepository(); - }); - - describe('CreateOrganizationCommandHandler', () => { - let handler: CreateOrganizationCommandHandler; - - beforeEach(() => { - handler = new CreateOrganizationCommandHandler(mockOrganizationRepository); - }); - - it('should create organization successfully', async () => { - // Arrange - const mockOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'john@testorg.com', - url: null, - state: OrganizationState.REGISTERED - }); - - mockOrganizationRepository.create.mockResolvedValue(mockOrgData); - - const command: CreateOrganizationCommand = { - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@testorg.com', - contactphone: '+1234567890' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns ShortOrganizationDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Test Organization', - state: 0, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - expect(mockOrganizationRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@testorg.com', - contactphone: '+1234567890', - state: OrganizationState.REGISTERED - }) - ); - }); - - it('should create organization with optional URL', async () => { - // Arrange - const mockOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Org with URL', - contactfname: 'Jane', - contactlname: 'Smith', - contactphone: '+1987654321', - contactemail: 'jane@orgwithurl.com', - url: 'https://orgwithurl.com', - state: OrganizationState.REGISTERED - }); - - mockOrganizationRepository.create.mockResolvedValue(mockOrgData); - - const command: CreateOrganizationCommand = { - name: 'Org with URL', - contactfname: 'Jane', - contactlname: 'Smith', - contactemail: 'jane@orgwithurl.com', - contactphone: '+1987654321', - url: 'https://orgwithurl.com' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Org with URL', - state: 0, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - }); - - it('should handle duplicate organization name error', async () => { - // Arrange - const command: CreateOrganizationCommand = { - name: 'Duplicate Org', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@duplicate.com', - contactphone: '+1234567890' - }; - - const duplicateError = new Error('duplicate key value violates unique constraint "organization_name_unique"'); - mockOrganizationRepository.create.mockRejectedValue(duplicateError); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Organization with this name or contact email already exists'); - }); - - it('should handle generic database errors', async () => { - // Arrange - const command: CreateOrganizationCommand = { - name: 'Error Org', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@error.com', - contactphone: '+1234567890' - }; - - mockOrganizationRepository.create.mockRejectedValue(new Error('Database connection failed')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create organization'); - }); - - it('should handle non-Error exceptions', async () => { - // Arrange - const command: CreateOrganizationCommand = { - name: 'Non-Error Exception Org', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@exception.com', - contactphone: '+1234567890' - }; - - mockOrganizationRepository.create.mockRejectedValue('String error'); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create organization'); - }); - }); - - describe('UpdateOrganizationCommandHandler', () => { - let handler: UpdateOrganizationCommandHandler; - - beforeEach(() => { - handler = new UpdateOrganizationCommandHandler(mockOrganizationRepository); - }); - - it('should update organization successfully', async () => { - // Arrange - const updatedOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Updated Organization', - contactemail: 'john@updated.com', - url: 'https://updated.com', - state: OrganizationState.ACTIVE - }); - - mockOrganizationRepository.update.mockResolvedValue(updatedOrgData); - - const command: UpdateOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Updated Organization', - contactemail: 'john@updated.com', - url: 'https://updated.com' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns ShortOrganizationDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Updated Organization', - state: 1, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - expect(mockOrganizationRepository.update).toHaveBeenCalledWith( - '550e8400-e29b-41d4-a716-446655440000', - command - ); - }); - - it('should return null when organization not found', async () => { - // Arrange - mockOrganizationRepository.update.mockResolvedValue(null); - - const command: UpdateOrganizationCommand = { - id: 'non-existent-id', - name: 'Non-existent Organization' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeNull(); - expect(mockOrganizationRepository.update).toHaveBeenCalledWith('non-existent-id', command); - }); - - it('should update organization with partial data', async () => { - // Arrange - const partialUpdatedOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Original Name', - contactemail: 'john@newmail.com', - state: OrganizationState.ACTIVE - }); - - mockOrganizationRepository.update.mockResolvedValue(partialUpdatedOrgData); - - const command: UpdateOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - contactemail: 'john@newmail.com' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Original Name', - state: 1, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - }); - }); - - describe('DeleteOrganizationCommandHandler', () => { - let handler: DeleteOrganizationCommandHandler; - - beforeEach(() => { - handler = new DeleteOrganizationCommandHandler(mockOrganizationRepository); - }); - - it('should perform soft delete successfully', async () => { - // Arrange - mockOrganizationRepository.softDelete.mockResolvedValue(null); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: true - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockOrganizationRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockOrganizationRepository.delete).not.toHaveBeenCalled(); - }); - - it('should perform hard delete successfully', async () => { - // Arrange - mockOrganizationRepository.delete.mockResolvedValue(true); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: false - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should default to hard delete when soft flag not specified', async () => { - // Arrange - mockOrganizationRepository.delete.mockResolvedValue(true); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should handle soft delete with repository error gracefully', async () => { - // Arrange - mockOrganizationRepository.softDelete.mockRejectedValue(new Error('Database error')); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: true - }; - - // Act & Assert - Handler doesn't catch errors, they bubble up - await expect(handler.execute(command)).rejects.toThrow('Database error'); - }); - - it('should handle hard delete with repository error gracefully', async () => { - // Arrange - mockOrganizationRepository.delete.mockRejectedValue(new Error('Database error')); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: false - }; - - // Act & Assert - Handler doesn't catch errors, they bubble up - await expect(handler.execute(command)).rejects.toThrow('Database error'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/AuthMiddleware.test.ts b/SerpentRace_Backend/tests/Application/Services/AuthMiddleware.test.ts deleted file mode 100644 index ddac0cac..00000000 --- a/SerpentRace_Backend/tests/Application/Services/AuthMiddleware.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -// Mock JWTService before importing anything else -const mockJWTService = { - verify: jest.fn(), - refreshIfNeeded: jest.fn(), - create: jest.fn(), - shouldRefreshToken: jest.fn(), - test: jest.fn(), -}; - -jest.mock('../../../src/Application/Services/JWTService', () => { - return { - JWTService: jest.fn().mockImplementation(() => mockJWTService) - }; -}); - -// Now import the middleware which will use the mocked JWTService -import { authRequired, adminRequired } from '../../../src/Application/Services/AuthMiddleware'; - -describe('AuthMiddleware', () => { - let mockRequest: Partial; - let mockResponse: Partial; - let mockNext: NextFunction; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRequest = { - cookies: {} - }; - - mockResponse = { - status: jest.fn().mockReturnThis(), - json: jest.fn().mockReturnThis(), - cookie: jest.fn() - }; - - mockNext = jest.fn(); - }); - - describe('authRequired', () => { - it('should call next() when token is valid', () => { - // Arrange - const validPayload = { - userId: 'user-123', - authLevel: 0 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(validPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(false); // Token doesn't need refresh - - // Act - authRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(validPayload, mockResponse); - expect((mockRequest as any).user).toBe(validPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it('should return 401 when token is invalid', () => { - // Arrange - mockJWTService.verify.mockReturnValue(null); - - // Act - authRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled(); - expect(mockNext).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Unauthorized' }); - }); - - it('should refresh token when needed', () => { - // Arrange - const validPayload = { - userId: 'user-123', - authLevel: 0 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(validPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(true); // Token needs refresh - - // Act - authRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(validPayload, mockResponse); - expect((mockRequest as any).user).toBe(validPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - }); - - describe('adminRequired', () => { - it('should call next() when token is valid and user is admin', () => { - // Arrange - const adminPayload = { - userId: 'admin-123', - authLevel: 1 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(adminPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(false); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(adminPayload, mockResponse); - expect((mockRequest as any).user).toBe(adminPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it('should return 403 when token is invalid', () => { - // Arrange - mockJWTService.verify.mockReturnValue(null); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled(); - expect(mockNext).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Forbidden' }); - }); - - it('should return 403 when user is not admin', () => { - // Arrange - const regularUserPayload = { - userId: 'user-123', - authLevel: 0 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(regularUserPayload); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled(); - expect(mockNext).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Forbidden' }); - }); - - it('should refresh token for valid admin user', () => { - // Arrange - const adminPayload = { - userId: 'admin-123', - authLevel: 1 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(adminPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(true); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(adminPayload, mockResponse); - expect((mockRequest as any).user).toBe(adminPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/ChatConfiguration.test.ts b/SerpentRace_Backend/tests/Application/Services/ChatConfiguration.test.ts deleted file mode 100644 index acde9202..00000000 --- a/SerpentRace_Backend/tests/Application/Services/ChatConfiguration.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { WebSocketService } from '../../../src/Application/Services/WebSocketService'; -import { Server as HttpServer } from 'http'; -import { EventEmitter } from 'events'; - -describe('Chat Configuration', () => { - let mockHttpServer: HttpServer; - - beforeAll(() => { - // Create a more complete HTTP server mock that extends EventEmitter - const httpServerMock = new EventEmitter(); - - // Add necessary methods that Socket.IO expects - Object.assign(httpServerMock, { - on: jest.fn(), - listen: jest.fn(), - close: jest.fn(), - listeners: jest.fn().mockReturnValue([]), - removeListener: jest.fn(), - removeAllListeners: jest.fn(), - setMaxListeners: jest.fn(), - getMaxListeners: jest.fn().mockReturnValue(0), - listenerCount: jest.fn().mockReturnValue(0), - prependListener: jest.fn(), - prependOnceListener: jest.fn(), - off: jest.fn(), - once: jest.fn(), - emit: jest.fn(), - // HTTP server specific - timeout: 0, - keepAliveTimeout: 5000, - maxHeadersCount: null, - headersTimeout: 60000, - requestTimeout: 0 - }); - - mockHttpServer = httpServerMock as unknown as HttpServer; - }); - - afterEach(() => { - // Clean up environment variables - delete process.env.CHAT_MAX_MESSAGES_PER_USER; - delete process.env.CHAT_MESSAGE_CLEANUP_WEEKS; - delete process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES; - }); - - describe('Environment Variable Configuration', () => { - it('should use default chat configuration values', () => { - const service = new WebSocketService(mockHttpServer); - - expect(service['maxMessagesPerUser']).toBe(100); - expect(service['messageCleanupWeeks']).toBe(4); - expect(service['chatTimeout']).toBe(30); - }); - - it('should use environment variable for CHAT_MAX_MESSAGES_PER_USER', () => { - process.env.CHAT_MAX_MESSAGES_PER_USER = '50'; - - const service = new WebSocketService(mockHttpServer); - - expect(service['maxMessagesPerUser']).toBe(50); - }); - - it('should use environment variable for CHAT_MESSAGE_CLEANUP_WEEKS', () => { - // Arrange - process.env.CHAT_MESSAGE_CLEANUP_WEEKS = '8'; - - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - expect(service['messageCleanupWeeks']).toBe(8); - }); - - it('should use environment variable for CHAT_INACTIVITY_TIMEOUT_MINUTES', () => { - // Arrange - process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES = '60'; - - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - expect(service['chatTimeout']).toBe(60); - }); - - it('should handle invalid numeric environment variables gracefully', () => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = 'invalid'; - process.env.CHAT_MESSAGE_CLEANUP_WEEKS = 'also-invalid'; - process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES = 'not-a-number'; - - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - parseInt of invalid strings returns NaN - expect(service['maxMessagesPerUser']).toBe(NaN); - expect(service['messageCleanupWeeks']).toBe(NaN); - expect(service['chatTimeout']).toBe(NaN); - }); - }); - - describe('Rate Limiting Logic', () => { - it('should initialize with empty user message counts', () => { - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - expect(service['userMessageCounts']).toBeDefined(); - expect(service['userMessageCounts'].size).toBe(0); - }); - - it('should allow messages within rate limit', () => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = '5'; - const service = new WebSocketService(mockHttpServer); - const userId = 'test-user'; - - // Act & Assert - should allow first 5 messages - for (let i = 0; i < 5; i++) { - expect(service['checkMessageRateLimit'](userId)).toBe(true); - } - }); - - it('should block messages when rate limit exceeded', () => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = '3'; - const service = new WebSocketService(mockHttpServer); - const userId = 'test-user'; - - // Act - send 3 messages (should be allowed) - for (let i = 0; i < 3; i++) { - expect(service['checkMessageRateLimit'](userId)).toBe(true); - } - - // Assert - 4th message should be blocked - expect(service['checkMessageRateLimit'](userId)).toBe(false); - }); - - it('should reset rate limit after time window', (done) => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = '2'; - const service = new WebSocketService(mockHttpServer); - const userId = 'test-user'; - - // Act - exhaust rate limit - expect(service['checkMessageRateLimit'](userId)).toBe(true); - expect(service['checkMessageRateLimit'](userId)).toBe(true); - expect(service['checkMessageRateLimit'](userId)).toBe(false); // Should be blocked - - // Mock time passage by manipulating the internal state - const userStats = service['userMessageCounts'].get(userId)!; - userStats.lastReset = Date.now() - (60 * 1000 + 1); // More than 1 minute ago - service['userMessageCounts'].set(userId, userStats); - - // Assert - should be allowed again after reset - expect(service['checkMessageRateLimit'](userId)).toBe(true); - done(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/DIContainer.test.ts b/SerpentRace_Backend/tests/Application/Services/DIContainer.test.ts deleted file mode 100644 index 0b174b32..00000000 --- a/SerpentRace_Backend/tests/Application/Services/DIContainer.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { container } from '../../../src/Application/Services/DIContainer'; -import { IUserRepository } from '../../../src/Domain/IRepository/IUserRepository'; -import { IChatRepository } from '../../../src/Domain/IRepository/IChatRepository'; -import { LoggingService } from '../../../src/Application/Services/LoggingService'; - -describe('DIContainer', () => { - // Cleanup after all tests to prevent Jest hanging - afterAll(async () => { - await LoggingService.getInstance().shutdown(); - }); - - describe('Repositories', () => { - it('should return singleton IUserRepository instance', () => { - const repo1 = container.userRepository; - const repo2 = container.userRepository; - - expect(repo1).toBeTruthy(); - expect(repo1).toBe(repo2); // Same instance (singleton) - expect(typeof repo1.findById).toBe('function'); // Has interface methods - }); - - it('should return singleton IChatRepository instance', () => { - const repo1 = container.chatRepository; - const repo2 = container.chatRepository; - - expect(repo1).toBeTruthy(); - expect(repo1).toBe(repo2); // Same instance (singleton) - expect(typeof repo1.findById).toBe('function'); // Has interface methods - }); - }); - - describe('Command Handlers', () => { - it('should return singleton CreateUserCommandHandler instance', () => { - const handler1 = container.createUserCommandHandler; - const handler2 = container.createUserCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton LoginCommandHandler instance', () => { - const handler1 = container.loginCommandHandler; - const handler2 = container.loginCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeactivateUserCommandHandler instance', () => { - const handler1 = container.deactivateUserCommandHandler; - const handler2 = container.deactivateUserCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeleteUserCommandHandler instance', () => { - const handler1 = container.deleteUserCommandHandler; - const handler2 = container.deleteUserCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeleteDeckCommandHandler instance', () => { - const handler1 = container.deleteDeckCommandHandler; - const handler2 = container.deleteDeckCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeleteOrganizationCommandHandler instance', () => { - const handler1 = container.deleteOrganizationCommandHandler; - const handler2 = container.deleteOrganizationCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - }); - - describe('Query Handlers', () => { - it('should return singleton GetUserByIdQueryHandler instance', () => { - const handler1 = container.getUserByIdQueryHandler; - const handler2 = container.getUserByIdQueryHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton GetUsersByPageQueryHandler instance', () => { - const handler1 = container.getUsersByPageQueryHandler; - const handler2 = container.getUsersByPageQueryHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - }); - - describe('Services', () => { - it('should return singleton JWTService instance', () => { - const service1 = container.jwtService; - const service2 = container.jwtService; - - expect(service1).toBeTruthy(); - expect(service1).toBe(service2); // Same instance (singleton) - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/EmailService.test.ts b/SerpentRace_Backend/tests/Application/Services/EmailService.test.ts deleted file mode 100644 index d0b1ab3a..00000000 --- a/SerpentRace_Backend/tests/Application/Services/EmailService.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { EmailService, EmailOptions } from '../../../src/Application/Services/EmailService'; -import * as nodemailer from 'nodemailer'; -import * as fs from 'fs'; - -// Mock nodemailer -jest.mock('nodemailer'); -jest.mock('fs'); - -// Mock logger -jest.mock('../../../src/Application/Services/Logger', () => ({ - logError: jest.fn(), - logAuth: jest.fn(), - logStartup: jest.fn(), -})); - -describe('EmailService', () => { - let emailService: EmailService; - let mockTransporter: jest.Mocked; - let mockCreateTransporter: jest.MockedFunction; - - beforeEach(() => { - jest.clearAllMocks(); - - // Mock nodemailer.createTransporter - mockTransporter = { - sendMail: jest.fn(), - } as any; - - mockCreateTransporter = nodemailer.createTransport as jest.MockedFunction; - mockCreateTransporter.mockReturnValue(mockTransporter); - - // Mock fs - (fs.readFileSync as jest.Mock).mockImplementation((filePath: string) => { - if (filePath.includes('html')) { - return 'HTML template: {{name}}'; - } - return 'Text template: {{name}}'; - }); - - (fs.existsSync as jest.Mock).mockReturnValue(true); - - emailService = new EmailService(); - }); - - describe('sendEmail', () => { - it('should send email successfully', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - html: '

Test HTML

', - text: 'Test Text', - }; - - mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(true); - expect(mockTransporter.sendMail).toHaveBeenCalledWith({ - from: process.env.EMAIL_FROM || 'noreply@serpentrace.com', - to: emailOptions.to, - subject: emailOptions.subject, - html: emailOptions.html, - text: emailOptions.text, - attachments: expect.any(Array), - }); - }); - - it('should send email with template', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'verification', - templateData: { name: 'John', token: 'abc123' }, - }; - - mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(true); - expect(mockTransporter.sendMail).toHaveBeenCalledWith({ - from: process.env.EMAIL_FROM || 'noreply@serpentrace.com', - to: emailOptions.to, - subject: emailOptions.subject, - html: expect.any(String), - text: expect.any(String), - attachments: expect.any(Array), - }); - }); - - it('should handle email send failure', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - text: 'Test Text', - }; - - mockTransporter.sendMail.mockRejectedValue(new Error('SMTP Error')); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(false); - }); - - it('should handle missing template files', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'nonexistent', - templateData: { name: 'John' }, - }; - - (fs.existsSync as jest.Mock).mockReturnValue(false); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(false); - }); - - it('should handle template processing errors', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'verification', - templateData: { name: 'John' }, - }; - - (fs.readFileSync as jest.Mock).mockImplementation(() => { - throw new Error('File read error'); - }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(false); - }); - - it('should use fallback content when template data is missing', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'verification', - }; - - mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(true); - }); - }); - - describe('constructor', () => { - it('should initialize with environment variables', () => { - // Arrange - const originalEnv = process.env; - process.env = { - ...originalEnv, - EMAIL_HOST: 'test-smtp.com', - EMAIL_PORT: '465', - EMAIL_SECURE: 'true', - EMAIL_USER: 'test@example.com', - EMAIL_PASS: 'testpass', - EMAIL_FROM: 'sender@example.com', - }; - - // Act - const service = new EmailService(); - - // Assert - expect(mockCreateTransporter).toHaveBeenCalledWith({ - host: 'test-smtp.com', - port: 465, - secure: true, - auth: { - user: 'test@example.com', - pass: 'testpass', - }, - }); - - // Restore environment - process.env = originalEnv; - }); - - it('should use default values when environment variables are missing', () => { - // Arrange - const originalEnv = process.env; - process.env = {}; - - // Act - const service = new EmailService(); - - // Assert - expect(mockCreateTransporter).toHaveBeenCalledWith({ - host: 'smtp.gmail.com', - port: 587, - secure: false, - auth: { - user: '', - pass: '', - }, - }); - - // Restore environment - process.env = originalEnv; - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/JWTService.refresh.test.ts b/SerpentRace_Backend/tests/Application/Services/JWTService.refresh.test.ts deleted file mode 100644 index b679f25c..00000000 --- a/SerpentRace_Backend/tests/Application/Services/JWTService.refresh.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService'; -import { Request, Response } from 'express'; -import { UserState } from '../../../src/Domain/User/UserAggregate'; - -describe('JWTService - Token Refresh Logic', () => { - let jwtService: JWTService; - let mockRequest: Partial; - let mockResponse: Partial; - let dateNowSpy: jest.SpyInstance; - - beforeEach(() => { - jwtService = new JWTService(); - - mockRequest = { - cookies: {} - }; - - mockResponse = { - cookie: jest.fn() - }; - - // Create a fresh spy for Date.now in each test - dateNowSpy = jest.spyOn(Date, 'now'); - }); - - afterEach(() => { - // Always restore Date.now after each test - dateNowSpy.mockRestore(); - }); - - describe('shouldRefreshToken', () => { - it('should return true when token is 75% through its lifetime', () => { - // Token issued at time 100, expires at 900 (lifetime: 800) - // 75% of 800 = 600, so at time 700 (100 + 600), it should refresh - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 700 (which is 75% through the token lifetime) - dateNowSpy.mockReturnValue(700 * 1000); - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(true); - }); - - it('should return true when token is more than 75% through its lifetime', () => { - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 750 (which is 81.25% through the token lifetime) - dateNowSpy.mockReturnValue(750 * 1000); - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(true); - }); - - it('should return false when token is less than 75% through its lifetime', () => { - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 600 (which is 62.5% through the token lifetime) - dateNowSpy.mockReturnValue(600 * 1000); - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(false); - }); - - it('should return false when payload does not have required timestamp fields', () => { - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org' - }; - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(false); - }); - }); - - describe('refreshIfNeeded', () => { - it('should return new token when refresh is needed', () => { - // Setup a payload that needs refresh (75% through lifetime) - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 700 (75% through the token lifetime) - dateNowSpy.mockReturnValue(700 * 1000); - - const result = jwtService.refreshIfNeeded(payload, mockResponse as Response); - - expect(result).toBe(true); - expect(mockResponse.cookie).toHaveBeenCalled(); - }); - - it('should return false when refresh is not needed', () => { - // Setup a payload that doesn't need refresh (less than 75% through lifetime) - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 600 (62.5% through the token lifetime) - dateNowSpy.mockReturnValue(600 * 1000); - - const result = jwtService.refreshIfNeeded(payload, mockResponse as Response); - - expect(result).toBe(false); - expect(mockResponse.cookie).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts b/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts deleted file mode 100644 index 0a003163..00000000 --- a/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService'; -import { Request, Response } from 'express'; -import { UserState } from '../../../src/Domain/User/UserAggregate'; - - -describe('JWTService', () => { - let jwtService: JWTService; - let mockRequest: Partial; - let mockResponse: Partial; - - beforeEach(() => { - jest.clearAllMocks(); - jwtService = new JWTService(); - - // Set a test secret for consistent testing - process.env.JWT_SECRET = 'test-secret-key-for-testing'; - process.env.JWT_EXPIRY = '3600'; // 1 hour - - // Mock express Request and Response - mockRequest = { - cookies: {} - }; - - mockResponse = { - cookie: jest.fn() - }; - }); - - afterEach(() => { - // Clean up environment - delete process.env.JWT_SECRET; - delete process.env.JWT_EXPIRY; - }); - - describe('create', () => { - it('should create a valid JWT token and set cookie', () => { - // Arrange - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - // Act - const token = jwtService.create(payload, mockResponse as Response); - - // Assert - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - expect(token.split('.')).toHaveLength(3); // JWT has 3 parts - expect(mockResponse.cookie).toHaveBeenCalledWith( - 'auth_token', - token, - expect.objectContaining({ - httpOnly: true, - sameSite: 'strict', - maxAge: 86400000 // 24 hours in milliseconds - }) - ); - }); - - it('should create different tokens for different payloads', () => { - // Arrange - const payload1: TokenPayload = { - userId: 'user-1', - authLevel: 0 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-1' - }; - - const payload2: TokenPayload = { - userId: 'user-2', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_PREMIUM, - orgId: 'org-2' - }; - - // Act - const token1 = jwtService.create(payload1, mockResponse as Response); - const token2 = jwtService.create(payload2, mockResponse as Response); - - // Assert - expect(token1).toBeDefined(); - expect(token2).toBeDefined(); - expect(token1).not.toBe(token2); - }); - - it('should set secure cookie in production environment', () => { - // Arrange - process.env.NODE_ENV = 'production'; - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - // Act - const token = jwtService.create(payload, mockResponse as Response); - - // Assert - expect(mockResponse.cookie).toHaveBeenCalledWith( - 'auth_token', - token, - expect.objectContaining({ - secure: true - }) - ); - - // Clean up - delete process.env.NODE_ENV; - }); - }); - - describe('verify', () => { - it('should verify a valid token from cookies', () => { - // Arrange - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = jwtService.create(payload, mockResponse as Response); - mockRequest.cookies = { auth_token: token }; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeDefined(); - expect(result!.userId).toBe('user-123'); - expect(result!.authLevel).toBe(1); - expect(result!.orgId).toBe('org-456'); - }); - - it('should return null when no token is present in cookies', () => { - // Arrange - mockRequest.cookies = {}; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeNull(); - }); - - it('should return null for invalid token', () => { - // Arrange - mockRequest.cookies = { auth_token: 'invalid.jwt.token' }; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeNull(); - }); - - it('should return null for malformed token', () => { - // Arrange - mockRequest.cookies = { auth_token: 'not-a-jwt-token' }; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeNull(); - }); - }); - - describe('token creation with different payloads', () => { - it('should create tokens with dynamic user data', () => { - // Arrange - const timestamp = Date.now(); - const testPayload: TokenPayload = { - userId: `test-user-${timestamp}`, - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: `test-org-${timestamp}` - }; - - // Act - const token = jwtService.create(testPayload, mockResponse as Response); - - // Assert - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - expect(mockResponse.cookie).toHaveBeenCalled(); - - // Verify we can decode it back - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = jwtService.verify(mockRequest as Request); - - expect(verifiedPayload).toBeDefined(); - expect(verifiedPayload!.userId).toBe(testPayload.userId); - expect(verifiedPayload!.orgId).toBe(testPayload.orgId); - expect(verifiedPayload!.authLevel).toBe(testPayload.authLevel); - }); - - it('should create different tokens for different timestamps', async () => { - // Arrange - const timestamp1 = Date.now(); - const payload1: TokenPayload = { - userId: `test-user-${timestamp1}`, - authLevel: 0 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: `test-org-${timestamp1}` - }; - - // Add a small delay to ensure different timestamps - await new Promise(resolve => setTimeout(resolve, 1)); - - const timestamp2 = Date.now(); - const payload2: TokenPayload = { - userId: `test-user-${timestamp2}`, - authLevel: 1 as const, - userStatus: UserState.VERIFIED_PREMIUM, - orgId: `test-org-${timestamp2}` - }; - - // Act - const token1 = jwtService.create(payload1, mockResponse as Response); - const token2 = jwtService.create(payload2, mockResponse as Response); - - // Assert - expect(token1).not.toBe(token2); - expect(payload1.userId).not.toBe(payload2.userId); - expect(payload1.orgId).not.toBe(payload2.orgId); - }); - }); - - describe('integration scenarios', () => { - it('should create and verify token in complete flow', () => { - // Arrange - const originalPayload: TokenPayload = { - userId: 'integration-user', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'integration-org' - }; - - // Act - Complete flow - const token = jwtService.create(originalPayload, mockResponse as Response); - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = jwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - expect(verifiedPayload!.userId).toBe('integration-user'); - expect(verifiedPayload!.authLevel).toBe(1); - expect(verifiedPayload!.orgId).toBe('integration-org'); - }); - }); - - describe('JWT_EXPIRATION duration parsing', () => { - it('should parse JWT_EXPIRATION in hours format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = '2h'; - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - expect(verifiedPayload!.exp).toBeDefined(); - - // Token should expire in approximately 2 hours (7200 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 7200; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - - it('should parse JWT_EXPIRATION in days format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = '7d'; - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - - // Token should expire in approximately 7 days (604800 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 604800; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - - it('should parse JWT_EXPIRATION in minutes format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = '30m'; - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - - // Token should expire in approximately 30 minutes (1800 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 1800; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - - it('should prioritize JWT_EXPIRY over JWT_EXPIRATION when both are set', () => { - // Arrange - process.env.JWT_EXPIRY = '1800'; // 30 minutes in seconds - process.env.JWT_EXPIRATION = '1h'; // 1 hour - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - - // Should use JWT_EXPIRY (1800 seconds), not JWT_EXPIRATION (3600 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 1800; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRY; - delete process.env.JWT_EXPIRATION; - }); - - it('should throw error for invalid JWT_EXPIRATION format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = 'invalid-format'; - - // Act & Assert - expect(() => { - new JWTService(); - }).toThrow('Invalid duration format: invalid-format. Use format like \'24h\', \'7d\', \'30m\''); - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/LoggingService.test.ts b/SerpentRace_Backend/tests/Application/Services/LoggingService.test.ts deleted file mode 100644 index 9978141d..00000000 --- a/SerpentRace_Backend/tests/Application/Services/LoggingService.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { LoggingService, LogLevel } from '../../../src/Application/Services/LoggingService'; -import { logAuth, logError, logDatabase, logStartup } from '../../../src/Application/Services/Logger'; -import fs from 'fs'; -import path from 'path'; - -describe('LoggingService', () => { - let loggingService: LoggingService; - const testLogsDir = path.join(process.cwd(), 'test-logs'); - - beforeEach(() => { - // Clean up any existing test logs - if (fs.existsSync(testLogsDir)) { - fs.rmSync(testLogsDir, { recursive: true, force: true }); - } - - // Mock environment variables for testing - process.env.MAX_LOGS_PER_FILE = '10'; - process.env.MINIO_ENDPOINT = ''; - - loggingService = LoggingService.getInstance(); - }); - - afterEach(() => { - // Clean up test logs - if (fs.existsSync(testLogsDir)) { - fs.rmSync(testLogsDir, { recursive: true, force: true }); - } - - // Clean up environment variables - delete process.env.MAX_LOGS_PER_FILE; - delete process.env.MINIO_ENDPOINT; - }); - - describe('Log Level Functions', () => { - it('should log authentication events', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); - - logAuth('Test auth message', 'user123', { action: 'login' }); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[AUTH]'); - expect(logCall).toContain('Test auth message'); - - consoleSpy.mockRestore(); - }); - - it('should log error events with stack trace', () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const testError = new Error('Test error message'); - - logError('Test error occurred', testError); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[ERROR]'); - expect(logCall).toContain('Test error occurred'); - - consoleSpy.mockRestore(); - }); - - it('should log database operations with timing', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); - - logDatabase('Query executed', 'SELECT * FROM users', 45); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[DATABASE]'); - expect(logCall).toContain('Query executed'); - - consoleSpy.mockRestore(); - }); - - it('should log startup events', () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - logStartup('Application started', { version: '1.0.0' }); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[STARTUP]'); - expect(logCall).toContain('Application started'); - - consoleSpy.mockRestore(); - }); - }); - - describe('Log Formatting', () => { - it('should include timestamp in log entries', () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - logStartup('Test message'); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - - // Check if timestamp is in ISO format - const timestampRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/; - expect(logCall).toMatch(timestampRegex); - - consoleSpy.mockRestore(); - }); - - it('should include metadata in log entries', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); - const metadata = { userId: '123', action: 'test' }; - - logAuth('Test with metadata', 'user123', metadata); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('Meta:'); - expect(logCall).toContain('"userId":"123"'); - expect(logCall).toContain('"action":"test"'); - - consoleSpy.mockRestore(); - }); - }); - - describe('Request Logging Middleware', () => { - it('should create request logging middleware', () => { - const middleware = loggingService.requestLoggingMiddleware(); - - expect(typeof middleware).toBe('function'); - expect(middleware.length).toBe(3); // req, res, next - }); - - it('should create error logging middleware', () => { - const middleware = loggingService.errorLoggingMiddleware(); - - expect(typeof middleware).toBe('function'); - expect(middleware.length).toBe(4); // error, req, res, next - }); - }); - - describe('Log Levels', () => { - it('should have all required log levels defined', () => { - expect(LogLevel.REQUEST).toBe('REQUEST'); - expect(LogLevel.ERROR).toBe('ERROR'); - expect(LogLevel.WARNING).toBe('WARNING'); - expect(LogLevel.AUTH).toBe('AUTH'); - expect(LogLevel.DATABASE).toBe('DATABASE'); - expect(LogLevel.STARTUP).toBe('STARTUP'); - expect(LogLevel.CONNECTION).toBe('CONNECTION'); - expect(LogLevel.OTHER).toBe('OTHER'); - }); - }); - - describe('Singleton Pattern', () => { - it('should return the same instance', () => { - const instance1 = LoggingService.getInstance(); - const instance2 = LoggingService.getInstance(); - - expect(instance1).toBe(instance2); - }); - }); - - describe('File Operations', () => { - it('should handle missing Minio configuration gracefully', () => { - // Test that the service starts without Minio config - expect(() => LoggingService.getInstance()).not.toThrow(); - }); - - it('should generate monthly directory structure', () => { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const expectedPath = path.join('logs', `${year}-${month}`); - - // This tests the internal logic through the public interface - logStartup('Test for directory creation'); - - // Since we can't directly test the private method, we verify the service doesn't crash - expect(loggingService).toBeDefined(); - }); - }); - - describe('Error Handling', () => { - it('should handle logging errors gracefully', () => { - // Mock fs.appendFileSync to throw an error - const originalAppendFileSync = fs.appendFileSync; - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - fs.appendFileSync = jest.fn(() => { - throw new Error('Disk full'); - }); - - expect(() => { - logStartup('This should not crash'); - }).not.toThrow(); - - // Restore original function - fs.appendFileSync = originalAppendFileSync; - consoleSpy.mockRestore(); - }); - - it('should continue logging to console even if file logging fails', () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - // Mock file system to fail - const originalAppendFileSync = fs.appendFileSync; - fs.appendFileSync = jest.fn(() => { - throw new Error('File system error'); - }); - - logStartup('Test message'); - - // Should still log to console - expect(consoleSpy).toHaveBeenCalled(); - - // Restore - fs.appendFileSync = originalAppendFileSync; - consoleSpy.mockRestore(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/PasswordService.test.ts b/SerpentRace_Backend/tests/Application/Services/PasswordService.test.ts deleted file mode 100644 index a517c6f2..00000000 --- a/SerpentRace_Backend/tests/Application/Services/PasswordService.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { PasswordService } from '../../../src/Application/Services/PasswordService'; - -// Mock bcrypt completely -jest.mock('bcrypt'); - -describe('PasswordService', () => { - // Mock functions for bcrypt - const mockBcryptHash = jest.fn(); - const mockBcryptCompare = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - // Reset console.error mock to avoid noise in tests - jest.spyOn(console, 'error').mockImplementation(() => {}); - - // Setup bcrypt mocks - const bcrypt = require('bcrypt'); - bcrypt.hash = mockBcryptHash; - bcrypt.compare = mockBcryptCompare; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('hashPassword', () => { - it('should hash a valid password successfully', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptHash.mockResolvedValue(hashedPassword); - - // Act - const result = await PasswordService.hashPassword(password); - - // Assert - expect(result).toBe(hashedPassword); - expect(mockBcryptHash).toHaveBeenCalledWith(password, 12); - }); - - it('should throw error for empty password', async () => { - // Arrange - const password = ''; - - // Act & Assert - await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string'); - expect(mockBcryptHash).not.toHaveBeenCalled(); - }); - - it('should throw error for non-string password', async () => { - // Arrange - const password = null as any; - - // Act & Assert - await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string'); - expect(mockBcryptHash).not.toHaveBeenCalled(); - }); - - it('should handle bcrypt errors and throw generic error', async () => { - // Arrange - const password = 'validPassword123!'; - mockBcryptHash.mockRejectedValue(new Error('Bcrypt error')); - - // Act & Assert - await expect(PasswordService.hashPassword(password)).rejects.toThrow('Failed to hash password'); - expect(mockBcryptHash).toHaveBeenCalledWith(password, 12); - }); - }); - - describe('verifyPassword', () => { - it('should return true for matching password and hash', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptCompare.mockResolvedValue(true); - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(true); - expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword); - }); - - it('should return false for non-matching password and hash', async () => { - // Arrange - const password = 'wrongPassword'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptCompare.mockResolvedValue(false); - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword); - }); - - it('should return false for empty password', async () => { - // Arrange - const password = ''; - const hashedPassword = '$2b$12$hashed.password.here'; - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).not.toHaveBeenCalled(); - }); - - it('should return false for empty hashed password', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = ''; - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).not.toHaveBeenCalled(); - }); - - it('should return false for non-string inputs', async () => { - // Arrange - const password = null as any; - const hashedPassword = undefined as any; - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).not.toHaveBeenCalled(); - }); - - it('should return false when bcrypt throws error', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptCompare.mockRejectedValue(new Error('Bcrypt compare error')); - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword); - }); - }); - - describe('validatePasswordStrength', () => { - it('should return valid for strong password', () => { - // Arrange - const password = 'StrongPass123!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(true); - expect(result.errors).toEqual([]); - }); - - it('should return invalid for short password', () => { - // Arrange - const password = 'Short1!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must be at least 8 characters long'); - }); - - it('should return invalid for password without uppercase', () => { - // Arrange - const password = 'lowercase123!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one uppercase letter'); - }); - - it('should return invalid for password without lowercase', () => { - // Arrange - const password = 'UPPERCASE123!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one lowercase letter'); - }); - - it('should return invalid for password without numbers', () => { - // Arrange - const password = 'NoNumbers!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one number'); - }); - - it('should return invalid for password without special characters', () => { - // Arrange - const password = 'NoSpecial123'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one special character'); - }); - - it('should return multiple errors for weak password', () => { - // Arrange - const password = 'weak'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toHaveLength(4); - expect(result.errors).toContain('Password must be at least 8 characters long'); - expect(result.errors).toContain('Password must contain at least one uppercase letter'); - expect(result.errors).toContain('Password must contain at least one number'); - expect(result.errors).toContain('Password must contain at least one special character'); - }); - - it('should handle empty password', () => { - // Arrange - const password = ''; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must be provided as a string'); - }); - - it('should handle null password', () => { - // Arrange - const password = null as any; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must be provided as a string'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/RedisService.test.ts b/SerpentRace_Backend/tests/Application/Services/RedisService.test.ts deleted file mode 100644 index 4b558575..00000000 --- a/SerpentRace_Backend/tests/Application/Services/RedisService.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { RedisService } from '../../../src/Application/Services/RedisService'; -import { logStartup, logError } from '../../../src/Application/Services/Logger'; - -describe('RedisService', () => { - let redisService: RedisService; - - beforeAll(async () => { - redisService = RedisService.getInstance(); - - try { - await redisService.connect(); - } catch (error) { - console.log('Redis not available for testing, skipping Redis tests'); - return; - } - }); - - afterAll(async () => { - if (redisService.isRedisConnected()) { - await redisService.disconnect(); - } - }); - - beforeEach(async () => { - // Skip tests if Redis is not connected - if (!redisService.isRedisConnected()) { - return; - } - - // Clean up test data - const activeChats = await redisService.getAllActiveChats(); - for (const chat of activeChats) { - if (chat.chatId.startsWith('test-')) { - await redisService.removeActiveChat(chat.chatId); - } - } - - await redisService.removeActiveUser('test-user-1'); - await redisService.removeActiveUser('test-user-2'); - }); - - describe('Active Chat Management', () => { - it('should store and retrieve active chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const testChatData = { - chatId: 'test-chat-1', - participants: ['user-1', 'user-2'], - lastActivity: new Date(), - messageCount: 5, - chatType: 'direct' as const, - name: 'Test Chat' - }; - - await redisService.setActiveChat('test-chat-1', testChatData); - const retrieved = await redisService.getActiveChat('test-chat-1'); - - expect(retrieved).toBeDefined(); - expect(retrieved!.chatId).toBe('test-chat-1'); - expect(retrieved!.participants).toEqual(['user-1', 'user-2']); - expect(retrieved!.messageCount).toBe(5); - expect(retrieved!.chatType).toBe('direct'); - expect(retrieved!.name).toBe('Test Chat'); - }); - - it('should return null for non-existent chat', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const retrieved = await redisService.getActiveChat('non-existent-chat'); - expect(retrieved).toBeNull(); - }); - - it('should remove active chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const testChatData = { - chatId: 'test-chat-2', - participants: ['user-1', 'user-2'], - lastActivity: new Date(), - messageCount: 0, - chatType: 'group' as const - }; - - await redisService.setActiveChat('test-chat-2', testChatData); - let retrieved = await redisService.getActiveChat('test-chat-2'); - expect(retrieved).toBeDefined(); - - await redisService.removeActiveChat('test-chat-2'); - retrieved = await redisService.getActiveChat('test-chat-2'); - expect(retrieved).toBeNull(); - }); - - it('should update chat activity', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const originalTime = new Date(Date.now() - 60000); // 1 minute ago - const testChatData = { - chatId: 'test-chat-3', - participants: ['user-1', 'user-2'], - lastActivity: originalTime, - messageCount: 5, - chatType: 'direct' as const - }; - - await redisService.setActiveChat('test-chat-3', testChatData); - - // Wait a bit to ensure timestamp difference - await new Promise(resolve => setTimeout(resolve, 10)); - - await redisService.updateChatActivity('test-chat-3', 6); - - const retrieved = await redisService.getActiveChat('test-chat-3'); - expect(retrieved).toBeDefined(); - expect(retrieved!.messageCount).toBe(6); - expect(retrieved!.lastActivity.getTime()).toBeGreaterThan(originalTime.getTime()); - }); - }); - - describe('Active User Management', () => { - it('should store and retrieve active users', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const testUserData = { - userId: 'test-user-1', - activeChatIds: ['chat-1', 'chat-2'], - lastActivity: new Date(), - isOnline: true - }; - - await redisService.setActiveUser('test-user-1', testUserData); - const retrieved = await redisService.getActiveUser('test-user-1'); - - expect(retrieved).toBeDefined(); - expect(retrieved!.userId).toBe('test-user-1'); - expect(retrieved!.activeChatIds).toEqual(['chat-1', 'chat-2']); - expect(retrieved!.isOnline).toBe(true); - }); - - it('should manage user-chat associations', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - // Add user to chats - await redisService.addUserToChat('test-user-2', 'chat-1'); - await redisService.addUserToChat('test-user-2', 'chat-2'); - - let activeChatIds = await redisService.getUserActiveChats('test-user-2'); - expect(activeChatIds).toContain('chat-1'); - expect(activeChatIds).toContain('chat-2'); - - // Remove user from one chat - await redisService.removeUserFromChat('test-user-2', 'chat-1'); - activeChatIds = await redisService.getUserActiveChats('test-user-2'); - expect(activeChatIds).not.toContain('chat-1'); - expect(activeChatIds).toContain('chat-2'); - }); - }); - - describe('Inactive Chat Cleanup', () => { - it('should identify inactive chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago - const recentTime = new Date(); - - // Create an inactive chat - await redisService.setActiveChat('test-inactive-chat', { - chatId: 'test-inactive-chat', - participants: ['user-1', 'user-2'], - lastActivity: oldTime, - messageCount: 3, - chatType: 'direct' - }); - - // Create an active chat - await redisService.setActiveChat('test-active-chat', { - chatId: 'test-active-chat', - participants: ['user-1', 'user-3'], - lastActivity: recentTime, - messageCount: 1, - chatType: 'direct' - }); - - const inactiveChats = await redisService.getInactiveChats(60); // 60 minutes - expect(inactiveChats).toContain('test-inactive-chat'); - expect(inactiveChats).not.toContain('test-active-chat'); - - // Cleanup - await redisService.removeActiveChat('test-inactive-chat'); - await redisService.removeActiveChat('test-active-chat'); - }); - - it('should cleanup inactive chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago - - await redisService.setActiveChat('test-cleanup-chat', { - chatId: 'test-cleanup-chat', - participants: ['user-1', 'user-2'], - lastActivity: oldTime, - messageCount: 0, - chatType: 'direct' - }); - - const cleanedUp = await redisService.cleanupInactiveChats(60); - expect(cleanedUp).toContain('test-cleanup-chat'); - - // Verify chat was removed - const retrieved = await redisService.getActiveChat('test-cleanup-chat'); - expect(retrieved).toBeNull(); - }); - }); - - describe('Health Check', () => { - it('should ping Redis successfully', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const pingResult = await redisService.ping(); - expect(pingResult).toBe(true); - }); - - it('should report connection status', () => { - const isConnected = redisService.isRedisConnected(); - expect(typeof isConnected).toBe('boolean'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/TokenService.test.ts b/SerpentRace_Backend/tests/Application/Services/TokenService.test.ts deleted file mode 100644 index e70c1c84..00000000 --- a/SerpentRace_Backend/tests/Application/Services/TokenService.test.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { TokenService } from '../../../src/Application/Services/TokenService'; -import * as crypto from 'crypto'; - -// Mock crypto module -jest.mock('crypto'); - -describe('TokenService', () => { - let mockRandomBytes: jest.Mock; - let mockCreateHash: jest.Mock; - let mockHashUpdate: jest.Mock; - let mockHashDigest: jest.Mock; - let dateSpy: jest.SpyInstance; - - beforeEach(() => { - jest.clearAllMocks(); - - // Restore Date mock if it exists - if (dateSpy) { - dateSpy.mockRestore(); - } - - mockRandomBytes = jest.mocked(crypto.randomBytes); - mockHashUpdate = jest.fn().mockReturnThis(); - mockHashDigest = jest.fn(); - mockCreateHash = jest.fn().mockReturnValue({ - update: mockHashUpdate, - digest: mockHashDigest - }); - - // Mock crypto.createHash properly - jest.mocked(crypto.createHash).mockImplementation(mockCreateHash); - }); - - afterEach(() => { - // Clean up Date mock - if (dateSpy) { - dateSpy.mockRestore(); - dateSpy = undefined as any; - } - }); - - describe('generateSecureToken', () => { - it('should generate a secure token with default length', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('abcdef1234567890') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - - // Act - const token = TokenService.generateSecureToken(); - - // Assert - expect(token).toBe('abcdef1234567890'); - expect(mockRandomBytes).toHaveBeenCalledWith(32); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should generate a secure token with custom length', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('abcdef') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - - // Act - const token = TokenService.generateSecureToken(16); - - // Assert - expect(token).toBe('abcdef'); - expect(mockRandomBytes).toHaveBeenCalledWith(16); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should handle crypto errors', () => { - // Arrange - mockRandomBytes.mockImplementation(() => { - throw new Error('Crypto error'); - }); - - // Act & Assert - expect(() => TokenService.generateSecureToken()).toThrow('Failed to generate secure token'); - expect(mockRandomBytes).toHaveBeenCalledWith(32); - }); - }); - - describe('generateVerificationToken', () => { - it('should generate verification token with correct expiration', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('verification123') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - const mockDate = new Date('2023-01-01T12:00:00Z'); - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); - - // Act - const result = TokenService.generateVerificationToken(); - - // Assert - expect(result.token).toBe('verification123'); - expect(result.createdAt).toEqual(mockDate); - expect(result.expiresAt).toEqual(new Date('2023-01-02T12:00:00Z')); // 24 hours later - expect(mockRandomBytes).toHaveBeenCalledWith(32); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should handle token generation errors', () => { - // Arrange - mockRandomBytes.mockImplementation(() => { - throw new Error('Random bytes failed'); - }); - - // Act & Assert - expect(() => TokenService.generateVerificationToken()).toThrow('Failed to generate verification token'); - }); - }); - - describe('generatePasswordResetToken', () => { - it('should generate password reset token with correct expiration', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('reset456') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - const mockDate = new Date('2023-01-01T12:00:00Z'); - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); - - // Act - const result = TokenService.generatePasswordResetToken(); - - // Assert - expect(result.token).toBe('reset456'); - expect(result.createdAt).toEqual(mockDate); - expect(result.expiresAt).toEqual(new Date('2023-01-01T13:00:00Z')); // 1 hour later - expect(mockRandomBytes).toHaveBeenCalledWith(32); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should handle token generation errors', () => { - // Arrange - mockRandomBytes.mockImplementation(() => { - throw new Error('Random bytes failed'); - }); - - // Act & Assert - expect(() => TokenService.generatePasswordResetToken()).toThrow('Failed to generate password reset token'); - }); - }); - - describe('hashToken', () => { - it('should hash token correctly', async () => { - // Arrange - const token = 'test-token-123'; - const hashedToken = 'hashed-token-result'; - mockHashDigest.mockReturnValue(hashedToken); - - // Act - const result = await TokenService.hashToken(token); - - // Assert - expect(result).toBe(hashedToken); - expect(mockCreateHash).toHaveBeenCalledWith('sha256'); - expect(mockHashUpdate).toHaveBeenCalledWith(token); - expect(mockHashDigest).toHaveBeenCalledWith('hex'); - }); - - it('should handle hashing errors', async () => { - // Arrange - const token = 'test-token-123'; - mockCreateHash.mockImplementation(() => { - throw new Error('Hashing failed'); - }); - - // Act & Assert - await expect(TokenService.hashToken(token)).rejects.toThrow('Failed to hash token'); - }); - }); - - describe('verifyToken', () => { - it('should return true when tokens match', async () => { - // Arrange - const plainToken = 'plain-token'; - const hashedToken = 'expected-hash'; - mockHashDigest.mockReturnValue(hashedToken); - - // Act - const result = await TokenService.verifyToken(plainToken, hashedToken); - - // Assert - expect(result).toBe(true); - expect(mockCreateHash).toHaveBeenCalledWith('sha256'); - expect(mockHashUpdate).toHaveBeenCalledWith(plainToken); - expect(mockHashDigest).toHaveBeenCalledWith('hex'); - }); - - it('should return false when tokens do not match', async () => { - // Arrange - const plainToken = 'plain-token'; - const hashedToken = 'expected-hash'; - const actualHash = 'different-hash'; - mockHashDigest.mockReturnValue(actualHash); - - // Act - const result = await TokenService.verifyToken(plainToken, hashedToken); - - // Assert - expect(result).toBe(false); - expect(mockCreateHash).toHaveBeenCalledWith('sha256'); - expect(mockHashUpdate).toHaveBeenCalledWith(plainToken); - expect(mockHashDigest).toHaveBeenCalledWith('hex'); - }); - - it('should handle verification errors', async () => { - // Arrange - const plainToken = 'plain-token'; - const hashedToken = 'expected-hash'; - mockCreateHash.mockImplementation(() => { - throw new Error('Hash creation failed'); - }); - - // Act & Assert - TokenService.verifyToken catches errors and returns false, doesn't throw - const result = await TokenService.verifyToken(plainToken, hashedToken); - expect(result).toBe(false); - }); - }); - - describe('isTokenExpired', () => { - it('should return false for non-expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const futureDate = new Date('2023-01-01T13:00:00Z'); // 1 hour from now - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.isTokenExpired(futureDate); - - // Assert - expect(result).toBe(false); - - // Cleanup - dateSpy.mockRestore(); - }); - - it('should return true for expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const pastDate = new Date('2023-01-01T11:00:00Z'); // 1 hour ago - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.isTokenExpired(pastDate); - - // Assert - expect(result).toBe(true); - - // Cleanup - dateSpy.mockRestore(); - }); - - it('should return true for exactly expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const exactlyNow = new Date('2023-01-01T12:00:00Z'); - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.isTokenExpired(exactlyNow); - - // Assert - expect(result).toBe(false); // new Date() > expiresAt is false when they're equal - - // Cleanup - dateSpy.mockRestore(); - }); - }); - - describe('generateTokenWithExpiry', () => { - it('should validate token format correctly', () => { - // Arrange - valid hex token with expected length (64 chars for 32 bytes) - const validToken = 'a'.repeat(64); // 64 hex characters - - // Act - const result = TokenService.isValidTokenFormat(validToken); - - // Assert - expect(result).toBe(true); - }); - - it('should reject invalid token format', () => { - // Arrange - const invalidTokens = [ - '', // empty - 'invalid-token-with-dashes', // non-hex characters - 'abc123', // too short - null as any, // null - undefined as any, // undefined - 123 as any // not string - ]; - - invalidTokens.forEach(invalidToken => { - // Act - const result = TokenService.isValidTokenFormat(invalidToken); - - // Assert - expect(result).toBe(false); - }); - }); - }); - - describe('generateVerificationUrl', () => { - it('should generate correct verification URL', () => { - // Arrange - const baseUrl = 'https://example.com'; - const token = 'verification-token-123'; - - // Act - const url = TokenService.generateVerificationUrl(baseUrl, token); - - // Assert - expect(url).toBe('https://example.com/verify-email?token=verification-token-123'); - }); - - it('should handle base URL with trailing slash', () => { - // Arrange - const baseUrl = 'https://example.com/'; - const token = 'verification-token-123'; - - // Act - const url = TokenService.generateVerificationUrl(baseUrl, token); - - // Assert - expect(url).toBe('https://example.com/verify-email?token=verification-token-123'); - }); - - it('should encode special characters in token', () => { - // Arrange - const baseUrl = 'https://example.com'; - const token = 'token+with/special=chars'; - - // Act - const url = TokenService.generateVerificationUrl(baseUrl, token); - - // Assert - expect(url).toContain(encodeURIComponent(token)); - }); - }); - - describe('generatePasswordResetUrl', () => { - it('should generate correct password reset URL', () => { - // Arrange - const baseUrl = 'https://example.com'; - const token = 'reset-token-456'; - - // Act - const url = TokenService.generatePasswordResetUrl(baseUrl, token); - - // Assert - expect(url).toBe('https://example.com/reset-password?token=reset-token-456'); - }); - }); - - describe('getExpirationInfo', () => { - it('should return correct info for non-expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const futureDate = new Date('2023-01-01T14:00:00Z'); // 2 hours from now - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.getExpirationInfo(futureDate); - - // Assert - expect(result.expired).toBe(false); - expect(result.timeLeft).toContain('Expires in'); - expect(result.timeLeft).toContain('hour(s)'); - - // Cleanup - dateSpy.mockRestore(); - }); - - it('should return correct info for expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const pastDate = new Date('2023-01-01T11:30:00Z'); // 30 minutes ago - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.getExpirationInfo(pastDate); - - // Assert - expect(result.expired).toBe(true); - expect(result.timeLeft).toContain('Expired'); - expect(result.timeLeft).toContain('minute(s) ago'); - - // Cleanup - dateSpy.mockRestore(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/ValidationMiddleware.test.ts b/SerpentRace_Backend/tests/Application/Services/ValidationMiddleware.test.ts deleted file mode 100644 index 443688cc..00000000 --- a/SerpentRace_Backend/tests/Application/Services/ValidationMiddleware.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { ValidationMiddleware } from '../../../src/Application/Services/ValidationMiddleware'; -import { Request, Response, NextFunction } from 'express'; -import { ErrorResponseService } from '../../../src/Application/Services/ErrorResponseService'; - -jest.mock('../../../src/Application/Services/ErrorResponseService'); -jest.mock('../../../src/Application/Services/Logger'); - -describe('ValidationMiddleware', () => { - let req: Partial; - let res: Partial; - let next: NextFunction; - - beforeEach(() => { - req = { - body: {}, - params: {}, - query: {}, - path: '/test' - }; - res = { - status: jest.fn().mockReturnThis(), - json: jest.fn().mockReturnThis() - }; - next = jest.fn(); - jest.clearAllMocks(); - }); - - describe('validateRequiredFields', () => { - it('should pass validation when all required fields are present', () => { - req.body = { username: 'testuser', email: 'test@example.com' }; - - const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation when required fields are missing', () => { - req.body = { username: 'testuser' }; // missing email - - const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Missing required fields', - { missingFields: ['email'] } - ); - expect(next).not.toHaveBeenCalled(); - }); - - it('should fail validation when fields are empty strings', () => { - req.body = { username: '', email: 'test@example.com' }; - - const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Missing required fields', - { missingFields: ['username'] } - ); - }); - }); - - describe('validateEmailFormat', () => { - it('should pass validation for valid email', () => { - req.body = { email: 'test@example.com' }; - - const middleware = ValidationMiddleware.validateEmailFormat(['email']); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation for invalid email', () => { - req.body = { email: 'invalid-email' }; - - const middleware = ValidationMiddleware.validateEmailFormat(['email']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Email format validation failed', - { errors: ["Field 'email' must contain a valid email address"] } - ); - expect(next).not.toHaveBeenCalled(); - }); - }); - - describe('validateUUIDFormat', () => { - it('should pass validation for valid UUID', () => { - req.params = { userId: '123e4567-e89b-12d3-a456-426614174000' }; - - const middleware = ValidationMiddleware.validateUUIDFormat(['userId']); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation for invalid UUID', () => { - req.params = { userId: 'invalid-uuid' }; - - const middleware = ValidationMiddleware.validateUUIDFormat(['userId']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'UUID format validation failed', - { errors: ["Field 'userId' must contain a valid UUID"] } - ); - expect(next).not.toHaveBeenCalled(); - }); - }); - - describe('validateStringLength', () => { - it('should pass validation for strings within length constraints', () => { - req.body = { username: 'testuser', password: 'password123' }; - - const middleware = ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 20 }, - password: { min: 8, max: 50 } - }); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation for strings that are too short', () => { - req.body = { username: 'ab' }; // too short (min 3) - - const middleware = ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 20 } - }); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'String length validation failed', - { errors: ["Field 'username' must be at least 3 characters"] } - ); - }); - - it('should fail validation for strings that are too long', () => { - req.body = { username: 'a'.repeat(25) }; // too long (max 20) - - const middleware = ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 20 } - }); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'String length validation failed', - { errors: ["Field 'username' must not exceed 20 characters"] } - ); - }); - }); - - describe('combine', () => { - it('should run all validations in sequence and pass if all succeed', (done) => { - req.body = { username: 'testuser', email: 'test@example.com' }; - - const nextSpy = jest.fn(() => { - try { - expect(nextSpy).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - done(); - } catch (error) { - done(error); - } - }); - - const combinedMiddleware = ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'email']), - ValidationMiddleware.validateEmailFormat(['email']), - ValidationMiddleware.validateStringLength({ username: { min: 3, max: 20 } }) - ]); - - combinedMiddleware(req as Request, res as Response, nextSpy); - }); - - it('should stop at first validation failure', () => { - req.body = { username: 'testuser' }; // missing email - - const combinedMiddleware = ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'email']), - ValidationMiddleware.validateEmailFormat(['email']), // this won't run - ValidationMiddleware.validateStringLength({ username: { min: 3, max: 20 } }) // this won't run - ]); - - combinedMiddleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Missing required fields', - { missingFields: ['email'] } - ); - expect(next).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/User/commands/UserCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/User/commands/UserCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 42c92a51..00000000 --- a/SerpentRace_Backend/tests/Application/User/commands/UserCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,430 +0,0 @@ -// Comprehensive test coverage for User Command Handlers -import { CreateUserCommand } from '../../../../src/Application/User/commands/CreateUserCommand'; -import { CreateUserCommandHandler } from '../../../../src/Application/User/commands/CreateUserCommandHandler'; -import { LoginCommand } from '../../../../src/Application/User/commands/LoginCommand'; -import { LoginCommandHandler } from '../../../../src/Application/User/commands/LoginCommandHandler'; -import { UpdateUserCommand } from '../../../../src/Application/User/commands/UpdateUserCommand'; -import { UpdateUserCommandHandler } from '../../../../src/Application/User/commands/UpdateUserCommandHandler'; -import { DeactivateUserCommand } from '../../../../src/Application/User/commands/DeactivateUserCommand'; -import { DeactivateUserCommandHandler } from '../../../../src/Application/User/commands/DeactivateUserCommandHandler'; -import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository'; -import { JWTService } from '../../../../src/Application/Services/JWTService'; -import { PasswordService } from '../../../../src/Application/Services/PasswordService'; -import { UserState } from '../../../../src/Domain/User/UserAggregate'; -import { - createMockUser, - createMockUserRepository, - createMockOrganizationRepository, - createMockJWTService -} from '../../../testUtils'; - -// Mock PasswordService static methods -jest.mock('../../../../src/Application/Services/PasswordService', () => ({ - PasswordService: { - validatePasswordStrength: jest.fn().mockReturnValue({ isValid: true, errors: [] }), - hashPassword: jest.fn().mockResolvedValue('hashed-password'), - verifyPassword: jest.fn().mockResolvedValue(true) - } -})); - -describe('User Command Handlers - Comprehensive Coverage', () => { - describe('CreateUserCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let handler: CreateUserCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - handler = new CreateUserCommandHandler(mockUserRepository); - }); - - it('should create a new user successfully', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'testuser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - const mockUser = createMockUser({ - username: command.username, - email: command.email, - state: UserState.REGISTERED_NOT_VERIFIED - }); - - // CreateUserCommandHandler doesn't check existing users - goes directly to create - mockUserRepository.create.mockResolvedValue(mockUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - // CreateUserCommandHandler doesn't call findByUsername/findByEmail - expect(mockUserRepository.create).toHaveBeenCalled(); - }); - - it('should throw error when username already exists', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'existinguser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - // Simulate database constraint error for duplicate username - mockUserRepository.create.mockRejectedValue(new Error('duplicate key value violates unique constraint')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists'); - }); - - it('should throw error when email already exists', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'testuser', - email: 'existing@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - // Simulate database constraint error for duplicate email - mockUserRepository.create.mockRejectedValue(new Error('unique constraint violation')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists'); - }); - - it('should handle repository errors', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'testuser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - mockUserRepository.findByUsername.mockResolvedValue(null); - mockUserRepository.findByEmail.mockResolvedValue(null); - mockUserRepository.create.mockRejectedValue(new Error('Database error')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create user'); - }); - }); - - describe('LoginCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let mockOrgRepository: jest.Mocked; - let mockJwtService: jest.Mocked; - let handler: LoginCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - mockOrgRepository = createMockOrganizationRepository(); - mockJwtService = createMockJWTService(); - handler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository); - - // Reset all mocks - jest.clearAllMocks(); - - // Set default PasswordService behavior - const mockPasswordService = PasswordService as jest.Mocked; - mockPasswordService.verifyPassword.mockResolvedValue(true); // Default to valid password - }); - - it('should login user with valid credentials', async () => { - // Arrange - const command: LoginCommand = { - username: 'testuser', - password: 'Password123!' - }; - - const mockUser = createMockUser({ - username: command.username, - state: UserState.VERIFIED_REGULAR - }); - - mockUserRepository.findByUsername.mockResolvedValue(mockUser); - mockJwtService.create.mockReturnValue('jwt-token'); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(result!.token).toBe('jwt-token'); - expect(mockJwtService.create).toHaveBeenCalled(); - }); - - it('should handle user not found', async () => { - // Arrange - const command: LoginCommand = { - username: 'nonexistent', - password: 'password123' - }; - - mockUserRepository.findByUsername.mockResolvedValue(null); - - // Act & Assert - const result = await handler.execute(command); - expect(result).toBeNull(); - }); - - it('should handle invalid password', async () => { - // Arrange - const command: LoginCommand = { - username: 'testuser', - password: 'wrongpassword' - }; - - const mockUser = createMockUser({ - username: command.username, - password: 'hashedpassword' - }); - - mockUserRepository.findByUsername.mockResolvedValue(mockUser); - - // Mock password verification to return false for wrong password - const mockPasswordService = PasswordService as jest.Mocked; - mockPasswordService.verifyPassword.mockResolvedValue(false); - - // Act & Assert - const result = await handler.execute(command); - expect(result).toBeNull(); - }); - - it('should handle unverified user', async () => { - // Arrange - LoginCommandHandler doesn't reject unverified users, it processes them normally - const command: LoginCommand = { - username: 'testuser', - password: 'Password123!' - }; - - const mockUser = createMockUser({ - username: command.username, - password: 'hashedpassword', - state: UserState.REGISTERED_NOT_VERIFIED - }); - - mockUserRepository.findByUsername.mockResolvedValue(mockUser); - mockJwtService.create.mockReturnValue('jwt-token'); - - // Act - const result = await handler.execute(command); - - // Assert - LoginCommandHandler processes unverified users normally - expect(result).toBeDefined(); - expect(result!.user).toBeDefined(); - expect(result!.token).toBe('jwt-token'); - }); - }); - - describe('UpdateUserCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let handler: UpdateUserCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - handler = new UpdateUserCommandHandler(mockUserRepository); - }); - - it('should update user successfully', async () => { - // Arrange - const command: UpdateUserCommand = { - id: 'user-123', - email: 'newemail@example.com' - }; - - const existingUser = createMockUser({ id: command.id }); - const updatedUser = createMockUser({ - id: command.id, - email: command.email - }); - - mockUserRepository.findById.mockResolvedValue(existingUser); - mockUserRepository.update.mockResolvedValue(updatedUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object)); - }); - - it('should return null when user not found', async () => { - // Arrange - const command: UpdateUserCommand = { - id: 'nonexistent-user', - email: 'newemail@example.com' - }; - - mockUserRepository.update.mockResolvedValue(null); // UpdateUserCommandHandler calls update directly, not findById first - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeNull(); - expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object)); - }); - - it('should handle partial updates', async () => { - // Arrange - const command: UpdateUserCommand = { - id: 'user-123', - username: 'newusername' - }; - - const existingUser = createMockUser({ id: command.id }); - const updatedUser = createMockUser({ - id: command.id, - username: command.username - }); - - mockUserRepository.findById.mockResolvedValue(existingUser); - mockUserRepository.update.mockResolvedValue(updatedUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - }); - }); - - describe('DeactivateUserCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let handler: DeactivateUserCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - handler = new DeactivateUserCommandHandler(mockUserRepository); - }); - - it('should deactivate user successfully', async () => { - // Arrange - const command: DeactivateUserCommand = { - id: 'user-123' - }; - - const deactivatedUser = createMockUser({ - id: command.id, - state: UserState.DEACTIVATED - }); - - mockUserRepository.deactivate.mockResolvedValue(deactivatedUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockUserRepository.deactivate).toHaveBeenCalledWith(command.id); - }); - - it('should handle repository errors', async () => { - // Arrange - const command: DeactivateUserCommand = { - id: 'user-123' - }; - - mockUserRepository.deactivate.mockRejectedValue(new Error('Deactivation failed')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Deactivation failed'); - }); - }); - - describe('Cross-Command Integration Tests', () => { - let mockUserRepository: jest.Mocked; - let mockOrgRepository: jest.Mocked; - let mockJwtService: jest.Mocked; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - mockOrgRepository = createMockOrganizationRepository(); - mockJwtService = createMockJWTService(); - }); - - it('should create user and then login', async () => { - // Arrange - const createHandler = new CreateUserCommandHandler(mockUserRepository); - const loginHandler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository); - - const createCommand: CreateUserCommand = { - username: 'testuser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - const loginCommand: LoginCommand = { - username: 'testuser', - password: 'Password123!' // Strong password - }; - - const mockUser = createMockUser({ - username: createCommand.username, - email: createCommand.email, - state: UserState.VERIFIED_REGULAR - }); - - // Mock create user flow - mockUserRepository.findByUsername.mockResolvedValueOnce(null); - mockUserRepository.findByEmail.mockResolvedValue(null); - mockUserRepository.create.mockResolvedValue(mockUser); - - // Mock login flow - mockUserRepository.findByUsername.mockResolvedValueOnce(mockUser); - mockJwtService.create.mockReturnValue('jwt-token'); - - // Act - const createResult = await createHandler.execute(createCommand); - const loginResult = await loginHandler.execute(loginCommand); - - // Assert - expect(createResult).toBeDefined(); - expect(loginResult).toBeDefined(); - }); - - it('should update user after creation', async () => { - // Arrange - const updateHandler = new UpdateUserCommandHandler(mockUserRepository); - - const updateCommand: UpdateUserCommand = { - id: 'user-123', - email: 'updated@example.com' - }; - - const existingUser = createMockUser({ id: updateCommand.id }); - const updatedUser = createMockUser({ - id: updateCommand.id, - email: updateCommand.email - }); - - mockUserRepository.findById.mockResolvedValue(existingUser); - mockUserRepository.update.mockResolvedValue(updatedUser); - - // Act - const result = await updateHandler.execute(updateCommand); - - // Assert - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/comprehensive-repository-coverage.test.ts b/SerpentRace_Backend/tests/comprehensive-repository-coverage.test.ts deleted file mode 100644 index 2bbdaa39..00000000 --- a/SerpentRace_Backend/tests/comprehensive-repository-coverage.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -// Comprehensive test coverage for Repository layer -import { IUserRepository } from '../src/Domain/IRepository/IUserRepository'; -import { IDeckRepository } from '../src/Domain/IRepository/IDeckRepository'; -import { IOrganizationRepository } from '../src/Domain/IRepository/IOrganizationRepository'; -import { IContactRepository } from '../src/Domain/IRepository/IContactRepository'; -import { UserAggregate, UserState } from '../src/Domain/User/UserAggregate'; -import { DeckAggregate, Type as DeckType } from '../src/Domain/Deck/DeckAggregate'; -import { OrganizationAggregate } from '../src/Domain/Organization/OrganizationAggregate'; -import { ContactAggregate } from '../src/Domain/Contact/ContactAggregate'; -import { - createMockUser, - createMockDeck, - createMockOrganization, - createMockContact, - createMockUserRepository, - createMockDeckRepository, - createMockOrganizationRepository, - createMockContactRepository -} from './testUtils'; - -describe('Repository Layer - Comprehensive Coverage', () => { - describe('IUserRepository Interface Coverage', () => { - let mockUserRepository: jest.Mocked; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - }); - - it('should implement all required methods', () => { - expect(mockUserRepository.create).toBeDefined(); - expect(mockUserRepository.findByPage).toBeDefined(); - expect(mockUserRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockUserRepository.findById).toBeDefined(); - expect(mockUserRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockUserRepository.findByUsername).toBeDefined(); - expect(mockUserRepository.findByEmail).toBeDefined(); - expect(mockUserRepository.findByToken).toBeDefined(); - expect(mockUserRepository.search).toBeDefined(); - expect(mockUserRepository.searchIncludingDeleted).toBeDefined(); - expect(mockUserRepository.update).toBeDefined(); - expect(mockUserRepository.delete).toBeDefined(); - expect(mockUserRepository.softDelete).toBeDefined(); - expect(mockUserRepository.deactivate).toBeDefined(); - }); - - it('should handle user creation', async () => { - const userData = { username: 'testuser', email: 'test@example.com' }; - const mockUser = createMockUser(userData); - mockUserRepository.create.mockResolvedValue(mockUser); - - const result = await mockUserRepository.create(userData); - expect(result).toEqual(mockUser); - expect(mockUserRepository.create).toHaveBeenCalledWith(userData); - }); - - it('should handle paginated user retrieval', async () => { - const mockUsers = [createMockUser(), createMockUser({ id: 'user2' })]; - mockUserRepository.findByPage.mockResolvedValue({ users: mockUsers, totalCount: 2 }); - - const result = await mockUserRepository.findByPage(0, 10); - expect(result.users).toHaveLength(2); - expect(result.totalCount).toBe(2); - }); - - it('should handle user search operations', async () => { - const mockUsers = [createMockUser({ username: 'searchtest' })]; - mockUserRepository.search.mockResolvedValue({ users: mockUsers, totalCount: 1 }); - - const result = await mockUserRepository.search('searchtest'); - expect(result.users).toHaveLength(1); - expect(result.users[0].username).toBe('searchtest'); - }); - - it('should handle user state transitions', async () => { - const mockUser = createMockUser({ state: UserState.VERIFIED_REGULAR }); - mockUserRepository.deactivate.mockResolvedValue(mockUser); - - const result = await mockUserRepository.deactivate('user-id'); - expect(result).toEqual(mockUser); - }); - }); - - describe('IDeckRepository Interface Coverage', () => { - let mockDeckRepository: jest.Mocked; - - beforeEach(() => { - mockDeckRepository = createMockDeckRepository(); - }); - - it('should implement all required methods including new ones', () => { - expect(mockDeckRepository.create).toBeDefined(); - expect(mockDeckRepository.findByPage).toBeDefined(); - expect(mockDeckRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockDeckRepository.findById).toBeDefined(); - expect(mockDeckRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockDeckRepository.search).toBeDefined(); - expect(mockDeckRepository.searchIncludingDeleted).toBeDefined(); - expect(mockDeckRepository.update).toBeDefined(); - expect(mockDeckRepository.delete).toBeDefined(); - expect(mockDeckRepository.softDelete).toBeDefined(); - expect(mockDeckRepository.countActiveByUserId).toBeDefined(); - expect(mockDeckRepository.countOrganizationalByUserId).toBeDefined(); - expect(mockDeckRepository.findFilteredDecks).toBeDefined(); - }); - - it('should handle deck counting operations', async () => { - mockDeckRepository.countActiveByUserId.mockResolvedValue(5); - mockDeckRepository.countOrganizationalByUserId.mockResolvedValue(3); - - const activeCount = await mockDeckRepository.countActiveByUserId('user-id'); - const orgCount = await mockDeckRepository.countOrganizationalByUserId('user-id'); - - expect(activeCount).toBe(5); - expect(orgCount).toBe(3); - }); - - it('should handle filtered deck retrieval', async () => { - const mockDecks = [createMockDeck(), createMockDeck({ id: 'deck2' })]; - mockDeckRepository.findFilteredDecks.mockResolvedValue({ decks: mockDecks, totalCount: 2 }); - - const result = await mockDeckRepository.findFilteredDecks('user-id', 'org-id', false, 0, 10); - expect(result.decks).toHaveLength(2); - expect(result.totalCount).toBe(2); - }); - - it('should handle different deck types', async () => { - const jokerDeck = createMockDeck({ type: DeckType.JOKER }); - const luckDeck = createMockDeck({ type: DeckType.LUCK }); - const questionDeck = createMockDeck({ type: DeckType.QUESTION }); - - mockDeckRepository.create.mockResolvedValueOnce(jokerDeck); - mockDeckRepository.create.mockResolvedValueOnce(luckDeck); - mockDeckRepository.create.mockResolvedValueOnce(questionDeck); - - const result1 = await mockDeckRepository.create({ type: DeckType.JOKER }); - const result2 = await mockDeckRepository.create({ type: DeckType.LUCK }); - const result3 = await mockDeckRepository.create({ type: DeckType.QUESTION }); - - expect(result1.type).toBe(DeckType.JOKER); - expect(result2.type).toBe(DeckType.LUCK); - expect(result3.type).toBe(DeckType.QUESTION); - }); - }); - - describe('IOrganizationRepository Interface Coverage', () => { - let mockOrgRepository: jest.Mocked; - - beforeEach(() => { - mockOrgRepository = createMockOrganizationRepository(); - }); - - it('should implement all required methods', () => { - expect(mockOrgRepository.create).toBeDefined(); - expect(mockOrgRepository.findByPage).toBeDefined(); - expect(mockOrgRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockOrgRepository.findById).toBeDefined(); - expect(mockOrgRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockOrgRepository.search).toBeDefined(); - expect(mockOrgRepository.searchIncludingDeleted).toBeDefined(); - expect(mockOrgRepository.update).toBeDefined(); - expect(mockOrgRepository.delete).toBeDefined(); - expect(mockOrgRepository.softDelete).toBeDefined(); - }); - - it('should handle organization CRUD operations', async () => { - const orgData = { name: 'Test Org', contactemail: 'test@org.com' }; - const mockOrg = createMockOrganization(orgData); - - mockOrgRepository.create.mockResolvedValue(mockOrg); - mockOrgRepository.findById.mockResolvedValue(mockOrg); - mockOrgRepository.update.mockResolvedValue(mockOrg); - mockOrgRepository.softDelete.mockResolvedValue(mockOrg); - - const created = await mockOrgRepository.create(orgData); - const found = await mockOrgRepository.findById('org-id'); - const updated = await mockOrgRepository.update('org-id', { name: 'Updated Org' }); - const deleted = await mockOrgRepository.softDelete('org-id'); - - expect(created.name).toBe('Test Org'); - expect(found).toEqual(mockOrg); - expect(updated).toEqual(mockOrg); - expect(deleted).toEqual(mockOrg); - }); - }); - - describe('IContactRepository Interface Coverage', () => { - let mockContactRepository: jest.Mocked; - - beforeEach(() => { - mockContactRepository = createMockContactRepository(); - }); - - it('should implement all required methods', () => { - expect(mockContactRepository.create).toBeDefined(); - expect(mockContactRepository.findById).toBeDefined(); - expect(mockContactRepository.findByPage).toBeDefined(); - expect(mockContactRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockContactRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockContactRepository.search).toBeDefined(); - expect(mockContactRepository.searchIncludingDeleted).toBeDefined(); - expect(mockContactRepository.update).toBeDefined(); - expect(mockContactRepository.delete).toBeDefined(); - expect(mockContactRepository.softDelete).toBeDefined(); - }); - - it('should handle contact search operations', async () => { - const mockContacts = [createMockContact({ email: 'test@example.com' })]; - mockContactRepository.search.mockResolvedValue(mockContacts); - mockContactRepository.searchIncludingDeleted.mockResolvedValue(mockContacts); - - const activeResults = await mockContactRepository.search('test'); - const allResults = await mockContactRepository.searchIncludingDeleted('test'); - - expect(activeResults).toHaveLength(1); - expect(allResults).toHaveLength(1); - }); - - it('should handle contact lifecycle', async () => { - const contactData = { email: 'user@example.com', message: 'Help Request' }; - const mockContact = createMockContact(contactData); - - mockContactRepository.create.mockResolvedValue(mockContact); - mockContactRepository.findById.mockResolvedValue(mockContact); - mockContactRepository.findByIdIncludingDeleted.mockResolvedValue(mockContact); - - const created = await mockContactRepository.create(contactData); - const found = await mockContactRepository.findById('contact-id'); - const foundWithDeleted = await mockContactRepository.findByIdIncludingDeleted('contact-id'); - - expect(created.email).toBe('user@example.com'); - expect(found).toEqual(mockContact); - expect(foundWithDeleted).toEqual(mockContact); - }); - }); - - describe('Cross-Repository Integration Tests', () => { - let userRepo: jest.Mocked; - let deckRepo: jest.Mocked; - let orgRepo: jest.Mocked; - - beforeEach(() => { - userRepo = createMockUserRepository(); - deckRepo = createMockDeckRepository(); - orgRepo = createMockOrganizationRepository(); - }); - - it('should simulate user-deck relationship operations', async () => { - const mockUser = createMockUser({ id: 'user-123' }); - const mockDecks = [ - createMockDeck({ userid: 'user-123', name: 'Deck 1' }), - createMockDeck({ userid: 'user-123', name: 'Deck 2' }) - ]; - - userRepo.findById.mockResolvedValue(mockUser); - deckRepo.findFilteredDecks.mockResolvedValue({ decks: mockDecks, totalCount: 2 }); - deckRepo.countActiveByUserId.mockResolvedValue(2); - - const user = await userRepo.findById('user-123'); - const userDecks = await deckRepo.findFilteredDecks('user-123'); - const deckCount = await deckRepo.countActiveByUserId('user-123'); - - expect(user).toBeDefined(); - expect(userDecks.decks).toHaveLength(2); - expect(deckCount).toBe(2); - expect(userDecks.decks.every(deck => deck.userid === 'user-123')).toBe(true); - }); - - it('should simulate organization-user relationship operations', async () => { - const mockOrg = createMockOrganization({ id: 'org-123', name: 'Test Organization' }); - const mockUsers = [ - createMockUser({ orgid: 'org-123' }), - createMockUser({ orgid: 'org-123', id: 'user-2' }) - ]; - - orgRepo.findById.mockResolvedValue(mockOrg); - userRepo.findByPage.mockResolvedValue({ users: mockUsers, totalCount: 2 }); - - const org = await orgRepo.findById('org-123'); - const orgUsers = await userRepo.findByPage(0, 10); - - expect(org).toBeDefined(); - expect(orgUsers.users).toHaveLength(2); - expect(orgUsers.users.every(user => user.orgid === 'org-123')).toBe(true); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/jest.setup.ts b/SerpentRace_Backend/tests/jest.setup.ts deleted file mode 100644 index 9484f3b9..00000000 --- a/SerpentRace_Backend/tests/jest.setup.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Set the NODE_ENV to test for all Jest tests -process.env.NODE_ENV = 'test'; diff --git a/SerpentRace_Backend/tests/setup.ts b/SerpentRace_Backend/tests/setup.ts deleted file mode 100644 index 7cf900aa..00000000 --- a/SerpentRace_Backend/tests/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Jest test setup file -import { jest } from '@jest/globals'; -import { LoggingService } from '../src/Application/Services/LoggingService'; - -// Mock environment variables -process.env.NODE_ENV = 'test'; -process.env.JWT_SECRET = 'test-jwt-secret'; -process.env.EMAIL_HOST = 'test.smtp.com'; -process.env.EMAIL_PORT = '587'; -process.env.EMAIL_USER = 'test@example.com'; -process.env.EMAIL_PASS = 'testpass'; -process.env.EMAIL_FROM = 'test@example.com'; -process.env.APP_BASE_URL = 'http://localhost:3000'; - -// Global test timeout -jest.setTimeout(10000); - -// Global cleanup to prevent Jest from hanging -afterAll(async () => { - try { - await LoggingService.getInstance().shutdown(); - } catch (error) { - // Ignore cleanup errors in tests - console.log('Test cleanup completed'); - } -}); diff --git a/SerpentRace_Backend/tests/testUtils.ts b/SerpentRace_Backend/tests/testUtils.ts deleted file mode 100644 index 4f759a44..00000000 --- a/SerpentRace_Backend/tests/testUtils.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { UserAggregate, UserState } from '../src/Domain/User/UserAggregate'; -import { OrganizationAggregate, OrganizationState } from '../src/Domain/Organization/OrganizationAggregate'; -import { DeckAggregate, State as DeckState, Type as DeckType, CType } from '../src/Domain/Deck/DeckAggregate'; -import { ContactAggregate, ContactState, ContactType } from '../src/Domain/Contact/ContactAggregate'; -import { IUserRepository } from '../src/Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../src/Domain/IRepository/IOrganizationRepository'; -import { IDeckRepository } from '../src/Domain/IRepository/IDeckRepository'; -import { IContactRepository } from '../src/Domain/IRepository/IContactRepository'; - -export const createMockUser = (overrides: Partial = {}): UserAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174000', - username: 'testuser', - email: 'test@example.com', - password: 'hashedPassword', - fname: 'Test', - lname: 'User', - orgid: null, - token: null, - TokenExpires: null, - phone: null, - state: UserState.REGISTERED_NOT_VERIFIED, - regdate: new Date('2025-01-01'), - updateDate: new Date('2025-01-01'), - Orglogindate: null, - get isAdmin() { return this.state === UserState.ADMIN; }, - ...overrides -}); - -export const createMockOrganization = (overrides: Partial = {}): OrganizationAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174001', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'contact@testorg.com', - state: OrganizationState.ACTIVE, - regdate: new Date('2025-01-01'), - updateDate: new Date('2025-01-01'), - url: null, - userinorg: 0, - maxOrganizationalDecks: 10, - users: [], - ...overrides -}); - -export const createMockDeck = (overrides: Partial = {}): DeckAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174002', - name: 'Test Deck', - type: DeckType.JOKER, - userid: '123e4567-e89b-12d3-a456-426614174000', - creationdate: new Date('2025-01-01'), - cards: [], - playedNumber: 0, - ctype: CType.PUBLIC, - updateDate: new Date('2025-01-01'), - state: DeckState.ACTIVE, - organization: null, - user: null, - isEditable: jest.fn().mockReturnValue(true), - ...overrides -}); - -export const createMockContact = (overrides: Partial = {}): ContactAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174003', - name: 'John Doe', - email: 'john.doe@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'This is a test contact message.', - state: ContactState.ACTIVE, - createDate: new Date('2025-01-01'), - updateDate: new Date('2025-01-01'), - adminResponse: null, - responseDate: null, - respondedBy: null, - ...overrides -}); - -export const createMockDate = () => new Date('2025-01-01T00:00:00Z'); - -// Mock Repository Factory Functions -export const createMockUserRepository = (): jest.Mocked => ({ - create: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findById: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - findByUsername: jest.fn(), - findByEmail: jest.fn(), - findByToken: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), - deactivate: jest.fn(), - activate: jest.fn(), -} as jest.Mocked); - -export const createMockOrganizationRepository = (): jest.Mocked => ({ - create: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findById: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), -} as jest.Mocked); - -export const createMockDeckRepository = (): jest.Mocked => ({ - create: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findById: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), - countActiveByUserId: jest.fn(), - countOrganizationalByUserId: jest.fn(), - findFilteredDecks: jest.fn(), -} as jest.Mocked); - -export const createMockContactRepository = (): jest.Mocked => ({ - create: jest.fn(), - findById: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), -} as jest.Mocked); - -export const createMockJWTService = () => ({ - create: jest.fn(), - verify: jest.fn(), - shouldRefreshToken: jest.fn(), - parseDuration: jest.fn(), -} as any); - -export const createMockTokenService = () => ({ - generateSecureToken: jest.fn(), - generateVerificationToken: jest.fn(), - generatePasswordResetToken: jest.fn(), - isTokenExpired: jest.fn(), - validateToken: jest.fn(), -} as any); - -export const createMockEmailService = () => ({ - sendEmail: jest.fn(), - sendVerificationEmail: jest.fn(), - sendPasswordResetEmail: jest.fn(), - sendContactResponseEmail: jest.fn(), - loadTemplate: jest.fn(), -} as any); - -export const createMockPasswordService = () => ({ - hashPassword: jest.fn(), - verifyPassword: jest.fn(), - validatePasswordStrength: jest.fn(), - generateRandomPassword: jest.fn(), -} as any); diff --git a/SerpentRace_Backend/tsconfig.json b/SerpentRace_Backend/tsconfig.json deleted file mode 100644 index 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/.env.dev b/SerpentRace_Docker/.env.dev deleted file mode 100644 index 5621c2a8..00000000 --- a/SerpentRace_Docker/.env.dev +++ /dev/null @@ -1,55 +0,0 @@ -# ============================================== -# SerpentRace Backend Environment Configuration -# ============================================== -# Copy this file to .env and fill in your values - -# APPLICATION CONFIGURATION -NODE_ENV=development -PORT=3000 -APP_BASE_URL=http://localhost:3000 -FRONTEND_URL=http://localhost:5173 - -# DATABASE CONFIGURATION (PostgreSQL) -DB_HOST=postgres -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=postgres - -# REDIS CONFIGURATION -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_URL=redis://redis:6379 - -# MINIO CONFIGURATION -MINIO_ENDPOINT=minio -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -MINIO_USE_SSL=false -MINIO_BUCKET_NAME=serpentrace-logs - -# JWT CONFIGURATION -JWT_SECRET=your_super_secret_jwt_key_change_in_production -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d -GAME_TOKEN_EXPIRY=86400 - -# EMAIL SERVICE CONFIGURATION -EMAIL_HOST=mail.serpentrace.hu -EMAIL_PORT=465 -EMAIL_SECURE=true -EMAIL_USER=noreply@serpentrace.hu -EMAIL_PASS=ZUx720ece&Cin&F{ -EMAIL_FROM=noreply@serpentrace.hu - -# CHAT SYSTEM CONFIGURATION -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# GAME CONFIGURATION -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 diff --git a/SerpentRace_Docker/.env.example b/SerpentRace_Docker/.env.example deleted file mode 100644 index a1e8afbf..00000000 --- a/SerpentRace_Docker/.env.example +++ /dev/null @@ -1,222 +0,0 @@ -# ============================================== -# SerpentRace Backend Environment Configuration -# ============================================== -# Copy this file to .env and fill in your values -# This file contains all environment variables used by the backend - -# ============================================== -# APPLICATION CONFIGURATION -# ============================================== - -# Node.js environment (development, production, test) -NODE_ENV=development - -# Server port number -PORT=3000 - -# Base URL for the application (used for email links, etc.) -APP_BASE_URL=http://localhost:3000 - -# ============================================== -# DATABASE CONFIGURATION (PostgreSQL) -# ============================================== - -# Database connection details -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=your_db_password - -# Database URL (alternative to individual settings) -# DATABASE_URL=postgresql://username:password@localhost:5432/serpentrace - -# ============================================== -# REDIS CONFIGURATION -# ============================================== - -# Redis connection details (for caching and sessions) -REDIS_HOST=localhost -REDIS_PORT=6379 - -# Redis URL (alternative to individual settings) -REDIS_URL=redis://localhost:6379 - -# Redis password (if required) -# REDIS_PASSWORD=your_redis_password - -# ============================================== -# JWT (JSON Web Token) CONFIGURATION -# ============================================== - -# Secret key for JWT signing (REQUIRED - use a strong, random key in production) -JWT_SECRET=your_super_secret_jwt_key_change_in_production - -# JWT token expiration time -# Can be specified in seconds (e.g., 86400) or time format (e.g., 24h, 7d, 30m) -JWT_EXPIRY=86400 -# Alternative format -JWT_EXPIRATION=24h - -# JWT refresh token expiration (for future use) -JWT_REFRESH_EXPIRATION=7d - -# Game token expiration (for game session tokens) -GAME_TOKEN_EXPIRY=86400 - -# ============================================== -# EMAIL SERVICE CONFIGURATION (SMTP) -# ============================================== - -# SMTP server configuration -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_SECURE=false - -# Email authentication -EMAIL_USER=your_email@domain.com -EMAIL_PASS=your_email_password - -# From address for outgoing emails -EMAIL_FROM=noreply@serpentrace.com - -# ============================================== -# CHAT SYSTEM CONFIGURATION -# ============================================== - -# Chat inactivity timeout (in minutes) -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 - -# Maximum messages per user per session -CHAT_MAX_MESSAGES_PER_USER=100 - -# Cleanup old messages after X weeks -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# ============================================== -# GAME CONFIGURATION -# ============================================== - -# Board generation settings -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 - -# ============================================== -# MINIO/S3 CONFIGURATION (File Storage) -# ============================================== - -# MinIO server configuration (for file uploads) -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -MINIO_USE_SSL=false - -# S3 bucket name (if using S3 instead of MinIO) -# S3_BUCKET_NAME=serpentrace-files - -# ============================================== -# LOGGING CONFIGURATION -# ============================================== - -# Log level (error, warn, info, debug) -LOG_LEVEL=info - -# Log file retention (in days) -LOG_RETENTION_DAYS=30 - -# ============================================== -# SECURITY CONFIGURATION -# ============================================== - -# API rate limiting (requests per minute per IP) -RATE_LIMIT_RPM=60 - -# Maximum file upload size (in MB) -MAX_UPLOAD_SIZE_MB=10 - -# CORS allowed origins (comma-separated) -CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:8080 - -# ============================================== -# ADMIN CONFIGURATION -# ============================================== - -# Admin bypass settings -ADMIN_BYPASS_ENABLED=true - -# Default admin user (for development only) -# ADMIN_DEFAULT_EMAIL=admin@serpentrace.com -# ADMIN_DEFAULT_PASSWORD=change_this_password - -# ============================================== -# MONITORING & HEALTH CHECKS -# ============================================== - -# Health check endpoint timeout (in milliseconds) -HEALTH_CHECK_TIMEOUT=5000 - -# Database connection pool settings -DB_CONNECTION_POOL_MIN=2 -DB_CONNECTION_POOL_MAX=10 - -# ============================================== -# DEVELOPMENT ONLY SETTINGS -# ============================================== -# These settings should only be used in development - -# Enable detailed SQL logging -DB_LOGGING=true - -# Enable debug mode for various services -DEBUG_MODE=false - -# Disable email sending in development (logs emails instead) -EMAIL_DEBUG_MODE=true - -# ============================================== -# PRODUCTION ONLY SETTINGS -# ============================================== -# These settings are typically used only in production - -# Enable HTTPS (for production) -# HTTPS_ENABLED=true -# SSL_CERT_PATH=/path/to/cert.pem -# SSL_KEY_PATH=/path/to/key.pem - -# Sentry configuration (for error tracking) -# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id - -# New Relic configuration (for performance monitoring) -# NEW_RELIC_LICENSE_KEY=your_new_relic_license_key -# NEW_RELIC_APP_NAME=SerpentRace Backend - -# ============================================== -# EXTERNAL API KEYS (Optional) -# ============================================== - -# Third-party service API keys (if used) -# ANALYTICS_API_KEY=your_analytics_key -# PAYMENT_API_KEY=your_payment_processor_key - -# ============================================== -# NOTES & SECURITY WARNINGS -# ============================================== - -# SECURITY NOTES: -# - Never commit .env files to version control -# - Use strong, unique passwords and keys -# - Regularly rotate JWT secrets and API keys -# - Use environment-specific values for each deployment - -# REQUIRED VARIABLES: -# The following variables are required for the application to start: -# - NODE_ENV -# - DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD -# - REDIS_HOST, REDIS_PORT -# - JWT_SECRET -# - EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS - -# OPTIONAL VARIABLES: -# All other variables have sensible defaults and are optional diff --git a/SerpentRace_Docker/.env.prod b/SerpentRace_Docker/.env.prod deleted file mode 100644 index 5b578f34..00000000 --- a/SerpentRace_Docker/.env.prod +++ /dev/null @@ -1,55 +0,0 @@ -# Production Environment Variables - -# Production settings -NODE_ENV=production - -#Backend -# Database -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=serpentrace_secure_password_2024! - -# PostgreSQL Database (for docker-compose) -POSTGRES_PASSWORD=serpentrace_secure_password_2024! - -# Redis -REDIS_URL=redis://localhost:6379 -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= - -# JWT - Use JWT_EXPIRY (seconds) or JWT_EXPIRATION (duration format like 24h, 7d) -JWT_SECRET=serpentrace_super_secure_jwt_secret_key_2024_production! -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# Email -EMAIL_HOST=smtp.example.com -EMAIL_PORT=587 -EMAIL_SECURE=false -EMAIL_USER=your_email@example.com -EMAIL_PASS=your_email_password -EMAIL_FROM="SerpentRace " - -# MinIO Object Storage -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=serpentrace_minio_admin -MINIO_SECRET_KEY=serpentrace_minio_secret_key_2024! -MINIO_BUCKET_NAME=serpentrace-logs - -# Application -APP_BASE_URL=http://localhost:3000 -PORT=3000 - -# Chat Limits -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# Logging -MAX_LOGS_PER_FILE=10000 diff --git a/SerpentRace_Docker/.env.server b/SerpentRace_Docker/.env.server deleted file mode 100644 index 5b578f34..00000000 --- a/SerpentRace_Docker/.env.server +++ /dev/null @@ -1,55 +0,0 @@ -# Production Environment Variables - -# Production settings -NODE_ENV=production - -#Backend -# Database -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=serpentrace_secure_password_2024! - -# PostgreSQL Database (for docker-compose) -POSTGRES_PASSWORD=serpentrace_secure_password_2024! - -# Redis -REDIS_URL=redis://localhost:6379 -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= - -# JWT - Use JWT_EXPIRY (seconds) or JWT_EXPIRATION (duration format like 24h, 7d) -JWT_SECRET=serpentrace_super_secure_jwt_secret_key_2024_production! -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# Email -EMAIL_HOST=smtp.example.com -EMAIL_PORT=587 -EMAIL_SECURE=false -EMAIL_USER=your_email@example.com -EMAIL_PASS=your_email_password -EMAIL_FROM="SerpentRace " - -# MinIO Object Storage -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=serpentrace_minio_admin -MINIO_SECRET_KEY=serpentrace_minio_secret_key_2024! -MINIO_BUCKET_NAME=serpentrace-logs - -# Application -APP_BASE_URL=http://localhost:3000 -PORT=3000 - -# Chat Limits -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# Logging -MAX_LOGS_PER_FILE=10000 diff --git a/SerpentRace_Docker/DOCKER_README.md b/SerpentRace_Docker/DOCKER_README.md deleted file mode 100644 index 9b004668..00000000 --- a/SerpentRace_Docker/DOCKER_README.md +++ /dev/null @@ -1,267 +0,0 @@ -# SerpentRace Docker Development Environment - -This Docker setup provides a complete development environment for SerpentRace with hot reloading and all necessary services. - -## 🚀 Quick Start - -### Development Environment - -1. **Start the development environment:** - ```bash - # Windows - docker-manage.bat dev:start - - # Linux/Mac - ./docker-manage.sh dev:start - ``` - -2. **Access the applications:** - - **Frontend:** http://localhost:5173 - - **Backend API:** http://localhost:3000 - - **Swagger API Docs:** http://localhost:3000/api-docs - - **PostgreSQL:** localhost:5432 (user: postgres, password: postgres) - - **Redis:** localhost:6379 - - **MinIO Console:** http://localhost:9001 (serpentrace / serpentrace123!) - - **PgAdmin:** http://localhost:8080 (admin@serpentrace.dev / admin) - - **Redis Commander:** http://localhost:8081 - -3. **Stop the environment:** - ```bash - # Windows - docker-manage.bat dev:stop - - # Linux/Mac - ./docker-manage.sh dev:stop - ``` - -### Production Environment - -1. **Configure production environment:** - - Copy `.env.prod` and update all values with secure passwords - - Update JWT secrets and database passwords - -2. **Start production:** - ```bash - # Windows - docker-manage.bat prod:start - - # Linux/Mac - ./docker-manage.sh prod:start - ``` - -## 📁 File Structure - -``` -SzeSnake/ -├── docker-compose.dev.yml # Development environment -├── docker-compose.prod.yml # Production environment -├── docker-manage.sh # Linux/Mac management script -├── docker-manage.bat # Windows management script -├── .env.dev # Development environment variables -├── .env.prod # Production environment variables -├── SerpentRace_Backend/ -│ ├── Dockerfile # Production backend image -│ ├── Dockerfile.dev # Development backend image -│ └── .dockerignore -└── SerpentRace_Frontend/ - ├── Dockerfile # Production frontend image - ├── Dockerfile.dev # Development frontend image - ├── nginx.conf # Nginx configuration for production - └── .dockerignore -``` - -## 🛠 Development Features - -### Hot Reloading -- **Backend:** Uses `nodemon` and `ts-node` for automatic TypeScript compilation and server restart -- **Frontend:** Uses Vite's built-in HMR (Hot Module Replacement) - -### Volume Mapping -- Source code is mounted as volumes for instant file changes -- Node modules are preserved in named volumes for performance - -### Development Tools -- **PgAdmin:** Web-based PostgreSQL administration -- **Redis Commander:** Web-based Redis management -- **MinIO Console:** Object storage management - -### Database Initialization -- Automatic database setup with test data from `sql_dump_with_test_data.sql` - -## 🐳 Docker Services - -### Backend (`backend`) -- **Image:** Node.js 20 Alpine -- **Port:** 3000 -- **Features:** Hot reload, TypeScript support -- **Dependencies:** PostgreSQL, Redis, MinIO - -### Frontend (`frontend`) -- **Image:** Node.js 20 Alpine (dev) / Nginx Alpine (prod) -- **Port:** 5173 (dev) / 80 (prod) -- **Features:** Vite HMR, React Fast Refresh - -### PostgreSQL (`postgres`) -- **Image:** PostgreSQL 15 Alpine -- **Port:** 5432 -- **Database:** serpentrace -- **Credentials:** postgres/postgres (dev) - -### Redis (`redis`) -- **Image:** Redis 7 Alpine -- **Port:** 6379 -- **Features:** Persistence enabled - -### MinIO (`minio`) -- **Image:** MinIO latest -- **Ports:** 9000 (API), 9001 (Console) -- **Features:** S3-compatible object storage - -## 🔧 Management Commands - -### Using the Management Scripts - -```bash -# Start development environment -./docker-manage.sh dev:start - -# Stop development environment -./docker-manage.sh dev:stop - -# View logs for all services -./docker-manage.sh logs - -# View logs for specific service -./docker-manage.sh logs backend -./docker-manage.sh logs frontend - -# Clean up all resources -./docker-manage.sh cleanup - -# Production commands -./docker-manage.sh prod:start -./docker-manage.sh prod:stop -``` - -### Manual Docker Compose Commands - -```bash -# Development -docker-compose -f docker-compose.dev.yml --env-file .env.dev up --build -d -docker-compose -f docker-compose.dev.yml down - -# Production -docker-compose -f docker-compose.prod.yml --env-file .env.prod up --build -d -docker-compose -f docker-compose.prod.yml down - -# View logs -docker-compose -f docker-compose.dev.yml logs -f [service_name] - -# Rebuild specific service -docker-compose -f docker-compose.dev.yml up --build backend - -# Execute commands in running containers -docker-compose -f docker-compose.dev.yml exec backend npm run test -docker-compose -f docker-compose.dev.yml exec postgres psql -U postgres -d serpentrace -``` - -## 🔒 Security Considerations - -### Development -- Default passwords are used for convenience -- Services are exposed on localhost -- Debug tools are included - -### Production -- **IMPORTANT:** Update all passwords in `.env.prod` -- Use strong JWT secrets (256+ characters recommended) -- Services are not directly exposed -- No debug tools included - -## 🐛 Troubleshooting - -### Common Issues - -1. **Port conflicts:** - - Check if ports 3000, 5173, 5432, 6379, 9000, 9001, 8080, 8081 are available - - Modify port mappings in docker-compose files if needed - -2. **File watching issues on Windows:** - - WSL2 is recommended for better file system performance - - Ensure Docker Desktop is configured to use WSL2 - -3. **Database connection issues:** - - Wait for health checks to pass before the application starts - - Check logs: `./docker-manage.sh logs postgres` - -4. **Hot reload not working:** - - Ensure volumes are properly mounted - - Check file permissions on Linux/Mac systems - -### Performance Tips - -1. **Use WSL2 on Windows** for better file system performance -2. **Increase Docker memory** allocation if needed -3. **Use .dockerignore** to exclude unnecessary files -4. **Prune unused Docker resources** regularly: `docker system prune` - -## 📝 Environment Variables - -### Development (.env.dev) -```bash -POSTGRES_PASSWORD=postgres -JWT_SECRET=dev_jwt_secret_change_in_production -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -``` - -### Production (.env.prod) -```bash -POSTGRES_PASSWORD=your_secure_password -JWT_SECRET=your_very_long_secure_jwt_secret -MINIO_ACCESS_KEY=your_minio_access_key -MINIO_SECRET_KEY=your_secure_minio_secret -``` - -## 🔄 Health Checks - -All services include health checks to ensure proper startup order: -- **PostgreSQL:** `pg_isready` -- **Redis:** `redis-cli ping` -- **MinIO:** HTTP health endpoint -- **Backend:** HTTP health endpoint -- **Frontend:** HTTP health endpoint (production) - -The application will only start after all dependencies are healthy. - -## 📊 Monitoring - -### Logs -```bash -# All services -./docker-manage.sh logs - -# Specific service -./docker-manage.sh logs backend -./docker-manage.sh logs frontend -./docker-manage.sh logs postgres -``` - -### Service Status -```bash -# Check running containers -docker ps - -# Check service health -docker-compose -f docker-compose.dev.yml ps -``` - -## 🚀 Deployment - -For production deployment: - -1. Update `.env.prod` with secure values -2. Ensure proper firewall configuration -3. Use reverse proxy (nginx/traefik) for SSL termination -4. Consider using Docker Swarm or Kubernetes for orchestration -5. Set up monitoring and backup solutions diff --git a/SerpentRace_Docker/Dockerfile_backend b/SerpentRace_Docker/Dockerfile_backend deleted file mode 100644 index 236d55bf..00000000 --- a/SerpentRace_Docker/Dockerfile_backend +++ /dev/null @@ -1,60 +0,0 @@ -# Production Dockerfile for SerpentRace Backend -FROM node:20-alpine AS builder - -# Set working directory -WORKDIR /app - -# Install dependencies needed for native modules -RUN apk add --no-cache python3 make g++ - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install ALL dependencies for building (including devDependencies) -RUN npm ci - -# Copy source code -COPY . . - -# Build the application -RUN npm run build || echo "No build script found" - -# Production stage -FROM node:20-alpine AS production - -# Set working directory -WORKDIR /app - -# Install dependencies needed for native modules -RUN apk add --no-cache python3 make g++ - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install only production dependencies -RUN npm ci --only=production && npm cache clean --force - -# Copy built application from builder stage -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/package.json ./ - -# Create logs directory with proper permissions -RUN mkdir -p logs && chmod 777 logs - -# Create non-root user but don't switch to it for now -RUN addgroup -g 1001 -S nodejs -RUN adduser -S serpentrace -u 1001 -RUN chown -R serpentrace:nodejs /app - -# Keep running as root to avoid permission issues with mounted volumes -# USER serpentrace - -# Expose port -EXPOSE 3000 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3000/health || exit 1 - -# Production command -CMD ["npm", "start"] diff --git a/SerpentRace_Docker/Dockerfile_backend.dev b/SerpentRace_Docker/Dockerfile_backend.dev deleted file mode 100644 index 0f977a19..00000000 --- a/SerpentRace_Docker/Dockerfile_backend.dev +++ /dev/null @@ -1,29 +0,0 @@ -# Development Dockerfile for SerpentRace Backend -FROM node:20-alpine - -# Set working directory -WORKDIR /app - -# Install dependencies needed for native modules -RUN apk add --no-cache python3 make g++ - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install dependencies -RUN npm install - -# Install nodemon globally for development -RUN npm install -g nodemon ts-node - -# Copy source code -COPY . . - -# Create logs directory -RUN mkdir -p logs - -# Expose port -EXPOSE 3000 - -# Development command with hot reload -CMD ["npm", "run", "dev"] diff --git a/SerpentRace_Docker/Dockerfile_frontend b/SerpentRace_Docker/Dockerfile_frontend deleted file mode 100644 index a2a6fb87..00000000 --- a/SerpentRace_Docker/Dockerfile_frontend +++ /dev/null @@ -1,36 +0,0 @@ -# Production Dockerfile for SerpentRace Frontend -FROM node:20-alpine AS builder - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install dependencies (including devDependencies needed for build) -RUN npm ci - -# Copy source code -COPY . . - -# Build the application -RUN npm run build - -# Production stage with nginx -FROM nginx:alpine AS production - -# Copy built application -COPY --from=builder /app/dist /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# Expose port -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost || exit 1 - -# Start nginx -CMD ["nginx", "-g", "daemon off;"] diff --git a/SerpentRace_Docker/Dockerfile_frontend.dev b/SerpentRace_Docker/Dockerfile_frontend.dev deleted file mode 100644 index c05e37de..00000000 --- a/SerpentRace_Docker/Dockerfile_frontend.dev +++ /dev/null @@ -1,20 +0,0 @@ -# Development Dockerfile for SerpentRace Frontend -FROM node:20-alpine - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install dependencies -RUN npm install - -# Copy source code -COPY . . - -# Expose port -EXPOSE 5173 - -# Development command with hot reload -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/SerpentRace_Docker/docker-compose.deploy.yml b/SerpentRace_Docker/docker-compose.deploy.yml deleted file mode 100644 index 0e2e6af9..00000000 --- a/SerpentRace_Docker/docker-compose.deploy.yml +++ /dev/null @@ -1,188 +0,0 @@ -version: '3.8' - -services: - # Backend service using pre-built image - backend: - image: serpentrace-backend:latest - container_name: serpentrace-backend - restart: unless-stopped - ports: - - "3000:3000" - environment: - - NODE_ENV=production - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=${POSTGRES_PASSWORD} - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - JWT_SECRET=${JWT_SECRET} - - JWT_EXPIRATION=${JWT_EXPIRATION:-24h} - - JWT_REFRESH_EXPIRATION=${JWT_REFRESH_EXPIRATION:-7d} - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - - MINIO_USE_SSL=false - - EMAIL_HOST=${EMAIL_HOST} - - EMAIL_PORT=${EMAIL_PORT} - - EMAIL_SECURE=${EMAIL_SECURE} - - EMAIL_USER=${EMAIL_USER} - - EMAIL_PASS=${EMAIL_PASS} - - EMAIL_FROM=${EMAIL_FROM} - volumes: - - backend_logs:/app/logs - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service using pre-built image - frontend: - image: serpentrace-frontend:latest - container_name: serpentrace-frontend - restart: unless-stopped - ports: - - "80:80" - - "443:443" - depends_on: - - backend - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} - volumes: - - minio_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - -# Redis Commander for internal administration - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - networks: - - serpentrace-network - - # Database administration tool for internal administration - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin - restart: unless-stopped - ports: - - "5050:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_data:/var/lib/pgadmin - - ./deployment/pgadmin_servers_deployment.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - networks: - - serpentrace-network - - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - minio_data: - driver: local - backend_logs: - driver: local - pgadmin_data: - driver: local - -networks: - serpentrace-network: - driver: bridge \ No newline at end of file diff --git a/SerpentRace_Docker/docker-compose.dev.yml b/SerpentRace_Docker/docker-compose.dev.yml deleted file mode 100644 index bb5ac37c..00000000 --- a/SerpentRace_Docker/docker-compose.dev.yml +++ /dev/null @@ -1,184 +0,0 @@ -version: '3.8' - -services: - # Backend service with hot reload - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev - container_name: serpentrace-backend-dev - restart: unless-stopped - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - JWT_SECRET=dev_jwt_secret_change_in_production - - JWT_EXPIRATION=24h - - JWT_REFRESH_EXPIRATION=7d - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=serpentrace - - MINIO_SECRET_KEY=serpentrace123! - - MINIO_USE_SSL=false - volumes: - - ../SerpentRace_Backend:/app - - /app/node_modules - - ../SerpentRace_Backend/logs:/app/logs - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with hot reload - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev - container_name: serpentrace-frontend-dev - restart: unless-stopped - ports: - - "5173:5173" - environment: - - NODE_ENV=development - - API_URL=http://localhost:3000 - volumes: - - ../SerpentRace_Frontend:/app - - /app/node_modules - depends_on: - - backend - networks: - - serpentrace-network - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres-dev - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis-dev - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - command: redis-server --appendonly yes - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio-dev - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: serpentrace - MINIO_ROOT_PASSWORD: serpentrace123! - volumes: - - minio_dev_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Commander for development debugging - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - networks: - - serpentrace-network - - # Database administration tool - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin-dev - restart: unless-stopped - ports: - - "8080:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_dev_data:/var/lib/pgadmin - - ./pgadmin_servers.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - networks: - - serpentrace-network - -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - minio_dev_data: - driver: local - pgadmin_dev_data: - driver: local - -networks: - serpentrace-network: - driver: bridge diff --git a/SerpentRace_Docker/docker-compose.prod.yml b/SerpentRace_Docker/docker-compose.prod.yml deleted file mode 100644 index d6d9483c..00000000 --- a/SerpentRace_Docker/docker-compose.prod.yml +++ /dev/null @@ -1,161 +0,0 @@ -version: '3.8' - -services: - # Backend service - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend - container_name: serpentrace-backend - restart: unless-stopped - env_file: - - .env.prod - ports: - - "3000:3000" - environment: - - NODE_ENV=production - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=${DB_PASSWORD} - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - JWT_SECRET=${JWT_SECRET} - - JWT_EXPIRATION=${JWT_EXPIRATION:-24h} - - JWT_REFRESH_EXPIRATION=${JWT_REFRESH_EXPIRATION:-7d} - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - - MINIO_USE_SSL=false - - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-serpentrace-logs} - - EMAIL_HOST=${EMAIL_HOST} - - EMAIL_PORT=${EMAIL_PORT} - - EMAIL_SECURE=${EMAIL_SECURE} - - EMAIL_USER=${EMAIL_USER} - - EMAIL_PASS=${EMAIL_PASS} - - EMAIL_FROM=${EMAIL_FROM} - - APP_BASE_URL=${APP_BASE_URL:-http://localhost:3000} - - CHAT_INACTIVITY_TIMEOUT_MINUTES=${CHAT_INACTIVITY_TIMEOUT_MINUTES:-30} - - CHAT_MAX_MESSAGES_PER_USER=${CHAT_MAX_MESSAGES_PER_USER:-100} - - CHAT_MESSAGE_CLEANUP_WEEKS=${CHAT_MESSAGE_CLEANUP_WEEKS:-4} - - MAX_LOGS_PER_FILE=${MAX_LOGS_PER_FILE:-10000} - volumes: - - logs-data:/app/logs - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with nginx - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend - container_name: serpentrace-frontend - restart: unless-stopped - ports: - - "80:80" - depends_on: - - backend - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres - restart: unless-stopped - env_file: - - .env.prod - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio - restart: unless-stopped - env_file: - - .env.prod - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} - volumes: - - minio_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - minio_data: - driver: local - logs-data: - driver: local - -networks: - serpentrace-network: - driver: bridge diff --git a/SerpentRace_Docker/docker-compose.watch.nat.yml b/SerpentRace_Docker/docker-compose.watch.nat.yml deleted file mode 100644 index 6b13f4de..00000000 --- a/SerpentRace_Docker/docker-compose.watch.nat.yml +++ /dev/null @@ -1,206 +0,0 @@ -services: - # Backend service with hot reload - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev - container_name: serpentrace-backend-dev - restart: unless-stopped - env_file: - - .env.dev - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - - FRONTEND_URL=http://localhost:5173 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=serpentrace - - MINIO_SECRET_KEY=serpentrace123! - - MINIO_USE_SSL=false - volumes: [ ../SerpentRace_Backend/logs:/app/logs ] - develop: - watch: - - action: sync - path: ../SerpentRace_Backend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Backend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Backend/package-lock.json - - action: rebuild - path: ../SerpentRace_Backend/tsconfig.json - - action: rebuild - path: ./Dockerfile_backend.dev - - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - network_mode: host - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with hot reload - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev - container_name: serpentrace-frontend-dev - restart: unless-stopped - ports: - - "5173:5173" - environment: - - NODE_ENV=development - - VITE_API_URL=http://localhost:3000 - volumes: [] - develop: - watch: - - action: sync - path: ../SerpentRace_Frontend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Frontend/public - target: /app/public - - action: sync - path: ../SerpentRace_Frontend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Frontend/package-lock.json - - action: rebuild - path: ../SerpentRace_Frontend/vite.config.js - - action: rebuild - path: ./Dockerfile_frontend.dev - depends_on: - - backend - network_mode: host - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres-dev - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro - network_mode: host - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis-dev - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - command: redis-server --appendonly yes - network_mode: host - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio-dev - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: serpentrace - MINIO_ROOT_PASSWORD: serpentrace123! - volumes: - - minio_dev_data:/data - command: server /data --console-address ":9001" - network_mode: host - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Commander for development debugging - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - network_mode: host - - # Database administration tool - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin-dev - restart: unless-stopped - ports: - - "8080:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_dev_data:/var/lib/pgadmin - - ./pgadmin_servers.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - network_mode: host - -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - minio_dev_data: - driver: local - pgadmin_dev_data: - driver: local diff --git a/SerpentRace_Docker/docker-compose.watch.yml b/SerpentRace_Docker/docker-compose.watch.yml deleted file mode 100644 index 949a4770..00000000 --- a/SerpentRace_Docker/docker-compose.watch.yml +++ /dev/null @@ -1,217 +0,0 @@ -services: - # Backend service with hot reload - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev - container_name: serpentrace-backend-dev - restart: unless-stopped - env_file: - - .env.dev - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=serpentrace - - MINIO_SECRET_KEY=serpentrace123! - - MINIO_USE_SSL=false - volumes: [ ../SerpentRace_Backend/logs:/app/logs ] - develop: - watch: - - action: sync - path: ../SerpentRace_Backend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Backend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Backend/package-lock.json - - action: rebuild - path: ../SerpentRace_Backend/tsconfig.json - - action: rebuild - path: ./Dockerfile_backend.dev - - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with hot reload - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev - container_name: serpentrace-frontend-dev - restart: unless-stopped - ports: - - "5173:5173" - environment: - - NODE_ENV=development - - VITE_API_URL=http://localhost:3000 - volumes: - [] - develop: - watch: - - action: sync - path: ../SerpentRace_Frontend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Frontend/public - target: /app/public - - action: sync - path: ../SerpentRace_Frontend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Frontend/package-lock.json - - action: rebuild - path: ../SerpentRace_Frontend/vite.config.js - - action: rebuild - path: ./Dockerfile_frontend.dev - depends_on: - - backend - networks: - - serpentrace-network - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres-dev - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis-dev - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - command: redis-server --appendonly yes - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio-dev - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: serpentrace - MINIO_ROOT_PASSWORD: serpentrace123! - volumes: - - minio_dev_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Commander for development debugging - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - networks: - - serpentrace-network - - # Database administration tool - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin-dev - restart: unless-stopped - ports: - - "8080:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_dev_data:/var/lib/pgadmin - - ./pgadmin_servers.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - networks: - - serpentrace-network - -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - minio_dev_data: - driver: local - pgadmin_dev_data: - driver: local - -networks: - serpentrace-network: - driver: bridge diff --git a/SerpentRace_Docker/docker-manage.bat b/SerpentRace_Docker/docker-manage.bat deleted file mode 100644 index 856d6a97..00000000 --- a/SerpentRace_Docker/docker-manage.bat +++ /dev/null @@ -1,57 +0,0 @@ -```bat -@echo off -setlocal - -rem Define your services here -set SERVICES= - -rem Define the environment file -set ENV_FILE=.env - -rem Load the environment variables -if exist "%ENV_FILE%" ( - for /f "usebackq tokens=*" %%i in ("%ENV_FILE%") do ( - set "%%i" - ) -) - -rem Define the default action -set ACTION=up - -rem Parse command line arguments -:parse_args -if "%~1"=="" goto :end_parse -if "%~1"=="--build" ( - set ACTION=build -) else if "%~1"=="--down" ( - set ACTION=down -) else if "%~1"=="--help" ( - goto :help -) else if "%~1"=="dev:watch" ( - goto :dev_watch -) -shift -goto :parse_args - -:end_parse - -rem Display help -:help -echo Usage: docker-compose-wrapper [options] -echo. -echo Options: -echo --build Build the services -echo --down Stop and remove the containers -echo --help Display this help message -echo dev:watch Start development environment with file watchers -goto :eof - -rem Development watch mode -:dev_watch -echo Starting development environment with file watchers... -docker-compose -f docker-compose.watch.yml up --build -goto :eof - -rem Execute the docker-compose command with the parsed action -%DOCKER_COMPOSE% %ACTION% %SERVICES% -``` \ No newline at end of file diff --git a/SerpentRace_Docker/nginx.conf b/SerpentRace_Docker/nginx.conf deleted file mode 100644 index fa4630a8..00000000 --- a/SerpentRace_Docker/nginx.conf +++ /dev/null @@ -1,60 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html index.htm; - - # Enable gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Handle client routing - location / { - try_files $uri $uri/ /index.html; - } - - # API proxy to backend - location /api/ { - proxy_pass http://backend:3000/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } - - # WebSocket support - location /socket.io/ { - proxy_pass http://backend:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Static assets caching - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } -} diff --git a/SerpentRace_Docker/pgadmin_servers.json b/SerpentRace_Docker/pgadmin_servers.json deleted file mode 100644 index 828e872e..00000000 --- a/SerpentRace_Docker/pgadmin_servers.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "Servers": { - "1": { - "Name": "SerpentRace PostgreSQL Dev", - "Group": "Development", - "Host": "postgres", - "Port": 5432, - "MaintenanceDB": "serpentrace", - "Username": "postgres", - "UseSSLMode": "prefer", - "SSLMode": "prefer", - "SSLCompression": 0, - "Timeout": 10, - "UseSSHTunnel": 0, - "TunnelPort": "22", - "TunnelAuthentication": 0, - "KerberosAuthentication": false, - "ConnectionParameters": { - "sslmode": "prefer", - "connect_timeout": "10" - } - } - } -} diff --git a/SerpentRace_Docker/sql_dump_with_test_data.sql b/SerpentRace_Docker/sql_dump_with_test_data.sql deleted file mode 100644 index 7271b451..00000000 --- a/SerpentRace_Docker/sql_dump_with_test_data.sql +++ /dev/null @@ -1,369 +0,0 @@ --- SerpentRace Backend Database Schema and Test Data --- Generated on: August 22, 2025 --- PostgreSQL Database Dump - --- Enable UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- ============================================================================ --- DROP EXISTING TABLES (in reverse dependency order) --- ============================================================================ -DROP TABLE IF EXISTS "ChatArchives"; -DROP TABLE IF EXISTS "Chats"; -DROP TABLE IF EXISTS "Contacts"; -DROP TABLE IF EXISTS "Decks"; -DROP TABLE IF EXISTS "Users"; -DROP TABLE IF EXISTS "Organizations"; -DROP TABLE IF EXISTS "migrations"; - --- ============================================================================ --- CREATE TABLES --- ============================================================================ - --- Organizations Table -CREATE TABLE "Organizations" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "name" character varying(255) NOT NULL, - "contactfname" character varying(100) NOT NULL, - "contactlname" character varying(100) NOT NULL, - "contactphone" character varying(20) NOT NULL, - "contactemail" character varying(255) NOT NULL, - "state" integer NOT NULL DEFAULT 0, - "regdate" TIMESTAMP NOT NULL DEFAULT now(), - "updatedate" TIMESTAMP NOT NULL DEFAULT now(), - "url" character varying(500), - "userinorg" integer NOT NULL DEFAULT 0, - "maxOrganizationalDecks" integer, - CONSTRAINT "PK_Organizations" PRIMARY KEY ("id") -); - --- Users Table -CREATE TABLE "Users" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "orgid" uuid, - "username" character varying(100) NOT NULL UNIQUE, - "password" character varying(255) NOT NULL, - "email" character varying(255) NOT NULL UNIQUE, - "fname" character varying(100) NOT NULL, - "lname" character varying(100) NOT NULL, - "token" character varying(255), - "TokenExpires" TIMESTAMP, - "phone" character varying(20), - "state" integer NOT NULL DEFAULT 0, - "regdate" TIMESTAMP NOT NULL DEFAULT now(), - "updatedate" TIMESTAMP NOT NULL DEFAULT now(), - "Orglogindate" TIMESTAMP, - CONSTRAINT "PK_Users" PRIMARY KEY ("id"), - CONSTRAINT "FK_Users_Organizations" FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") -); - --- Decks Table -CREATE TABLE "Decks" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "name" character varying(255) NOT NULL, - "type" integer NOT NULL, - "user_id" uuid NOT NULL, - "creation_date" TIMESTAMP NOT NULL DEFAULT now(), - "cards" json NOT NULL, - "played_number" integer NOT NULL DEFAULT 0, - "ctype" integer NOT NULL DEFAULT 0, - "update_date" TIMESTAMP NOT NULL DEFAULT now(), - "state" integer NOT NULL DEFAULT 0, - "organization_id" uuid, - CONSTRAINT "PK_Decks" PRIMARY KEY ("id"), - CONSTRAINT "FK_Decks_Users" FOREIGN KEY ("user_id") REFERENCES "Users"("id"), - CONSTRAINT "FK_Decks_Organizations" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") -); - --- Chats Table -CREATE TABLE "Chats" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "users" uuid[] NOT NULL, - "messages" json NOT NULL DEFAULT '[]', - "updateDate" TIMESTAMP NOT NULL DEFAULT now(), - "state" integer NOT NULL DEFAULT 0, - "type" character varying(50) NOT NULL DEFAULT 'direct', - "name" character varying(255), - "gameId" uuid, - "createdBy" uuid, - "lastActivity" TIMESTAMP, - "createDate" TIMESTAMP NOT NULL DEFAULT now(), - "archiveDate" TIMESTAMP, - CONSTRAINT "PK_Chats" PRIMARY KEY ("id") -); - --- Chat Archives Table -CREATE TABLE "ChatArchives" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "chatId" uuid NOT NULL, - "archivedMessages" json NOT NULL, - "archivedAt" TIMESTAMP NOT NULL, - "createDate" TIMESTAMP NOT NULL DEFAULT now(), - "chatType" character varying(50) NOT NULL, - "chatName" character varying(255), - "gameId" uuid, - "participants" uuid[] NOT NULL, - CONSTRAINT "PK_ChatArchives" PRIMARY KEY ("id") -); - --- Contacts Table -CREATE TABLE "Contacts" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "name" character varying(255) NOT NULL, - "email" character varying(255) NOT NULL, - "userid" uuid, - "type" integer NOT NULL, - "txt" text NOT NULL, - "state" integer NOT NULL DEFAULT 0, - "createDate" TIMESTAMP NOT NULL DEFAULT now(), - "updateDate" TIMESTAMP NOT NULL DEFAULT now(), - "adminResponse" text, - "responseDate" TIMESTAMP, - "respondedBy" uuid, - CONSTRAINT "PK_Contacts" PRIMARY KEY ("id"), - CONSTRAINT "FK_Contacts_Users" FOREIGN KEY ("userid") REFERENCES "Users"("id"), - CONSTRAINT "FK_Contacts_Admins" FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") -); - --- Migrations table (for TypeORM) -CREATE TABLE "migrations" ( - "id" SERIAL NOT NULL, - "timestamp" bigint NOT NULL, - "name" character varying NOT NULL, - CONSTRAINT "PK_migrations" PRIMARY KEY ("id") -); - --- ============================================================================ --- CREATE INDEXES --- ============================================================================ -CREATE INDEX "IDX_DECK_USER_STATE_CTYPE" ON "Decks" ("user_id", "state", "ctype"); -CREATE INDEX "IDX_DECK_ORG_CTYPE_STATE" ON "Decks" ("organization_id", "ctype", "state"); -CREATE INDEX "IDX_USERS_EMAIL" ON "Users" ("email"); -CREATE INDEX "IDX_USERS_USERNAME" ON "Users" ("username"); -CREATE INDEX "IDX_USERS_ORGID" ON "Users" ("orgid"); - --- ============================================================================ --- INSERT TEST DATA --- ============================================================================ - --- Organizations Test Data -INSERT INTO "Organizations" ("id", "name", "contactfname", "contactlname", "contactphone", "contactemail", "state", "regdate", "updatedate", "url", "userinorg", "maxOrganizationalDecks") VALUES -('11111111-1111-1111-1111-111111111111', 'Tech Solutions Inc', 'John', 'Smith', '+1-555-0001', 'john.smith@techsolutions.com', 1, '2024-01-15 10:00:00', '2024-01-15 10:00:00', 'https://techsolutions.com', 5, 20), -('22222222-2222-2222-2222-222222222222', 'Educational Institute', 'Sarah', 'Johnson', '+1-555-0002', 'sarah.johnson@eduinst.edu', 1, '2024-02-01 09:30:00', '2024-02-01 09:30:00', 'https://eduinstitute.edu', 15, 50), -('33333333-3333-3333-3333-333333333333', 'Healthcare Corp', 'Michael', 'Brown', '+1-555-0003', 'michael.brown@healthcorp.com', 0, '2024-03-10 14:20:00', '2024-03-10 14:20:00', NULL, 0, 10); - --- Users Test Data -INSERT INTO "Users" ("id", "orgid", "username", "password", "email", "fname", "lname", "token", "TokenExpires", "phone", "state", "regdate", "updatedate", "Orglogindate") VALUES --- Regular users -('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', NULL, 'john_doe', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'john.doe@email.com', 'John', 'Doe', NULL, NULL, '+1-555-1001', 1, '2024-01-20 11:00:00', '2024-01-20 11:00:00', NULL), -('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'jane_premium', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'jane.smith@email.com', 'Jane', 'Smith', NULL, NULL, '+1-555-1002', 2, '2024-01-25 12:30:00', '2024-01-25 12:30:00', '2024-01-25 12:30:00'), -('cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-2222-2222-2222-222222222222', 'teacher_bob', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'bob.teacher@eduinst.edu', 'Bob', 'Teacher', NULL, NULL, '+1-555-1003', 2, '2024-02-05 09:15:00', '2024-02-05 09:15:00', '2024-02-05 09:15:00'), --- Admin user -('dddddddd-dddd-dddd-dddd-dddddddddddd', NULL, 'admin_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'admin@serpentrace.com', 'Admin', 'User', NULL, NULL, 'admin', '+1-555-9999', 5, '2024-01-01 08:00:00', '2024-01-01 08:00:00', NULL), --- Unverified user -('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', NULL, 'new_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'newuser@email.com', 'New', 'User', 'verification_token_12345', '2025-08-23 23:59:59', 'personal', NULL, 0, '2025-08-22 16:00:00', '2025-08-22 16:00:00', NULL); - --- Decks Test Data -INSERT INTO "Decks" ("id", "name", "type", "user_id", "creation_date", "cards", "played_number", "ctype", "update_date", "state", "organization_id") VALUES --- Public decks -('dddd1111-1111-1111-1111-111111111111', 'General Knowledge Quiz', 2, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-02-01 10:00:00', -'[ - {"id": "c1", "type": 0, "text": "What is the capital of France?", "answer": "Paris", "options": ["London", "Paris", "Berlin", "Madrid"]}, - {"id": "c2", "type": 0, "text": "Which planet is known as the Red Planet?", "answer": "Mars", "options": ["Venus", "Mars", "Jupiter", "Saturn"]}, - {"id": "c3", "type": 1, "text": "The Great Wall of China", "answer": "is visible from space", "options": ["is visible from space", "was built in one century"]}, - {"id": "c4", "type": 2, "text": "Describe the process of photosynthesis", "answer": null}, - {"id": "c5", "type": 3, "text": "The Earth is flat", "answer": false} -]', -25, 0, '2024-02-01 10:00:00', 0, NULL), - -('dddd2222-2222-2222-2222-222222222222', 'Math Fundamentals', 2, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2024-02-05 14:30:00', -'[ - {"id": "m1", "type": 0, "text": "What is 2 + 2?", "answer": "4", "options": ["3", "4", "5", "6"]}, - {"id": "m2", "type": 0, "text": "What is the square root of 16?", "answer": "4", "options": ["2", "4", "8", "16"]}, - {"id": "m3", "type": 3, "text": "Pi is approximately 3.14", "answer": true}, - {"id": "m4", "type": 4, "text": "Complete the sequence: 2, 4, 6, ?", "answer": "8"} -]', -15, 0, '2024-02-05 14:30:00', 0, NULL), - --- Private decks -('dddd3333-3333-3333-3333-333333333333', 'My Personal Study Notes', 2, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-02-10 16:45:00', -'[ - {"id": "p1", "type": 2, "text": "What did I learn about React hooks today?", "answer": null}, - {"id": "p2", "type": 2, "text": "Key points from the management meeting", "answer": null} -]', -3, 1, '2024-02-10 16:45:00', 0, NULL), - --- Organizational decks -('dddd4444-4444-4444-4444-444444444444', 'Company Training Module', 2, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2024-02-15 11:20:00', -'[ - {"id": "o1", "type": 0, "text": "What is our company policy on remote work?", "answer": "Flexible hybrid model", "options": ["No remote work", "Full remote", "Flexible hybrid model", "Weekends only"]}, - {"id": "o2", "type": 3, "text": "All employees must attend the monthly all-hands meeting", "answer": true}, - {"id": "o3", "type": 2, "text": "Describe the steps for requesting vacation time", "answer": null} -]', -8, 2, '2024-02-15 11:20:00', 0, '11111111-1111-1111-1111-111111111111'), - -('dddd5555-5555-5555-5555-555555555555', 'Educational Content for Students', 2, 'cccccccc-cccc-cccc-cccc-cccccccccccc', '2024-03-01 08:15:00', -'[ - {"id": "e1", "type": 0, "text": "When did World War II end?", "answer": "1945", "options": ["1943", "1944", "1945", "1946"]}, - {"id": "e2", "type": 1, "text": "Shakespeare wrote", "answer": "Romeo and Juliet", "options": ["Romeo and Juliet", "The Great Gatsby"]}, - {"id": "e3", "type": 3, "text": "The American Revolution began in 1776", "answer": false}, - {"id": "e4", "type": 4, "text": "Name three primary colors", "answer": "Red, Blue, Yellow"} -]', -42, 2, '2024-03-01 08:15:00', 0, '22222222-2222-2222-2222-222222222222'), - --- Joker and Luck type decks -('dddd6666-6666-6666-6666-666666666666', 'Lucky Challenges', 0, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-03-05 13:40:00', -'[ - {"id": "l1", "type": 4, "text": "Do 10 jumping jacks", "answer": null}, - {"id": "l2", "type": 4, "text": "Name your favorite childhood memory", "answer": null}, - {"id": "l3", "type": 4, "text": "Sing happy birthday", "answer": null} -]', -7, 0, '2024-03-05 13:40:00', 0, NULL), - -('dddd7777-7777-7777-7777-777777777777', 'Wild Cards', 1, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2024-03-08 19:25:00', -'[ - {"id": "j1", "type": 4, "text": "Skip your next turn", "answer": null}, - {"id": "j2", "type": 4, "text": "Draw two extra cards", "answer": null}, - {"id": "j3", "type": 4, "text": "Trade places with another player", "answer": null}, - {"id": "j4", "type": 4, "text": "Double your next score", "answer": null} -]', -12, 0, '2024-03-08 19:25:00', 0, NULL); - --- Chats Test Data -INSERT INTO "Chats" ("id", "users", "messages", "updateDate", "state", "type", "name", "gameId", "createdBy", "lastActivity", "createDate", "archiveDate") VALUES --- Direct message between two users -('chat1111-1111-1111-1111-111111111111', -'{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}', -'[ - {"id": "msg1", "date": "2024-03-20T10:30:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Hey Jane! How are you doing?"}, - {"id": "msg2", "date": "2024-03-20T10:32:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Hi John! I'\''m great, thanks for asking. How about you?"}, - {"id": "msg3", "date": "2024-03-20T10:35:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Doing well! Want to play a quiz game later?"}, - {"id": "msg4", "date": "2024-03-20T10:37:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Absolutely! I'\''ll prepare some questions."} -]', -'2024-03-20 10:37:00', 0, 'direct', NULL, NULL, NULL, '2024-03-20 10:37:00', '2024-03-20 10:30:00', NULL), - --- Group chat for organization -('chat2222-2222-2222-2222-222222222222', -'{"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "cccccccc-cccc-cccc-cccc-cccccccccccc", "dddddddd-dddd-dddd-dddd-dddddddddddd"}', -'[ - {"id": "msg5", "date": "2024-03-21T14:15:00Z", "userid": "dddddddd-dddd-dddd-dddd-dddddddddddd", "text": "Welcome everyone to the study group!"}, - {"id": "msg6", "date": "2024-03-21T14:16:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Thanks for organizing this!"}, - {"id": "msg7", "date": "2024-03-21T14:18:00Z", "userid": "cccccccc-cccc-cccc-cccc-cccccccccccc", "text": "I'\''ve prepared some educational content to share"}, - {"id": "msg8", "date": "2024-03-21T14:20:00Z", "userid": "dddddddd-dddd-dddd-dddd-dddddddddddd", "text": "Great! Let'\''s start with the basics"} -]', -'2024-03-21 14:20:00', 0, 'group', 'Study Group', NULL, 'dddddddd-dddd-dddd-dddd-dddddddddddd', '2024-03-21 14:20:00', '2024-03-21 14:15:00', NULL), - --- Game chat -('chat3333-3333-3333-3333-333333333333', -'{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}', -'[ - {"id": "msg9", "date": "2024-03-22T16:45:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Ready to start the quiz game?"}, - {"id": "msg10", "date": "2024-03-22T16:46:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Yes! Let'\''s do this!"}, - {"id": "msg11", "date": "2024-03-22T16:50:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Great job on that last question!"}, - {"id": "msg12", "date": "2024-03-22T16:52:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Thanks! This is fun!"} -]', -'2024-03-22 16:52:00', 0, 'game', 'Quiz Game Session', 'game1111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-03-22 16:52:00', '2024-03-22 16:45:00', NULL); - --- Chat Archives Test Data -INSERT INTO "ChatArchives" ("id", "chatId", "archivedMessages", "archivedAt", "createDate", "chatType", "chatName", "gameId", "participants") VALUES -('arch1111-1111-1111-1111-111111111111', 'chat0000-0000-0000-0000-000000000000', -'[ - {"id": "oldmsg1", "date": "2024-01-15T09:00:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "This is an old conversation"}, - {"id": "oldmsg2", "date": "2024-01-15T09:05:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Yes, from last month"}, - {"id": "oldmsg3", "date": "2024-01-15T09:10:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Good times!"} -]', -'2024-02-15 00:00:00', '2024-02-15 00:00:00', 'direct', NULL, NULL, '{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}'); - --- Contacts Test Data -INSERT INTO "Contacts" ("id", "name", "email", "userid", "type", "txt", "state", "createDate", "updateDate", "adminResponse", "responseDate", "respondedBy") VALUES --- Bug report from registered user -('cont1111-1111-1111-1111-111111111111', 'John Doe', 'john.doe@email.com', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 0, 'I found a bug when creating a new deck. The cards are not saving properly when I add more than 10 cards.', 1, '2024-03-18 14:30:00', '2024-03-19 09:15:00', 'Thank you for reporting this issue. We have identified the problem and deployed a fix. Please try creating your deck again.', '2024-03-19 09:15:00', 'dddddddd-dddd-dddd-dddd-dddddddddddd'), - --- General question from anonymous user -('cont2222-2222-2222-2222-222222222222', 'Sarah Wilson', 'sarah.wilson@email.com', NULL, 2, 'Hi, I'\''m interested in using SerpentRace for my classroom. Do you have any educational pricing or features specifically designed for teachers?', 0, '2024-03-19 11:20:00', '2024-03-19 11:20:00', NULL, NULL, NULL), - --- Problem report from premium user -('cont3333-3333-3333-3333-333333333333', 'Jane Smith', 'jane.smith@email.com', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 1, 'I'\''m having trouble with the organization deck sharing feature. When I share a deck with my team, they can'\''t see the latest updates I made.', 0, '2024-03-20 16:45:00', '2024-03-20 16:45:00', NULL, NULL, NULL), - --- Sales inquiry -('cont4444-4444-4444-4444-444444444444', 'Michael Chen', 'michael.chen@company.com', NULL, 3, 'Our company is interested in purchasing premium licenses for 50 employees. Could you provide pricing information and enterprise features?', 0, '2024-03-21 10:10:00', '2024-03-21 10:10:00', NULL, NULL, NULL), - --- Other type of contact -('cont5555-5555-5555-5555-555555555555', 'Lisa Johnson', 'lisa.johnson@email.com', NULL, 4, 'I love using SerpentRace! Could you add support for audio questions in the quiz decks? This would be great for language learning.', 0, '2024-03-22 13:25:00', '2024-03-22 13:25:00', NULL, NULL, NULL); - --- Migration entries -INSERT INTO "migrations" ("timestamp", "name") VALUES -(1755691733404, 'test1755691733404'), -(1755706019351, 'AddEmailVerificationFields1755706019351'), -(1755817306222, 'AddChatMessagingSystem1755817306222'), -(1755855028839, 'CreateContactTable1755855028839'), -(1692712800000, 'AddMaxOrganizationalDecksToOrganization1692712800000'); - --- ============================================================================ --- UPDATE ORGANIZATION USER COUNTS --- ============================================================================ -UPDATE "Organizations" SET "userinorg" = ( - SELECT COUNT(*) FROM "Users" WHERE "Users"."orgid" = "Organizations"."id" -); - --- ============================================================================ --- HELPFUL QUERIES FOR TESTING --- ============================================================================ - --- Query to see all users with their organizations --- SELECT u.username, u.email, u.state, o.name as organization_name --- FROM "Users" u --- LEFT JOIN "Organizations" o ON u.orgid = o.id; - --- Query to see deck distribution by type and visibility --- SELECT --- CASE ctype --- WHEN 0 THEN 'Public' --- WHEN 1 THEN 'Private' --- WHEN 2 THEN 'Organization' --- END as deck_type, --- CASE type --- WHEN 0 THEN 'Luck' --- WHEN 1 THEN 'Joker' --- WHEN 2 THEN 'Question' --- END as card_type, --- COUNT(*) as count --- FROM "Decks" --- WHERE state = 0 --- GROUP BY ctype, type --- ORDER BY ctype, type; - --- Query to see active chats with participant count --- SELECT --- id, --- type, --- name, --- array_length(users, 1) as participant_count, --- json_array_length(messages) as message_count, --- lastActivity --- FROM "Chats" --- WHERE state = 0 --- ORDER BY lastActivity DESC; - --- Query to see contact distribution by type and status --- SELECT --- CASE type --- WHEN 0 THEN 'Bug' --- WHEN 1 THEN 'Problem' --- WHEN 2 THEN 'Question' --- WHEN 3 THEN 'Sales' --- WHEN 4 THEN 'Other' --- END as contact_type, --- CASE state --- WHEN 0 THEN 'Active' --- WHEN 1 THEN 'Resolved' --- WHEN 2 THEN 'Deleted' --- END as status, --- COUNT(*) as count --- FROM "Contacts" --- GROUP BY type, state --- ORDER BY type, state; - --- ============================================================================ --- END OF SQL DUMP --- ============================================================================ diff --git a/SerpentRace_Docker/sql_schema_only.sql b/SerpentRace_Docker/sql_schema_only.sql deleted file mode 100644 index a5d28f0e..00000000 --- a/SerpentRace_Docker/sql_schema_only.sql +++ /dev/null @@ -1,180 +0,0 @@ --- This script was generated by the ERD tool in pgAdmin 4. --- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps. -BEGIN; - --- =================================================================== --- STEP 1: Enable Required Extensions --- =================================================================== -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- =================================================================== --- STEP 2: Create Tables --- =================================================================== - -CREATE TABLE IF NOT EXISTS public."ChatArchives" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - "chatId" uuid NOT NULL, - "archivedMessages" json NOT NULL, - "archivedAt" timestamp without time zone NOT NULL, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - "chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL, - "chatName" character varying(255) COLLATE pg_catalog."default", - "gameId" uuid, - participants uuid[] NOT NULL, - CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Chats" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying, - name character varying(255) COLLATE pg_catalog."default", - "gameId" uuid, - "createdBy" uuid, - users uuid[] NOT NULL, - messages json NOT NULL DEFAULT '[]'::json, - "lastActivity" timestamp without time zone, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - state integer NOT NULL DEFAULT 0, - "archiveDate" timestamp without time zone, - CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Contacts" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - name character varying(255) COLLATE pg_catalog."default" NOT NULL, - email character varying(255) COLLATE pg_catalog."default" NOT NULL, - userid uuid, - type integer NOT NULL, - txt text COLLATE pg_catalog."default" NOT NULL, - state integer NOT NULL DEFAULT 0, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - "adminResponse" text COLLATE pg_catalog."default", - "responseDate" timestamp without time zone, - "respondedBy" uuid, - CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Decks" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - name character varying(255) COLLATE pg_catalog."default" NOT NULL, - type integer NOT NULL, - user_id uuid NOT NULL, - creation_date timestamp without time zone NOT NULL DEFAULT now(), - cards json NOT NULL, - played_number integer NOT NULL DEFAULT 0, - ctype integer NOT NULL DEFAULT 0, - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - state integer NOT NULL DEFAULT 0, - organization_id uuid, - CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Games" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL, - maxplayers integer NOT NULL, - logintype integer NOT NULL DEFAULT 0, - boardsize integer NOT NULL DEFAULT 50, - "createdBy" uuid NOT NULL, - organizationid uuid, - decks jsonb NOT NULL DEFAULT '[]'::jsonb, - playerids uuid[] NOT NULL DEFAULT '{}'::uuid[], - "winnerId" uuid, - state integer NOT NULL DEFAULT 0, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - start_date timestamp without time zone, - "finishDate" timestamp without time zone, - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - "organizationId" uuid, - CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id), - CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode) -); - -CREATE TABLE IF NOT EXISTS public."Organizations" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - name character varying(255) COLLATE pg_catalog."default" NOT NULL, - contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL, - contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL, - contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL, - contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL, - state integer NOT NULL DEFAULT 0, - regdate timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - url character varying(500) COLLATE pg_catalog."default", - userinorg integer NOT NULL DEFAULT 0, - "maxOrganizationalDecks" integer, - CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Users" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - orgid uuid, - username character varying(100) COLLATE pg_catalog."default" NOT NULL, - password character varying(255) COLLATE pg_catalog."default" NOT NULL, - email character varying(255) COLLATE pg_catalog."default" NOT NULL, - fname character varying(100) COLLATE pg_catalog."default" NOT NULL, - lname character varying(100) COLLATE pg_catalog."default" NOT NULL, - token character varying(255) COLLATE pg_catalog."default", - "TokenExpires" timestamp without time zone, - phone character varying(20) COLLATE pg_catalog."default", - state integer NOT NULL DEFAULT 0, - regdate timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - "Orglogindate" timestamp without time zone, - CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id), - CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email), - CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username) -); - -CREATE TABLE IF NOT EXISTS public.migrations -( - id serial NOT NULL, - "timestamp" bigint NOT NULL, - name character varying COLLATE pg_catalog."default" NOT NULL, - CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id) -); - -ALTER TABLE IF EXISTS public."Decks" - ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id) - REFERENCES public."Organizations" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Decks" - ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id) - REFERENCES public."Users" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Games" - ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId") - REFERENCES public."Users" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Games" - ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId") - REFERENCES public."Organizations" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Games" - ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy") - REFERENCES public."Users" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - -END; \ No newline at end of file diff --git a/SerpentRace_Frontend/.dockerignore b/SerpentRace_Frontend/.dockerignore deleted file mode 100644 index 51fba3e0..00000000 --- a/SerpentRace_Frontend/.dockerignore +++ /dev/null @@ -1,27 +0,0 @@ -node_modules -npm-debug.log -.git -.gitignore -README.md -.env -.env.local -.env.development.local -.env.test.local -.env.production.local -.cache -logs -*.log -.DS_Store -.vscode -.idea -*.swp -*.swo -dist -build -.next -.nuxt -.vuepress/dist -.serverless -.fusebox/ -.dynamodb/ -.tern-port diff --git a/SerpentRace_Frontend/.gitignore b/SerpentRace_Frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/SerpentRace_Frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/SerpentRace_Frontend/CLEANUP_SUMMARY.md b/SerpentRace_Frontend/CLEANUP_SUMMARY.md deleted file mode 100644 index e6c9a58a..00000000 --- a/SerpentRace_Frontend/CLEANUP_SUMMARY.md +++ /dev/null @@ -1,96 +0,0 @@ -# ⚡ Gyors ÖsszefoglalĂł - Felesleges Adatok TisztĂ­tĂĄs - -## 🎯 Mi a problĂ©ma? - -A frontend **10 felesleges mezƑt** kĂŒld a backendnek minden kĂĄrtya mentĂ©sekor. - -## 📊 SzĂĄmok - -- **Felesleges deck mezƑk:** 1 db (`description`) -- **Felesleges kĂĄrtya mezƑk:** 9 db -- **Payload csökkenĂ©s:** ~32-60% -- **ImplementĂĄciĂłs idƑ:** ~3-4 Ăłra - -## ✅ HasznĂĄlt mezƑk (BACKEND) - -```javascript -{ - name: "Pakli neve", - type: 2, // 0=LUCK, 1=JOKER, 2=QUESTION - ctype: 1, // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION - cards: [ - { - text: "KĂ©rdĂ©s szövege", - type: 0, // CardType enum (0-4) - answer: "..." // TÍPUS-SPECIFIKUS formĂĄtum! - } - ] -} -``` - -## ❌ Felesleges mezƑk (TÖRLENDƐ) - -### Deck: -- `description` - nincs a backend sĂ©mĂĄban - -### KĂĄrtya: -- `id` (frontend generĂĄlt) - backend UUID-t hasznĂĄl -- `question` - duplikĂĄciĂł (`text` hasznĂĄlandĂł) -- `statement` - duplikĂĄciĂł (`text` hasznĂĄlandĂł) -- `options` - `answer` array-ben kell lennie -- `correctAnswer` - `answer` array-ben kell lennie -- `leftItems`, `rightItems`, `correctPairs` - `answer` array-ben kell lennie -- `acceptedAnswers` - `answer` array-kĂ©nt kell lennie -- `hint` - nincs implementĂĄlva - -## 🔄 Helyes answer formĂĄtumok - -| TĂ­pus | answer formĂĄtum | -|-------|----------------| -| QUIZ (0) | `[{answer: "A", text: "...", correct: true}, ...]` | -| PAIRING (1) | `[{left: "...", right: "..."}, ...]` | -| OWN_ANSWER (2) | `["answer1", "answer2", ...]` | -| TRUE_FALSE (3) | `true` vagy `false` | -| CLOSER (4) | `{correct: 123, percent: 10}` | - -## đŸ› ïž KövetkezƑ lĂ©pĂ©sek - -1. ✅ Olvasd el: `FRONTEND_TO_BACKEND_DATA_CLEANUP.md` -2. 🔧 ImplementĂĄld: `cardBackendConverter.js` utility -3. 🔄 MĂłdosĂ­tsd: `DeckCreator.jsx` mentĂ©s logikĂĄt -4. ✅ Teszteld: minden kĂĄrtyatĂ­pust - -## 📁 KapcsolĂłdĂł fĂĄjlok - -- **RĂ©szletes dokumentĂĄciĂł:** `FRONTEND_TO_BACKEND_DATA_CLEANUP.md` -- **MĂłdosĂ­tandĂł frontend:** `src/pages/DeckCreator/DeckCreator.jsx` -- **Backend referencia:** `SerpentRace_Backend/src/Application/Services/CardProcessingService.ts` - ---- - -**Gyors pĂ©lda:** - -```javascript -// ❌ ROSSZ (jelenleg) -{ - text: "KĂ©rdĂ©s", - question: "KĂ©rdĂ©s", // DuplikĂĄciĂł - options: ["A", "B", "C"], // Felesleges - correctAnswer: 0 // Felesleges -} - -// ✅ JÓ (cĂ©lĂĄllapot) -{ - text: "KĂ©rdĂ©s", - type: 0, - answer: [ - {answer: "A", text: "A", correct: true}, - {answer: "B", text: "B", correct: false}, - {answer: "C", text: "C", correct: false} - ] -} -``` - ---- - -📖 **Teljes dokumentĂĄciĂł:** LĂĄsd `FRONTEND_TO_BACKEND_DATA_CLEANUP.md` diff --git a/SerpentRace_Frontend/README.md b/SerpentRace_Frontend/README.md deleted file mode 100644 index 7059a962..00000000 --- a/SerpentRace_Frontend/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/SerpentRace_Frontend/eslint.config.js b/SerpentRace_Frontend/eslint.config.js deleted file mode 100644 index ec2b712d..00000000 --- a/SerpentRace_Frontend/eslint.config.js +++ /dev/null @@ -1,33 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' - -export default [ - { ignores: ['dist'] }, - { - files: ['**/*.{js,jsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...js.configs.recommended.rules, - ...reactHooks.configs.recommended.rules, - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - }, -] diff --git a/SerpentRace_Frontend/index.html b/SerpentRace_Frontend/index.html deleted file mode 100644 index 8ce9494b..00000000 --- a/SerpentRace_Frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - SerpentRace - - -
- - - diff --git a/SerpentRace_Frontend/nginx.conf b/SerpentRace_Frontend/nginx.conf deleted file mode 100644 index fa4630a8..00000000 --- a/SerpentRace_Frontend/nginx.conf +++ /dev/null @@ -1,60 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html index.htm; - - # Enable gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Handle client routing - location / { - try_files $uri $uri/ /index.html; - } - - # API proxy to backend - location /api/ { - proxy_pass http://backend:3000/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } - - # WebSocket support - location /socket.io/ { - proxy_pass http://backend:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Static assets caching - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } -} diff --git a/SerpentRace_Frontend/package-lock.json b/SerpentRace_Frontend/package-lock.json deleted file mode 100644 index 4470baf7..00000000 --- a/SerpentRace_Frontend/package-lock.json +++ /dev/null @@ -1,4002 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@tailwindcss/vite": "^4.1.7", - "axios": "^1.12.2", - "framer-motion": "^12.19.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-icons": "^5.5.0", - "react-router-dom": "^7.6.0", - "react-toastify": "^11.0.5", - "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.7" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "terser": "^5.36.0", - "vite": "^6.3.5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", - "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.7" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", - "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-x64": "4.1.7", - "@tailwindcss/oxide-freebsd-x64": "4.1.7", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-x64-musl": "4.1.7", - "@tailwindcss/oxide-wasm32-wasi": "4.1.7", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", - "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", - "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", - "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", - "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", - "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", - "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", - "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", - "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", - "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", - "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", - "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", - "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", - "integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "tailwindcss": "4.1.7" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", - "dev": true, - "license": "ISC" - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/framer-motion": { - "version": "12.19.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.1.tgz", - "integrity": "sha512-nq9hwWAEKf4gzprbOZzKugLV5OVKF7zrNDY6UOVu+4D3ZgIkg8L9Jy6AMrpBM06fhbKJ6LEG6UY5+t7Eq6wNlg==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.19.0", - "motion-utils": "^12.19.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/motion-dom": { - "version": "12.19.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.19.0.tgz", - "integrity": "sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.19.0" - } - }, - "node_modules/motion-utils": { - "version": "12.19.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", - "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", - "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz", - "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==", - "license": "MIT", - "dependencies": { - "react-router": "7.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/react-toastify": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", - "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", - "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/SerpentRace_Frontend/package.json b/SerpentRace_Frontend/package.json deleted file mode 100644 index 3a4763d8..00000000 --- a/SerpentRace_Frontend/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@tailwindcss/vite": "^4.1.7", - "axios": "^1.12.2", - "framer-motion": "^12.19.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-icons": "^5.5.0", - "react-router-dom": "^7.6.0", - "react-toastify": "^11.0.5", - "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.7" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "terser": "^5.36.0", - "vite": "^6.3.5" - } -} diff --git a/SerpentRace_Frontend/src/App.jsx b/SerpentRace_Frontend/src/App.jsx deleted file mode 100644 index 9a0c72b9..00000000 --- a/SerpentRace_Frontend/src/App.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState, useEffect } from "react" -import { BrowserRouter as Router, Route, Routes } from "react-router-dom" -import { ROUTES } from "./utils/routes" -import AuthRegister from "./pages/Auth/AuthRegister" -import AuthLogin from "./pages/Auth/AuthLogin" -import Test from "./pages/Testing/Test" -import ForgotPassword from "./pages/Auth/ForgotPassword" -import ResetPassword from "./pages/Auth/ResetPassword" -import Landingpage from "./pages/Landing/Landingpage" -import Home from "./pages/Landing/Home" -import DeckManagerPage from "./pages/Decks/DeckManagerPage" -import Card_display from "./pages/Decks/Card_display" -import DeckCreator from "./pages/DeckCreator/DeckCreator" -import CompanyHub from "./pages/Contacts/Contacts" -import About from "./pages/About/About" -import ScrollToTop from "./components/ScrollToTop" -import GameScreen from "./pages/Game/GameScreen" -import GameTest from "./pages/Game/GameTest" -import Reports from "./pages/Report/Reports" -import Lobby from "./pages/Game/Lobby" -import ProfileCard from "./components/Userdetails/Userdetails" -import { ToastConfig } from "./components/Toastify/toastifyServices" // ✅ fontos: named import, nem default! -import VerifyEmailPage from "./pages/Auth/VerifyEmailPage" -import ChooseDeck from "./pages/Game/ChooseDeck" -import PlayerSetup from "./pages/Game/PlayerSetup" -import GameModalsDemo from "./pages/Game/GameModalsDemo" -import { GameWebSocketProvider } from "./contexts/GameWebSocketContext" - -function App() { - const [isMobile, setIsMobile] = useState(false) - - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth <= 1280) - } - - handleResize() - window.addEventListener("resize", handleResize) - - return () => window.removeEventListener("resize", handleResize) - }, []) - - // if (isMobile) { - // return ( - // - // - // } /> - // } /> - // } /> - // - // - // ); - // } - - return ( - <> - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* } /> */} - } /> - } /> - } /> - - - - - {/* ✅ Toastify Container */} - - - ) -} - -export default App diff --git a/SerpentRace_Frontend/src/api/deckApi.js b/SerpentRace_Frontend/src/api/deckApi.js deleted file mode 100644 index 1a9460a7..00000000 --- a/SerpentRace_Frontend/src/api/deckApi.js +++ /dev/null @@ -1,58 +0,0 @@ -import { apiClient } from './userApi' - -// Create a new deck in the backend -export const createDeck = async (deck) => { - try { - const response = await apiClient.post('/decks', deck) - return response.data - } catch (err) { - throw err - } -} - -// Get paginated decks (authenticated) -export const getDecksPage = async (from = 0, to = 49) => { - try { - const response = await apiClient.get(`/decks/page/${from}/${to}`) - return response.data - } catch (err) { - throw err - } -} - -// Get a specific deck by ID (authenticated) -export const getDeckById = async (deckId) => { - try { - const response = await apiClient.get(`/decks/${deckId}`) - return response.data - } catch (err) { - throw err - } -} - -// Update an existing deck (authenticated) -export const updateDeck = async (deckId, deck) => { - try { - const response = await apiClient.patch(`/decks/${deckId}`, deck) - return response.data - } catch (err) { - throw err - } -} - -// Delete a deck (soft delete) (authenticated) -export const deleteDeck = async (deckId) => { - try { - const response = await apiClient.delete(`/decks/${deckId}`) - return response.data - } catch (err) { - throw err - } -} - -export default { - createDeck, - getDeckById, - updateDeck, - deleteDeck -} diff --git a/SerpentRace_Frontend/src/api/gameApi.js b/SerpentRace_Frontend/src/api/gameApi.js deleted file mode 100644 index 7a512847..00000000 --- a/SerpentRace_Frontend/src/api/gameApi.js +++ /dev/null @@ -1,80 +0,0 @@ -import { apiClient } from './userApi'; - -/** - * Create a new game - * @param {Object} gameData - Game creation data - * @param {string[]} gameData.deckids - Array of deck UUIDs - * @param {number} gameData.maxplayers - Maximum players (2-8) - * @param {number} gameData.logintype - 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION - * @returns {Promise} Game data with gameCode - */ -export const createGame = async (gameData) => { - try { - const response = await apiClient.post('/games/start', gameData); - return response.data; - } catch (error) { - console.error('Error creating game:', error); - throw error; - } -}; - -/** - * Join an existing game - * @param {Object} joinData - Join game data - * @param {string} joinData.gameCode - 6-character game code - * @param {string} [joinData.playerName] - Player name (required for public games) - * @returns {Promise} Game data with gameToken - */ -export const joinGame = async (joinData) => { - try { - const response = await apiClient.post('/games/join', joinData); - return response.data; - } catch (error) { - console.error('Error joining game:', error); - console.error('Join game error response:', error.response?.data); - throw error; - } -}; - -/** - * Start the game (gamemaster only) - * @param {string} gameId - Game UUID - * @returns {Promise} Game data with board - */ -export const startGame = async (gameId) => { - try { - const response = await apiClient.post(`/games/${gameId}/start`); - return response.data; - } catch (error) { - console.error('Error starting game:', error); - throw error; - } -}; - -/** - * Get user's games - * @returns {Promise} Array of games - */ -export const getMyGames = async () => { - try { - const response = await apiClient.get('/games/my-games'); - return response.data; - } catch (error) { - console.error('Error fetching games:', error); - throw error; - } -}; - -/** - * Get active public games - * @returns {Promise} Array of active games - */ -export const getActiveGames = async () => { - try { - const response = await apiClient.get('/games/active'); - return response.data; - } catch (error) { - console.error('Error fetching active games:', error); - throw error; - } -}; diff --git a/SerpentRace_Frontend/src/api/userApi.js b/SerpentRace_Frontend/src/api/userApi.js deleted file mode 100644 index af231c48..00000000 --- a/SerpentRace_Frontend/src/api/userApi.js +++ /dev/null @@ -1,109 +0,0 @@ -import axios from "axios" - -export const API_CONFIG = { - baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : "") + "/api", - wsURL: "http://localhost:3000", - timeout: 10000, - retryAttempts: 3, -} - -export const apiClient = axios.create({ - baseURL: API_CONFIG.baseURL, - timeout: API_CONFIG.timeout, - withCredentials: true, // Important for cookie-based auth - headers: { - "Content-Type": "application/json", - }, -}) - -//login -export const login = async (username, password) => { - try { - const response = await apiClient.post("/users/login", { username, password }) - return response - } catch (error) { - throw error - } -} - -//register -export const register = async (username, email, password, fname, lname, phone) => { - try { - const response = await apiClient.post("/users/create", { username, email, password, fname, lname, phone }) - return response - } catch (error) { - throw error - } -} - -// Get current user's game statistics -export const getUserStats = async () => { - try { - const response = await apiClient.get("/users/me/stats") - return response.data - } catch (error) { - throw error - } -} - - -// Email verification - POST -export const verifyEmail = async (token) => { - try { - const response = await apiClient.post(`/users/verify-email/${token}`); - return response; - } catch (error) { - throw error; - } -}; - -// Get current user profile -export const getUserProfile = async () => { - try { - const response = await apiClient.get("/users/profile"); - return response.data; - } catch (error) { - throw error; - } -}; - -// Update current user profile -export const updateUserProfile = async (data) => { - try { - const response = await apiClient.patch("/users/profile", data); - return response.data; - } catch (error) { - throw error; - } -}; - -// Delete current user profile -export const deleteUserProfile = async () => { - try { - const response = await apiClient.delete("/users/profile"); - return response.data; - } catch (error) { - throw error; - } -}; - -// Request password reset -export const forgotPassword = async (email) => { - try { - const response = await apiClient.post("/users/forgot-password", { email }); - return response.data; - } catch (error) { - throw error; - } -}; - -// Reset password with token -export const resetPassword = async (token, newPassword) => { - try { - const response = await apiClient.post("/users/reset-password", { token, newPassword }); - return response.data; - } catch (error) { - throw error; - } -}; - diff --git a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/Path.module.css b/SerpentRace_Frontend/src/assets/SerpentRace_Animation/Path.module.css deleted file mode 100644 index fb29c73e..00000000 --- a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/Path.module.css +++ /dev/null @@ -1,96 +0,0 @@ -.animation { - animation: fill 0.5s ease forwards 2.9s; -} - -.path0 { - stroke-dasharray: 603.0596923828125; - stroke-dashoffset: 603.0596923828125; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.45s; -} - -.path1 { - stroke-dasharray: 503.0904846191406; - stroke-dashoffset: 503.0904846191406; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.5s; -} - -.path2 { - stroke-dasharray: 625.779541015625; - stroke-dashoffset: 625.779541015625; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.45s; -} - -.path3 { - stroke-dasharray: 714.129638671875; - stroke-dashoffset: 714.129638671875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.4s; -} - -.path4 { - stroke-dasharray: 427.98114013671875; - stroke-dashoffset: 427.98114013671875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.35s; -} - -.path5 { - stroke-dasharray: 593.7645263671875; - stroke-dashoffset: 593.7645263671875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.3s; -} - -.path6 { - stroke-dasharray: 603.0399780273438; - stroke-dashoffset: 603.0399780273438; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.25s; -} - -.path7 { - stroke-dasharray: 731.757568359375; - stroke-dashoffset: 731.757568359375; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.2s; -} - -.path8 { - stroke-dasharray: 382.3065185546875; - stroke-dashoffset: 382.3065185546875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.2s; -} - -.path9 { - stroke-dasharray: 603.0382690429688; - stroke-dashoffset: 603.0382690429688; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.15s; -} - -.path10 { - stroke-dasharray: 652.2447509765625; - stroke-dashoffset: 652.2447509765625; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.1s; -} - -@keyframes draw { - to { - stroke-dashoffset: 0; - } -} - -@keyframes fill { - from { - fill: transparent; - } - to { - fill: #ffffff; - } -} - diff --git a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx b/SerpentRace_Frontend/src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx deleted file mode 100644 index 376466b0..00000000 --- a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx +++ /dev/null @@ -1,34 +0,0 @@ -// src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx -// Animációs kiírás: SerpentRace - -import styles from "./Path.module.css"; -import React, { useRef } from "react"; - -const Animation = ({ sizePercentage = 100 }) => { - const width = (1253 * sizePercentage) / 100; - const height = (136 * sizePercentage) / 100; - - // 11 path-hoz refs - const pathRefs = Array.from({ length: 11 }, () => useRef(null)); - - return ( -
- {/* prettier-ignore */} - - - - - - - - - - - - - -
- ); -}; - -export default Animation; diff --git a/SerpentRace_Frontend/src/assets/backgrounds/Background.jsx b/SerpentRace_Frontend/src/assets/backgrounds/Background.jsx deleted file mode 100644 index d198d8a2..00000000 --- a/SerpentRace_Frontend/src/assets/backgrounds/Background.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useEffect, useState } from "react" -import { motion } from "framer-motion" - -const Background = () => { - const [gridSize, setGridSize] = useState({ cols: 12, rows: 6 }) - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) - const [path, setPath] = useState([]) - - useEffect(() => { - const updateGrid = () => { - const width = window.innerWidth - const height = window.innerHeight - const cols = Math.max(8, Math.floor(width / 100)) - const rows = Math.max(5, Math.floor(height / 100)) - setGridSize({ cols, rows }) - } - - const handleMouseMove = (e) => { - setMousePos({ x: e.clientX, y: e.clientY }) - } - - updateGrid() - window.addEventListener("resize", updateGrid) - window.addEventListener("mousemove", handleMouseMove) - - return () => { - window.removeEventListener("resize", updateGrid) - window.removeEventListener("mousemove", handleMouseMove) - } - }, []) - - useEffect(() => { - const interval = setInterval(() => { - const newCol = Math.floor(Math.random() * gridSize.cols) - const newRow = Math.floor(Math.random() * gridSize.rows) - setPath((prevPath) => { - const newPath = [...prevPath, { col: newCol, row: newRow, opacity: 1 }] - if (newPath.length > 10) newPath.shift() - return newPath - }) - }, 500) - - const fadeInterval = setInterval(() => { - setPath((prevPath) => - prevPath - .map((point) => ({ ...point, opacity: Math.max(0, point.opacity - 0.05) })) - .filter((point) => point.opacity > 0) - ) - }, 100) - - return () => { - clearInterval(interval) - clearInterval(fadeInterval) - } - }, [gridSize]) - - return ( -
-
- {[...Array(gridSize.cols * gridSize.rows)].map((_, i) => { - const col = i % gridSize.cols - const row = Math.floor(i / gridSize.cols) - const cellX = (col + 0.5) * (window.innerWidth / gridSize.cols) - const cellY = (row + 0.5) * (window.innerHeight / gridSize.rows) - - const dx = cellX - mousePos.x - const dy = cellY - mousePos.y - const distance = Math.sqrt(dx * dx + dy * dy) - const distanceFactor = Math.max(0, 1 - distance / 300) - - const pathPoint = path.find((p) => p.col === col && p.row === row) - const pathOpacity = pathPoint ? pathPoint.opacity : 0 - - return ( - - ) - })} -
-
- ) -} - -export default Background diff --git a/SerpentRace_Frontend/src/assets/pictures/Logo.jsx b/SerpentRace_Frontend/src/assets/pictures/Logo.jsx deleted file mode 100644 index 28376797..00000000 --- a/SerpentRace_Frontend/src/assets/pictures/Logo.jsx +++ /dev/null @@ -1,18 +0,0 @@ -// src/assets/pictures/Logo.png -// Logo kép importålåsa és paraméterezése - -import React from 'react'; -import logo from './Logo.png'; - -const Logo = ({ size = 100 }) => ( - Logo -); - -export default Logo; - diff --git a/SerpentRace_Frontend/src/assets/pictures/Logo.png b/SerpentRace_Frontend/src/assets/pictures/Logo.png deleted file mode 100644 index 480d8c5d..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/Logo.png and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/LogoCard.jsx b/SerpentRace_Frontend/src/assets/pictures/LogoCard.jsx deleted file mode 100644 index a356f976..00000000 --- a/SerpentRace_Frontend/src/assets/pictures/LogoCard.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useRef, useState } from "react" -import { motion, useMotionValue, useSpring } from "framer-motion" - -const springValues = { - damping: 30, - stiffness: 100, - mass: 2, -} - -export default function LogoCard({ - imageSrc, - altText = "Tilted card image", - captionText = "", - containerHeight = "300px", - containerWidth = "100%", - imageHeight = "300px", - imageWidth = "300px", - scaleOnHover = 1.1, - rotateAmplitude = 14, - showMobileWarning = true, - showTooltip = true, - overlayContent = null, - displayOverlayContent = false, -}) { - const ref = useRef(null) - const x = useMotionValue(0) - const y = useMotionValue(0) - const rotateX = useSpring(useMotionValue(0), springValues) - const rotateY = useSpring(useMotionValue(0), springValues) - const scale = useSpring(1, springValues) - const opacity = useSpring(0) - const rotateFigcaption = useSpring(0, { - stiffness: 350, - damping: 30, - mass: 1, - }) - - const [lastY, setLastY] = useState(0) - - function handleMouse(e) { - if (!ref.current) return - - const rect = ref.current.getBoundingClientRect() - const offsetX = e.clientX - rect.left - rect.width / 2 - const offsetY = e.clientY - rect.top - rect.height / 2 - - const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude - const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude - - rotateX.set(rotationX) - rotateY.set(rotationY) - - x.set(e.clientX - rect.left) - y.set(e.clientY - rect.top) - - const velocityY = offsetY - lastY - rotateFigcaption.set(-velocityY * 0.6) - setLastY(offsetY) - } - - function handleMouseEnter() { - scale.set(scaleOnHover) - opacity.set(1) - } - - function handleMouseLeave() { - opacity.set(0) - scale.set(1) - rotateX.set(0) - rotateY.set(0) - rotateFigcaption.set(0) - } - - return ( -
- {showMobileWarning && ( -
- This effect is not optimized for mobile. Check on desktop. -
- )} - - - - - {displayOverlayContent && overlayContent && ( - - {overlayContent} - - )} - - - {showTooltip && ( - - {captionText} - - )} -
- ) -} diff --git a/SerpentRace_Frontend/src/assets/pictures/busi.JPG b/SerpentRace_Frontend/src/assets/pictures/busi.JPG deleted file mode 100644 index ba062184..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/busi.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/donat.JPG b/SerpentRace_Frontend/src/assets/pictures/donat.JPG deleted file mode 100644 index d1fb7651..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/donat.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/gege.JPG b/SerpentRace_Frontend/src/assets/pictures/gege.JPG deleted file mode 100644 index cbef7bca..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/gege.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/piskor.JPG b/SerpentRace_Frontend/src/assets/pictures/piskor.JPG deleted file mode 100644 index 0d88b774..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/piskor.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/turo.JPG b/SerpentRace_Frontend/src/assets/pictures/turo.JPG deleted file mode 100644 index 07bf7ba8..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/turo.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/walke.JPG b/SerpentRace_Frontend/src/assets/pictures/walke.JPG deleted file mode 100644 index f8701e85..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/walke.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/zsola.JPG b/SerpentRace_Frontend/src/assets/pictures/zsola.JPG deleted file mode 100644 index d210a8c6..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/zsola.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/components/Buttons/Button.jsx b/SerpentRace_Frontend/src/components/Buttons/Button.jsx deleted file mode 100644 index a658e91a..00000000 --- a/SerpentRace_Frontend/src/components/Buttons/Button.jsx +++ /dev/null @@ -1,22 +0,0 @@ -// src/components/Inputs/InputBox.jsx -// Gomb komponens - -import { motion } from "framer-motion" - -export default function Button({ text, type, onClick, width, className }) { - const widthClass = width ? width : "w-full" - - return ( - - {text} - - ) -} diff --git a/SerpentRace_Frontend/src/components/Buttons/ButtonDark.jsx b/SerpentRace_Frontend/src/components/Buttons/ButtonDark.jsx deleted file mode 100644 index 896e75d5..00000000 --- a/SerpentRace_Frontend/src/components/Buttons/ButtonDark.jsx +++ /dev/null @@ -1,20 +0,0 @@ -// src/components/Inputs/InputBox.jsx -// Gomb komponens - -import { motion } from "framer-motion" - -export default function Button({ text, type, onClick, width }) { - const widthClass = width ? width : "w-full" - - return ( - - {text} - - ) -} diff --git a/SerpentRace_Frontend/src/components/Buttons/ButtonGreen.jsx b/SerpentRace_Frontend/src/components/Buttons/ButtonGreen.jsx deleted file mode 100644 index 459348c6..00000000 --- a/SerpentRace_Frontend/src/components/Buttons/ButtonGreen.jsx +++ /dev/null @@ -1,20 +0,0 @@ -// src/components/Buttons/ButtonGreen.jsx -// Zöld gomb komponens (ButtonGreen) - -import { motion } from "framer-motion" - -export default function ButtonGreen({ text, type, onClick, width }) { - const widthClass = width ? width : "w-full" - - return ( - - {text} - - ) -} diff --git a/SerpentRace_Frontend/src/components/Card/Card.jsx b/SerpentRace_Frontend/src/components/Card/Card.jsx deleted file mode 100644 index 675a6cb6..00000000 --- a/SerpentRace_Frontend/src/components/Card/Card.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -export default function Card({ title, children, onClose }) { - return ( -
- - {title &&

{title}

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

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

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

- 🃏 Kártyák -

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

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

-

- Ez a mƱvelet nem visszavonható. -

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

- 📝 Pakli SzerkesztĂ©s -

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

RendezĂ©si lehetƑsĂ©gek magyarĂĄzata

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

- {deck.name} -

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

- - Joker kårtya mƱködése: -

-

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

-

- CĂ©l: SzĂłrakozĂĄs Ă©s jĂĄtĂ©kosok közötti kapcsolat erƑsĂ­tĂ©se -

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

- - Kårtya szövege -

- -
- -