diff --git a/package.json b/package.json index 6ebb831..d957a7e 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,11 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-fastify": "^11.1.3", + "@nestjs/platform-socket.io": "^11.1.3", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", "@nestjs/throttler": "^6.4.0", + "@nestjs/websockets": "^11.1.3", "axios": "^1.10.0", "bcrypt": "^5.1.1", "bullmq": "^5.56.1", @@ -67,6 +69,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "slugify": "^1.6.6", + "socket.io": "^4.8.1", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d86fa3..917730f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 6.4.16 '@mikro-orm/nestjs': specifier: ^6.1.1 - version: 6.1.1(@mikro-orm/core@6.4.16)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 6.1.1(@mikro-orm/core@6.4.16)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) '@mikro-orm/postgresql': specifier: ^6.4.16 version: 6.4.16(@mikro-orm/core@6.4.16) @@ -31,22 +31,22 @@ importers: version: 6.4.16(@mikro-orm/core@6.4.16) '@nest-lab/fastify-multer': specifier: ^1.3.0 - version: 1.3.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-fastify@11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(rxjs@7.8.2) + version: 1.3.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-fastify@11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3))(rxjs@7.8.2) '@nest-lab/throttler-storage-redis': specifier: ^1.1.0 - version: 1.1.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/throttler@6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(ioredis@5.6.1)(reflect-metadata@0.2.2) + version: 1.1.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(@nestjs/throttler@6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(reflect-metadata@0.2.2))(ioredis@5.6.1)(reflect-metadata@0.2.2) '@nestjs-modules/mailer': specifier: ^2.0.2 - version: 2.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(nodemailer@7.0.4) + version: 2.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(nodemailer@7.0.4) '@nestjs/axios': specifier: ^4.0.0 version: 4.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.10.0)(rxjs@7.8.2) '@nestjs/bullmq': specifier: ^11.0.2 - version: 11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.56.1) + version: 11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(bullmq@5.56.1) '@nestjs/cache-manager': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(cache-manager@7.0.0)(keyv@5.3.4)(rxjs@7.8.2) + version: 3.0.1(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(cache-manager@7.0.0)(keyv@5.3.4)(rxjs@7.8.2) '@nestjs/cli': specifier: ^11.0.7 version: 11.0.7(@swc/cli@0.6.0(@swc/core@1.12.9)(chokidar@4.0.3))(@swc/core@1.12.9)(@types/node@22.16.0) @@ -58,7 +58,7 @@ importers: version: 4.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.1.3 - version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/jwt': specifier: ^11.0.0 version: 11.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) @@ -67,16 +67,22 @@ importers: version: 11.0.5(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-fastify': specifier: ^11.1.3 - version: 11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) + '@nestjs/platform-socket.io': + specifier: ^11.1.3 + version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 6.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) '@nestjs/swagger': specifier: ^11.2.0 - version: 11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + version: 11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) '@nestjs/throttler': specifier: ^6.4.0 - version: 6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + version: 6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(reflect-metadata@0.2.2) + '@nestjs/websockets': + specifier: ^11.1.3 + version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(@nestjs/platform-socket.io@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) axios: specifier: ^1.10.0 version: 1.10.0 @@ -119,6 +125,9 @@ importers: slugify: specifier: ^1.6.6 version: 1.6.6 + socket.io: + specifier: ^4.8.1 + version: 4.8.1 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -140,7 +149,7 @@ importers: version: 11.0.5(chokidar@4.0.3)(typescript@5.8.3) '@nestjs/testing': specifier: ^11.1.3 - version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) '@swc/cli': specifier: ^0.6.0 version: 0.6.0(@swc/core@1.12.9)(chokidar@4.0.3) @@ -1266,6 +1275,13 @@ packages: '@fastify/view': optional: true + '@nestjs/platform-socket.io@11.1.3': + resolution: {integrity: sha512-jQ+ccprmh3kKolBp+bb97zoaS3vKaiyeNqyctGqV4CSG8P6mXSaaUObWxAsw6Jdgn5YQAVEBWJ6FhvF4s6QZbg==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + rxjs: ^7.1.0 + '@nestjs/schedule@6.0.0': resolution: {integrity: sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==} peerDependencies: @@ -1314,6 +1330,18 @@ packages: '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 reflect-metadata: ^0.1.13 || ^0.2.0 + '@nestjs/websockets@11.1.3': + resolution: {integrity: sha512-IjhWKfRf0D247JxYIEs8USblJJbcxUsKJpzbCPaZ7TrVy4LrpG3IRQDlSTOw599TRIYP5ixyH9C0+v5DyaI9uA==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/platform-socket.io': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1382,6 +1410,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@swc/cli@0.6.0': resolution: {integrity: sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==} engines: {node: '>= 16.14.0'} @@ -1521,6 +1552,9 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/ejs@3.1.5': resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} @@ -1897,6 +1931,10 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2124,6 +2162,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + bcrypt@5.1.1: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} @@ -2426,6 +2468,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -2436,6 +2482,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.1.0: resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} engines: {node: '>=v18'} @@ -2736,6 +2786,14 @@ packages: resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} engines: {node: '>=8.10.0'} + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + enhanced-resolve@5.18.2: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} @@ -4407,6 +4465,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -4493,6 +4555,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -5195,6 +5261,17 @@ packages: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -5667,6 +5744,10 @@ packages: resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -5771,6 +5852,18 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + ws@8.17.1: + resolution: {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 + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -6797,11 +6890,11 @@ snapshots: - supports-color - tedious - '@mikro-orm/nestjs@6.1.1(@mikro-orm/core@6.4.16)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@mikro-orm/nestjs@6.1.1(@mikro-orm/core@6.4.16)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)': dependencies: '@mikro-orm/core': 6.4.16 '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@mikro-orm/postgresql@6.4.16(@mikro-orm/core@6.4.16)': dependencies: @@ -6921,27 +7014,27 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@nest-lab/fastify-multer@1.3.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-fastify@11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(rxjs@7.8.2)': + '@nest-lab/fastify-multer@1.3.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-fastify@11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3))(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-fastify': 11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/platform-fastify': 11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) fastify-multer: 2.0.3 rxjs: 7.8.2 - '@nest-lab/throttler-storage-redis@1.1.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/throttler@6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(ioredis@5.6.1)(reflect-metadata@0.2.2)': + '@nest-lab/throttler-storage-redis@1.1.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(@nestjs/throttler@6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(reflect-metadata@0.2.2))(ioredis@5.6.1)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/throttler': 6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': 6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(reflect-metadata@0.2.2) ioredis: 5.6.1 reflect-metadata: 0.2.2 tslib: 2.8.1 - '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(nodemailer@7.0.4)': + '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(nodemailer@7.0.4)': dependencies: '@css-inline/css-inline': 0.14.1 '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) glob: 10.3.12 nodemailer: 7.0.4 optionalDependencies: @@ -6963,24 +7056,24 @@ snapshots: axios: 1.10.0 rxjs: 7.8.2 - '@nestjs/bull-shared@11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/bull-shared@11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.56.1)': + '@nestjs/bullmq@11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(bullmq@5.56.1)': dependencies: - '@nestjs/bull-shared': 11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/bull-shared': 11.0.2(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) bullmq: 5.56.1 tslib: 2.8.1 - '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(cache-manager@7.0.0)(keyv@5.3.4)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(cache-manager@7.0.0)(keyv@5.3.4)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) cache-manager: 7.0.0 keyv: 5.3.4 rxjs: 7.8.2 @@ -7038,7 +7131,7 @@ snapshots: lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 @@ -7049,6 +7142,8 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + '@nestjs/websockets': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(@nestjs/platform-socket.io@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/jwt@11.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: @@ -7069,13 +7164,13 @@ snapshots: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-fastify@11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/platform-fastify@11.1.3(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)': dependencies: '@fastify/cors': 11.0.1 '@fastify/formbody': 8.0.2 '@fastify/middie': 9.0.3 '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 fastify: 5.3.3 light-my-request: 6.6.0 @@ -7084,10 +7179,22 @@ snapshots: optionalDependencies: '@fastify/static': 8.2.0 - '@nestjs/schedule@6.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/platform-socket.io@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(@nestjs/platform-socket.io@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + rxjs: 7.8.2 + socket.io: 4.8.1 + tslib: 2.8.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@nestjs/schedule@6.0.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)': + dependencies: + '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.3.0 '@nestjs/schematics@11.0.5(chokidar@4.0.3)(typescript@5.8.3)': @@ -7101,11 +7208,11 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.1 '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) js-yaml: 4.1.0 lodash: 4.17.21 @@ -7117,18 +7224,30 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.2 - '@nestjs/testing@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/testing@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/throttler@6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.4.0(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 + '@nestjs/websockets@11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)(@nestjs/platform-socket.io@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-socket.io': 11.1.3(@nestjs/common@11.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.3)(rxjs@7.8.2) + '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -7191,6 +7310,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@socket.io/component-emitter@3.1.2': {} + '@swc/cli@0.6.0(@swc/core@1.12.9)(chokidar@4.0.3)': dependencies: '@swc/core': 1.12.9 @@ -7325,6 +7446,10 @@ snapshots: '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.16.0 + '@types/ejs@3.1.5': optional: true @@ -7778,6 +7903,11 @@ snapshots: abstract-logging@2.0.1: {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8037,6 +8167,8 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + bcrypt@5.1.1: dependencies: '@mapbox/node-pre-gyp': 1.0.11 @@ -8378,12 +8510,19 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.7.2: {} + cookie@1.0.2: {} cookiejar@2.1.4: {} core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig-typescript-loader@6.1.0(@types/node@22.16.0)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): dependencies: '@types/node': 22.16.0 @@ -8678,6 +8817,24 @@ snapshots: encoding-japanese@2.2.0: optional: true + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 22.16.0 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + enhanced-resolve@5.18.2: dependencies: graceful-fs: 4.2.11 @@ -10963,6 +11120,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + neo-async@2.6.2: {} nice-try@1.0.5: @@ -11039,6 +11198,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -11796,6 +11957,36 @@ snapshots: slugify@1.6.6: {} + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.4 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -12302,6 +12493,8 @@ snapshots: validator@13.15.15: {} + vary@1.1.2: {} + void-elements@3.1.0: optional: true @@ -12469,6 +12662,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + ws@8.17.1: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/src/app.module.ts b/src/app.module.ts index 83b8f97..4a0ab3b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,12 +4,14 @@ import { BullModule } from "@nestjs/bullmq"; import { CacheModule } from "@nestjs/cache-manager"; import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; +import { JwtModule } from "@nestjs/jwt"; import { ScheduleModule } from "@nestjs/schedule"; import { ThrottlerModule } from "@nestjs/throttler"; import { bullMqConfig } from "./configs/bullmq.config"; import { cacheConfig } from "./configs/cache.config"; import { httpConfig } from "./configs/http.config"; +import { jwtConfig } from "./configs/jwt.config"; import { databaseConfig } from "./configs/mikro-orm.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; @@ -25,6 +27,7 @@ import { UtilsModule } from "./modules/utils/utils.module"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, cache: true }), + JwtModule.registerAsync(jwtConfig()), ScheduleModule.forRoot(), BullModule.forRootAsync(bullMqConfig()), CacheModule.registerAsync(cacheConfig()), diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 42c17de..3812ea9 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -62,6 +62,7 @@ export const enum AuthMessage { TOKEN_EXPIRED_OR_INVALID = "توکن منقضی شده یا نامعتبر است", ACCESS_DENIED = "دسترسی شما محدود شده است", USER_ACCOUNT_INACTIVE = "حساب کاربری شما غیر فعال است", + TOKEN_MISSED_OR_EXPIRED = "TOKEN_MISSED_OR_EXPIRED", } export const enum UserMessage { @@ -201,6 +202,97 @@ export const enum EmailMessage { MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به علاقه مندی ها منتقل شد", } +export const enum WebSocketMessage { + // Connection Messages + WS_CONNECTION_ESTABLISHED = "اتصال وب سوکت با موفقیت برقرار شد", + WS_CONNECTION_FAILED = "اتصال وب سوکت ناموفق بود", + WS_AUTHENTICATION_REQUIRED = "احراز هویت وب سوکت مورد نیاز است", + WS_AUTHENTICATION_FAILED = "احراز هویت وب سوکت ناموفق بود", + WS_AUTHENTICATION_SUCCESS = "احراز هویت وب سوکت با موفقیت انجام شد", + WS_INVALID_TOKEN = "توکن وب سوکت نامعتبر است", + WS_TOKEN_EXPIRED = "توکن وب سوکت منقضی شده است", + + // Room Management Messages + JOINED_ROOM_SUCCESS = "با موفقیت به اتاق متصل شدید", + LEFT_ROOM_SUCCESS = "با موفقیت از اتاق خارج شدید", + ROOM_NOT_FOUND = "اتاق یافت نشد", + UNAUTHORIZED_ROOM_ACCESS = "دسترسی غیرمجاز به اتاق", + + // Session Management Messages + WS_SESSION_CREATED = "جلسه وب سوکت ایجاد شد", + WS_SESSION_JOINED = "به جلسه وب سوکت پیوستید", + WS_SESSION_LEFT = "از جلسه وب سوکت خارج شدید", + WS_SESSION_EXPIRED = "جلسه وب سوکت منقضی شده است", + WS_SESSION_NOT_FOUND = "جلسه وب سوکت یافت نشد", + + // Error Messages + WS_CONNECTION_ERROR = "خطا در اتصال وب سوکت", + INVALID_EVENT = "رویداد نامعتبر", + INVALID_DATA = "داده نامعتبر", + WS_RATE_LIMIT_EXCEEDED = "تعداد درخواست‌های وب سوکت از حد مجاز بیشتر است", + INTERNAL_ERROR = "خطای داخلی سرور", + + // Notification Messages + NEW_MESSAGE_NOTIFICATION = "پیام جدید دریافت شد", + MESSAGE_STATUS_UPDATED = "وضعیت پیام به‌روزرسانی شد", + MESSAGE_MOVED_NOTIFICATION = "پیام جابجا شد", + MESSAGE_DELETED_NOTIFICATION = "پیام حذف شد", + + // General Messages + OPERATION_SUCCESS = "عملیات با موفقیت انجام شد", + OPERATION_FAILED = "عملیات ناموفق بود", + PERMISSION_DENIED = "مجوز دسترسی ندارید", + RESOURCE_NOT_FOUND = "منبع یافت نشد", + + // Authentication errors + AUTHENTICATION_REQUIRED = "احراز هویت ضروری است", + INVALID_TOKEN = "توکن احراز هویت نامعتبر است", + TOKEN_EXPIRED = "توکن منقضی شده است", + UNAUTHORIZED_ACCESS = "دسترسی غیرمجاز", + USER_NOT_AUTHENTICATED = "کاربر احراز هویت نشده است", + + // Connection errors + CONNECTION_FAILED = "اتصال برقرار نشد", + WEBSOCKET_ERROR = "خطا در ارتباط WebSocket", + CONNECTION_TIMEOUT = "زمان اتصال به پایان رسید", + CONNECTION_REFUSED = "اتصال رد شد", + + // Session errors + SESSION_NOT_FOUND = "جلسه چت یافت نشد", + SESSION_CREATION_FAILED = "ایجاد جلسه چت با شکست مواجه شد", + SESSION_ACCESS_DENIED = "دسترسی به جلسه چت مجاز نیست", + SESSION_EXPIRED = "جلسه چت منقضی شده است", + SESSION_ALREADY_EXISTS = "جلسه چت قبلاً وجود دارد", + + // Message errors + MESSAGE_TOO_LONG = "پیام از حداکثر طول مجاز تجاوز کرده است", + MESSAGE_EMPTY = "پیام نمی‌تواند خالی باشد", + MESSAGE_SEND_FAILED = "ارسال پیام با شکست مواجه شد", + INVALID_MESSAGE_FORMAT = "فرمت پیام نامعتبر است", + + // Rate limiting + RATE_LIMIT_EXCEEDED = "تعداد پیام‌های ارسالی بیش از حد مجاز است", + TOO_MANY_CONNECTIONS = "تعداد اتصالات بیش از حد مجاز است", + + // Service errors + LLM_SERVICE_ERROR = "سرویس هوش مصنوعی موقتاً در دسترس نیست", + SERVICE_UNAVAILABLE = "سرویس موقتاً در دسترس نیست", + INTERNAL_SERVER_ERROR = ".خطای داخلی سرور", + + // Validation errors + INVALID_SESSION_ID = "شناسه جلسه نامعتبر است", + INVALID_USER_ID = "شناسه کاربر نامعتبر است", + INVALID_MESSAGE_ID = "شناسه پیام نامعتبر است", + REQUIRED_FIELD_MISSING = "فیلد ضروری موجود نیست", + + // Success messages + AUTHENTICATED = "احراز هویت موفقیت‌آمیز", + SESSION_CREATED = "جلسه چت با موفقیت ایجاد شد", + CHAT_JOINED = "با موفقیت به چت متصل شدید", + MESSAGE_SENT = "پیام ارسال شد", + CONNECTION_ESTABLISHED = "اتصال برقرار شد", +} + export const enum SmsMessage { SEND_SMS_ERROR = "خطا در ارسال پیامک", } diff --git a/src/configs/jwt.config.ts b/src/configs/jwt.config.ts index d334969..57a3134 100755 --- a/src/configs/jwt.config.ts +++ b/src/configs/jwt.config.ts @@ -4,8 +4,10 @@ import { JwtModuleAsyncOptions } from "@nestjs/jwt"; export function jwtConfig(): JwtModuleAsyncOptions { return { inject: [ConfigService], + useFactory: (configService: ConfigService) => { return { + global: true, secret: configService.getOrThrow("JWT_SECRET"), signOptions: { issuer: configService.getOrThrow("JWT_ISSUER"), diff --git a/src/core/filters/ws-exception.filter.ts b/src/core/filters/ws-exception.filter.ts new file mode 100644 index 0000000..f911f01 --- /dev/null +++ b/src/core/filters/ws-exception.filter.ts @@ -0,0 +1,246 @@ +import { ArgumentsHost, Catch, HttpException, Logger, WsExceptionFilter as NestWsExceptionFilter } from "@nestjs/common"; +import { WsException } from "@nestjs/websockets"; +import { Socket } from "socket.io"; + +import { WebSocketMessage } from "../../common/enums/message.enum"; +import { WEBSOCKET_EVENTS } from "../../modules/email/constants/email-events.constant"; + +export interface WebSocketErrorResponse { + error: string; + message: string; + timestamp: string; + event?: string; + statusCode?: number; +} + +/** + * Global WebSocket exception filter + * Catches and handles all WebSocket-related errors + */ +@Catch() +export class WsExceptionFilter implements NestWsExceptionFilter { + private readonly logger = new Logger(WsExceptionFilter.name); + + catch(exception: Error, host: ArgumentsHost): void { + const client: Socket = host.switchToWs().getClient(); + const data = host.switchToWs().getData(); + + const errorResponse = this.createErrorResponse(exception, data); + + // Log the error for monitoring + this.logError(exception, client, data); + + // Emit error to client + client.emit(WEBSOCKET_EVENTS.CONNECTION_ERROR, errorResponse); + + // Handle specific error types + this.handleSpecificErrors(exception, client); + } + + private createErrorResponse(exception: Error, data?: any): WebSocketErrorResponse { + let error: string; + let message: string; + let statusCode: number | undefined; + + if (exception instanceof WsException) { + const wsError = exception.getError(); + + if (typeof wsError === "string") { + error = wsError; + message = wsError; + } else if (typeof wsError === "object" && wsError !== null) { + error = (wsError as any).error || "WebSocket Error"; + message = (wsError as any).message || "An error occurred"; + statusCode = (wsError as any).statusCode; + } else { + error = "WebSocket Error"; + message = "An unknown WebSocket error occurred"; + } + } else if (exception instanceof HttpException) { + statusCode = exception.getStatus(); + const response = exception.getResponse(); + + if (typeof response === "string") { + error = response; + message = response; + } else if (typeof response === "object" && response !== null) { + error = (response as any).error || "HTTP Error"; + message = (response as any).message || exception.message; + } else { + error = "HTTP Error"; + message = exception.message; + } + } else { + // Generic error handling + error = "Internal Server Error"; + message = this.getSafeErrorMessage(exception); + } + + return { + error, + message, + timestamp: new Date().toISOString(), + event: data?.event || "unknown", + statusCode, + }; + } + + private logError(exception: Error, client: Socket, data?: any): void { + const errorContext = { + clientId: client.id, + userId: client.data?.user?.id, + sessionId: client.data?.sessionId, + event: data?.event, + errorType: exception.constructor.name, + errorMessage: exception.message, + stack: exception.stack, + timestamp: new Date().toISOString(), + }; + + if (exception instanceof WsException) { + this.logger.warn("WebSocket exception occurred", errorContext); + } else if (exception instanceof HttpException) { + this.logger.warn("HTTP exception in WebSocket context", { + ...errorContext, + statusCode: exception.getStatus(), + }); + } else { + this.logger.error("Unexpected error in WebSocket context", errorContext); + } + } + + private handleSpecificErrors(exception: Error, client: Socket): void { + if (exception instanceof WsException) { + const wsError = exception.getError(); + + // Handle authentication errors + if (this.isAuthenticationError(wsError)) { + this.handleAuthenticationError(client); + return; + } + + // Handle permission errors + if (this.isPermissionError(wsError)) { + this.handlePermissionError(client); + return; + } + + // Handle rate limiting errors + if (this.isRateLimitError(wsError)) { + this.handleRateLimitError(client); + return; + } + } + + // Handle connection errors + if (this.isConnectionError(exception)) { + this.handleConnectionError(client); + return; + } + + // Handle validation errors + if (this.isValidationError(exception)) { + this.handleValidationError(client); + return; + } + } + + private isAuthenticationError(error: any): boolean { + if (typeof error === "string") { + return error.includes("authentication") || error.includes("token") || error.includes("unauthorized"); + } + if (typeof error === "object" && error !== null) { + const errorMessage = (error as any).message || (error as any).error || ""; + return errorMessage.includes("authentication") || errorMessage.includes("token") || errorMessage.includes("unauthorized"); + } + return false; + } + + private isPermissionError(error: any): boolean { + if (typeof error === "string") { + return error.includes("permission") || error.includes("access denied") || error.includes("forbidden"); + } + if (typeof error === "object" && error !== null) { + const errorMessage = (error as any).message || (error as any).error || ""; + return errorMessage.includes("permission") || errorMessage.includes("access denied") || errorMessage.includes("forbidden"); + } + return false; + } + + private isRateLimitError(error: any): boolean { + if (typeof error === "string") { + return error.includes("rate limit") || error.includes("too many requests"); + } + if (typeof error === "object" && error !== null) { + const errorMessage = (error as any).message || (error as any).error || ""; + return errorMessage.includes("rate limit") || errorMessage.includes("too many requests"); + } + return false; + } + + private isConnectionError(exception: Error): boolean { + return exception.message.includes("connection") || exception.message.includes("disconnect") || exception.message.includes("timeout"); + } + + private isValidationError(exception: Error): boolean { + return exception.message.includes("validation") || exception.message.includes("invalid") || exception.name.includes("ValidationError"); + } + + private handleAuthenticationError(client: Socket): void { + client.emit(WEBSOCKET_EVENTS.AUTHENTICATION_FAILED, { + message: WebSocketMessage.WS_AUTHENTICATION_FAILED, + timestamp: new Date().toISOString(), + reconnectAllowed: true, + }); + + // Disconnect after a short delay to allow client to handle the error + setTimeout(() => { + if (client.connected) { + client.disconnect(true); + } + }, 500); + } + + private handlePermissionError(client: Socket): void { + client.emit(WEBSOCKET_EVENTS.CONNECTION_ERROR, { + error: WebSocketMessage.PERMISSION_DENIED, + message: WebSocketMessage.PERMISSION_DENIED, + timestamp: new Date().toISOString(), + }); + } + + private handleRateLimitError(client: Socket): void { + client.emit(WEBSOCKET_EVENTS.CONNECTION_ERROR, { + error: WebSocketMessage.WS_RATE_LIMIT_EXCEEDED, + message: WebSocketMessage.WS_RATE_LIMIT_EXCEEDED, + timestamp: new Date().toISOString(), + retryAfter: 60000, // 1 minute + }); + } + + private handleConnectionError(client: Socket): void { + client.emit(WEBSOCKET_EVENTS.CONNECTION_ERROR, { + error: WebSocketMessage.WS_CONNECTION_ERROR, + message: WebSocketMessage.WS_CONNECTION_ERROR, + timestamp: new Date().toISOString(), + reconnectAllowed: true, + }); + } + + private handleValidationError(client: Socket): void { + client.emit(WEBSOCKET_EVENTS.CONNECTION_ERROR, { + error: WebSocketMessage.INVALID_DATA, + message: WebSocketMessage.INVALID_DATA, + timestamp: new Date().toISOString(), + }); + } + + private getSafeErrorMessage(exception: Error): string { + // In production, don't expose internal error details + if (process.env.NODE_ENV === "production") { + return WebSocketMessage.INTERNAL_ERROR; + } + + return exception.message || WebSocketMessage.INTERNAL_ERROR; + } +} diff --git a/src/main.ts b/src/main.ts index 7791d3b..b9768a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { Logger, ValidationPipe } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { NestFactory } from "@nestjs/core"; import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify"; +import { IoAdapter } from "@nestjs/platform-socket.io"; import { AppModule } from "./app.module"; import { getSwaggerDocument } from "./configs/swagger.config"; @@ -25,6 +26,9 @@ async function bootstrap() { const app = await NestFactory.create(AppModule, fastifyAdapter); + // Setup WebSocket adapter + app.useWebSocketAdapter(new IoAdapter(app)); + app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })); app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor()); @@ -37,6 +41,7 @@ async function bootstrap() { credentials: true, optionsSuccessStatus: 204, }); + const configService = app.get(ConfigService); getSwaggerDocument(app); @@ -51,8 +56,10 @@ async function bootstrap() { await app.listen(PORT, "0.0.0.0", () => { logger.log(`Application is running on: http://localhost:${PORT}`); logger.log(`Swagger documentation: http://localhost:${PORT}/api-docs`); + logger.log(`WebSocket server is running on: ws://localhost:${PORT}/email`); }); } + bootstrap().catch((error) => { const logger = new Logger("Bootstrap"); logger.error("Failed to start application", error); diff --git a/src/modules/auth/guards/ws-auth.guard.ts b/src/modules/auth/guards/ws-auth.guard.ts new file mode 100644 index 0000000..84370eb --- /dev/null +++ b/src/modules/auth/guards/ws-auth.guard.ts @@ -0,0 +1,240 @@ +import { CanActivate, ExecutionContext, Injectable, Logger } from "@nestjs/common"; +import { WsException } from "@nestjs/websockets"; +import { Socket } from "socket.io"; + +import { WebSocketMessage } from "../../../common/enums/message.enum"; +import { WebSocketAuthService } from "../../email/services/websocket-auth.service"; + +/** + * WebSocket authentication guard + * Protects WebSocket endpoints by verifying JWT tokens + */ +@Injectable() +export class WsAuthGuard implements CanActivate { + private readonly logger = new Logger(WsAuthGuard.name); + + constructor(private readonly authService: WebSocketAuthService) {} + + async canActivate(context: ExecutionContext): Promise { + try { + const client: Socket = context.switchToWs().getClient(); + const data = context.switchToWs().getData(); + + // Skip authentication for certain events + if (this.shouldSkipAuthentication(data?.event)) { + return true; + } + + // Check if client is already authenticated + if (this.isClientAuthenticated(client)) { + // Update last activity + this.authService.updateLastActivity(client); + return true; + } + + // Attempt to authenticate client + const authResult = await this.authService.authenticateClient(client); + + if (!authResult.success) { + this.logger.warn("WebSocket authentication failed in guard", { + clientId: client.id, + event: data?.event, + error: authResult.error, + }); + + throw new WsException({ + error: authResult.error || WebSocketMessage.WS_AUTHENTICATION_FAILED, + message: authResult.error || WebSocketMessage.WS_AUTHENTICATION_FAILED, + statusCode: 401, + }); + } + + this.logger.log("WebSocket authentication successful in guard", { + clientId: client.id, + userId: authResult.user?.id, + event: data?.event, + }); + + return true; + } catch (error) { + this.logger.error("WebSocket guard error", { + error: error instanceof Error ? error.message : String(error), + context: context.getType(), + }); + + if (error instanceof WsException) { + throw error; + } + + throw new WsException({ + error: WebSocketMessage.WS_AUTHENTICATION_FAILED, + message: WebSocketMessage.WS_AUTHENTICATION_FAILED, + statusCode: 401, + }); + } + } + + /** + * Checks if client is already authenticated + */ + private isClientAuthenticated(client: Socket): boolean { + return !!(client.data?.user?.id && client.data?.connectedAt); + } + + /** + * Determines if authentication should be skipped for certain events + */ + private shouldSkipAuthentication(event?: string): boolean { + const skipEvents = ["connect", "disconnect", "ping", "pong", "error", "connection_error", "authenticate"]; + + return skipEvents.includes(event || ""); + } +} + +// /** +// * Optional: Role-based WebSocket guard +// * Extends authentication to include role-based access control +// */ +// @Injectable() +// export class WsRoleGuard implements CanActivate { +// private readonly logger = new Logger(WsRoleGuard.name); +// private readonly requiredRoles: string[] = []; + +// constructor( +// private readonly authService: WebSocketAuthService, +// requiredRoles: string[] = [], +// ) { +// this.requiredRoles = requiredRoles; +// } + +// async canActivate(context: ExecutionContext): Promise { +// try { +// const client: Socket = context.switchToWs().getClient(); +// const data = context.switchToWs().getData(); + +// // First check authentication +// if (!this.isClientAuthenticated(client)) { +// throw new WsException({ +// error: WebSocketMessage.WS_AUTHENTICATION_REQUIRED, +// message: WebSocketMessage.WS_AUTHENTICATION_REQUIRED, +// statusCode: 401, +// }); +// } + +// // Then check roles if required +// if (this.requiredRoles.length > 0) { +// const user = client.data.user; +// const hasRequiredRole = this.checkUserRoles(user.permissions || [], this.requiredRoles); + +// if (!hasRequiredRole && !user.isAdmin) { +// this.logger.warn("WebSocket role check failed", { +// clientId: client.id, +// userId: user.id, +// userRoles: user.permissions, +// requiredRoles: this.requiredRoles, +// event: data?.event, +// }); + +// throw new WsException({ +// error: WebSocketMessage.PERMISSION_DENIED, +// message: WebSocketMessage.PERMISSION_DENIED, +// statusCode: 403, +// }); +// } +// } + +// return true; +// } catch (error) { +// this.logger.error("WebSocket role guard error", { +// error: error instanceof Error ? error.message : String(error), +// requiredRoles: this.requiredRoles, +// }); + +// if (error instanceof WsException) { +// throw error; +// } + +// throw new WsException({ +// error: WebSocketMessage.PERMISSION_DENIED, +// message: WebSocketMessage.PERMISSION_DENIED, +// statusCode: 403, +// }); +// } +// } + +// private isClientAuthenticated(client: Socket): boolean { +// return !!(client.data?.user?.id && client.data?.connectedAt); +// } + +// private checkUserRoles(userRoles: string[], requiredRoles: string[]): boolean { +// return requiredRoles.some((role) => userRoles.includes(role)); +// } +// } + +// /** +// * Room-based WebSocket guard +// * Checks if user has permission to access specific rooms +// */ +// @Injectable() +// export class WsRoomGuard implements CanActivate { +// private readonly logger = new Logger(WsRoomGuard.name); + +// constructor(private readonly authService: WebSocketAuthService) {} + +// async canActivate(context: ExecutionContext): Promise { +// try { +// const client: Socket = context.switchToWs().getClient(); +// const data = context.switchToWs().getData(); + +// // Check authentication first +// if (!this.isClientAuthenticated(client)) { +// throw new WsException({ +// error: WebSocketMessage.WS_AUTHENTICATION_REQUIRED, +// message: WebSocketMessage.WS_AUTHENTICATION_REQUIRED, +// statusCode: 401, +// }); +// } + +// // Check room access if roomId is provided in data +// if (data?.roomId) { +// const user = client.data.user; +// const canAccess = this.authService.canAccessRoom(user, data.roomId); + +// if (!canAccess) { +// this.logger.warn("WebSocket room access denied", { +// clientId: client.id, +// userId: user.id, +// roomId: data.roomId, +// event: data?.event, +// }); + +// throw new WsException({ +// error: WebSocketMessage.PERMISSION_DENIED, +// message: WebSocketMessage.UNAUTHORIZED_ROOM_ACCESS, +// statusCode: 403, +// }); +// } +// } + +// return true; +// } catch (error) { +// this.logger.error("WebSocket room guard error", { +// error: error instanceof Error ? error.message : String(error), +// }); + +// if (error instanceof WsException) { +// throw error; +// } + +// throw new WsException({ +// error: WebSocketMessage.PERMISSION_DENIED, +// message: WebSocketMessage.PERMISSION_DENIED, +// statusCode: 403, +// }); +// } +// } + +// private isClientAuthenticated(client: Socket): boolean { +// return !!(client.data?.user?.id && client.data?.connectedAt); +// } +// } diff --git a/src/modules/email/WEBSOCKET_USAGE.md b/src/modules/email/WEBSOCKET_USAGE.md new file mode 100644 index 0000000..32468ee --- /dev/null +++ b/src/modules/email/WEBSOCKET_USAGE.md @@ -0,0 +1,318 @@ +# Email WebSocket Gateway Usage + +This document explains how to use the WebSocket gateway for real-time email notifications. + +## Overview + +The Email WebSocket Gateway provides real-time notifications for email events such as: + +- New message arrivals +- Message status updates (read/unread, flagged/unflagged) +- Message movements (archive, favorite, delete) +- Connection status updates + +## Connection + +### WebSocket URL + +``` +ws://localhost:3000/email +``` + +### Authentication + +The gateway requires JWT authentication. Pass the token in one of these ways: + +**Method 1: Auth object** + +```javascript +const socket = io("ws://localhost:3000/email", { + auth: { + token: "your-jwt-token", + }, +}); +``` + +**Method 2: Query parameter** + +```javascript +const socket = io("ws://localhost:3000/email?token=your-jwt-token"); +``` + +## Client-Side Implementation + +### Basic Setup (JavaScript) + +```javascript +import { io } from "socket.io-client"; + +const socket = io("ws://localhost:3000/email", { + auth: { + token: localStorage.getItem("authToken"), // Your JWT token + }, +}); + +// Connection events +socket.on("connect", () => { + console.log("Connected to email gateway"); +}); + +socket.on("connection_established", (data) => { + console.log("Connection established:", data); +}); + +socket.on("connection_error", (error) => { + console.error("Connection error:", error); +}); + +socket.on("disconnect", () => { + console.log("Disconnected from email gateway"); +}); +``` + +### Listening for Email Events + +```javascript +// New message notification +socket.on("new_message", (message) => { + console.log("New message received:", message); + // Update UI with new message + updateInboxUI(message); + showNotification(`New email from ${message.from.name}: ${message.subject}`); +}); + +// Message status update +socket.on("message_status_update", (update) => { + console.log("Message status updated:", update); + // Update message status in UI + updateMessageStatus(update.messageId, update.status); +}); + +// Message moved +socket.on("message_moved", (moveData) => { + console.log("Message moved:", moveData); + // Update UI to reflect message movement + moveMessageInUI(moveData.messageId, moveData.fromMailboxName, moveData.toMailboxName); +}); + +// Message deleted +socket.on("message_deleted", (deleteData) => { + console.log("Message deleted:", deleteData); + // Remove message from UI + removeMessageFromUI(deleteData.messageId); +}); +``` + +### Joining/Leaving User Rooms + +```javascript +// Join user room (usually done automatically on connection) +socket.emit("join_user_room", { + userId: "user-id", + clientId: socket.id, + timestamp: new Date().toISOString(), +}); + +// Leave user room +socket.emit("leave_user_room", { + userId: "user-id", + clientId: socket.id, + timestamp: new Date().toISOString(), +}); +``` + +### React Hook Example + +```javascript +import { useEffect, useState } from "react"; +import { io } from "socket.io-client"; + +export function useEmailWebSocket(authToken) { + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const [newMessages, setNewMessages] = useState([]); + + useEffect(() => { + if (!authToken) return; + + const newSocket = io("ws://localhost:3000/email", { + auth: { token: authToken }, + }); + + newSocket.on("connect", () => { + setIsConnected(true); + }); + + newSocket.on("disconnect", () => { + setIsConnected(false); + }); + + newSocket.on("new_message", (message) => { + setNewMessages((prev) => [...prev, message]); + }); + + newSocket.on("message_status_update", (update) => { + // Handle message status updates + console.log("Message status update:", update); + }); + + setSocket(newSocket); + + return () => { + newSocket.disconnect(); + }; + }, [authToken]); + + return { socket, isConnected, newMessages }; +} +``` + +## Event Types + +### Server to Client Events + +#### `new_message` + +Emitted when a new message arrives. + +```typescript +interface EmailNotificationPayload { + messageId: number; + userId: string; + from: { name?: string; address: string }; + to: Array<{ name?: string; address: string }>; + subject: string; + preview?: string; + hasAttachments: boolean; + received: string; + mailboxId: string; + mailboxName: string; + isRead: boolean; + isFlagged: boolean; + size: number; +} +``` + +#### `message_status_update` + +Emitted when a message's status changes. + +```typescript +interface EmailStatusUpdatePayload { + messageId: number; + userId: string; + status: "read" | "unread" | "flagged" | "unflagged" | "archived" | "deleted"; + mailboxId: string; + timestamp: string; +} +``` + +#### `message_moved` + +Emitted when a message is moved between mailboxes. + +```typescript +interface EmailMovePayload { + messageId: number; + userId: string; + fromMailboxId: string; + toMailboxId: string; + fromMailboxName: string; + toMailboxName: string; + timestamp: string; +} +``` + +#### `message_deleted` + +Emitted when a message is deleted. + +```typescript +interface EmailDeletePayload { + messageId: number; + userId: string; + mailboxId: string; + mailboxName: string; + timestamp: string; +} +``` + +### Client to Server Events + +#### `join_user_room` + +Join a user's room to receive notifications. + +```typescript +interface ClientJoinPayload { + userId: string; + clientId: string; + timestamp: string; +} +``` + +#### `leave_user_room` + +Leave a user's room to stop receiving notifications. + +## Error Handling + +```javascript +socket.on("connection_error", (error) => { + console.error("WebSocket connection error:", error); + + switch (error.error) { + case "Authentication required": + // Redirect to login + break; + case "Invalid token": + // Refresh token and reconnect + break; + case "Authentication failed": + // Show error message + break; + default: + console.error("Unknown error:", error); + } +}); +``` + +## Best Practices + +1. **Authentication**: Always include a valid JWT token +2. **Reconnection**: Handle connection drops gracefully +3. **Error Handling**: Implement proper error handling for all events +4. **Memory Management**: Clean up event listeners when components unmount +5. **Rate Limiting**: Don't overwhelm the server with too many connections + +## Testing + +You can test the WebSocket gateway using the simulation endpoint: + +```javascript +// Call the simulation endpoint from your backend +await emailNotificationService.simulateNewMessage("user-id", { + subject: "Test Message", + from: { name: "Test User", address: "test@example.com" }, +}); +``` + +## Troubleshooting + +### Common Issues + +1. **Connection Refused**: Check if the server is running and WebSocket dependencies are installed +2. **Authentication Failed**: Verify JWT token is valid and not expired +3. **No Events Received**: Ensure you're subscribed to the correct user room +4. **CORS Issues**: Configure CORS settings in the gateway properly + +### Debug Mode + +Enable debug mode to see detailed logs: + +```javascript +const socket = io("ws://localhost:3000/email", { + auth: { token: authToken }, + debug: true, +}); +``` diff --git a/src/modules/email/constants/email-events.constant.ts b/src/modules/email/constants/email-events.constant.ts new file mode 100644 index 0000000..e11d524 --- /dev/null +++ b/src/modules/email/constants/email-events.constant.ts @@ -0,0 +1,47 @@ +export const WEBSOCKET_EVENTS = { + // Connection Events + CONNECTION_ESTABLISHED: "connection_established", + CONNECTION_ERROR: "connection_error", + USER_JOINED: "user_joined", + USER_LEFT: "user_left", + + // Email Events - Server to Client + NEW_MESSAGE: "new_message", + MESSAGE_STATUS_UPDATE: "message_status_update", + MESSAGE_MOVED: "message_moved", + MESSAGE_DELETED: "message_deleted", + + // Room Management - Client to Server + JOIN_USER_ROOM: "join_user_room", + LEAVE_USER_ROOM: "leave_user_room", + JOIN_SESSION: "join_session", + LEAVE_SESSION: "leave_session", + + // Authentication Events + AUTHENTICATE: "authenticate", + AUTHENTICATION_SUCCESS: "authentication_success", + AUTHENTICATION_FAILED: "authentication_failed", + + // System Events + PING: "ping", + PONG: "pong", + RECONNECT: "reconnect", + FORCE_DISCONNECT: "force_disconnect", +} as const; + +export const WEBSOCKET_ROOMS = { + USER: (userId: string) => `user_${userId}`, + SESSION: (sessionId: string) => `session_${sessionId}`, + BROADCAST: "broadcast", +} as const; + +export const WEBSOCKET_NAMESPACES = { + EMAIL: "/email", + NOTIFICATIONS: "/notifications", + ADMIN: "/admin", +} as const; + +// Type definitions for better TypeScript support +export type WebSocketEvent = (typeof WEBSOCKET_EVENTS)[keyof typeof WEBSOCKET_EVENTS]; +export type WebSocketRoom = string; +export type WebSocketNamespace = (typeof WEBSOCKET_NAMESPACES)[keyof typeof WEBSOCKET_NAMESPACES]; diff --git a/src/modules/email/email.gateway.ts b/src/modules/email/email.gateway.ts new file mode 100644 index 0000000..f983aa0 --- /dev/null +++ b/src/modules/email/email.gateway.ts @@ -0,0 +1,227 @@ +import { Logger, UseFilters, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"; +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + OnGatewayDisconnect, + OnGatewayInit, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from "@nestjs/websockets"; +import { Server, Socket } from "socket.io"; + +import { WEBSOCKET_EVENTS } from "./constants/email-events.constant"; +import { + ClientJoinPayload, + EmailDeletePayload, + EmailMovePayload, + EmailNotificationPayload, + EmailStatusUpdatePayload, +} from "./interfaces/email-events.interface"; +import { ConnectedUserInfo } from "./interfaces/websocket.interface"; +import { WebSocketAuthService } from "./services/websocket-auth.service"; +import { WebSocketMessage } from "../../common/enums/message.enum"; +import { WsExceptionFilter } from "../../core/filters/ws-exception.filter"; +import { WsAuthGuard } from "../auth/guards/ws-auth.guard"; + +@WebSocketGateway({ + cors: { origin: "*", credentials: true }, + namespace: "/email", +}) +@UseFilters(WsExceptionFilter) +@UseGuards(WsAuthGuard) +@UsePipes(new ValidationPipe()) +export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { + private readonly logger = new Logger(EmailGateway.name); + + // Connection state management + private readonly userConnections: Map> = new Map(); // userId -> Set of socketIds + private readonly connectedUsers = new Map(); + private readonly userSessions = new Map>(); + + @WebSocketServer() + server: Server; + constructor(private readonly authService: WebSocketAuthService) {} + + afterInit(server: Server): void { + this.logger.log("WebSocket Gateway initialized", { + namespace: "/email", + cors: true, + serverInstance: !!server, + }); + } + + /** + * Handles new WebSocket connections with authentication + * Implements fail-fast principle for security + */ + async handleConnection(client: Socket): Promise { + const connectionContext = { + clientId: client.id, + clientIP: client.handshake.address, + userAgent: client.handshake.headers["user-agent"], + timestamp: new Date().toISOString(), + }; + + this.logger.log("New WebSocket connection attempt", connectionContext); + + try { + const authResult = await this.authService.authenticateClient(client); + + if (!authResult.success) { + this.authService.handleAuthenticationFailure(client, authResult.error!); + return; + } + + const user = authResult.user!; + this.registerUserConnection(client.id, user.id); + this.authService.emitAuthenticationSuccess(client, user); + + this.logger.log("WebSocket connection established", { + ...connectionContext, + userId: user.id, + authenticated: true, + }); + } catch (error) { + this.logger.error("WebSocket connection error", { + ...connectionContext, + error: error instanceof Error ? error.message : String(error), + }); + + this.authService.handleAuthenticationFailure(client, WebSocketMessage.WS_CONNECTION_FAILED); + } + } + + /** + * Handles client disconnections with cleanup + */ + handleDisconnect(client: Socket): void { + const userInfo = this.connectedUsers.get(client.id); + + this.logger.log("WebSocket client disconnected", { + clientId: client.id, + userId: userInfo?.userId, + sessionId: userInfo?.sessionId, + }); + + if (userInfo) { + this.cleanupUserConnection(client, userInfo); + } + + this.connectedUsers.delete(client.id); + } + + @SubscribeMessage("join_user_room") + async handleJoinUserRoom(@ConnectedSocket() client: Socket, @MessageBody() data: ClientJoinPayload) { + try { + const userId = client.data.userId; + + if (userId !== data.userId) { + client.emit("connection_error", { error: "Unauthorized", timestamp: new Date().toISOString() }); + return; + } + + await client.join(`user:${userId}`); + this.logger.log(`Client ${client.id} joined room for user ${userId}`); + + return { success: true, message: "Joined user room successfully" }; + } catch (error) { + this.logger.error(`Error joining user room: ${error instanceof Error ? error.message : "Unknown error"}`); + client.emit("connection_error", { error: "Failed to join room", timestamp: new Date().toISOString() }); + } + } + + @SubscribeMessage("leave_user_room") + async handleLeaveUserRoom(@ConnectedSocket() client: Socket, @MessageBody() data: ClientJoinPayload) { + try { + const userId = client.data.userId; + + if (userId !== data.userId) { + client.emit("connection_error", { error: "Unauthorized", timestamp: new Date().toISOString() }); + return; + } + + await client.leave(`user:${userId}`); + this.logger.log(`Client ${client.id} left room for user ${userId}`); + + return { success: true, message: "Left user room successfully" }; + } catch (error) { + this.logger.error(`Error leaving user room: ${error instanceof Error ? error.message : "Unknown error"}`); + client.emit("connection_error", { error: "Failed to leave room", timestamp: new Date().toISOString() }); + } + } + + // Methods to emit events to clients + async notifyNewMessage(payload: EmailNotificationPayload) { + this.logger.log(`Emitting new message notification to user ${payload.userId}`); + this.server.to(`user:${payload.userId}`).emit("new_message", payload); + } + + async notifyMessageStatusUpdate(payload: EmailStatusUpdatePayload) { + this.logger.log(`Emitting message status update to user ${payload.userId}`); + this.server.to(`user:${payload.userId}`).emit("message_status_update", payload); + } + + async notifyMessageMoved(payload: EmailMovePayload) { + this.logger.log(`Emitting message moved notification to user ${payload.userId}`); + this.server.to(`user:${payload.userId}`).emit("message_moved", payload); + } + + async notifyMessageDeleted(payload: EmailDeletePayload) { + this.logger.log(`Emitting message deleted notification to user ${payload.userId}`); + this.server.to(`user:${payload.userId}`).emit("message_deleted", payload); + } + + // ============================================================================ + // PRIVATE HELPER METHODS + // ============================================================================ + /** + * Cleans up user connection on disconnect + */ + private cleanupUserConnection(client: Socket, userInfo: ConnectedUserInfo): void { + // Remove from user sessions + const userSockets = this.userSessions.get(userInfo.userId); + if (userSockets) { + userSockets.delete(client.id); + if (userSockets.size === 0) { + this.userSessions.delete(userInfo.userId); + } + } + + // Leave session room if joined + if (userInfo.sessionId) { + client.leave(`session_${userInfo.sessionId}`); + + // Notify others in session + this.server.to(`session_${userInfo.sessionId}`).emit(WEBSOCKET_EVENTS.USER_LEFT, { + userId: userInfo.userId, + sessionId: userInfo.sessionId, + }); + } + } + + /** + * Registers a new user connection + */ + private registerUserConnection(clientId: string, userId: string): void { + this.connectedUsers.set(clientId, { userId }); + + if (!this.userSessions.has(userId)) { + this.userSessions.set(userId, new Set()); + } + this.userSessions.get(userId)!.add(clientId); + } + + public getUserConnections(userId: string): number { + return this.userConnections.get(userId)?.size || 0; + } + + public isUserConnected(userId: string): boolean { + return this.userConnections.has(userId) && this.userConnections.get(userId)!.size > 0; + } + + public getAllConnectedUsers(): string[] { + return Array.from(this.userConnections.keys()); + } +} diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index f513d6d..6e1517b 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -1,15 +1,20 @@ import { Module } from "@nestjs/common"; +import { JwtModule } from "@nestjs/jwt"; import { EmailController } from "./email.controller"; +import { EmailGateway } from "./email.gateway"; +import { EmailNotificationService } from "./services/email-notification.service"; import { EmailService } from "./services/email.service"; import { MailboxResolverService } from "./services/mailbox-resolver.service"; +import { WebSocketAuthService } from "./services/websocket-auth.service"; +import { jwtConfig } from "../../configs/jwt.config"; import { MailServerModule } from "../mail-server/mail-server.module"; import { UsersModule } from "../users/users.module"; @Module({ - imports: [MailServerModule, UsersModule], + imports: [MailServerModule, UsersModule, JwtModule.registerAsync(jwtConfig())], controllers: [EmailController], - providers: [EmailService, MailboxResolverService], - exports: [EmailService, MailboxResolverService], + providers: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService], + exports: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService], }) export class EmailModule {} diff --git a/src/modules/email/interfaces/email-events.interface.ts b/src/modules/email/interfaces/email-events.interface.ts new file mode 100644 index 0000000..a26d332 --- /dev/null +++ b/src/modules/email/interfaces/email-events.interface.ts @@ -0,0 +1,69 @@ +export interface EmailNotificationPayload { + messageId: number; + userId: string; + from: { + name?: string; + address: string; + }; + to: Array<{ + name?: string; + address: string; + }>; + subject: string; + preview?: string; + hasAttachments: boolean; + received: string; + mailboxId: string; + mailboxName: string; + isRead: boolean; + isFlagged: boolean; + size: number; +} + +export interface EmailStatusUpdatePayload { + messageId: number; + userId: string; + status: "read" | "unread" | "flagged" | "unflagged" | "archived" | "deleted"; + mailboxId: string; + timestamp: string; +} + +export interface EmailMovePayload { + messageId: number; + userId: string; + fromMailboxId: string; + toMailboxId: string; + fromMailboxName: string; + toMailboxName: string; + timestamp: string; +} + +export interface EmailDeletePayload { + messageId: number; + userId: string; + mailboxId: string; + mailboxName: string; + timestamp: string; +} + +export interface ClientJoinPayload { + userId: string; + clientId: string; + timestamp: string; +} + +export interface EmailWebSocketEvents { + // Client to Server events + join_user_room: ClientJoinPayload; + leave_user_room: ClientJoinPayload; + + // Server to Client events + new_message: EmailNotificationPayload; + message_status_update: EmailStatusUpdatePayload; + message_moved: EmailMovePayload; + message_deleted: EmailDeletePayload; + user_joined: ClientJoinPayload; + user_left: ClientJoinPayload; + connection_established: { userId: string; timestamp: string }; + connection_error: { error: string; timestamp: string }; +} diff --git a/src/modules/email/interfaces/websocket.interface.ts b/src/modules/email/interfaces/websocket.interface.ts new file mode 100644 index 0000000..449b20c --- /dev/null +++ b/src/modules/email/interfaces/websocket.interface.ts @@ -0,0 +1,134 @@ +import { Socket } from "socket.io"; + +/** + * Information about a connected user + */ +export interface ConnectedUserInfo { + userId: string; + sessionId?: string; + connectedAt?: Date; + lastActivity?: Date; + metadata?: Record; +} + +/** + * WebSocket authentication result + */ +export interface WebSocketAuthResult { + success: boolean; + user?: WebSocketUser; + error?: string; + errorCode?: string; +} + +/** + * User information extracted from JWT token + */ +export interface WebSocketUser { + id: string; + email?: string; + permissions?: string[]; + isAdmin?: boolean; + businessId?: string; + sessionId?: string; +} + +/** + * WebSocket connection context for logging and debugging + */ +export interface WebSocketConnectionContext { + clientId: string; + clientIP: string; + userAgent?: string; + timestamp: string; + userId?: string; + sessionId?: string; +} + +/** + * WebSocket session information + */ +export interface WebSocketSession { + id: string; + userId: string; + createdAt: Date; + lastActivity: Date; + clientInfo: { + userAgent: string; + ip: string; + }; + isActive: boolean; +} + +/** + * WebSocket room subscription information + */ +export interface RoomSubscription { + roomId: string; + userId: string; + subscribedAt: Date; + permissions?: string[]; +} + +/** + * WebSocket error information + */ +export interface WebSocketError { + code: string; + message: string; + timestamp: string; + context?: Record; +} + +/** + * WebSocket authentication payload from client + */ +export interface WebSocketAuthPayload { + token: string; + sessionId?: string; + clientInfo?: { + userAgent?: string; + deviceId?: string; + platform?: string; + }; +} + +/** + * Extended Socket interface with user data + */ +export interface AuthenticatedSocket extends Socket { + data: { + user: WebSocketUser; + sessionId?: string; + connectedAt: Date; + lastActivity: Date; + }; +} + +/** + * WebSocket health check information + */ +export interface WebSocketHealthInfo { + totalConnections: number; + activeUsers: number; + activeSessions: number; + uptime: number; + memoryUsage?: { + used: number; + total: number; + }; +} + +/** + * WebSocket metrics for monitoring + */ +export interface WebSocketMetrics { + connectionsPerSecond: number; + messagesPerSecond: number; + errorsPerSecond: number; + averageResponseTime: number; + activeConnections: number; + peakConnections: number; + totalMessages: number; + totalErrors: number; +} diff --git a/src/modules/email/services/email-notification.service.ts b/src/modules/email/services/email-notification.service.ts new file mode 100644 index 0000000..43ae0b4 --- /dev/null +++ b/src/modules/email/services/email-notification.service.ts @@ -0,0 +1,158 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; + +import { MailServerService } from "../../mail-server/services/mail-server.service"; +import { EmailGateway } from "../email.gateway"; +import { MailboxResolverService } from "./mailbox-resolver.service"; +import { EmailNotificationPayload } from "../interfaces/email-events.interface"; + +@Injectable() +export class EmailNotificationService { + private readonly logger = new Logger(EmailNotificationService.name); + + constructor( + private readonly emailGateway: EmailGateway, + private readonly mailServerService: MailServerService, + private readonly mailboxResolverService: MailboxResolverService, + ) {} + + /** + * Handle new incoming message and emit WebSocket notification + * This method can be called by webhook handlers, polling services, etc. + */ + async handleNewMessage(userId: string, messageId: number, mailboxId?: string) { + try { + this.logger.log(`Processing new message notification for user ${userId}, message ${messageId}`); + + // Get the mailbox ID if not provided + const targetMailboxId = mailboxId || (await this.mailboxResolverService.getInboxMailboxId(userId)); + + // Fetch the message details + const message = await firstValueFrom(this.mailServerService.messages.getMessage(userId, targetMailboxId, messageId)); + + // Determine mailbox name + const mailboxName = await this.getMailboxName(userId, targetMailboxId); + + // Create notification payload + const notificationPayload: EmailNotificationPayload = { + messageId, + userId, + from: { + name: message.from?.name || message.from?.address?.split("@")[0] || "Unknown", + address: message.from?.address || "unknown@example.com", + }, + to: + message.to?.map((recipient) => ({ + name: recipient.name || recipient.address?.split("@")[0] || "Unknown", + address: recipient.address || "unknown@example.com", + })) || [], + subject: message.subject || "No Subject", + preview: this.extractPreview( + typeof message.text === "string" ? message.text : Array.isArray(message.html) ? message.html.join("") : message.html || "", + ), + hasAttachments: Array.isArray(message.attachments) && message.attachments.length > 0, + received: message.idate || new Date().toISOString(), + mailboxId: targetMailboxId, + mailboxName, + isRead: message.seen || false, + isFlagged: message.flagged || false, + size: message.size || 0, + }; + + // Emit WebSocket notification + await this.emailGateway.notifyNewMessage(notificationPayload); + + this.logger.log(`New message notification sent for user ${userId}, message ${messageId}`); + + return notificationPayload; + } catch (error) { + this.logger.error( + `Failed to process new message notification for user ${userId}, message ${messageId}: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ); + throw error; + } + } + + /** + * Handle bulk new messages (useful for initial sync or batch processing) + */ + async handleBulkNewMessages(userId: string, messageIds: number[], mailboxId?: string) { + this.logger.log(`Processing bulk new message notifications for user ${userId}, ${messageIds.length} messages`); + + const results = []; + for (const messageId of messageIds) { + try { + const result = await this.handleNewMessage(userId, messageId, mailboxId); + results.push(result); + } catch (error) { + this.logger.error(`Failed to process message ${messageId} in bulk notification: ${error}`); + } + } + + return results; + } + + /** + * Simulate new message arrival (for testing purposes) + */ + async simulateNewMessage(userId: string, messageData: Partial) { + this.logger.log(`Simulating new message for user ${userId}`); + + const simulatedPayload: EmailNotificationPayload = { + messageId: Math.floor(Math.random() * 10000), + userId, + from: { + name: "Test Sender", + address: "test@example.com", + }, + to: [ + { + name: "Test Recipient", + address: "recipient@example.com", + }, + ], + subject: "Test Message", + preview: "This is a test message for WebSocket notification", + hasAttachments: false, + received: new Date().toISOString(), + mailboxId: "inbox", + mailboxName: "Inbox", + isRead: false, + isFlagged: false, + size: 1024, + ...messageData, + }; + + await this.emailGateway.notifyNewMessage(simulatedPayload); + + return simulatedPayload; + } + + private async getMailboxName(userId: string, mailboxId: string): Promise { + try { + const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(userId); + + // Check which mailbox this ID corresponds to + if (mailboxIds.inbox === mailboxId) return "Inbox"; + if (mailboxIds.sent === mailboxId) return "Sent"; + if (mailboxIds.drafts === mailboxId) return "Drafts"; + if (mailboxIds.trash === mailboxId) return "Trash"; + if (mailboxIds.junk === mailboxId) return "Junk"; + if (mailboxIds.archive === mailboxId) return "Archive"; + if (mailboxIds.favorite === mailboxId) return "Favorite"; + + return "Unknown"; + } catch (error) { + this.logger.error(`Failed to get mailbox name for ${mailboxId}: ${error}`); + return "Unknown"; + } + } + + private extractPreview(content: string): string { + // Remove HTML tags and get first 150 characters + const textContent = content.replace(/<[^>]*>/g, "").trim(); + return textContent.length > 150 ? textContent.substring(0, 150) + "..." : textContent; + } +} diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 723cb97..78a5e4b 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -6,6 +6,8 @@ import { EmailMessage } from "../../../common/enums/message.enum"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto"; import { SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto"; +import { EmailGateway } from "../email.gateway"; +import { EmailDeletePayload, EmailMovePayload, EmailStatusUpdatePayload } from "../interfaces/email-events.interface"; @Injectable() export class EmailService { @@ -14,6 +16,7 @@ export class EmailService { constructor( private readonly mailServerService: MailServerService, private readonly mailboxResolverService: MailboxResolverService, + private readonly emailGateway: EmailGateway, // private readonly userService: UsersService, ) {} @@ -131,6 +134,16 @@ export class EmailService { const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(userId, inboxMailboxId, messageId)); + // Emit WebSocket notification + const payload: EmailDeletePayload = { + messageId, + userId, + mailboxId: inboxMailboxId, + mailboxName: "Inbox", + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageDeleted(payload); + return { success: true, message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY, @@ -148,6 +161,16 @@ export class EmailService { const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); const result = await firstValueFrom(this.mailServerService.messages.updateMessage(userId, inboxMailboxId, messageId, { seen: true })); + // Emit WebSocket notification + const payload: EmailStatusUpdatePayload = { + messageId, + userId, + status: "read", + mailboxId: inboxMailboxId, + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageStatusUpdate(payload); + return { success: true, message: EmailMessage.MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY, @@ -380,6 +403,18 @@ export class EmailService { }), ); + // Emit WebSocket notification + const payload: EmailMovePayload = { + messageId, + userId, + fromMailboxId: inboxMailboxId, + toMailboxId: archiveMailboxId, + fromMailboxName: "Inbox", + toMailboxName: "Archive", + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageMoved(payload); + this.logger.log(`Message ${messageId} moved to archive successfully`); return { @@ -409,6 +444,18 @@ export class EmailService { }), ); + // Emit WebSocket notification + const payload: EmailMovePayload = { + messageId, + userId, + fromMailboxId: inboxMailboxId, + toMailboxId: favoriteMailboxId, + fromMailboxName: "Inbox", + toMailboxName: "Favorite", + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageMoved(payload); + this.logger.log(`Message ${messageId} moved to favorite successfully`); return { diff --git a/src/modules/email/services/websocket-auth.service.ts b/src/modules/email/services/websocket-auth.service.ts new file mode 100644 index 0000000..8682143 --- /dev/null +++ b/src/modules/email/services/websocket-auth.service.ts @@ -0,0 +1,240 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import { Socket } from "socket.io"; + +import { WebSocketMessage } from "../../../common/enums/message.enum"; +import { WEBSOCKET_EVENTS, WEBSOCKET_ROOMS } from "../constants/email-events.constant"; +import { AuthenticatedSocket, WebSocketAuthResult, WebSocketConnectionContext, WebSocketUser } from "../interfaces/websocket.interface"; + +@Injectable() +export class WebSocketAuthService { + private readonly logger = new Logger(WebSocketAuthService.name); + private readonly connectionTimeouts = new Map(); + + constructor(private readonly jwtService: JwtService) {} + + /** + * Authenticates a WebSocket client using JWT token + */ + async authenticateClient(client: Socket): Promise { + try { + const token = this.extractToken(client); + + if (!token) { + return { + success: false, + error: WebSocketMessage.WS_AUTHENTICATION_REQUIRED, + }; + } + + const decoded = await this.verifyToken(token); + if (!decoded) { + return { + success: false, + error: WebSocketMessage.WS_INVALID_TOKEN, + }; + } + + const user = this.extractUserFromToken(decoded); + if (!user) { + return { + success: false, + error: WebSocketMessage.WS_INVALID_TOKEN, + }; + } + + // Set user data on socket + client.data = { + user, + connectedAt: new Date(), + lastActivity: new Date(), + sessionId: user.sessionId || this.generateSessionId(), + }; + + // Join user to their personal room + await client.join(WEBSOCKET_ROOMS.USER(user.id)); + + return { + success: true, + user, + }; + } catch (error) { + this.logger.error("WebSocket authentication error", { + clientId: client.id, + error: error instanceof Error ? error.message : String(error), + }); + + return { + success: false, + error: WebSocketMessage.WS_AUTHENTICATION_FAILED, + }; + } + } + + /** + * Handles authentication failure by emitting error and disconnecting client + */ + handleAuthenticationFailure(client: Socket, errorMessage: string): void { + const context: WebSocketConnectionContext = { + clientId: client.id, + clientIP: client.handshake.address, + userAgent: client.handshake.headers["user-agent"], + timestamp: new Date().toISOString(), + }; + + this.logger.warn("WebSocket authentication failed", { + ...context, + errorMessage, + }); + + // Emit error to client + client.emit(WEBSOCKET_EVENTS.CONNECTION_ERROR, { + error: errorMessage, + timestamp: new Date().toISOString(), + retryAllowed: this.shouldAllowRetry(client), + }); + + // Set timeout before disconnecting to allow client to handle error + const timeout = setTimeout(() => { + if (client.connected) { + client.disconnect(true); + } + this.connectionTimeouts.delete(client.id); + }, 1000); + + this.connectionTimeouts.set(client.id, timeout); + } + + /** + * Emits successful authentication event to client + */ + emitAuthenticationSuccess(client: Socket, user: WebSocketUser): void { + const authSocket = client as AuthenticatedSocket; + + client.emit(WEBSOCKET_EVENTS.CONNECTION_ESTABLISHED, { + userId: user.id, + sessionId: authSocket.data.sessionId, + timestamp: new Date().toISOString(), + permissions: user.permissions || [], + }); + + this.logger.log("WebSocket authentication successful", { + clientId: client.id, + userId: user.id, + sessionId: authSocket.data.sessionId, + }); + } + + /** + * Verifies if user has permission to access a specific room + */ + canAccessRoom(user: WebSocketUser, roomId: string): boolean { + // Basic room access control + if (roomId.startsWith("user_")) { + const targetUserId = roomId.replace("user_", ""); + return user.id === targetUserId || user.isAdmin === true; + } + + if (roomId.startsWith("session_")) { + return true; // Sessions are generally accessible + } + + if (roomId === "broadcast") { + return user.isAdmin === true; + } + + // Default deny for unknown room patterns + return false; + } + + /** + * Updates last activity timestamp for authenticated socket + */ + updateLastActivity(client: Socket): void { + if (client.data?.user) { + client.data.lastActivity = new Date(); + } + } + + /** + * Cleans up connection timeouts on disconnect + */ + cleanupConnection(client: Socket): void { + const timeout = this.connectionTimeouts.get(client.id); + if (timeout) { + clearTimeout(timeout); + this.connectionTimeouts.delete(client.id); + } + } + + // Private helper methods + + private extractToken(client: Socket): string | null { + // Try to get token from auth object first (recommended) + const authToken = client.handshake.auth?.token; + if (authToken && typeof authToken === "string") { + return authToken; + } + + // Fallback to query parameter + const queryToken = client.handshake.query?.token; + if (queryToken && typeof queryToken === "string") { + return queryToken; + } + + // Try headers as last resort + const headerToken = client.handshake.headers.authorization; + if (headerToken && typeof headerToken === "string") { + return headerToken.replace("Bearer ", ""); + } + + return null; + } + + private async verifyToken(token: string): Promise { + try { + return await this.jwtService.verifyAsync(token); + } catch (error) { + if (error instanceof Error) { + if (error.name === "TokenExpiredError") { + throw new Error(WebSocketMessage.WS_TOKEN_EXPIRED); + } + if (error.name === "JsonWebTokenError") { + throw new Error(WebSocketMessage.WS_INVALID_TOKEN); + } + if (error.name === "NotBeforeError") { + throw new Error(WebSocketMessage.WS_INVALID_TOKEN); + } + } + throw new Error(WebSocketMessage.WS_INVALID_TOKEN); + } + } + + private extractUserFromToken(decoded: any): WebSocketUser | null { + try { + return { + id: decoded.wildduckUserId || decoded.sub || decoded.id, + email: decoded.email, + permissions: decoded.permissions || [], + isAdmin: decoded.isAdmin || false, + businessId: decoded.businessId, + sessionId: decoded.sessionId, + }; + } catch (error) { + this.logger.error("Failed to extract user from token", { + error: error instanceof Error ? error.message : String(error), + }); + return null; + } + } + + private shouldAllowRetry(_client: Socket): boolean { + // Implement retry logic based on client IP or other factors + // For now, always allow retry + return true; + } + + private generateSessionId(): string { + return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } +} diff --git a/src/modules/utils/services/extract-token.utils.ts b/src/modules/utils/services/extract-token.utils.ts new file mode 100644 index 0000000..8467993 --- /dev/null +++ b/src/modules/utils/services/extract-token.utils.ts @@ -0,0 +1,29 @@ +import { Socket } from "socket.io"; + +/** + * Extracts authentication token from WebSocket client handshake + * Supports multiple token sources for flexibility + * + * @param client - Socket.IO client instance + * @returns Authentication token string or undefined if not found + */ +export function extractTokenFromClient(client: Socket): string | undefined { + // Priority order: auth.token > query.token > headers.authorization + const authToken = client.handshake?.auth?.token; + if (authToken) { + return authToken; + } + + const queryToken = client.handshake?.query?.token; + if (queryToken && typeof queryToken === "string") { + return queryToken; + } + + const authHeader = client.handshake?.headers?.authorization; + if (authHeader && typeof authHeader === "string") { + // Remove 'Bearer ' prefix if present + return authHeader.replace(/^Bearer\s+/i, ""); + } + + return undefined; +}