diff --git a/backend/docker-compose.override.yml.example b/backend/docker-compose.override.yml.example new file mode 100644 index 0000000..d624402 --- /dev/null +++ b/backend/docker-compose.override.yml.example @@ -0,0 +1,16 @@ +version: '3.8' + +services: + # Override ค่า Config ของ Service Backend (เมื่อเราสร้าง Container Backend ในอนาคต) + backend: + environment: + # ใส่ Secrets จริงในไฟล์ override นี้ (และห้าม commit ไฟล์นี้) + - DB_PASSWORD=Center2025 + - JWT_SECRET=eebc122aa65adde8c76c6a0847d9649b2b67a06db1504693e6c912e51499b76e + - NODE_ENV=development + + # Override สำหรับ Database (Local Dev) + mariadb: + environment: + - MYSQL_ROOT_PASSWORD=Center#2025 + - MYSQL_PASSWORD=Center2025 \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index d2a7b58..3f40a6e 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,9 +1,7 @@ -version: '3.8' - services: mariadb: image: mariadb:11.8 - container_name: lcbp3-db-local + container_name: mariadb-local restart: always environment: MYSQL_ROOT_PASSWORD: Center#2025 @@ -19,8 +17,8 @@ services: # Optional: phpMyAdmin สำหรับจัดการ DB ง่ายๆ pma: - image: phpmyadmin/phpmyadmin - container_name: lcbp3-pma-local + image: phpmyadmin + container_name: pma-local environment: PMA_HOST: mariadb ports: @@ -30,8 +28,22 @@ services: networks: - lcbp3-net + redis: + image: redis:7-alpine + container_name: lcbp3-redis-local + restart: always + # ใช้ Command นี้เพื่อตั้ง Password + command: redis-server --requirepass "redis_password_secure" + ports: + - '6379:6379' + volumes: + - redis_data:/data + networks: + - lcbp3-net + volumes: db_data: + redis_data: # เพิ่ม Volume networks: lcbp3-net: diff --git a/backend/package.json b/backend/package.json index ec02de5..5427774 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,12 +20,24 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@casl/ability": "^6.7.3", + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.1", + "@nestjs/mapped-types": "^2.1.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "bcrypt": "^6.0.0", + "bullmq": "^5.63.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "joi": "^18.0.1", "mysql2": "^3.15.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.27" @@ -36,9 +48,11 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index ec061f9..719d3a3 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -8,24 +8,60 @@ importers: .: dependencies: + '@casl/ability': + specifier: ^6.7.3 + version: 6.7.3 + '@nestjs/bullmq': + specifier: ^11.0.4 + version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.63.2) '@nestjs/common': specifier: ^11.0.1 - version: 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.1.9(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.0.1 - version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.1 + version: 11.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/mapped-types': + specifier: ^2.1.0 + version: 2.1.0(@nestjs/common@11.1.9(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) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.9(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-express': specifier: ^11.0.1 - version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 + bullmq: + specifier: ^5.63.2 + version: 5.63.2 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.2 + version: 0.14.2 + joi: + specifier: ^18.0.1 + version: 18.0.1 mysql2: specifier: ^3.15.3 version: 3.15.3 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -34,7 +70,7 @@ importers: version: 7.8.2 typeorm: specifier: ^0.3.27 - version: 0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -50,7 +86,10 @@ importers: version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.1 - version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9) + '@types/bcrypt': + specifier: ^6.0.0 + version: 6.0.0 '@types/express': specifier: ^5.0.0 version: 5.0.5 @@ -60,6 +99,9 @@ importers: '@types/node': specifier: ^22.10.7 version: 22.19.1 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 '@types/supertest': specifier: ^6.0.2 version: 6.0.3 @@ -307,6 +349,9 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@casl/ability@6.7.3': + resolution: {integrity: sha512-A4L28Ko+phJAsTDhRjzCOZWECQWN2jzZnJPnROWWHjJpyMq1h7h9ZqjwS2WbIUa3Z474X1ZPSgW0f1PboZGC0A==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -362,6 +407,26 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hapi/address@5.1.1': + resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} + engines: {node: '>=14.0.0'} + + '@hapi/formula@3.0.2': + resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/pinpoint@2.0.1': + resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + + '@hapi/tlds@1.1.4': + resolution: {integrity: sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==} + engines: {node: '>=14.0.0'} + + '@hapi/topo@6.0.2': + resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -521,6 +586,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.4.0': + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -649,9 +717,52 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@nestjs/bull-shared@11.0.4': + resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + + '@nestjs/bullmq@11.0.4': + resolution: {integrity: sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + '@nestjs/cli@11.0.11': resolution: {integrity: sha512-phKImmBK2qc0dqMPz+vnBlb+xcAxyZ5yiCKOdcgq9DwFsswL6jn3l2leKXQLIyM2bqIecE5T3tdWKLZl7wgW0w==} engines: {node: '>= 20.11'} @@ -702,6 +813,30 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.1': + resolution: {integrity: sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + + '@nestjs/mapped-types@2.1.0': + resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/platform-express@11.1.9': resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==} peerDependencies: @@ -779,6 +914,9 @@ packages: '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@tokenizer/inflate@0.3.1': resolution: {integrity: sha512-4oeoZEBQdLdt5WmP/hx1KZ6D3/Oid/0cUb2nk4F0pTDAWy+KCH3/EnAkZF/bvckWo8I33EqBm01lIPgmgc8rCA==} engines: {node: '>=18'} @@ -813,6 +951,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@6.0.0': + resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -855,15 +996,30 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.1': resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -888,6 +1044,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -953,6 +1112,18 @@ packages: resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ucast/core@1.10.2': + resolution: {integrity: sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==} + + '@ucast/js@3.0.4': + resolution: {integrity: sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q==} + + '@ucast/mongo2js@1.4.0': + resolution: {integrity: sha512-vR9RJ3BHlkI3RfKJIZFdVktxWvBCQRiSTeJSWN9NPxP5YJkpfXvcBWAMLwvyJx4HbB+qib5/AlSDEmQiuQyx2w==} + + '@ucast/mongo@2.4.3': + resolution: {integrity: sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1266,6 +1437,10 @@ packages: resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} hasBin: true + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1295,6 +1470,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1304,6 +1482,9 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bullmq@5.63.2: + resolution: {integrity: sha512-c1K5gcAh0a+C9lcRXaA1GePDYtmUywCH1pNXkUlZ8lFlqQnrtKyZpcr5aZJcjyZVx6y7t5259ru+ttJFNUQ5kw==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1365,6 +1546,12 @@ packages: cjs-module-lexer@2.1.1: resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.2: + resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1389,6 +1576,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -1473,6 +1664,10 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1523,6 +1718,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1553,6 +1752,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1967,6 +2169,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ioredis@5.8.2: + resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2189,6 +2395,10 @@ packages: node-notifier: optional: true + joi@18.0.1: + resolution: {integrity: sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==} + engines: {node: '>= 20'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2231,6 +2441,16 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2242,6 +2462,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.12.27: + resolution: {integrity: sha512-8gHhHzzcnY1EF4BS5L/lrjv2VAZWd6ltU7c/sqoktRZSQvZl4g8hrgXtXHXGkSFKFYArFON12zUNJrNVqJ9u4g==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2261,12 +2484,39 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -2295,6 +2545,10 @@ packages: resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2393,6 +2647,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + multer@2.0.2: resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} engines: {node: '>= 10.16.0'} @@ -2427,9 +2688,21 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + engines: {node: ^18 || ^20 || >= 21} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -2506,6 +2779,17 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2533,6 +2817,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2621,6 +2908,14 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -2782,6 +3077,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -3112,6 +3410,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -3123,6 +3425,10 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validator@13.15.23: + resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3468,6 +3774,10 @@ snapshots: '@borewit/text-codec@0.1.1': {} + '@casl/ability@6.7.3': + dependencies: + '@ucast/mongo2js': 1.4.0 + '@colors/colors@1.5.0': optional: true @@ -3537,6 +3847,22 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@hapi/address@5.1.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/formula@3.0.2': {} + + '@hapi/hoek@11.0.7': {} + + '@hapi/pinpoint@2.0.1': {} + + '@hapi/tlds@1.1.4': {} + + '@hapi/topo@6.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -3688,6 +4014,8 @@ snapshots: optionalDependencies: '@types/node': 22.19.1 + '@ioredis/commands@1.4.0': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -3923,6 +4251,24 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.7.1 @@ -3930,6 +4276,20 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.63.2)': + dependencies: + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.63.2 + tslib: 2.8.1 + '@nestjs/cli@11.0.11(@types/node@22.19.1)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) @@ -3956,7 +4316,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.1.0 iterare: 1.2.1 @@ -3965,20 +4325,23 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.2(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 16.4.7 dotenv-expand: 12.0.1 lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -3988,12 +4351,31 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/jwt@11.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.2 + + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(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)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 + + '@nestjs/passport@11.0.5(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.9(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-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.1.0 multer: 2.0.2 @@ -4013,21 +4395,21 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': + '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': dependencies: - '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) '@noble/hashes@1.8.0': {} @@ -4068,6 +4450,8 @@ snapshots: '@sqltools/formatter@1.2.5': {} + '@standard-schema/spec@1.0.0': {} + '@tokenizer/inflate@0.3.1': dependencies: debug: 4.4.3 @@ -4112,6 +4496,10 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/bcrypt@6.0.0': + dependencies: + '@types/node': 22.19.1 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -4167,14 +4555,35 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.19.1 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@22.19.1': dependencies: undici-types: 6.21.0 + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.5 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.5 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -4208,6 +4617,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/validator@13.15.10': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -4307,6 +4718,22 @@ snapshots: '@typescript-eslint/types': 8.47.0 eslint-visitor-keys: 4.2.1 + '@ucast/core@1.10.2': {} + + '@ucast/js@3.0.4': + dependencies: + '@ucast/core': 1.10.2 + + '@ucast/mongo2js@1.4.0': + dependencies: + '@ucast/core': 1.10.2 + '@ucast/js': 3.0.4 + '@ucast/mongo': 2.4.3 + + '@ucast/mongo@2.4.3': + dependencies: + '@ucast/core': 1.10.2 + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -4607,6 +5034,11 @@ snapshots: baseline-browser-mapping@2.8.29: {} + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.5.0 + node-gyp-build: 4.8.4 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -4656,6 +5088,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -4668,6 +5102,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bullmq@5.63.2: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.8.2 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.3 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -4718,6 +5164,14 @@ snapshots: cjs-module-lexer@2.1.1: {} + class-transformer@0.5.1: {} + + class-validator@0.14.2: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.27 + validator: 13.15.23 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -4740,6 +5194,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -4807,6 +5263,10 @@ snapshots: create-require@1.1.1: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4841,6 +5301,9 @@ snapshots: depd@2.0.0: {} + detect-libc@2.1.2: + optional: true + detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -4866,6 +5329,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.256: {} @@ -5341,6 +5808,20 @@ snapshots: inherits@2.0.4: {} + ioredis@5.8.2: + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -5738,6 +6219,16 @@ snapshots: - supports-color - ts-node + joi@18.0.1: + dependencies: + '@hapi/address': 5.1.1 + '@hapi/formula': 3.0.2 + '@hapi/hoek': 11.0.7 + '@hapi/pinpoint': 2.0.1 + '@hapi/tlds': 1.1.4 + '@hapi/topo': 6.0.2 + '@standard-schema/spec': 1.0.0 + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -5771,6 +6262,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5782,6 +6297,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.12.27: {} + lines-and-columns@1.2.4: {} load-esm@1.0.3: {} @@ -5796,10 +6313,28 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -5821,6 +6356,8 @@ snapshots: lru.min@1.1.3: {} + luxon@3.7.2: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5896,6 +6433,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + multer@2.0.2: dependencies: append-field: 1.0.0 @@ -5934,10 +6487,19 @@ snapshots: node-abort-controller@3.1.1: {} + node-addon-api@8.5.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} node-releases@2.0.27: {} @@ -6018,6 +6580,19 @@ snapshots: parseurl@1.3.3: {} + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.2 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -6038,6 +6613,8 @@ snapshots: path-type@4.0.0: {} + pause@0.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -6108,6 +6685,12 @@ snapshots: readdirp@4.1.2: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect-metadata@0.2.2: {} require-directory@2.1.1: {} @@ -6286,6 +6869,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} statuses@2.0.2: {} @@ -6508,7 +7093,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -6526,6 +7111,7 @@ snapshots: uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: + ioredis: 5.8.2 mysql2: 3.15.3 ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) transitivePeerDependencies: @@ -6596,6 +7182,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@11.1.0: {} v8-compile-cache-lib@3.0.1: {} @@ -6606,6 +7194,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validator@13.15.23: {} + vary@1.1.2: {} walker@1.0.8: diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 3b99ef0..93ba30a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,39 +1,68 @@ -// src/app.module.ts +// File: src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bullmq'; // Import BullModule import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { envValidationSchema } from './common/config/env.validation.js'; // สังเกต .js สำหรับ ESM +import { CommonModule } from './common/common.module'; +import { UserModule } from './modules/user/user.module'; +import { ProjectModule } from './modules/project/project.module'; @Module({ imports: [ - // 1. Load Config Module + // 1. Setup Config Module พร้อม Validation ConfigModule.forRoot({ - isGlobal: true, // ให้เรียกใช้ได้ทุกที่โดยไม่ต้อง import ใหม่ - envFilePath: '.env', // อ่านค่าจากไฟล์ .env + isGlobal: true, // เรียกใช้ได้ทั่วทั้ง App ไม่ต้อง import ซ้ำ + envFilePath: '.env', // อ่านไฟล์ .env (สำหรับ Dev) + validationSchema: envValidationSchema, // ใช้ Schema ที่เราสร้างเพื่อตรวจสอบ + validationOptions: { + // ถ้ามีค่าไหนไม่ผ่าน Validation ให้ Error และหยุดทำงานทันที + abortEarly: true, + }, }), - // 2. Setup TypeORM Connection (Async เพื่อรออ่าน Config ก่อน) + // 2. Setup TypeORM (MariaDB) TypeOrmModule.forRootAsync({ imports: [ConfigModule], + inject: [ConfigService], useFactory: async (configService: ConfigService) => ({ - type: 'mariadb', // หรือ 'mysql' ก็ได้เพราะใช้ driver เดียวกัน + type: 'mariadb', host: configService.get('DB_HOST'), port: configService.get('DB_PORT'), username: configService.get('DB_USERNAME'), password: configService.get('DB_PASSWORD'), database: configService.get('DB_DATABASE'), - // Auto Load Entities: โหลด Entity ทั้งหมดที่อยู่ในโปรเจกต์อัตโนมัติ autoLoadEntities: true, - // Synchronize: true เฉพาะ Dev environment (ห้ามใช้ใน Prod) - synchronize: configService.get('NODE_ENV') === 'development', - // Logging: เปิดดู Query SQL ตอน Dev - logging: configService.get('NODE_ENV') === 'development', + // synchronize: true เฉพาะตอน Dev เท่านั้น ห้ามใช้บน Prod + // synchronize: configService.get('NODE_ENV') === 'development', + // แก้บรรทัดนี้เป็น false ครับ + // เพราะเราใช้ SQL Script สร้าง DB แล้ว ไม่ต้องการให้ TypeORM มาแก้ Structure อัตโนมัติ + synchronize: false, }), - inject: [ConfigService], }), + + // 3. BullMQ (Redis) Setup [NEW] + BullModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + connection: { + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + password: configService.get('REDIS_PASSWORD'), + }, + }), + }), + + CommonModule, + + UserModule, + + ProjectModule, ], controllers: [AppController], providers: [AppService], }) -export class AppModule {} \ No newline at end of file +export class AppModule {} diff --git a/backend/src/common/aut/aut.controller.spec.ts b/backend/src/common/aut/aut.controller.spec.ts new file mode 100644 index 0000000..2b3355e --- /dev/null +++ b/backend/src/common/aut/aut.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AutController } from './aut.controller'; + +describe('AutController', () => { + let controller: AutController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AutController], + }).compile(); + + controller = module.get(AutController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/common/aut/aut.controller.ts b/backend/src/common/aut/aut.controller.ts new file mode 100644 index 0000000..196d7c3 --- /dev/null +++ b/backend/src/common/aut/aut.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('aut') +export class AutController {} diff --git a/backend/src/common/auth/auth.controller.spec.ts b/backend/src/common/auth/auth.controller.spec.ts new file mode 100644 index 0000000..ab3caf8 --- /dev/null +++ b/backend/src/common/auth/auth.controller.spec.ts @@ -0,0 +1,30 @@ +import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; +import { AuthService } from './auth.service.js'; +import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO +import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @Post('login') + // เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto + async login(@Body() loginDto: LoginDto) { + const user = await this.authService.validateUser( + loginDto.username, + loginDto.password, + ); + + if (!user) { + throw new UnauthorizedException('Invalid credentials'); + } + + return this.authService.login(user); + } + + @Post('register-admin') + // เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto + async register(@Body() registerDto: RegisterDto) { + return this.authService.register(registerDto); + } +} diff --git a/backend/src/common/auth/auth.controller.ts b/backend/src/common/auth/auth.controller.ts new file mode 100644 index 0000000..ab3caf8 --- /dev/null +++ b/backend/src/common/auth/auth.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; +import { AuthService } from './auth.service.js'; +import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO +import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @Post('login') + // เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto + async login(@Body() loginDto: LoginDto) { + const user = await this.authService.validateUser( + loginDto.username, + loginDto.password, + ); + + if (!user) { + throw new UnauthorizedException('Invalid credentials'); + } + + return this.authService.login(user); + } + + @Post('register-admin') + // เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto + async register(@Body() registerDto: RegisterDto) { + return this.authService.register(registerDto); + } +} diff --git a/backend/src/common/auth/auth.module.ts b/backend/src/common/auth/auth.module.ts new file mode 100644 index 0000000..d038604 --- /dev/null +++ b/backend/src/common/auth/auth.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service.js'; +import { AuthController } from './auth.controller.js'; +import { UserModule } from '../../modules/user/user.module.js'; +import { JwtStrategy } from './jwt.strategy.js'; + +@Module({ + imports: [ + UserModule, + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { + // Cast เป็น any เพื่อแก้ปัญหา Type ไม่ตรงกับ Library + expiresIn: (configService.get('JWT_EXPIRATION') || + '8h') as any, + }, + }), + }), + ], + providers: [AuthService, JwtStrategy], + controllers: [AuthController], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/backend/src/common/auth/auth.service.spec.ts b/backend/src/common/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/backend/src/common/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/common/auth/auth.service.ts b/backend/src/common/auth/auth.service.ts new file mode 100644 index 0000000..7950287 --- /dev/null +++ b/backend/src/common/auth/auth.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import * as bcrypt from 'bcrypt'; +import { UserService } from '../../modules/user/user.service.js'; +import { RegisterDto } from './dto/register.dto.js'; // Import DTO + +@Injectable() +export class AuthService { + constructor( + private userService: UserService, + private jwtService: JwtService, + ) {} + + async validateUser(username: string, pass: string): Promise { + const user = await this.userService.findOneByUsername(username); + if (user && (await bcrypt.compare(pass, user.password))) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password, ...result } = user; + return result; + } + return null; + } + + async login(user: any) { + const payload = { username: user.username, sub: user.user_id }; + return { + access_token: this.jwtService.sign(payload), + }; + } + + async register(userDto: RegisterDto) { + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(userDto.password, salt); + + // ใช้ค่าจาก DTO ที่ Validate มาแล้ว + return this.userService.create({ + ...userDto, + password: hashedPassword, + }); + } +} diff --git a/backend/src/common/auth/dto/login.dto.ts b/backend/src/common/auth/dto/login.dto.ts new file mode 100644 index 0000000..e7af3e1 --- /dev/null +++ b/backend/src/common/auth/dto/login.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class LoginDto { + @IsString() + @IsNotEmpty() + username!: string; + + @IsString() + @IsNotEmpty() + password!: string; +} diff --git a/backend/src/common/auth/dto/register.dto.ts b/backend/src/common/auth/dto/register.dto.ts new file mode 100644 index 0000000..4ba003d --- /dev/null +++ b/backend/src/common/auth/dto/register.dto.ts @@ -0,0 +1,30 @@ +import { + IsEmail, + IsNotEmpty, + IsString, + MinLength, + IsOptional, +} from 'class-validator'; + +export class RegisterDto { + @IsString() + @IsNotEmpty() + username!: string; + + @IsString() + @IsNotEmpty() + @MinLength(6, { message: 'Password must be at least 6 characters' }) + password!: string; + + @IsEmail() + @IsNotEmpty() + email!: string; + + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; +} diff --git a/backend/src/common/auth/jwt-auth.guard.ts b/backend/src/common/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..2155290 --- /dev/null +++ b/backend/src/common/auth/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/backend/src/common/auth/jwt.strategy.ts b/backend/src/common/auth/jwt.strategy.ts new file mode 100644 index 0000000..0b08ddb --- /dev/null +++ b/backend/src/common/auth/jwt.strategy.ts @@ -0,0 +1,26 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +// Interface สำหรับ Payload ใน Token +interface JwtPayload { + sub: number; + username: string; +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + // ใส่ ! เพื่อยืนยันว่ามีค่าแน่นอน (ConfigValidation เช็คให้แล้ว) + secretOrKey: configService.get('JWT_SECRET')!, + }); + } + + async validate(payload: JwtPayload) { + return { userId: payload.sub, username: payload.username }; + } +} diff --git a/backend/src/common/auth/rbac.guard.ts b/backend/src/common/auth/rbac.guard.ts new file mode 100644 index 0000000..ec2287b --- /dev/null +++ b/backend/src/common/auth/rbac.guard.ts @@ -0,0 +1,55 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PERMISSION_KEY } from '../decorators/require-permission.decorator.js'; +import { UserService } from '../../modules/user/user.service.js'; + +@Injectable() +export class RbacGuard implements CanActivate { + constructor( + private reflector: Reflector, + private userService: UserService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + // 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร? + const requiredPermission = this.reflector.getAllAndOverride( + PERMISSION_KEY, + [context.getHandler(), context.getClass()], + ); + + // ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน + if (!requiredPermission) { + return true; + } + + // 2. ดึง User จาก Request (ที่ JwtAuthGuard แปะไว้ให้) + const { user } = context.switchToHttp().getRequest(); + if (!user) { + throw new ForbiddenException('User not found in request'); + } + + // 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database + // เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ) + const userPermissions = await this.userService.getUserPermissions( + user.userId, + ); + + // 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม? + const hasPermission = userPermissions.some( + (p) => p === requiredPermission || p === 'system.manage_all', // Superadmin ทะลุทุกสิทธิ์ + ); + + if (!hasPermission) { + throw new ForbiddenException( + `You do not have permission: ${requiredPermission}`, + ); + } + + return true; + } +} diff --git a/backend/src/common/common.module.ts b/backend/src/common/common.module.ts new file mode 100644 index 0000000..dd36dc6 --- /dev/null +++ b/backend/src/common/common.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from './auth/auth.module'; +import { AutController } from './aut/aut.controller'; + +@Module({ + imports: [AuthModule], + controllers: [AutController] +}) +export class CommonModule {} diff --git a/backend/src/common/config/env.validation.ts b/backend/src/common/config/env.validation.ts new file mode 100644 index 0000000..855f53e --- /dev/null +++ b/backend/src/common/config/env.validation.ts @@ -0,0 +1,31 @@ +// File: src/common/config/env.validation.ts +import Joi from 'joi'; + +// สร้าง Schema สำหรับตรวจสอบค่า Environment Variables +export const envValidationSchema = Joi.object({ + // 1. Application Environment + NODE_ENV: Joi.string() + .valid('development', 'production', 'test', 'provision') + .default('development'), + PORT: Joi.number().default(3000), + + // 2. Database Configuration (MariaDB) + // ห้ามเป็นค่าว่าง (required) + DB_HOST: Joi.string().required(), + DB_PORT: Joi.number().default(3306), + DB_USERNAME: Joi.string().required(), + DB_PASSWORD: Joi.string().required(), + DB_DATABASE: Joi.string().required(), + + // 3. Security (JWT) + // ต้องมีค่า และควรยาวพอ (ตรวจสอบความยาวได้ถ้าระบุ min) + JWT_SECRET: Joi.string() + .required() + .min(32) + .message('JWT_SECRET must be at least 32 characters long for security.'), + JWT_EXPIRATION: Joi.string().default('8h'), + // 4. Redis Configuration (เพิ่มส่วนนี้) + REDIS_HOST: Joi.string().required(), + REDIS_PORT: Joi.number().default(6379), + REDIS_PASSWORD: Joi.string().required(), +}); diff --git a/backend/src/common/decorators/require-permission.decorator.ts b/backend/src/common/decorators/require-permission.decorator.ts new file mode 100644 index 0000000..c9bfd95 --- /dev/null +++ b/backend/src/common/decorators/require-permission.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +export const PERMISSION_KEY = 'permissions'; + +// ใช้สำหรับแปะหน้า Controller/Method +// ตัวอย่าง: @RequirePermission('user.create') +export const RequirePermission = (permission: string) => + SetMetadata(PERMISSION_KEY, permission); diff --git a/backend/src/common/entities/base.entity.ts b/backend/src/common/entities/base.entity.ts new file mode 100644 index 0000000..1c527dc --- /dev/null +++ b/backend/src/common/entities/base.entity.ts @@ -0,0 +1,20 @@ +import { + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, +} from 'typeorm'; + +export abstract class BaseEntity { + // @PrimaryGeneratedColumn() + // id!: number; + + @CreateDateColumn({ name: 'created_at' }) + created_at!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updated_at!: Date; + + @DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default + deleted_at!: Date; +} diff --git a/backend/src/common/exceptions/http-exception.filter.ts b/backend/src/common/exceptions/http-exception.filter.ts new file mode 100644 index 0000000..9a9eba9 --- /dev/null +++ b/backend/src/common/exceptions/http-exception.filter.ts @@ -0,0 +1,50 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const exceptionResponse = + exception instanceof HttpException + ? exception.getResponse() + : 'Internal server error'; + + // จัดรูปแบบ Error Message + const message = + typeof exceptionResponse === 'string' + ? exceptionResponse + : (exceptionResponse as any).message || exceptionResponse; + // 👇👇 เพิ่มบรรทัดนี้ครับ (สำคัญมาก!) 👇👇 + console.error('💥 REAL ERROR:', exception); + + // Log Error (สำคัญมากสำหรับการ Debug แต่ไม่ส่งให้ Client เห็นทั้งหมด) + this.logger.error( + `Http Status: ${status} Error Message: ${JSON.stringify(message)}`, + ); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message: status === 500 ? 'Internal server error' : message, // ซ่อน Detail กรณี 500 + }); + } +} diff --git a/backend/src/common/interceptors/transform.interceptor.ts b/backend/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..b225d22 --- /dev/null +++ b/backend/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,32 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface Response { + statusCode: number; + message: string; + data: T; +} + +@Injectable() +export class TransformInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + return next.handle().pipe( + map((data) => ({ + statusCode: context.switchToHttp().getResponse().statusCode, + message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success' + data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา + })), + ); + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index f76bc8d..0b8507b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,8 +1,31 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; +import { TransformInterceptor } from './common/interceptors/transform.interceptor.js'; // อย่าลืม .js ถ้าใช้ ESM +import { HttpExceptionFilter } from './common/exceptions/http-exception.filter.js'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 3000); + + // 1. Global Prefix (เช่น /api/v1) + app.setGlobalPrefix('api'); + + // 2. Global Validation Pipe (ตรวจสอบ Input DTO) + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // ตัด field ส่วนเกินทิ้ง + transform: true, // แปลง Type อัตโนมัติ (เช่น string -> number) + forbidNonWhitelisted: true, // แจ้ง Error ถ้าส่ง field แปลกปลอมมา + }), + ); + + // 3. Global Interceptor (จัด Format Response) + app.useGlobalInterceptors(new TransformInterceptor()); + + // 4. Global Exception Filter (จัดการ Error) + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(process.env.PORT || 3000); + console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap(); diff --git a/backend/src/modules/project/entities/contract-organization.entity.ts b/backend/src/modules/project/entities/contract-organization.entity.ts new file mode 100644 index 0000000..87747a7 --- /dev/null +++ b/backend/src/modules/project/entities/contract-organization.entity.ts @@ -0,0 +1,25 @@ +import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Contract } from './contract.entity.js'; +import { Organization } from './organization.entity.js'; + +@Entity('contract_organizations') +export class ContractOrganization { + @PrimaryColumn({ name: 'contract_id' }) + contractId!: number; + + @PrimaryColumn({ name: 'organization_id' }) + organizationId!: number; + + @Column({ name: 'role_in_contract', nullable: true, length: 100 }) + roleInContract?: string; + + // Relation ไปยัง Contract + @ManyToOne(() => Contract, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'contract_id' }) + contract?: Contract; + + // Relation ไปยัง Organization + @ManyToOne(() => Organization, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'organization_id' }) + organization?: Organization; +} diff --git a/backend/src/modules/project/entities/contract.entity.ts b/backend/src/modules/project/entities/contract.entity.ts new file mode 100644 index 0000000..d9091ae --- /dev/null +++ b/backend/src/modules/project/entities/contract.entity.ts @@ -0,0 +1,41 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { BaseEntity } from '../../../common/entities/base.entity.js'; +import { Project } from './project.entity.js'; + +@Entity('contracts') +export class Contract extends BaseEntity { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'project_id' }) + projectId!: number; + + @Column({ name: 'contract_code', unique: true, length: 50 }) + contractCode!: string; + + @Column({ name: 'contract_name', length: 255 }) + contractName!: string; + + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ name: 'start_date', type: 'date', nullable: true }) + startDate?: Date; + + @Column({ name: 'end_date', type: 'date', nullable: true }) + endDate?: Date; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; + + // Relation + @ManyToOne(() => Project) + @JoinColumn({ name: 'project_id' }) + project?: Project; +} diff --git a/backend/src/modules/project/entities/organization.entity.ts b/backend/src/modules/project/entities/organization.entity.ts new file mode 100644 index 0000000..9de52cd --- /dev/null +++ b/backend/src/modules/project/entities/organization.entity.ts @@ -0,0 +1,17 @@ +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { BaseEntity } from '../../../common/entities/base.entity.js'; + +@Entity('organizations') +export class Organization extends BaseEntity { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'organization_code', unique: true, length: 20 }) + organizationCode!: string; + + @Column({ name: 'organization_name', length: 255 }) + organizationName!: string; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; +} diff --git a/backend/src/modules/project/entities/project-organization.entity.ts b/backend/src/modules/project/entities/project-organization.entity.ts new file mode 100644 index 0000000..e518e9e --- /dev/null +++ b/backend/src/modules/project/entities/project-organization.entity.ts @@ -0,0 +1,23 @@ +import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Project } from './project.entity.js'; +import { Organization } from './organization.entity.js'; + +@Entity('project_organizations') +export class ProjectOrganization { + // Composite Primary Key (ใช้ 2 คอลัมน์รวมกันเป็น PK) + @PrimaryColumn({ name: 'project_id' }) + projectId!: number; + + @PrimaryColumn({ name: 'organization_id' }) + organizationId!: number; + + // Relation ไปยัง Project + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project?: Project; + + // Relation ไปยัง Organization + @ManyToOne(() => Organization, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'organization_id' }) + organization?: Organization; +} diff --git a/backend/src/modules/project/entities/project.entity.ts b/backend/src/modules/project/entities/project.entity.ts new file mode 100644 index 0000000..9e09f34 --- /dev/null +++ b/backend/src/modules/project/entities/project.entity.ts @@ -0,0 +1,17 @@ +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { BaseEntity } from '../../../common/entities/base.entity.js'; + +@Entity('projects') +export class Project extends BaseEntity { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ name: 'project_code', unique: true, length: 50 }) + projectCode!: string; + + @Column({ name: 'project_name', length: 255 }) + projectName!: string; + + @Column({ name: 'is_active', default: 1, type: 'tinyint' }) + isActive!: boolean; +} diff --git a/backend/src/modules/project/project.controller.spec.ts b/backend/src/modules/project/project.controller.spec.ts new file mode 100644 index 0000000..7b67323 --- /dev/null +++ b/backend/src/modules/project/project.controller.spec.ts @@ -0,0 +1,19 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ProjectService } from './project.service.js'; +import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js'; + +@Controller('projects') +@UseGuards(JwtAuthGuard) +export class ProjectController { + constructor(private readonly projectService: ProjectService) {} + + @Get() + findAll() { + return this.projectService.findAllProjects(); + } + + @Get('organizations') + findAllOrgs() { + return this.projectService.findAllOrganizations(); + } +} diff --git a/backend/src/modules/project/project.controller.ts b/backend/src/modules/project/project.controller.ts new file mode 100644 index 0000000..66980ae --- /dev/null +++ b/backend/src/modules/project/project.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('project') +export class ProjectController {} diff --git a/backend/src/modules/project/project.module.ts b/backend/src/modules/project/project.module.ts new file mode 100644 index 0000000..159ed1a --- /dev/null +++ b/backend/src/modules/project/project.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ProjectService } from './project.service.js'; +import { ProjectController } from './project.controller.js'; +import { Project } from './entities/project.entity.js'; +import { Organization } from './entities/organization.entity.js'; +import { Contract } from './entities/contract.entity.js'; +import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม +import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Project, + Organization, + Contract, + ProjectOrganization, // ลงทะเบียน + ContractOrganization, // ลงทะเบียน + ]), + ], + controllers: [ProjectController], + providers: [ProjectService], + exports: [ProjectService], // Export เผื่อ Module อื่นใช้ +}) +export class ProjectModule {} diff --git a/backend/src/modules/project/project.service.spec.ts b/backend/src/modules/project/project.service.spec.ts new file mode 100644 index 0000000..126b6cc --- /dev/null +++ b/backend/src/modules/project/project.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectService } from './project.service'; + +describe('ProjectService', () => { + let service: ProjectService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectService], + }).compile(); + + service = module.get(ProjectService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/modules/project/project.service.ts b/backend/src/modules/project/project.service.ts new file mode 100644 index 0000000..79f5a9e --- /dev/null +++ b/backend/src/modules/project/project.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Project } from './entities/project.entity.js'; +import { Organization } from './entities/organization.entity.js'; + +@Injectable() +export class ProjectService { + constructor( + @InjectRepository(Project) + private projectRepository: Repository, + @InjectRepository(Organization) + private organizationRepository: Repository, + ) {} + + // ดึงรายการ Project ทั้งหมด + async findAllProjects() { + return this.projectRepository.find(); + } + + // ดึงรายการ Organization ทั้งหมด + async findAllOrganizations() { + return this.organizationRepository.find(); + } +} diff --git a/backend/src/modules/user/dto/create-user.dto.ts b/backend/src/modules/user/dto/create-user.dto.ts new file mode 100644 index 0000000..f00c8f8 --- /dev/null +++ b/backend/src/modules/user/dto/create-user.dto.ts @@ -0,0 +1,44 @@ +import { + IsString, + IsEmail, + IsNotEmpty, + MinLength, + IsOptional, + IsBoolean, + IsInt, +} from 'class-validator'; + +export class CreateUserDto { + @IsString() + @IsNotEmpty() + username!: string; + + @IsString() + @IsNotEmpty() + @MinLength(6, { message: 'Password must be at least 6 characters' }) + password!: string; + + @IsEmail() + @IsNotEmpty() + email!: string; + + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; + + @IsString() + @IsOptional() + lineId?: string; + + @IsInt() + @IsOptional() + primaryOrganizationId?: number; // รับเป็น ID ของ Organization + + @IsBoolean() + @IsOptional() + isActive?: boolean; +} diff --git a/backend/src/modules/user/dto/update-user.dto.ts b/backend/src/modules/user/dto/update-user.dto.ts new file mode 100644 index 0000000..912cdc5 --- /dev/null +++ b/backend/src/modules/user/dto/update-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto.js'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/backend/src/modules/user/entities/user.entity.ts b/backend/src/modules/user/entities/user.entity.ts new file mode 100644 index 0000000..553e83f --- /dev/null +++ b/backend/src/modules/user/entities/user.entity.ts @@ -0,0 +1,53 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, + ManyToOne, // <--- เพิ่มตรงนี้ + JoinColumn, // <--- เพิ่มตรงนี้ +} from 'typeorm'; +import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization + +@Entity('users') +export class User { + @PrimaryGeneratedColumn({ name: 'user_id' }) + user_id!: number; + + @Column({ unique: true, length: 50 }) + username!: string; + + @Column({ name: 'password_hash' }) + password!: string; + + @Column({ unique: true, length: 100 }) + email!: string; + + @Column({ name: 'first_name', nullable: true, length: 50 }) + firstName?: string; + + @Column({ name: 'last_name', nullable: true, length: 50 }) + lastName?: string; + + @Column({ name: 'is_active', default: true }) + isActive!: boolean; + + // Relation กับ Organization + @Column({ name: 'primary_organization_id', nullable: true }) + primaryOrganizationId?: number; + + @ManyToOne(() => Organization) + @JoinColumn({ name: 'primary_organization_id' }) + organization?: Organization; + + // Base Entity Fields (ที่เราแยกมาเขียนเองเพราะเรื่อง deleted_at) + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; + + @DeleteDateColumn({ name: 'deleted_at', select: false }) + deletedAt?: Date; +} diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts new file mode 100644 index 0000000..2eeed61 --- /dev/null +++ b/backend/src/modules/user/user.controller.ts @@ -0,0 +1,57 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, + ParseIntPipe, +} from '@nestjs/common'; +import { UserService } from './user.service.js'; +import { CreateUserDto } from './dto/create-user.dto.js'; +import { UpdateUserDto } from './dto/update-user.dto.js'; +import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js'; +import { RequirePermission } from '../../common/decorators/require-permission.decorator.js'; +import { RbacGuard } from '../../common/auth/rbac.guard.js'; + +@Controller('users') +@UseGuards(JwtAuthGuard, RbacGuard) // 🔒 เพิ่ม RbacGuard ต่อท้าย) // 🔒 บังคับ Login ทุก Endpoints ในนี้ +export class UserController { + constructor(private readonly userService: UserService) {} + + // 1. สร้างผู้ใช้ใหม่ + @Post() + @RequirePermission('user.create') // 🔒 ต้องมีสิทธิ์ user.create ถึงจะเข้าได้ + create(@Body() createUserDto: CreateUserDto) { + return this.userService.create(createUserDto); + } + + // 2. ดูรายชื่อผู้ใช้ทั้งหมด + @Get() + findAll() { + return this.userService.findAll(); + } + + // 3. ดูข้อมูลผู้ใช้รายคน (ตาม ID) + @Get(':id') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.userService.findOne(id); + } + + // 4. แก้ไขข้อมูลผู้ใช้ + @Patch(':id') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateUserDto: UpdateUserDto, + ) { + return this.userService.update(id, updateUserDto); + } + + // 5. ลบผู้ใช้ (Soft Delete) + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number) { + return this.userService.remove(id); + } +} diff --git a/backend/src/modules/user/user.module.ts b/backend/src/modules/user/user.module.ts new file mode 100644 index 0000000..ef6725b --- /dev/null +++ b/backend/src/modules/user/user.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserService } from './user.service.js'; +import { UserController } from './user.controller.js'; // 1. Import Controller +import { User } from './entities/user.entity.js'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], // จดทะเบียน Entity + // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่ + controllers: [UserController], + providers: [UserService], + exports: [UserService], // Export ให้ AuthModule เรียกใช้ได้ +}) +export class UserModule {} diff --git a/backend/src/modules/user/user.service.spec.ts b/backend/src/modules/user/user.service.spec.ts new file mode 100644 index 0000000..873de8a --- /dev/null +++ b/backend/src/modules/user/user.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService], + }).compile(); + + service = module.get(UserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts new file mode 100644 index 0000000..1f8fd34 --- /dev/null +++ b/backend/src/modules/user/user.service.ts @@ -0,0 +1,119 @@ +import { + Injectable, + NotFoundException, + ConflictException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { User } from './entities/user.entity.js'; +import { CreateUserDto } from './dto/create-user.dto.js'; +import { UpdateUserDto } from './dto/update-user.dto.js'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private usersRepository: Repository, + ) {} + + // 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก) + async create(createUserDto: CreateUserDto): Promise { + // สร้าง Salt และ Hash Password + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(createUserDto.password, salt); + + // เตรียมข้อมูล (เปลี่ยน password ธรรมดา เป็น password_hash) + const newUser = this.usersRepository.create({ + ...createUserDto, + password: hashedPassword, + }); + + try { + // บันทึกลง DB + return await this.usersRepository.save(newUser); + } catch (error: any) { + // เช็ค Error กรณี Username/Email ซ้ำ (MySQL Error Code 1062) + if (error.code === 'ER_DUP_ENTRY') { + throw new ConflictException('Username or Email already exists'); + } + throw error; + } + } + + // 2. ดึงข้อมูลทั้งหมด + async findAll(): Promise { + return this.usersRepository.find({ + // ไม่ส่ง password กลับไปเพื่อความปลอดภัย + select: [ + 'user_id', + 'username', + 'email', + 'firstName', + 'lastName', + 'isActive', + 'createdAt', + 'updatedAt', + ], + }); + } + + // 3. ดึงข้อมูลรายคน + async findOne(id: number): Promise { + const user = await this.usersRepository.findOne({ + where: { user_id: id }, // ใช้ user_id ตามที่คุณตั้งชื่อไว้ + }); + + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + + return user; + } + + // ฟังก์ชันแถม: สำหรับ AuthService ใช้ (ต้องเห็น Password เพื่อเอาไปเทียบ) + async findOneByUsername(username: string): Promise { + return this.usersRepository.findOne({ where: { username } }); + } + + // 4. แก้ไขข้อมูล + async update(id: number, updateUserDto: UpdateUserDto): Promise { + // เช็คก่อนว่ามี User นี้ไหม + const user = await this.findOne(id); + + // ถ้ามีการแก้รหัสผ่าน ต้อง Hash ใหม่ด้วย + if (updateUserDto.password) { + const salt = await bcrypt.genSalt(); + updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt); + } + + // รวมร่างข้อมูลเดิม + ข้อมูลใหม่ + const updatedUser = this.usersRepository.merge(user, updateUserDto); + + return this.usersRepository.save(updatedUser); + } + + // 5. ลบผู้ใช้ (Soft Delete) + async remove(id: number): Promise { + const result = await this.usersRepository.softDelete(id); + + if (result.affected === 0) { + throw new NotFoundException(`User with ID ${id} not found`); + } + } + + // 👇👇 เพิ่มฟังก์ชันใหม่นี้ 👇👇 + async getUserPermissions(userId: number): Promise { + // Query ข้อมูลจาก View: v_user_all_permissions (ที่เราสร้างไว้ใน SQL Script) + // เนื่องจาก TypeORM ไม่รองรับ View โดยตรงในบางท่า เราใช้ query builder หรือ query raw ได้ + // แต่เพื่อความง่ายและประสิทธิภาพ เราจะใช้ query raw ครับ + + const permissions = await this.usersRepository.query( + `SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`, + [userId], + ); + + // แปลงผลลัพธ์เป็น Array ของ string ['user.create', 'project.view', ...] + return permissions.map((row: any) => row.permission_name); + } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 5ffe6df..e74ae8f 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "commonjs", + "module": "NodeNext", "moduleResolution": "nodenext", "resolvePackageJsonExports": true, "esModuleInterop": true,