From fb73d37e2fb7c28ec89e2cefd38ef58e16936bca Mon Sep 17 00:00:00 2001 From: Alper Alkan Date: Sun, 31 Mar 2024 01:02:27 +0100 Subject: [PATCH] Release 11.0.0 (#266) * format, update deps and fix underscores not being allowed in usernames * format * fix undefined toLowerCase for Emails, when updating users * Fixed bug where `gvbot_` users were showing up in user list * implement demo mode flag * deprecate /users/all * fixed config name * update deps --- CHANGELOG.md | 12 ++ package.json | 6 +- pnpm-lock.yaml | 167 ++++++++---------- src/app.module.ts | 10 +- src/configuration.ts | 3 + .../conditional-registration.decorator.ts | 8 + src/decorators/disable-api-if.decorator.ts | 4 + src/decorators/minimum-role.decorator.ts | 3 +- .../disable-api-if.interceptor.ts | 34 ++++ src/modules/files/files.service.ts | 8 +- src/modules/guards/authorization.guard.ts | 3 +- src/modules/images/images.controller.ts | 2 + src/modules/progress/progress.controller.ts | 3 + src/modules/providers/rawg/rawg.service.ts | 4 +- src/modules/users/users.controller.ts | 32 ++-- src/modules/users/users.service.ts | 12 +- 16 files changed, 191 insertions(+), 120 deletions(-) create mode 100644 src/decorators/conditional-registration.decorator.ts create mode 100644 src/decorators/disable-api-if.decorator.ts create mode 100644 src/interceptors/disable-api-if.interceptor.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 861590f8..eb2514f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # GameVault Backend Server Changelog +## 11.0.0 + +### Breaking Changes & Migration + +- Deprecated `/api/users/all`, Admins should just use `/api/users` to get a list of all users. (will be removed in v12) + +### Changes + +- Fixed a bug where updating a users email, which was set to null, would throw an error. +- Fixed bug where `gvbot_` users were showing up in user list +- Implement `SERVER_DEMO_MODE_ENABLED` to enable/disable demo mode. You wont need this but it will improve the functionality of our demo server. + ## 10.3.2 ### Changes diff --git a/package.json b/package.json index d6541e2d..7340cb0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gamevault-backend", - "version": "10.3.2", + "version": "10.3.3", "description": "the self-hosted gaming platform for drm-free games", "author": "Alkan Alper, Schäfer Philip GbR / Phalcode", "private": true, @@ -62,7 +62,7 @@ "node-7z": "3.0.0", "passport": "0.7.0", "passport-http": "0.3.0", - "pg": "8.11.3", + "pg": "8.11.4", "reflect-metadata": "0.1.14", "rimraf": "5.0.5", "rxjs": "7.8.1", @@ -90,7 +90,7 @@ "@types/mime": "3.0.4", "@types/morgan": "1.9.9", "@types/multer": "^1.4.11", - "@types/node": "20.11.30", + "@types/node": "20.12.2", "@types/node-7z": "2.1.8", "@types/passport-http": "0.3.11", "@types/string-similarity": "4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dc9ba05..ff43e365 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,7 +103,7 @@ dependencies: version: 1.9.4(@nestjs/common@10.3.7)(winston@3.13.0) nestjs-asyncapi: specifier: 1.3.0 - version: 1.3.0(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(@nestjs/swagger@7.3.1)(@nestjs/websockets@10.3.7)(@types/node@20.11.30) + version: 1.3.0(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(@nestjs/swagger@7.3.1)(@nestjs/websockets@10.3.7)(@types/node@20.12.2) nestjs-paginate: specifier: 8.6.2 version: 8.6.2(@nestjs/common@10.3.7)(@nestjs/swagger@7.3.1)(express@4.19.2)(fastify@4.26.2)(typeorm@0.3.20) @@ -117,8 +117,8 @@ dependencies: specifier: 0.3.0 version: 0.3.0 pg: - specifier: 8.11.3 - version: 8.11.3 + specifier: 8.11.4 + version: 8.11.4 reflect-metadata: specifier: 0.1.14 version: 0.1.14 @@ -145,7 +145,7 @@ dependencies: version: 2.1.4 typeorm: specifier: 0.3.20 - version: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.3)(ts-node@10.9.2) + version: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.4)(ts-node@10.9.2) typeorm-naming-strategies: specifier: 4.1.0 version: 4.1.0(typeorm@0.3.20) @@ -197,8 +197,8 @@ devDependencies: specifier: ^1.4.11 version: 1.4.11 '@types/node': - specifier: 20.11.30 - version: 20.11.30 + specifier: 20.12.2 + version: 20.12.2 '@types/node-7z': specifier: 2.1.8 version: 2.1.8 @@ -231,7 +231,7 @@ devDependencies: version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) prettier: specifier: 3.2.5 version: 3.2.5 @@ -246,7 +246,7 @@ devDependencies: version: 29.1.2(@babel/core@7.12.9)(jest@29.7.0)(typescript@5.4.3) ts-node: specifier: 10.9.2 - version: 10.9.2(@types/node@20.11.30)(typescript@5.4.3) + version: 10.9.2(@types/node@20.12.2)(typescript@5.4.3) typescript: specifier: 5.4.3 version: 5.4.3 @@ -356,7 +356,7 @@ packages: - supports-color dev: false - /@asyncapi/generator@1.13.1(@types/node@20.11.30): + /@asyncapi/generator@1.13.1(@types/node@20.12.2): resolution: {integrity: sha512-+6pQE9OlXue79AO0hMJwlzwH48vED/rPGU1NAQlDyuTvo8599JTd3zUPFDafExRn/arQltJPDE+Z5jtNBifXjw==} engines: {node: '>12.16', npm: '>6.13.7'} hasBin: true @@ -384,7 +384,7 @@ packages: semver: 7.5.4 simple-git: 3.20.0 source-map-support: 0.5.21 - ts-node: 10.9.2(@types/node@20.11.30)(typescript@4.9.5) + ts-node: 10.9.2(@types/node@20.12.2)(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - '@swc/core' @@ -2418,7 +2418,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -2439,14 +2439,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -2474,7 +2474,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 jest-mock: 29.7.0 dev: true @@ -2501,7 +2501,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.11.30 + '@types/node': 20.12.2 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2534,7 +2534,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.11.30 + '@types/node': 20.12.2 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2622,7 +2622,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.5 '@types/istanbul-reports': 3.0.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 '@types/yargs': 17.0.29 chalk: 4.1.2 dev: true @@ -3029,7 +3029,7 @@ packages: '@nestjs/core': 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(@nestjs/websockets@10.3.7)(reflect-metadata@0.1.14)(rxjs@7.8.1) reflect-metadata: 0.1.14 rxjs: 7.8.1 - typeorm: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.3)(ts-node@10.9.2) + typeorm: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.4)(ts-node@10.9.2) uuid: 9.0.1 dev: false @@ -3552,14 +3552,14 @@ packages: /@types/bcrypt@5.0.2: resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true /@types/compression@1.7.5: @@ -3571,7 +3571,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true /@types/cookie-parser@1.4.7: @@ -3586,7 +3586,7 @@ packages: /@types/cors@2.8.15: resolution: {integrity: sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 /@types/debug@4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} @@ -3603,7 +3603,7 @@ packages: /@types/es-aggregate-error@1.0.4: resolution: {integrity: sha512-95tL6tLR8P3Utx4SxXUEc0e+k2B9VhtBozhgxKGpv30ylIuxGxf080d7mYZ08sH5UjpDv/Nd6F80tH1p+KuPIg==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: false /@types/eslint-scope@3.7.4: @@ -3631,7 +3631,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -3648,7 +3648,7 @@ packages: /@types/graceful-fs@4.1.8: resolution: {integrity: sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true /@types/istanbul-lib-coverage@2.0.5: @@ -3698,7 +3698,7 @@ packages: /@types/morgan@1.9.9: resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true /@types/ms@0.7.31: @@ -3714,11 +3714,11 @@ packages: /@types/node-7z@2.1.8: resolution: {integrity: sha512-VjiU7yEbczNc3EFKN4GJcAUqAMkn92P/92r6ARjMSXEdixunMD9lC79mTX81vKxTlNYXuvCJ7zvnzlDbFTt2Vw==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true - /@types/node@20.11.30: - resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + /@types/node@20.12.2: + resolution: {integrity: sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==} dependencies: undici-types: 5.26.5 @@ -3738,7 +3738,7 @@ packages: /@types/protocol-buffers-schema@3.4.2: resolution: {integrity: sha512-GaQpfsfFk4wGU3//d7uCGy9zy6B8QBEyWYd6+maZH+S6m861QrFvLWS5RyHj4UfIiON9tmqCz9C+oNpebDgGIw==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: false /@types/qs@6.9.7: @@ -3757,7 +3757,7 @@ packages: resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} dependencies: '@types/mime': 3.0.4 - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: true /@types/stack-utils@2.0.2: @@ -3767,7 +3767,7 @@ packages: /@types/stream-throttle@0.1.4: resolution: {integrity: sha512-VxXIHGjVuK8tYsVm60rIQMmF/0xguCeen5OmK5S4Y6K64A+z+y4/GI6anRnVzaUZaJB9Ah9IfbDcO0o1gZCc/w==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: false /@types/string-similarity@4.0.2: @@ -3807,7 +3807,7 @@ packages: resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==} requiresBuild: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 dev: false optional: true @@ -4770,11 +4770,6 @@ packages: /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - /buffer-writer@2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - dev: false - /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -5290,7 +5285,7 @@ packages: typescript: 5.3.3 dev: true - /create-jest@29.7.0(@types/node@20.11.30)(ts-node@10.9.2): + /create-jest@29.7.0(@types/node@20.12.2)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -5299,7 +5294,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 - jest-config: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -5668,7 +5663,7 @@ packages: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.15 - '@types/node': 20.11.30 + '@types/node': 20.12.2 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.1 @@ -7366,7 +7361,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -7387,7 +7382,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@20.11.30)(ts-node@10.9.2): + /jest-cli@29.7.0(@types/node@20.12.2)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -7401,10 +7396,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + create-jest: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.6.2 @@ -7415,7 +7410,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.11.30)(ts-node@10.9.2): + /jest-config@29.7.0(@types/node@20.12.2)(ts-node@10.9.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -7430,7 +7425,7 @@ packages: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 babel-jest: 29.7.0(@babel/core@7.23.9) chalk: 4.1.2 ci-info: 3.9.0 @@ -7450,7 +7445,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.2(@types/node@20.11.30)(typescript@5.4.3) + ts-node: 10.9.2(@types/node@20.12.2)(typescript@5.4.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -7491,7 +7486,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -7507,7 +7502,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.8 - '@types/node': 20.11.30 + '@types/node': 20.12.2 anymatch: 3.1.2 fb-watchman: 2.0.2 graceful-fs: 4.2.10 @@ -7558,7 +7553,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 jest-util: 29.7.0 dev: true @@ -7613,7 +7608,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.10 @@ -7644,7 +7639,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -7696,7 +7691,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.10 @@ -7721,7 +7716,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -7733,7 +7728,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -7742,13 +7737,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@20.11.30)(ts-node@10.9.2): + /jest@29.7.0(@types/node@20.12.2)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -7761,7 +7756,7 @@ packages: '@jest/core': 29.7.0(ts-node@10.9.2) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + jest-cli: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8704,7 +8699,7 @@ packages: winston: 3.13.0 dev: false - /nestjs-asyncapi@1.3.0(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(@nestjs/swagger@7.3.1)(@nestjs/websockets@10.3.7)(@types/node@20.11.30): + /nestjs-asyncapi@1.3.0(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(@nestjs/swagger@7.3.1)(@nestjs/websockets@10.3.7)(@types/node@20.12.2): resolution: {integrity: sha512-ZtCrPdGGPBIRupI+Zt/9EmYuO6gU9O7snLB81on1nLIoir0pPcWCzIjiq5FafVwiHrV4ideUWklWdqh/xZU5PQ==} peerDependencies: '@nestjs/common': ^10.0.0 || ^9.0.0 @@ -8715,7 +8710,7 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@asyncapi/generator': 1.13.1(@types/node@20.11.30) + '@asyncapi/generator': 1.13.1(@types/node@20.12.2) '@asyncapi/html-template': 0.28.4 '@nestjs/common': 10.3.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/core': 10.3.7(@nestjs/common@10.3.7)(@nestjs/platform-express@10.3.7)(@nestjs/websockets@10.3.7)(reflect-metadata@0.1.14)(rxjs@7.8.1) @@ -8750,7 +8745,7 @@ packages: express: 4.19.2 fastify: 4.26.2 lodash: 4.17.21 - typeorm: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.3)(ts-node@10.9.2) + typeorm: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.4)(ts-node@10.9.2) dev: false /nimma@0.2.2: @@ -9171,10 +9166,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - /packet-reader@1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - dev: false - /pacote@11.3.5: resolution: {integrity: sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg==} engines: {node: '>=10'} @@ -9324,8 +9315,8 @@ packages: dev: false optional: true - /pg-connection-string@2.6.2: - resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + /pg-connection-string@2.6.3: + resolution: {integrity: sha512-77FxhhKJQH+xJx6tDqkhhMa0nZvv3U1HYLDQgwZxZafVD583++O5LXn5oo5HaQZ0vXwYcZA1koYAJM3JvD6Gtw==} dev: false /pg-int8@1.0.1: @@ -9333,16 +9324,16 @@ packages: engines: {node: '>=4.0.0'} dev: false - /pg-pool@3.6.1(pg@8.11.3): - resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + /pg-pool@3.6.2(pg@8.11.4): + resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} peerDependencies: pg: '>=8.0' dependencies: - pg: 8.11.3 + pg: 8.11.4 dev: false - /pg-protocol@1.6.0: - resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + /pg-protocol@1.6.1: + resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} dev: false /pg-types@2.2.0: @@ -9356,8 +9347,8 @@ packages: postgres-interval: 1.2.0 dev: false - /pg@8.11.3: - resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} + /pg@8.11.4: + resolution: {integrity: sha512-pWb7JKPxGk1UFbtq7jQ0m3IfPpb7LLACCEyN8/u9DYEom+Q/BSKy+4TRl4+Hh003AOYhppB/z+QK87/hx/bk0w==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -9365,11 +9356,9 @@ packages: pg-native: optional: true dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 - pg-connection-string: 2.6.2 - pg-pool: 3.6.1(pg@8.11.3) - pg-protocol: 1.6.0 + pg-connection-string: 2.6.3 + pg-pool: 3.6.2(pg@8.11.4) + pg-protocol: 1.6.1 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -10871,7 +10860,7 @@ packages: '@babel/core': 7.12.9 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.11.30)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -10881,7 +10870,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-node@10.9.2(@types/node@20.11.30)(typescript@4.9.5): + /ts-node@10.9.2(@types/node@20.12.2)(typescript@4.9.5): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -10900,7 +10889,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -10912,7 +10901,7 @@ packages: yn: 3.1.1 dev: false - /ts-node@10.9.2(@types/node@20.11.30)(typescript@5.4.3): + /ts-node@10.9.2(@types/node@20.12.2)(typescript@5.4.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -10931,7 +10920,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 20.11.30 + '@types/node': 20.12.2 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -11066,10 +11055,10 @@ packages: peerDependencies: typeorm: ^0.2.0 || ^0.3.0 dependencies: - typeorm: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.3)(ts-node@10.9.2) + typeorm: 0.3.20(better-sqlite3@8.7.0)(pg@8.11.4)(ts-node@10.9.2) dev: false - /typeorm@0.3.20(better-sqlite3@8.7.0)(pg@8.11.3)(ts-node@10.9.2): + /typeorm@0.3.20(better-sqlite3@8.7.0)(pg@8.11.4)(ts-node@10.9.2): resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} engines: {node: '>=16.13.0'} hasBin: true @@ -11138,10 +11127,10 @@ packages: dotenv: 16.4.5 glob: 10.3.10 mkdirp: 2.1.6 - pg: 8.11.3 + pg: 8.11.4 reflect-metadata: 0.2.1 sha.js: 2.4.11 - ts-node: 10.9.2(@types/node@20.11.30)(typescript@5.4.3) + ts-node: 10.9.2(@types/node@20.12.2)(typescript@5.4.3) tslib: 2.6.2 uuid: 9.0.1 yargs: 17.6.2 diff --git a/src/app.module.ts b/src/app.module.ts index 175dd109..792094b4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,6 +19,8 @@ import { RawgModule } from "./modules/providers/rawg/rawg.module"; import { DefaultStrategy } from "./modules/guards/basic-auth.strategy"; import { GarbageCollectionModule } from "./modules/garbage-collection/garbage-collection.module"; import { EventEmitterModule } from "@nestjs/event-emitter"; +import { APP_INTERCEPTOR } from "@nestjs/core"; +import { DisableApiIfInterceptor } from "./interceptors/disable-api-if.interceptor"; @Module({ imports: [ @@ -42,6 +44,12 @@ import { EventEmitterModule } from "@nestjs/event-emitter"; EventEmitterModule.forRoot(), PluginModule, ], - providers: [DefaultStrategy], + providers: [ + DefaultStrategy, + { + provide: APP_INTERCEPTOR, + useClass: DisableApiIfInterceptor, + }, + ], }) export class AppModule {} diff --git a/src/configuration.ts b/src/configuration.ts index 9b8624bb..eb4c1ca0 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -48,6 +48,9 @@ const configuration = { SERVER: { PORT: Number(process.env.SERVER_PORT) || 8080, VERSION: process.env.npm_package_version || packageJson.version, + DEMO_MODE_ENABLED: parseBooleanEnvVariable( + process.env.SERVER_DEMO_MODE_ENABLED, + ), LOG_LEVEL: process.env.SERVER_LOG_LEVEL || "info", LOG_FILES_ENABLED: parseBooleanEnvVariable( process.env.SERVER_LOG_FILES_ENABLED, diff --git a/src/decorators/conditional-registration.decorator.ts b/src/decorators/conditional-registration.decorator.ts new file mode 100644 index 00000000..c3754892 --- /dev/null +++ b/src/decorators/conditional-registration.decorator.ts @@ -0,0 +1,8 @@ +import { noop } from "rxjs"; +import configuration from "../configuration"; +import { Public } from "./public.decorator"; + +export const ConditionalRegistration = configuration.SERVER + .REGISTRATION_DISABLED + ? noop + : Public(); diff --git a/src/decorators/disable-api-if.decorator.ts b/src/decorators/disable-api-if.decorator.ts new file mode 100644 index 00000000..5de70b6b --- /dev/null +++ b/src/decorators/disable-api-if.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from "@nestjs/common"; +export const DISABLE_API_IF_KEY = "disableApiIf"; +export const DisableApiIf = (disabled: boolean) => + SetMetadata(DISABLE_API_IF_KEY, disabled); diff --git a/src/decorators/minimum-role.decorator.ts b/src/decorators/minimum-role.decorator.ts index c8ee14b7..6610dc9d 100644 --- a/src/decorators/minimum-role.decorator.ts +++ b/src/decorators/minimum-role.decorator.ts @@ -1,4 +1,5 @@ import { SetMetadata } from "@nestjs/common"; import { Role } from "../modules/users/models/role.enum"; -export const MinimumRole = (role: Role) => SetMetadata("minimumRole", role); +export const MINIMUM_ROLE_KEY = "minimumRole"; +export const MinimumRole = (role: Role) => SetMetadata(MINIMUM_ROLE_KEY, role); diff --git a/src/interceptors/disable-api-if.interceptor.ts b/src/interceptors/disable-api-if.interceptor.ts new file mode 100644 index 00000000..d1d18813 --- /dev/null +++ b/src/interceptors/disable-api-if.interceptor.ts @@ -0,0 +1,34 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + MethodNotAllowedException, +} from "@nestjs/common"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { Reflector } from "@nestjs/core"; +import { DISABLE_API_IF_KEY } from "../decorators/disable-api-if.decorator"; + +@Injectable() +export class DisableApiIfInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + intercept(context: ExecutionContext, next: CallHandler): Observable { + const disabled = this.reflector.get( + DISABLE_API_IF_KEY, + context.getHandler(), + ); + + if (disabled) { + return next.handle().pipe( + map(() => { + throw new MethodNotAllowedException("This API endpoint is disabled."); + }), + ); + } + + return next.handle(); + } +} diff --git a/src/modules/files/files.service.ts b/src/modules/files/files.service.ts index 89a8ad2a..68da2805 100644 --- a/src/modules/files/files.service.ts +++ b/src/modules/files/files.service.ts @@ -170,7 +170,7 @@ export class FilesService implements OnApplicationBootstrap { if ( !configuration.GAMES.SUPPORTED_FILE_FORMATS.includes( - extname(actualFilename).toLowerCase(), + extname(actualFilename)?.toLowerCase(), ) ) { this.logger.debug( @@ -248,7 +248,7 @@ export class FilesService implements OnApplicationBootstrap { const detectedPatterns: string[] = []; for (const file of files) { - const fileName = basename(file).toLowerCase(); + const fileName = basename(file)?.toLowerCase(); for (const pattern of windowsInstallerPatterns) { if (pattern.regex.test(fileName)) { @@ -295,14 +295,14 @@ export class FilesService implements OnApplicationBootstrap { } // Detect single File executables - if (path.toLowerCase().endsWith(".exe")) { + if (path?.toLowerCase().endsWith(".exe")) { this.logger.debug( `Detected game "${path}" type as ${GameType.WINDOWS_SETUP}, because it ends with .exe`, ); return GameType.WINDOWS_SETUP; } - if (path.toLowerCase().endsWith(".sh")) { + if (path?.toLowerCase().endsWith(".sh")) { this.logger.debug( `Detected game "${path}" type as ${GameType.LINUX_PORTABLE}, because it ends with .sh`, ); diff --git a/src/modules/guards/authorization.guard.ts b/src/modules/guards/authorization.guard.ts index 7fa47ae5..dc68df48 100644 --- a/src/modules/guards/authorization.guard.ts +++ b/src/modules/guards/authorization.guard.ts @@ -8,6 +8,7 @@ import { import { Reflector } from "@nestjs/core"; import configuration from "../../configuration"; import { Role } from "../users/models/role.enum"; +import { MINIMUM_ROLE_KEY } from "../../decorators/minimum-role.decorator"; @Injectable() export class AuthorizationGuard implements CanActivate { @@ -36,7 +37,7 @@ export class AuthorizationGuard implements CanActivate { } const requiredRole = this.reflector.get( - "minimumRole", + MINIMUM_ROLE_KEY, context.getHandler(), ); diff --git a/src/modules/images/images.controller.ts b/src/modules/images/images.controller.ts index cbd66e75..14f20542 100644 --- a/src/modules/images/images.controller.ts +++ b/src/modules/images/images.controller.ts @@ -30,6 +30,7 @@ import { FileInterceptor } from "@nestjs/platform-express"; import { Image } from "./image.entity"; import configuration from "../../configuration"; import { GamevaultUser } from "../users/gamevault-user.entity"; +import { DisableApiIf } from "../../decorators/disable-api-if.decorator"; @ApiTags("images") @Controller("images") @@ -85,6 +86,7 @@ export class ImagesController { }) @UseInterceptors(FileInterceptor("file")) @MinimumRole(Role.USER) + @DisableApiIf(configuration.SERVER.DEMO_MODE_ENABLED) postImage( @Request() req: { gamevaultuser: GamevaultUser }, @UploadedFile( diff --git a/src/modules/progress/progress.controller.ts b/src/modules/progress/progress.controller.ts index 7e93831c..457a258a 100644 --- a/src/modules/progress/progress.controller.ts +++ b/src/modules/progress/progress.controller.ts @@ -24,6 +24,8 @@ import { Role } from "../users/models/role.enum"; import { GamevaultUser } from "../users/gamevault-user.entity"; import { UpdateProgressDto } from "./models/update-progress.dto"; import { UserIdGameIdDto } from "./models/user-id-game-id.dto"; +import { DisableApiIf } from "../../decorators/disable-api-if.decorator"; +import configuration from "../../configuration"; @Controller("progresses") @ApiTags("progress") @@ -79,6 +81,7 @@ export class ProgressController { }) @ApiOkResponse({ type: () => Progress, isArray: true }) @MinimumRole(Role.USER) + @DisableApiIf(configuration.SERVER.DEMO_MODE_ENABLED) async deleteProgressByProgressId( @Param() params: IdDto, @Request() req: { gamevaultuser: GamevaultUser }, diff --git a/src/modules/providers/rawg/rawg.service.ts b/src/modules/providers/rawg/rawg.service.ts index 747d755d..f98c9637 100644 --- a/src/modules/providers/rawg/rawg.service.ts +++ b/src/modules/providers/rawg/rawg.service.ts @@ -206,9 +206,9 @@ export class RawgService { // Calculate the probability of matching for each game searchResults.forEach((searchResult) => { - const cleanedGameTitle = title.toLowerCase().replace(/[^\w\s]/g, ""); + const cleanedGameTitle = title?.toLowerCase().replace(/[^\w\s]/g, ""); const cleanedSearchResultTitle = searchResult.name - .toLowerCase() + ?.toLowerCase() .replace(/[^\w\s]/g, ""); // Calculate string similarity between the title and game name diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 2979e41f..412a447a 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -24,14 +24,9 @@ import { UsersService } from "./users.service"; import { UpdateUserDto } from "./models/update-user.dto"; import { MinimumRole } from "../../decorators/minimum-role.decorator"; import { Role } from "./models/role.enum"; -import { Public } from "../../decorators/public.decorator"; import { SocketSecretService } from "./socket-secret.service"; -import { noop } from "rxjs"; - -const ConditionalRegistrationDecorator = configuration.SERVER - .REGISTRATION_DISABLED - ? noop - : Public(); +import { ConditionalRegistration } from "../../decorators/conditional-registration.decorator"; +import { DisableApiIf } from "../../decorators/disable-api-if.decorator"; @ApiBasicAuth() @ApiTags("user") @@ -42,24 +37,33 @@ export class UsersController { private socketSecretService: SocketSecretService, ) {} - /** Get an overview of all activated and non-deleted users. */ @Get() @ApiOperation({ - summary: "get an overview of all activated and non-deleted users", + summary: + "get an overview of all users. admins can see hidden users using this endpoint aswell.", operationId: "getUsers", }) @ApiOkResponse({ type: () => GamevaultUser, isArray: true }) @MinimumRole(Role.GUEST) - async getUsers(): Promise { - return await this.usersService.getAll(); + async getUsers( + @Request() req: { gamevaultuser: GamevaultUser }, + ): Promise { + const includeHidden = req.gamevaultuser.role >= Role.ADMIN; + return await this.usersService.getAll(includeHidden); } - /** Get an overview of all users. */ + /** + * Get an overview of all users.\ + * TODO: REMOVE with v12 + * + * @deprecated Use `getUsers` instead. + */ @Get("all") @MinimumRole(Role.ADMIN) @ApiOperation({ summary: "get an overview of all users", operationId: "getUsersAdmin", + deprecated: true, }) @ApiOkResponse({ type: () => GamevaultUser, isArray: true }) async getUsersAdmin(): Promise { @@ -95,6 +99,7 @@ export class UsersController { }) @MinimumRole(Role.USER) @ApiOkResponse({ type: () => GamevaultUser }) + @DisableApiIf(configuration.SERVER.DEMO_MODE_ENABLED) async putUserMe( @Body() dto: UpdateUserDto, @Request() request: { gamevaultuser: GamevaultUser }, @@ -113,6 +118,7 @@ export class UsersController { }) @ApiOkResponse({ type: () => GamevaultUser }) @MinimumRole(Role.USER) + @DisableApiIf(configuration.SERVER.DEMO_MODE_ENABLED) async deleteUserMe(@Request() request): Promise { const user = await this.usersService.findByUsernameOrFail( request.gamevaultuser.username, @@ -184,7 +190,7 @@ export class UsersController { }) @ApiOkResponse({ type: () => GamevaultUser }) @ApiBody({ type: () => RegisterUserDto }) - @ConditionalRegistrationDecorator + @ConditionalRegistration async postUserRegister( @Body() dto: RegisterUserDto, @Request() req: { gamevaultuser: GamevaultUser }, diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index db3888f8..fe1856d1 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -142,8 +142,8 @@ export class UsersService implements OnApplicationBootstrap { order: { id: "ASC" }, withDeleted: includeHidden, where: includeHidden - ? { username: Not(ILike("gvbot_%")) } - : { activated: true }, + ? undefined + : { activated: true, username: Not(ILike("gvbot_%")) }, }; return await this.userRepository.find(query); @@ -266,7 +266,7 @@ export class UsersService implements OnApplicationBootstrap { dto: UpdateUserDto, user: GamevaultUser, ): Promise { - if (dto.username.toLowerCase() !== user.username.toLowerCase()) { + if (dto.username?.toLowerCase() !== user.username?.toLowerCase()) { await this.throwIfAlreadyExists(dto.username, undefined); } user.username = dto.username; @@ -276,7 +276,7 @@ export class UsersService implements OnApplicationBootstrap { dto: UpdateUserDto, user: GamevaultUser, ): Promise { - if (dto.email.toLowerCase() !== user.email.toLowerCase()) { + if (dto.email?.toLowerCase() !== user.email?.toLowerCase()) { await this.throwIfAlreadyExists(undefined, dto.email); } user.email = dto.email; @@ -318,7 +318,7 @@ export class UsersService implements OnApplicationBootstrap { if (user.role === Role.ADMIN) { return true; } - if (user.username.toLowerCase() !== username.toLowerCase()) { + if (user.username?.toLowerCase() !== username?.toLowerCase()) { throw new ForbiddenException( { requestedId: userId, @@ -358,7 +358,7 @@ export class UsersService implements OnApplicationBootstrap { if (existingUser) { const duplicateField = - existingUser.username.toLowerCase() === username?.toLowerCase() + existingUser.username?.toLowerCase() === username?.toLowerCase() ? "username" : "email"; throw new ForbiddenException(