Spaces:
Runtime error
✨ feat(admin): introduce super admin models and authentication
Browse files- 【db】add `SuperAdmin`, `SuperAdminSession`, `SuperAdminCredential` models
- 【core】create dedicated core services for new super admin entities
- 【auth】implement a separate authentication module for super admins
- 【auth】add jwt strategy, guard, and auth endpoints for super admin
- 【shared】introduce `GetSuperAdmin` decorator and session types
♻️ refactor(auth): restructure modules and user authentication
- 【app】replace generic `AuthModule` with `UserModule` and `SuperAdminModule`
- 【auth】relocate user auth logic into a dedicated `modules/user/auth`
- 【db】move password from `User` model to `UserCredential` for separation
- 【repo】enhance base repository with `upsert` and auto soft-delete filter
✨ feat(query): implement advanced multi-criteria search
- 【query】add `multi_search` capability to base query service
- 【query】support `contains` and `equals` search types
- 【query】implement dynamic relationship detection for accurate filtering
🔧 chore(project): update dependencies and improve tooling
- 【build】update `p-limit` dependency and adapt for esm compatibility
- 【chore】add vscode settings for typescript sdk path
- 【docs】enhance swagger with dynamic branch titles and custom styling
- .vscode/settings.json +3 -0
- package-lock.json +126 -11
- package.json +1 -0
- prisma/schema.prisma +62 -1
- src/app.module.ts +5 -10
- src/core/base-query-core/base-query-core.service.ts +185 -8
- src/core/base-query-core/dto/base-query-core.dto.ts +19 -0
- src/core/super-admin-core/dto/super-admin-core.dto.ts +10 -0
- src/core/super-admin-core/super-admin-core.module.ts +9 -0
- src/core/super-admin-core/super-admin-core.service.ts +29 -0
- src/core/super-admin-credential-core/dto/super-admin-credential-core.dto.ts +10 -0
- src/core/super-admin-credential-core/super-admin-credential-core.module.ts +9 -0
- src/core/super-admin-credential-core/super-admin-credential-core.service.ts +29 -0
- src/core/super-admin-session-core/dto/super-admin-session-core.dto.ts +10 -0
- src/core/super-admin-session-core/super-admin-session-core.module.ts +9 -0
- src/core/super-admin-session-core/super-admin-session-core.service.ts +29 -0
- src/core/user-core/user-core.service.ts +2 -1
- src/core/user-credential-core/user-credential-core.service.ts +2 -1
- src/core/user-session-core/user-session-core.service.ts +2 -1
- src/modules/auth/auth.controller.ts +0 -52
- src/modules/auth/strategies/jwt.strategy.ts +0 -28
- src/modules/super-admin/auth/auth.controller.ts +55 -0
- src/modules/super-admin/auth/auth.module.ts +29 -0
- src/modules/super-admin/auth/auth.service.ts +310 -0
- src/modules/super-admin/auth/dto/login.dto.ts +15 -0
- src/modules/{auth → super-admin/auth}/dto/refresh-token.dto.ts +3 -6
- src/modules/{auth/dto/login.dto.ts → super-admin/auth/dto/register.dto.ts} +21 -14
- src/modules/super-admin/auth/guards/super-admin-auth.guard.ts +27 -0
- src/modules/super-admin/auth/strategies/super-admin-jwt.strategy.ts +59 -0
- src/modules/super-admin/super-admin.module.ts +8 -0
- src/modules/user/auth/auth.controller.ts +52 -0
- src/modules/{auth → user/auth}/auth.module.ts +13 -13
- src/modules/{auth → user/auth}/auth.service.ts +162 -106
- src/modules/user/auth/dto/login.dto.ts +15 -0
- src/modules/user/auth/dto/refresh-token.dto.ts +9 -0
- src/modules/{auth → user/auth}/dto/register.dto.ts +27 -20
- src/modules/{auth/guards/jwt-auth.guard.ts → user/auth/guards/user-auth.guard.ts} +7 -4
- src/modules/user/auth/strategies/user-jwt.strategy.ts +56 -0
- src/modules/user/user.module.ts +8 -0
- src/shared/decorators/get-session.decorator.ts +8 -0
- src/shared/decorators/get-super-admin.decorator.ts +9 -0
- src/shared/keys/auth.keys.ts +9 -5
- src/shared/keys/super-admin-credential.keys.ts +4 -0
- src/shared/keys/super-admin-session.keys.ts +4 -0
- src/shared/keys/super-admin.keys.ts +4 -0
- src/shared/libs/include-filter.helper.ts +62 -0
- src/shared/libs/prisma-base.repository.ts +85 -0
- src/shared/modules/prisma/safe-prisma-call.ts +14 -7
- src/shared/types/super-admin-session.type.ts +6 -0
- src/swagger-setup.ts +26 -22
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"typescript.tsdk": "node_modules\\typescript\\lib"
|
| 3 |
+
}
|
|
@@ -28,6 +28,7 @@
|
|
| 28 |
"geoip-lite": "^1.4.10",
|
| 29 |
"helmet": "^8.1.0",
|
| 30 |
"luxon": "^3.7.2",
|
|
|
|
| 31 |
"passport": "^0.7.0",
|
| 32 |
"passport-jwt": "^4.0.1",
|
| 33 |
"passport-local": "^1.0.0",
|
|
@@ -7386,6 +7387,35 @@
|
|
| 7386 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 7387 |
}
|
| 7388 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7389 |
"node_modules/jest-circus": {
|
| 7390 |
"version": "30.2.0",
|
| 7391 |
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz",
|
|
@@ -7418,6 +7448,35 @@
|
|
| 7418 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 7419 |
}
|
| 7420 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7421 |
"node_modules/jest-cli": {
|
| 7422 |
"version": "30.2.0",
|
| 7423 |
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz",
|
|
@@ -7826,6 +7885,22 @@
|
|
| 7826 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 7827 |
}
|
| 7828 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7829 |
"node_modules/jest-runner/node_modules/source-map": {
|
| 7830 |
"version": "0.6.1",
|
| 7831 |
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
@@ -7847,6 +7922,19 @@
|
|
| 7847 |
"source-map": "^0.6.0"
|
| 7848 |
}
|
| 7849 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7850 |
"node_modules/jest-runtime": {
|
| 7851 |
"version": "30.2.0",
|
| 7852 |
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz",
|
|
@@ -9137,16 +9225,15 @@
|
|
| 9137 |
}
|
| 9138 |
},
|
| 9139 |
"node_modules/p-limit": {
|
| 9140 |
-
"version": "
|
| 9141 |
-
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-
|
| 9142 |
-
"integrity": "sha512-
|
| 9143 |
-
"dev": true,
|
| 9144 |
"license": "MIT",
|
| 9145 |
"dependencies": {
|
| 9146 |
-
"yocto-queue": "^
|
| 9147 |
},
|
| 9148 |
"engines": {
|
| 9149 |
-
"node": ">=
|
| 9150 |
},
|
| 9151 |
"funding": {
|
| 9152 |
"url": "https://github.com/sponsors/sindresorhus"
|
|
@@ -9168,6 +9255,35 @@
|
|
| 9168 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 9169 |
}
|
| 9170 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9171 |
"node_modules/p-try": {
|
| 9172 |
"version": "2.2.0",
|
| 9173 |
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
|
@@ -11979,13 +12095,12 @@
|
|
| 11979 |
}
|
| 11980 |
},
|
| 11981 |
"node_modules/yocto-queue": {
|
| 11982 |
-
"version": "
|
| 11983 |
-
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-
|
| 11984 |
-
"integrity": "sha512-
|
| 11985 |
-
"dev": true,
|
| 11986 |
"license": "MIT",
|
| 11987 |
"engines": {
|
| 11988 |
-
"node": ">=
|
| 11989 |
},
|
| 11990 |
"funding": {
|
| 11991 |
"url": "https://github.com/sponsors/sindresorhus"
|
|
|
|
| 28 |
"geoip-lite": "^1.4.10",
|
| 29 |
"helmet": "^8.1.0",
|
| 30 |
"luxon": "^3.7.2",
|
| 31 |
+
"p-limit": "^7.2.0",
|
| 32 |
"passport": "^0.7.0",
|
| 33 |
"passport-jwt": "^4.0.1",
|
| 34 |
"passport-local": "^1.0.0",
|
|
|
|
| 7387 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 7388 |
}
|
| 7389 |
},
|
| 7390 |
+
"node_modules/jest-changed-files/node_modules/p-limit": {
|
| 7391 |
+
"version": "3.1.0",
|
| 7392 |
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
| 7393 |
+
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
| 7394 |
+
"dev": true,
|
| 7395 |
+
"license": "MIT",
|
| 7396 |
+
"dependencies": {
|
| 7397 |
+
"yocto-queue": "^0.1.0"
|
| 7398 |
+
},
|
| 7399 |
+
"engines": {
|
| 7400 |
+
"node": ">=10"
|
| 7401 |
+
},
|
| 7402 |
+
"funding": {
|
| 7403 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 7404 |
+
}
|
| 7405 |
+
},
|
| 7406 |
+
"node_modules/jest-changed-files/node_modules/yocto-queue": {
|
| 7407 |
+
"version": "0.1.0",
|
| 7408 |
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
| 7409 |
+
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
| 7410 |
+
"dev": true,
|
| 7411 |
+
"license": "MIT",
|
| 7412 |
+
"engines": {
|
| 7413 |
+
"node": ">=10"
|
| 7414 |
+
},
|
| 7415 |
+
"funding": {
|
| 7416 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 7417 |
+
}
|
| 7418 |
+
},
|
| 7419 |
"node_modules/jest-circus": {
|
| 7420 |
"version": "30.2.0",
|
| 7421 |
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz",
|
|
|
|
| 7448 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 7449 |
}
|
| 7450 |
},
|
| 7451 |
+
"node_modules/jest-circus/node_modules/p-limit": {
|
| 7452 |
+
"version": "3.1.0",
|
| 7453 |
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
| 7454 |
+
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
| 7455 |
+
"dev": true,
|
| 7456 |
+
"license": "MIT",
|
| 7457 |
+
"dependencies": {
|
| 7458 |
+
"yocto-queue": "^0.1.0"
|
| 7459 |
+
},
|
| 7460 |
+
"engines": {
|
| 7461 |
+
"node": ">=10"
|
| 7462 |
+
},
|
| 7463 |
+
"funding": {
|
| 7464 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 7465 |
+
}
|
| 7466 |
+
},
|
| 7467 |
+
"node_modules/jest-circus/node_modules/yocto-queue": {
|
| 7468 |
+
"version": "0.1.0",
|
| 7469 |
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
| 7470 |
+
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
| 7471 |
+
"dev": true,
|
| 7472 |
+
"license": "MIT",
|
| 7473 |
+
"engines": {
|
| 7474 |
+
"node": ">=10"
|
| 7475 |
+
},
|
| 7476 |
+
"funding": {
|
| 7477 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 7478 |
+
}
|
| 7479 |
+
},
|
| 7480 |
"node_modules/jest-cli": {
|
| 7481 |
"version": "30.2.0",
|
| 7482 |
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz",
|
|
|
|
| 7885 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 7886 |
}
|
| 7887 |
},
|
| 7888 |
+
"node_modules/jest-runner/node_modules/p-limit": {
|
| 7889 |
+
"version": "3.1.0",
|
| 7890 |
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
| 7891 |
+
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
| 7892 |
+
"dev": true,
|
| 7893 |
+
"license": "MIT",
|
| 7894 |
+
"dependencies": {
|
| 7895 |
+
"yocto-queue": "^0.1.0"
|
| 7896 |
+
},
|
| 7897 |
+
"engines": {
|
| 7898 |
+
"node": ">=10"
|
| 7899 |
+
},
|
| 7900 |
+
"funding": {
|
| 7901 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 7902 |
+
}
|
| 7903 |
+
},
|
| 7904 |
"node_modules/jest-runner/node_modules/source-map": {
|
| 7905 |
"version": "0.6.1",
|
| 7906 |
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
|
|
| 7922 |
"source-map": "^0.6.0"
|
| 7923 |
}
|
| 7924 |
},
|
| 7925 |
+
"node_modules/jest-runner/node_modules/yocto-queue": {
|
| 7926 |
+
"version": "0.1.0",
|
| 7927 |
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
| 7928 |
+
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
| 7929 |
+
"dev": true,
|
| 7930 |
+
"license": "MIT",
|
| 7931 |
+
"engines": {
|
| 7932 |
+
"node": ">=10"
|
| 7933 |
+
},
|
| 7934 |
+
"funding": {
|
| 7935 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 7936 |
+
}
|
| 7937 |
+
},
|
| 7938 |
"node_modules/jest-runtime": {
|
| 7939 |
"version": "30.2.0",
|
| 7940 |
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz",
|
|
|
|
| 9225 |
}
|
| 9226 |
},
|
| 9227 |
"node_modules/p-limit": {
|
| 9228 |
+
"version": "7.2.0",
|
| 9229 |
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz",
|
| 9230 |
+
"integrity": "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==",
|
|
|
|
| 9231 |
"license": "MIT",
|
| 9232 |
"dependencies": {
|
| 9233 |
+
"yocto-queue": "^1.2.1"
|
| 9234 |
},
|
| 9235 |
"engines": {
|
| 9236 |
+
"node": ">=20"
|
| 9237 |
},
|
| 9238 |
"funding": {
|
| 9239 |
"url": "https://github.com/sponsors/sindresorhus"
|
|
|
|
| 9255 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 9256 |
}
|
| 9257 |
},
|
| 9258 |
+
"node_modules/p-locate/node_modules/p-limit": {
|
| 9259 |
+
"version": "3.1.0",
|
| 9260 |
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
| 9261 |
+
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
| 9262 |
+
"dev": true,
|
| 9263 |
+
"license": "MIT",
|
| 9264 |
+
"dependencies": {
|
| 9265 |
+
"yocto-queue": "^0.1.0"
|
| 9266 |
+
},
|
| 9267 |
+
"engines": {
|
| 9268 |
+
"node": ">=10"
|
| 9269 |
+
},
|
| 9270 |
+
"funding": {
|
| 9271 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 9272 |
+
}
|
| 9273 |
+
},
|
| 9274 |
+
"node_modules/p-locate/node_modules/yocto-queue": {
|
| 9275 |
+
"version": "0.1.0",
|
| 9276 |
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
| 9277 |
+
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
| 9278 |
+
"dev": true,
|
| 9279 |
+
"license": "MIT",
|
| 9280 |
+
"engines": {
|
| 9281 |
+
"node": ">=10"
|
| 9282 |
+
},
|
| 9283 |
+
"funding": {
|
| 9284 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 9285 |
+
}
|
| 9286 |
+
},
|
| 9287 |
"node_modules/p-try": {
|
| 9288 |
"version": "2.2.0",
|
| 9289 |
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
|
|
|
| 12095 |
}
|
| 12096 |
},
|
| 12097 |
"node_modules/yocto-queue": {
|
| 12098 |
+
"version": "1.2.2",
|
| 12099 |
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
| 12100 |
+
"integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
|
|
|
|
| 12101 |
"license": "MIT",
|
| 12102 |
"engines": {
|
| 12103 |
+
"node": ">=12.20"
|
| 12104 |
},
|
| 12105 |
"funding": {
|
| 12106 |
"url": "https://github.com/sponsors/sindresorhus"
|
|
@@ -49,6 +49,7 @@
|
|
| 49 |
"geoip-lite": "^1.4.10",
|
| 50 |
"helmet": "^8.1.0",
|
| 51 |
"luxon": "^3.7.2",
|
|
|
|
| 52 |
"passport": "^0.7.0",
|
| 53 |
"passport-jwt": "^4.0.1",
|
| 54 |
"passport-local": "^1.0.0",
|
|
|
|
| 49 |
"geoip-lite": "^1.4.10",
|
| 50 |
"helmet": "^8.1.0",
|
| 51 |
"luxon": "^3.7.2",
|
| 52 |
+
"p-limit": "^7.2.0",
|
| 53 |
"passport": "^0.7.0",
|
| 54 |
"passport-jwt": "^4.0.1",
|
| 55 |
"passport-local": "^1.0.0",
|
|
@@ -18,7 +18,6 @@ model User {
|
|
| 18 |
email String? @unique
|
| 19 |
phone String?
|
| 20 |
code String? // country code
|
| 21 |
-
password String?
|
| 22 |
firstName String?
|
| 23 |
lastName String?
|
| 24 |
status USER_STATUS @default(INVITED)
|
|
@@ -70,3 +69,65 @@ model UserCredential {
|
|
| 70 |
|
| 71 |
@@map("user_credentials")
|
| 72 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
email String? @unique
|
| 19 |
phone String?
|
| 20 |
code String? // country code
|
|
|
|
| 21 |
firstName String?
|
| 22 |
lastName String?
|
| 23 |
status USER_STATUS @default(INVITED)
|
|
|
|
| 69 |
|
| 70 |
@@map("user_credentials")
|
| 71 |
}
|
| 72 |
+
|
| 73 |
+
enum SUPER_ADMIN_SESSION_STATUS {
|
| 74 |
+
CURRENT
|
| 75 |
+
EXPIRED
|
| 76 |
+
REVOKED
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
model SuperAdmin {
|
| 80 |
+
id String @id @default(cuid())
|
| 81 |
+
email String @unique
|
| 82 |
+
name String
|
| 83 |
+
profileImage String?
|
| 84 |
+
createdAt DateTime @default(now())
|
| 85 |
+
updatedAt DateTime @updatedAt
|
| 86 |
+
isDeleted Boolean @default(false)
|
| 87 |
+
|
| 88 |
+
// Relations
|
| 89 |
+
sessions SuperAdminSession[]
|
| 90 |
+
credentials SuperAdminCredential[]
|
| 91 |
+
|
| 92 |
+
@@map("super_admins")
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
model SuperAdminSession {
|
| 96 |
+
id String @id @default(cuid())
|
| 97 |
+
superAdminId String
|
| 98 |
+
accessToken String @db.Text
|
| 99 |
+
refreshToken String @db.Text
|
| 100 |
+
ipAddress String?
|
| 101 |
+
userAgent String? @db.Text
|
| 102 |
+
geoIpCountry String?
|
| 103 |
+
city String?
|
| 104 |
+
state String?
|
| 105 |
+
latitude Float?
|
| 106 |
+
longitude Float?
|
| 107 |
+
isFromAdmin Boolean @default(false)
|
| 108 |
+
status SUPER_ADMIN_SESSION_STATUS @default(CURRENT)
|
| 109 |
+
loginAt DateTime?
|
| 110 |
+
expiredAt DateTime?
|
| 111 |
+
createdAt DateTime @default(now())
|
| 112 |
+
updatedAt DateTime @updatedAt
|
| 113 |
+
isDeleted Boolean @default(false)
|
| 114 |
+
|
| 115 |
+
// Relations
|
| 116 |
+
superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id])
|
| 117 |
+
|
| 118 |
+
@@map("super_admin_sessions")
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
model SuperAdminCredential {
|
| 122 |
+
id String @id @default(cuid())
|
| 123 |
+
superAdminId String
|
| 124 |
+
password String
|
| 125 |
+
createdAt DateTime @default(now())
|
| 126 |
+
updatedAt DateTime @updatedAt
|
| 127 |
+
isDeleted Boolean @default(false)
|
| 128 |
+
|
| 129 |
+
// Relations
|
| 130 |
+
superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id])
|
| 131 |
+
|
| 132 |
+
@@map("super_admin_credentials")
|
| 133 |
+
}
|
|
@@ -2,12 +2,11 @@ import { Module } from '@nestjs/common';
|
|
| 2 |
import { ConfigModule } from '@nestjs/config';
|
| 3 |
import { APP_GUARD } from '@nestjs/core';
|
| 4 |
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
| 5 |
-
import {
|
|
|
|
| 6 |
import { PrismaService } from './shared/modules/prisma/prisma.service';
|
| 7 |
import { CommonModule } from './shared/modules/common/common.module';
|
| 8 |
import { BaseQueryCoreModule } from './core/base-query-core/base-query-core.module';
|
| 9 |
-
import { UserCoreModule } from './core/user-core/user-core.module';
|
| 10 |
-
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
|
| 11 |
|
| 12 |
@Module({
|
| 13 |
imports: [
|
|
@@ -18,20 +17,16 @@ import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
|
|
| 18 |
}),
|
| 19 |
ThrottlerModule.forRoot([
|
| 20 |
{
|
| 21 |
-
limit:
|
| 22 |
ttl: 60000, // 60 seconds in milliseconds
|
| 23 |
},
|
| 24 |
]),
|
| 25 |
CommonModule,
|
| 26 |
BaseQueryCoreModule,
|
| 27 |
-
|
| 28 |
-
|
| 29 |
],
|
| 30 |
providers: [
|
| 31 |
-
{
|
| 32 |
-
provide: APP_GUARD,
|
| 33 |
-
useClass: JwtAuthGuard,
|
| 34 |
-
},
|
| 35 |
{
|
| 36 |
provide: APP_GUARD,
|
| 37 |
useClass: ThrottlerGuard,
|
|
|
|
| 2 |
import { ConfigModule } from '@nestjs/config';
|
| 3 |
import { APP_GUARD } from '@nestjs/core';
|
| 4 |
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
| 5 |
+
import { UserModule } from './modules/user/user.module';
|
| 6 |
+
import { SuperAdminModule } from './modules/super-admin/super-admin.module';
|
| 7 |
import { PrismaService } from './shared/modules/prisma/prisma.service';
|
| 8 |
import { CommonModule } from './shared/modules/common/common.module';
|
| 9 |
import { BaseQueryCoreModule } from './core/base-query-core/base-query-core.module';
|
|
|
|
|
|
|
| 10 |
|
| 11 |
@Module({
|
| 12 |
imports: [
|
|
|
|
| 17 |
}),
|
| 18 |
ThrottlerModule.forRoot([
|
| 19 |
{
|
| 20 |
+
limit: 1000,
|
| 21 |
ttl: 60000, // 60 seconds in milliseconds
|
| 22 |
},
|
| 23 |
]),
|
| 24 |
CommonModule,
|
| 25 |
BaseQueryCoreModule,
|
| 26 |
+
UserModule,
|
| 27 |
+
SuperAdminModule,
|
| 28 |
],
|
| 29 |
providers: [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
{
|
| 31 |
provide: APP_GUARD,
|
| 32 |
useClass: ThrottlerGuard,
|
|
@@ -1,9 +1,13 @@
|
|
| 1 |
import { Injectable } from '@nestjs/common';
|
| 2 |
import { BaseQueryCoreDto } from './dto/base-query-core.dto';
|
| 3 |
import * as _ from 'lodash';
|
|
|
|
| 4 |
|
| 5 |
@Injectable()
|
| 6 |
export class BaseQueryCoreService {
|
|
|
|
|
|
|
|
|
|
| 7 |
generatePrismaQuery(query: BaseQueryCoreDto = {}) {
|
| 8 |
const tempQuery: any = { ...query };
|
| 9 |
if (typeof tempQuery?.orderBy === 'string') {
|
|
@@ -15,6 +19,10 @@ export class BaseQueryCoreService {
|
|
| 15 |
if (typeof tempQuery?.search_column === 'string') {
|
| 16 |
tempQuery.search_column = [tempQuery?.search_column];
|
| 17 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
if (tempQuery?.orderBy?.length > 0) {
|
| 20 |
const orderBy = {};
|
|
@@ -47,19 +55,32 @@ export class BaseQueryCoreService {
|
|
| 47 |
}
|
| 48 |
|
| 49 |
if (tempQuery?.search_column?.length > 0 && tempQuery?.search?.length > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
const formattedSearchQry = this.formatSearchQueryArray(
|
| 51 |
tempQuery.search_column.sort(),
|
| 52 |
-
|
| 53 |
-
contains: tempQuery?.search,
|
| 54 |
-
// mode: 'insensitive'
|
| 55 |
-
},
|
| 56 |
);
|
| 57 |
|
| 58 |
tempQuery['where'] = formattedSearchQry;
|
| 59 |
}
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
delete tempQuery?.search_column;
|
| 62 |
delete tempQuery?.search;
|
|
|
|
|
|
|
| 63 |
|
| 64 |
return tempQuery;
|
| 65 |
}
|
|
@@ -80,13 +101,73 @@ export class BaseQueryCoreService {
|
|
| 80 |
}
|
| 81 |
|
| 82 |
formatSearchQueryArray(queryArr: any, baseVal: any) {
|
| 83 |
-
const
|
| 84 |
|
| 85 |
-
queryArr.
|
| 86 |
-
|
| 87 |
});
|
| 88 |
|
| 89 |
-
return { OR:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
formatArray(qry: any, baseVal: any) {
|
|
@@ -113,6 +194,102 @@ export class BaseQueryCoreService {
|
|
| 113 |
return returnObj;
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
getOrderByArray(fieldArr: any, sort) {
|
| 117 |
fieldArr.shift();
|
| 118 |
|
|
|
|
| 1 |
import { Injectable } from '@nestjs/common';
|
| 2 |
import { BaseQueryCoreDto } from './dto/base-query-core.dto';
|
| 3 |
import * as _ from 'lodash';
|
| 4 |
+
import { Prisma } from '@prisma/client';
|
| 5 |
|
| 6 |
@Injectable()
|
| 7 |
export class BaseQueryCoreService {
|
| 8 |
+
// Cache for storing relationship metadata to avoid repeated lookups
|
| 9 |
+
private relationshipCache: Map<string, 'one-to-many' | 'one-to-one'> =
|
| 10 |
+
new Map();
|
| 11 |
generatePrismaQuery(query: BaseQueryCoreDto = {}) {
|
| 12 |
const tempQuery: any = { ...query };
|
| 13 |
if (typeof tempQuery?.orderBy === 'string') {
|
|
|
|
| 19 |
if (typeof tempQuery?.search_column === 'string') {
|
| 20 |
tempQuery.search_column = [tempQuery?.search_column];
|
| 21 |
}
|
| 22 |
+
// Add this: Convert multi_search to array if it's a string
|
| 23 |
+
if (typeof tempQuery?.multi_search === 'string') {
|
| 24 |
+
tempQuery.multi_search = [tempQuery?.multi_search];
|
| 25 |
+
}
|
| 26 |
|
| 27 |
if (tempQuery?.orderBy?.length > 0) {
|
| 28 |
const orderBy = {};
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
if (tempQuery?.search_column?.length > 0 && tempQuery?.search?.length > 0) {
|
| 58 |
+
const searchType = tempQuery?.search_type || 'contains';
|
| 59 |
+
const searchCondition = this.createSearchCondition(
|
| 60 |
+
tempQuery.search,
|
| 61 |
+
searchType,
|
| 62 |
+
);
|
| 63 |
+
|
| 64 |
const formattedSearchQry = this.formatSearchQueryArray(
|
| 65 |
tempQuery.search_column.sort(),
|
| 66 |
+
searchCondition,
|
|
|
|
|
|
|
|
|
|
| 67 |
);
|
| 68 |
|
| 69 |
tempQuery['where'] = formattedSearchQry;
|
| 70 |
}
|
| 71 |
|
| 72 |
+
// Handle multi-criteria search
|
| 73 |
+
if (tempQuery?.multi_search?.length > 0) {
|
| 74 |
+
const multiSearchConditions = this.formatMultiSearchQueryArray(
|
| 75 |
+
tempQuery.multi_search,
|
| 76 |
+
);
|
| 77 |
+
tempQuery['where'] = { AND: multiSearchConditions };
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
delete tempQuery?.search_column;
|
| 81 |
delete tempQuery?.search;
|
| 82 |
+
delete tempQuery?.search_type;
|
| 83 |
+
delete tempQuery?.multi_search;
|
| 84 |
|
| 85 |
return tempQuery;
|
| 86 |
}
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
formatSearchQueryArray(queryArr: any, baseVal: any) {
|
| 104 |
+
const searchConditions: any[] = [];
|
| 105 |
|
| 106 |
+
queryArr.forEach((qry: any) => {
|
| 107 |
+
searchConditions.push(this.formatArray(qry, baseVal));
|
| 108 |
});
|
| 109 |
|
| 110 |
+
return { OR: searchConditions };
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
formatMultiSearchQueryArray(multiSearchArr: string[]) {
|
| 114 |
+
// Group conditions by column
|
| 115 |
+
const columnGroups: { [key: string]: any[] } = {};
|
| 116 |
+
|
| 117 |
+
multiSearchArr.forEach((searchString: string) => {
|
| 118 |
+
const [searchTerm, column, searchType] = searchString.split('|');
|
| 119 |
+
|
| 120 |
+
if (searchTerm && column) {
|
| 121 |
+
const condition = this.createSearchCondition(
|
| 122 |
+
searchTerm.trim(),
|
| 123 |
+
(searchType || 'contains').trim() as 'contains' | 'equals',
|
| 124 |
+
);
|
| 125 |
+
|
| 126 |
+
const formattedCondition = this.formatMultiSearchArray(
|
| 127 |
+
column,
|
| 128 |
+
condition,
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
// Group by column name
|
| 132 |
+
if (!columnGroups[column]) {
|
| 133 |
+
columnGroups[column] = [];
|
| 134 |
+
}
|
| 135 |
+
columnGroups[column].push(formattedCondition);
|
| 136 |
+
}
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
// Convert grouped conditions to final query structure
|
| 140 |
+
const finalConditions: any[] = [];
|
| 141 |
+
|
| 142 |
+
Object.keys(columnGroups).forEach((column) => {
|
| 143 |
+
const conditions = columnGroups[column];
|
| 144 |
+
|
| 145 |
+
if (conditions.length === 1) {
|
| 146 |
+
// Single condition for this column
|
| 147 |
+
finalConditions.push(conditions[0]);
|
| 148 |
+
} else {
|
| 149 |
+
// Multiple conditions for same column - use OR
|
| 150 |
+
finalConditions.push({ OR: conditions });
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
return finalConditions;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
createSearchCondition(
|
| 158 |
+
searchValue: string,
|
| 159 |
+
searchType: 'contains' | 'equals',
|
| 160 |
+
) {
|
| 161 |
+
const condition: any = {};
|
| 162 |
+
|
| 163 |
+
if (searchType === 'contains') {
|
| 164 |
+
condition.contains = searchValue;
|
| 165 |
+
condition.mode = 'insensitive';
|
| 166 |
+
} else if (searchType === 'equals') {
|
| 167 |
+
condition.equals = searchValue;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
return condition;
|
| 171 |
}
|
| 172 |
|
| 173 |
formatArray(qry: any, baseVal: any) {
|
|
|
|
| 194 |
return returnObj;
|
| 195 |
}
|
| 196 |
|
| 197 |
+
/**
|
| 198 |
+
* Format array specifically for multi-search functionality
|
| 199 |
+
* This method handles nested relations and applies dynamic relationship detection
|
| 200 |
+
*/
|
| 201 |
+
formatMultiSearchArray(qry: any, baseVal: any) {
|
| 202 |
+
const returnObj = {};
|
| 203 |
+
|
| 204 |
+
const qArr = qry.split('.');
|
| 205 |
+
|
| 206 |
+
if (qArr.length > 1) {
|
| 207 |
+
const ky = qArr[0];
|
| 208 |
+
qArr.shift();
|
| 209 |
+
|
| 210 |
+
// Recursively build nested condition
|
| 211 |
+
const nestedCondition = this.formatMultiSearchArray(
|
| 212 |
+
qArr.join('.'),
|
| 213 |
+
baseVal,
|
| 214 |
+
);
|
| 215 |
+
|
| 216 |
+
if (typeof returnObj[ky] === 'undefined') {
|
| 217 |
+
// Apply dynamic relation wrapping for where conditions
|
| 218 |
+
returnObj[ky] = this.wrapRelationCondition(ky, nestedCondition);
|
| 219 |
+
} else {
|
| 220 |
+
returnObj[ky] = {
|
| 221 |
+
...returnObj[ky],
|
| 222 |
+
...this.wrapRelationCondition(ky, nestedCondition),
|
| 223 |
+
};
|
| 224 |
+
}
|
| 225 |
+
} else {
|
| 226 |
+
returnObj[qArr[0]] = baseVal;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
return returnObj;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Wrap relation conditions with proper Prisma syntax
|
| 234 |
+
* For one-to-many relations, use 'some'
|
| 235 |
+
* For one-to-one relations, use 'is'
|
| 236 |
+
* Dynamically detects relationship type from Prisma DMMF
|
| 237 |
+
*/
|
| 238 |
+
private wrapRelationCondition(relationName: string, condition: any) {
|
| 239 |
+
const relationType = this.detectRelationType(relationName);
|
| 240 |
+
|
| 241 |
+
if (relationType === 'one-to-many') {
|
| 242 |
+
return { some: condition };
|
| 243 |
+
} else {
|
| 244 |
+
return { is: condition };
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/**
|
| 249 |
+
* Dynamically detect if a relation is one-to-many or one-to-one
|
| 250 |
+
* Uses Prisma's DMMF (Data Model Meta Format) to introspect the schema
|
| 251 |
+
*/
|
| 252 |
+
private detectRelationType(
|
| 253 |
+
relationName: string,
|
| 254 |
+
): 'one-to-many' | 'one-to-one' {
|
| 255 |
+
// Check cache first
|
| 256 |
+
if (this.relationshipCache.has(relationName)) {
|
| 257 |
+
return this.relationshipCache.get(relationName)!;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
try {
|
| 261 |
+
// Access Prisma's DMMF (Data Model Meta Format) for schema introspection
|
| 262 |
+
const dmmf = Prisma.dmmf;
|
| 263 |
+
|
| 264 |
+
// Search through all models to find the relation
|
| 265 |
+
for (const model of dmmf.datamodel.models) {
|
| 266 |
+
const field = model.fields.find((f) => f.name === relationName);
|
| 267 |
+
|
| 268 |
+
if (field && field.kind === 'object') {
|
| 269 |
+
// Check if it's a list (array) which indicates one-to-many
|
| 270 |
+
const relationType = field.isList ? 'one-to-many' : 'one-to-one';
|
| 271 |
+
|
| 272 |
+
// Cache the result
|
| 273 |
+
this.relationshipCache.set(relationName, relationType);
|
| 274 |
+
|
| 275 |
+
return relationType;
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Default to one-to-one if not found (safer default for filtering)
|
| 280 |
+
// This prevents incorrect 'some' usage on non-existent relations
|
| 281 |
+
this.relationshipCache.set(relationName, 'one-to-one');
|
| 282 |
+
return 'one-to-one';
|
| 283 |
+
} catch {
|
| 284 |
+
// Fallback: If DMMF is not accessible, use one-to-one as safe default
|
| 285 |
+
console.warn(
|
| 286 |
+
`Could not detect relation type for "${relationName}". Defaulting to one-to-one.`,
|
| 287 |
+
);
|
| 288 |
+
this.relationshipCache.set(relationName, 'one-to-one');
|
| 289 |
+
return 'one-to-one';
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
getOrderByArray(fieldArr: any, sort) {
|
| 294 |
fieldArr.shift();
|
| 295 |
|
|
@@ -43,6 +43,25 @@ export class BaseQueryCoreDto {
|
|
| 43 |
@IsString()
|
| 44 |
@IsOptional()
|
| 45 |
search?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
export class CoreIncludesDto {
|
|
|
|
| 43 |
@IsString()
|
| 44 |
@IsOptional()
|
| 45 |
search?: string;
|
| 46 |
+
|
| 47 |
+
@ApiProperty({
|
| 48 |
+
required: false,
|
| 49 |
+
enum: ['contains', 'equals'],
|
| 50 |
+
default: 'contains',
|
| 51 |
+
})
|
| 52 |
+
@IsOptional()
|
| 53 |
+
@IsString()
|
| 54 |
+
search_type?: 'contains' | 'equals';
|
| 55 |
+
|
| 56 |
+
@ApiProperty({
|
| 57 |
+
required: false,
|
| 58 |
+
description:
|
| 59 |
+
'Multi-criteria search with format: searchTerm|column|searchType',
|
| 60 |
+
type: [String],
|
| 61 |
+
})
|
| 62 |
+
@IsOptional()
|
| 63 |
+
@IsString({ each: true })
|
| 64 |
+
multi_search?: string[];
|
| 65 |
}
|
| 66 |
|
| 67 |
export class CoreIncludesDto {
|
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { SuperAdmin } from '@prisma/client';
|
| 3 |
+
import { IsArray } from 'class-validator';
|
| 4 |
+
import { CorePaginateDto } from 'src/core/base-query-core/dto/base-query-core.dto';
|
| 5 |
+
|
| 6 |
+
export class SuperAdminCorePaginateDto extends CorePaginateDto {
|
| 7 |
+
@ApiProperty({ required: true })
|
| 8 |
+
@IsArray()
|
| 9 |
+
list?: SuperAdmin[];
|
| 10 |
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 3 |
+
import { SuperAdminCoreService } from './super-admin-core.service';
|
| 4 |
+
|
| 5 |
+
@Module({
|
| 6 |
+
providers: [PrismaService, SuperAdminCoreService],
|
| 7 |
+
exports: [SuperAdminCoreService, PrismaService],
|
| 8 |
+
})
|
| 9 |
+
export class SuperAdminCoreModule {}
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { SuperAdmin, Prisma } from '@prisma/client';
|
| 3 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 4 |
+
import { SuperAdminCorePaginateDto } from './dto/super-admin-core.dto';
|
| 5 |
+
import { PrismaBaseRepository } from 'src/shared/libs/prisma-base.repository';
|
| 6 |
+
import { SuperAdminMessages } from 'src/shared/keys/super-admin.keys';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class SuperAdminCoreService extends PrismaBaseRepository<
|
| 10 |
+
SuperAdmin,
|
| 11 |
+
SuperAdminCorePaginateDto,
|
| 12 |
+
Prisma.SuperAdminCreateArgs,
|
| 13 |
+
Prisma.SuperAdminUpdateArgs,
|
| 14 |
+
Prisma.SuperAdminUpdateManyArgs,
|
| 15 |
+
Prisma.SuperAdminFindUniqueArgs,
|
| 16 |
+
Prisma.SuperAdminFindFirstArgs,
|
| 17 |
+
Prisma.SuperAdminFindManyArgs,
|
| 18 |
+
Prisma.SuperAdminDeleteArgs,
|
| 19 |
+
Prisma.SuperAdminDeleteManyArgs,
|
| 20 |
+
Prisma.SuperAdminCountArgs,
|
| 21 |
+
Prisma.SuperAdminUpsertArgs
|
| 22 |
+
> {
|
| 23 |
+
constructor(private prismaService: PrismaService) {
|
| 24 |
+
super(prismaService.prisma.superAdmin, {
|
| 25 |
+
NOT_FOUND: SuperAdminMessages.NOT_FOUND,
|
| 26 |
+
DELETED: SuperAdminMessages.DELETED,
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { SuperAdminCredential } from '@prisma/client';
|
| 3 |
+
import { IsArray } from 'class-validator';
|
| 4 |
+
import { CorePaginateDto } from 'src/core/base-query-core/dto/base-query-core.dto';
|
| 5 |
+
|
| 6 |
+
export class SuperAdminCredentialCorePaginateDto extends CorePaginateDto {
|
| 7 |
+
@ApiProperty({ required: true })
|
| 8 |
+
@IsArray()
|
| 9 |
+
list?: SuperAdminCredential[];
|
| 10 |
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 3 |
+
import { SuperAdminCredentialCoreService } from './super-admin-credential-core.service';
|
| 4 |
+
|
| 5 |
+
@Module({
|
| 6 |
+
providers: [PrismaService, SuperAdminCredentialCoreService],
|
| 7 |
+
exports: [SuperAdminCredentialCoreService, PrismaService],
|
| 8 |
+
})
|
| 9 |
+
export class SuperAdminCredentialCoreModule {}
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { SuperAdminCredential, Prisma } from '@prisma/client';
|
| 3 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 4 |
+
import { SuperAdminCredentialCorePaginateDto } from './dto/super-admin-credential-core.dto';
|
| 5 |
+
import { PrismaBaseRepository } from 'src/shared/libs/prisma-base.repository';
|
| 6 |
+
import { SuperAdminCredentialMessages } from 'src/shared/keys/super-admin-credential.keys';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class SuperAdminCredentialCoreService extends PrismaBaseRepository<
|
| 10 |
+
SuperAdminCredential,
|
| 11 |
+
SuperAdminCredentialCorePaginateDto,
|
| 12 |
+
Prisma.SuperAdminCredentialCreateArgs,
|
| 13 |
+
Prisma.SuperAdminCredentialUpdateArgs,
|
| 14 |
+
Prisma.SuperAdminCredentialUpdateManyArgs,
|
| 15 |
+
Prisma.SuperAdminCredentialFindUniqueArgs,
|
| 16 |
+
Prisma.SuperAdminCredentialFindFirstArgs,
|
| 17 |
+
Prisma.SuperAdminCredentialFindManyArgs,
|
| 18 |
+
Prisma.SuperAdminCredentialDeleteArgs,
|
| 19 |
+
Prisma.SuperAdminCredentialDeleteManyArgs,
|
| 20 |
+
Prisma.SuperAdminCredentialCountArgs,
|
| 21 |
+
Prisma.SuperAdminCredentialUpsertArgs
|
| 22 |
+
> {
|
| 23 |
+
constructor(private prismaService: PrismaService) {
|
| 24 |
+
super(prismaService.prisma.superAdminCredential, {
|
| 25 |
+
NOT_FOUND: SuperAdminCredentialMessages.NOT_FOUND,
|
| 26 |
+
DELETED: SuperAdminCredentialMessages.DELETED,
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { SuperAdminSession } from '@prisma/client';
|
| 3 |
+
import { IsArray } from 'class-validator';
|
| 4 |
+
import { CorePaginateDto } from 'src/core/base-query-core/dto/base-query-core.dto';
|
| 5 |
+
|
| 6 |
+
export class SuperAdminSessionCorePaginateDto extends CorePaginateDto {
|
| 7 |
+
@ApiProperty({ required: true })
|
| 8 |
+
@IsArray()
|
| 9 |
+
list?: SuperAdminSession[];
|
| 10 |
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 3 |
+
import { SuperAdminSessionCoreService } from './super-admin-session-core.service';
|
| 4 |
+
|
| 5 |
+
@Module({
|
| 6 |
+
providers: [PrismaService, SuperAdminSessionCoreService],
|
| 7 |
+
exports: [SuperAdminSessionCoreService, PrismaService],
|
| 8 |
+
})
|
| 9 |
+
export class SuperAdminSessionCoreModule {}
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { SuperAdminSession, Prisma } from '@prisma/client';
|
| 3 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 4 |
+
import { SuperAdminSessionCorePaginateDto } from './dto/super-admin-session-core.dto';
|
| 5 |
+
import { PrismaBaseRepository } from 'src/shared/libs/prisma-base.repository';
|
| 6 |
+
import { SuperAdminSessionMessages } from 'src/shared/keys/super-admin-session.keys';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class SuperAdminSessionCoreService extends PrismaBaseRepository<
|
| 10 |
+
SuperAdminSession,
|
| 11 |
+
SuperAdminSessionCorePaginateDto,
|
| 12 |
+
Prisma.SuperAdminSessionCreateArgs,
|
| 13 |
+
Prisma.SuperAdminSessionUpdateArgs,
|
| 14 |
+
Prisma.SuperAdminSessionUpdateManyArgs,
|
| 15 |
+
Prisma.SuperAdminSessionFindUniqueArgs,
|
| 16 |
+
Prisma.SuperAdminSessionFindFirstArgs,
|
| 17 |
+
Prisma.SuperAdminSessionFindManyArgs,
|
| 18 |
+
Prisma.SuperAdminSessionDeleteArgs,
|
| 19 |
+
Prisma.SuperAdminSessionDeleteManyArgs,
|
| 20 |
+
Prisma.SuperAdminSessionCountArgs,
|
| 21 |
+
Prisma.SuperAdminSessionUpsertArgs
|
| 22 |
+
> {
|
| 23 |
+
constructor(private prismaService: PrismaService) {
|
| 24 |
+
super(prismaService.prisma.superAdminSession, {
|
| 25 |
+
NOT_FOUND: SuperAdminSessionMessages.NOT_FOUND,
|
| 26 |
+
DELETED: SuperAdminSessionMessages.DELETED,
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
}
|
|
@@ -17,7 +17,8 @@ export class UserCoreService extends PrismaBaseRepository<
|
|
| 17 |
Prisma.UserFindManyArgs,
|
| 18 |
Prisma.UserDeleteArgs,
|
| 19 |
Prisma.UserDeleteManyArgs,
|
| 20 |
-
Prisma.UserCountArgs
|
|
|
|
| 21 |
> {
|
| 22 |
constructor(private prismaService: PrismaService) {
|
| 23 |
super(prismaService.prisma.user, {
|
|
|
|
| 17 |
Prisma.UserFindManyArgs,
|
| 18 |
Prisma.UserDeleteArgs,
|
| 19 |
Prisma.UserDeleteManyArgs,
|
| 20 |
+
Prisma.UserCountArgs,
|
| 21 |
+
Prisma.UserUpsertArgs
|
| 22 |
> {
|
| 23 |
constructor(private prismaService: PrismaService) {
|
| 24 |
super(prismaService.prisma.user, {
|
|
@@ -17,7 +17,8 @@ export class UserCredentialCoreService extends PrismaBaseRepository<
|
|
| 17 |
Prisma.UserCredentialFindManyArgs,
|
| 18 |
Prisma.UserCredentialDeleteArgs,
|
| 19 |
Prisma.UserCredentialDeleteManyArgs,
|
| 20 |
-
Prisma.UserCredentialCountArgs
|
|
|
|
| 21 |
> {
|
| 22 |
constructor(private prismaService: PrismaService) {
|
| 23 |
super(prismaService.prisma.userCredential, {
|
|
|
|
| 17 |
Prisma.UserCredentialFindManyArgs,
|
| 18 |
Prisma.UserCredentialDeleteArgs,
|
| 19 |
Prisma.UserCredentialDeleteManyArgs,
|
| 20 |
+
Prisma.UserCredentialCountArgs,
|
| 21 |
+
Prisma.UserCredentialUpsertArgs
|
| 22 |
> {
|
| 23 |
constructor(private prismaService: PrismaService) {
|
| 24 |
super(prismaService.prisma.userCredential, {
|
|
@@ -17,7 +17,8 @@ export class UserSessionCoreService extends PrismaBaseRepository<
|
|
| 17 |
Prisma.UserSessionFindManyArgs,
|
| 18 |
Prisma.UserSessionDeleteArgs,
|
| 19 |
Prisma.UserSessionDeleteManyArgs,
|
| 20 |
-
Prisma.UserSessionCountArgs
|
|
|
|
| 21 |
> {
|
| 22 |
constructor(private prismaService: PrismaService) {
|
| 23 |
super(prismaService.prisma.userSession, {
|
|
|
|
| 17 |
Prisma.UserSessionFindManyArgs,
|
| 18 |
Prisma.UserSessionDeleteArgs,
|
| 19 |
Prisma.UserSessionDeleteManyArgs,
|
| 20 |
+
Prisma.UserSessionCountArgs,
|
| 21 |
+
Prisma.UserSessionUpsertArgs
|
| 22 |
> {
|
| 23 |
constructor(private prismaService: PrismaService) {
|
| 24 |
super(prismaService.prisma.userSession, {
|
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
import { Controller, Post, Body, Req, Get } from '@nestjs/common';
|
| 2 |
-
import {
|
| 3 |
-
ApiTags,
|
| 4 |
-
ApiOperation,
|
| 5 |
-
ApiResponse,
|
| 6 |
-
ApiBearerAuth,
|
| 7 |
-
} from '@nestjs/swagger';
|
| 8 |
-
import { AuthService } from './auth.service';
|
| 9 |
-
import { LoginDto } from './dto/login.dto';
|
| 10 |
-
import { RegisterDto } from './dto/register.dto';
|
| 11 |
-
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
| 12 |
-
import { Public } from 'src/shared/decorators/public.decorator';
|
| 13 |
-
import { GetUser } from 'src/shared/decorators/get-user.decorator';
|
| 14 |
-
import type { UserSessionType } from 'src/shared/types/user-session.type';
|
| 15 |
-
|
| 16 |
-
@ApiTags('Authentication')
|
| 17 |
-
@Controller('auth')
|
| 18 |
-
export class AuthController {
|
| 19 |
-
constructor(private readonly authService: AuthService) {}
|
| 20 |
-
|
| 21 |
-
@Public()
|
| 22 |
-
@Post('register')
|
| 23 |
-
@ApiOperation({ summary: 'Register a new user' })
|
| 24 |
-
@ApiResponse({ status: 201, description: 'User registered successfully' })
|
| 25 |
-
async register(@Body() registerDto: RegisterDto) {
|
| 26 |
-
return this.authService.register(registerDto);
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
@Public()
|
| 30 |
-
@Post('login')
|
| 31 |
-
@ApiOperation({ summary: 'Login user' })
|
| 32 |
-
@ApiResponse({ status: 200, description: 'Login successful' })
|
| 33 |
-
async login(@Body() loginDto: LoginDto, @Req() request: Request) {
|
| 34 |
-
return this.authService.login(loginDto, request);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
@Public()
|
| 38 |
-
@Post('refresh-token')
|
| 39 |
-
@ApiOperation({ summary: 'Refresh access token' })
|
| 40 |
-
@ApiResponse({ status: 200, description: 'Token refreshed successfully' })
|
| 41 |
-
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
| 42 |
-
return this.authService.refreshToken(refreshTokenDto);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
@ApiBearerAuth()
|
| 46 |
-
@Get('profile')
|
| 47 |
-
@ApiOperation({ summary: 'Get user profile' })
|
| 48 |
-
@ApiResponse({ status: 200, description: 'Profile retrieved successfully' })
|
| 49 |
-
async getProfile(@GetUser() userSession: UserSessionType) {
|
| 50 |
-
return { user: userSession.user };
|
| 51 |
-
}
|
| 52 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
import { Injectable } from '@nestjs/common';
|
| 2 |
-
import { PassportStrategy } from '@nestjs/passport';
|
| 3 |
-
import { ExtractJwt, Strategy } from 'passport-jwt';
|
| 4 |
-
import { AuthService } from '../auth.service';
|
| 5 |
-
import { jwtAuthConstants } from 'src/shared/keys/auth.keys';
|
| 6 |
-
import { User } from '@prisma/client';
|
| 7 |
-
|
| 8 |
-
@Injectable()
|
| 9 |
-
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
| 10 |
-
constructor(private readonly authService: AuthService) {
|
| 11 |
-
super({
|
| 12 |
-
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
| 13 |
-
ignoreExpiration: false,
|
| 14 |
-
secretOrKey: jwtAuthConstants.accessTokenSecret,
|
| 15 |
-
passReqToCallback: true,
|
| 16 |
-
});
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
async validate(
|
| 20 |
-
request: Request & { headers: { authorization: string } },
|
| 21 |
-
user: User,
|
| 22 |
-
) {
|
| 23 |
-
return this.authService.performJWTStrategy({
|
| 24 |
-
request,
|
| 25 |
-
user,
|
| 26 |
-
});
|
| 27 |
-
}
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Post, Body, Req, UseGuards } from '@nestjs/common';
|
| 2 |
+
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
| 3 |
+
import { SuperAdminAuthService } from './auth.service';
|
| 4 |
+
import { SuperAdminRegisterDto } from './dto/register.dto';
|
| 5 |
+
import { SuperAdminLoginDto } from './dto/login.dto';
|
| 6 |
+
import { SuperAdminRefreshTokenDto } from './dto/refresh-token.dto';
|
| 7 |
+
import { Public } from 'src/shared/decorators/public.decorator';
|
| 8 |
+
import { GetSuperAdmin } from 'src/shared/decorators/get-super-admin.decorator';
|
| 9 |
+
import type { SuperAdminSessionType } from 'src/shared/types/super-admin-session.type';
|
| 10 |
+
import { SuperAdminAuthGuard } from './guards/super-admin-auth.guard';
|
| 11 |
+
|
| 12 |
+
@ApiTags('Super Admin: Authentication')
|
| 13 |
+
@Controller('super-admin/auth')
|
| 14 |
+
@UseGuards(SuperAdminAuthGuard)
|
| 15 |
+
export class SuperAdminAuthController {
|
| 16 |
+
constructor(private readonly superAdminAuthService: SuperAdminAuthService) {}
|
| 17 |
+
|
| 18 |
+
@Public()
|
| 19 |
+
@Post('register')
|
| 20 |
+
@ApiOperation({ summary: 'Register a new super admin' })
|
| 21 |
+
async register(
|
| 22 |
+
@Body() registerDto: SuperAdminRegisterDto,
|
| 23 |
+
@Req() request: any,
|
| 24 |
+
) {
|
| 25 |
+
return this.superAdminAuthService.register(registerDto, request);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
@Public()
|
| 29 |
+
@Post('login')
|
| 30 |
+
@ApiOperation({ summary: 'Login super admin' })
|
| 31 |
+
async login(@Body() loginDto: SuperAdminLoginDto, @Req() request: any) {
|
| 32 |
+
return this.superAdminAuthService.login(loginDto, request);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
@ApiBearerAuth()
|
| 36 |
+
@Post('logout')
|
| 37 |
+
@ApiOperation({ summary: 'Logout super admin' })
|
| 38 |
+
async logout(@GetSuperAdmin() sessionData: SuperAdminSessionType) {
|
| 39 |
+
return this.superAdminAuthService.logout(sessionData);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
@Public()
|
| 43 |
+
@Post('refresh-token')
|
| 44 |
+
@ApiOperation({ summary: 'Refresh access token' })
|
| 45 |
+
async refreshToken(@Body() refreshTokenDto: SuperAdminRefreshTokenDto) {
|
| 46 |
+
return this.superAdminAuthService.refreshToken(refreshTokenDto);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@ApiBearerAuth()
|
| 50 |
+
@Post('profile')
|
| 51 |
+
@ApiOperation({ summary: 'Get super admin profile' })
|
| 52 |
+
async getProfile(@GetSuperAdmin() sessionData: SuperAdminSessionType) {
|
| 53 |
+
return { superAdmin: sessionData.superAdmin };
|
| 54 |
+
}
|
| 55 |
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { JwtModule } from '@nestjs/jwt';
|
| 3 |
+
import { PassportModule } from '@nestjs/passport';
|
| 4 |
+
import { SuperAdminAuthController } from './auth.controller';
|
| 5 |
+
import { SuperAdminAuthService } from './auth.service';
|
| 6 |
+
import { SuperAdminJwtStrategy } from './strategies/super-admin-jwt.strategy';
|
| 7 |
+
import { SuperAdminCoreModule } from 'src/core/super-admin-core/super-admin-core.module';
|
| 8 |
+
import { SuperAdminCredentialCoreModule } from 'src/core/super-admin-credential-core/super-admin-credential-core.module';
|
| 9 |
+
import { SuperAdminSessionCoreModule } from 'src/core/super-admin-session-core/super-admin-session-core.module';
|
| 10 |
+
import { CommonModule } from 'src/shared/modules/common/common.module';
|
| 11 |
+
import { jwtAuthConstants } from 'src/shared/keys/auth.keys';
|
| 12 |
+
|
| 13 |
+
@Module({
|
| 14 |
+
imports: [
|
| 15 |
+
PassportModule,
|
| 16 |
+
JwtModule.register({
|
| 17 |
+
secret: jwtAuthConstants.accessTokenSecret,
|
| 18 |
+
signOptions: { expiresIn: jwtAuthConstants.accessTokenExpiresIn },
|
| 19 |
+
}),
|
| 20 |
+
SuperAdminCoreModule,
|
| 21 |
+
SuperAdminCredentialCoreModule,
|
| 22 |
+
SuperAdminSessionCoreModule,
|
| 23 |
+
CommonModule,
|
| 24 |
+
],
|
| 25 |
+
controllers: [SuperAdminAuthController],
|
| 26 |
+
providers: [SuperAdminAuthService, SuperAdminJwtStrategy],
|
| 27 |
+
exports: [SuperAdminAuthService],
|
| 28 |
+
})
|
| 29 |
+
export class SuperAdminAuthModule {}
|
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Injectable,
|
| 3 |
+
UnauthorizedException,
|
| 4 |
+
ConflictException,
|
| 5 |
+
} from '@nestjs/common';
|
| 6 |
+
import { JwtService } from '@nestjs/jwt';
|
| 7 |
+
import { SuperAdmin, SUPER_ADMIN_SESSION_STATUS } from '@prisma/client';
|
| 8 |
+
import { SuperAdminCoreService } from 'src/core/super-admin-core/super-admin-core.service';
|
| 9 |
+
import { SuperAdminCredentialCoreService } from 'src/core/super-admin-credential-core/super-admin-credential-core.service';
|
| 10 |
+
import { SuperAdminSessionCoreService } from 'src/core/super-admin-session-core/super-admin-session-core.service';
|
| 11 |
+
import { CommonService } from 'src/shared/modules/common/common.service';
|
| 12 |
+
import { SuperAdminRegisterDto } from './dto/register.dto';
|
| 13 |
+
import { SuperAdminLoginDto } from './dto/login.dto';
|
| 14 |
+
import { SuperAdminRefreshTokenDto } from './dto/refresh-token.dto';
|
| 15 |
+
import { SuperAdminSessionType } from 'src/shared/types/super-admin-session.type';
|
| 16 |
+
import {
|
| 17 |
+
accessTokenSignSettings,
|
| 18 |
+
refreshTokenSignSettings,
|
| 19 |
+
refreshTokenVerifySettings,
|
| 20 |
+
TOKEN_TYPE,
|
| 21 |
+
TOKEN_USER_TYPE,
|
| 22 |
+
AuthMessages,
|
| 23 |
+
} from 'src/shared/keys/auth.keys';
|
| 24 |
+
|
| 25 |
+
@Injectable()
|
| 26 |
+
export class SuperAdminAuthService {
|
| 27 |
+
constructor(
|
| 28 |
+
private readonly jwtService: JwtService,
|
| 29 |
+
private readonly superAdminCoreService: SuperAdminCoreService,
|
| 30 |
+
private readonly superAdminCredentialCoreService: SuperAdminCredentialCoreService,
|
| 31 |
+
private readonly superAdminSessionCoreService: SuperAdminSessionCoreService,
|
| 32 |
+
private readonly commonService: CommonService,
|
| 33 |
+
) {}
|
| 34 |
+
|
| 35 |
+
async register(
|
| 36 |
+
registerDto: SuperAdminRegisterDto,
|
| 37 |
+
request: any,
|
| 38 |
+
): Promise<{
|
| 39 |
+
superAdmin: SuperAdmin;
|
| 40 |
+
accessToken: string;
|
| 41 |
+
refreshToken: string;
|
| 42 |
+
}> {
|
| 43 |
+
const { email, password, name, profileImage } = registerDto;
|
| 44 |
+
|
| 45 |
+
// Check if super admin already exists
|
| 46 |
+
const existingSuperAdmin = await this.superAdminCoreService
|
| 47 |
+
.findFirst({
|
| 48 |
+
where: { email, isDeleted: false },
|
| 49 |
+
})
|
| 50 |
+
.catch(() => null);
|
| 51 |
+
|
| 52 |
+
if (existingSuperAdmin) {
|
| 53 |
+
throw new ConflictException('Super admin with this email already exists');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Create super admin
|
| 57 |
+
const superAdmin = await this.superAdminCoreService.create({
|
| 58 |
+
data: {
|
| 59 |
+
email,
|
| 60 |
+
name,
|
| 61 |
+
profileImage,
|
| 62 |
+
},
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
// Hash and store password
|
| 66 |
+
const hashedPassword = await this.commonService.hashPassword(password);
|
| 67 |
+
await this.superAdminCredentialCoreService.create({
|
| 68 |
+
data: {
|
| 69 |
+
superAdminId: superAdmin.id,
|
| 70 |
+
password: hashedPassword,
|
| 71 |
+
},
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
// Generate tokens
|
| 75 |
+
const { accessToken, refreshToken } = await this.getNewToken(superAdmin);
|
| 76 |
+
|
| 77 |
+
// Get client info
|
| 78 |
+
const clientInfo = this.commonService.getClientInfo(request);
|
| 79 |
+
|
| 80 |
+
// Create session
|
| 81 |
+
await this.superAdminSessionCoreService.create({
|
| 82 |
+
data: {
|
| 83 |
+
superAdminId: superAdmin.id,
|
| 84 |
+
accessToken,
|
| 85 |
+
refreshToken,
|
| 86 |
+
ipAddress: clientInfo.ipAddress,
|
| 87 |
+
userAgent: clientInfo.userAgent,
|
| 88 |
+
geoIpCountry: clientInfo.geoLocation?.country || '',
|
| 89 |
+
city: clientInfo.geoLocation?.city || '',
|
| 90 |
+
state: clientInfo.geoLocation?.region || '',
|
| 91 |
+
latitude: clientInfo.geoLocation?.ll?.[0] || null,
|
| 92 |
+
longitude: clientInfo.geoLocation?.ll?.[1] || null,
|
| 93 |
+
isFromAdmin: true,
|
| 94 |
+
status: SUPER_ADMIN_SESSION_STATUS.CURRENT,
|
| 95 |
+
loginAt: new Date(),
|
| 96 |
+
},
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
return {
|
| 100 |
+
superAdmin,
|
| 101 |
+
accessToken,
|
| 102 |
+
refreshToken,
|
| 103 |
+
};
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
async login(
|
| 107 |
+
loginDto: SuperAdminLoginDto,
|
| 108 |
+
request: any,
|
| 109 |
+
): Promise<{
|
| 110 |
+
superAdmin: SuperAdmin;
|
| 111 |
+
accessToken: string;
|
| 112 |
+
refreshToken: string;
|
| 113 |
+
}> {
|
| 114 |
+
const { email, password } = loginDto;
|
| 115 |
+
|
| 116 |
+
// Find super admin
|
| 117 |
+
const superAdmin = await this.superAdminCoreService
|
| 118 |
+
.findFirst({
|
| 119 |
+
where: { email, isDeleted: false },
|
| 120 |
+
})
|
| 121 |
+
.catch(() => null);
|
| 122 |
+
|
| 123 |
+
if (!superAdmin) {
|
| 124 |
+
throw new UnauthorizedException('Invalid credentials');
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Get super admin credential
|
| 128 |
+
const credential = await this.superAdminCredentialCoreService
|
| 129 |
+
.findFirst({
|
| 130 |
+
where: { superAdminId: superAdmin.id, isDeleted: false },
|
| 131 |
+
})
|
| 132 |
+
.catch(() => null);
|
| 133 |
+
|
| 134 |
+
if (!credential) {
|
| 135 |
+
throw new UnauthorizedException('Invalid credentials');
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Verify password
|
| 139 |
+
const isPasswordValid = await this.commonService.comparePassword(
|
| 140 |
+
password,
|
| 141 |
+
credential.password,
|
| 142 |
+
);
|
| 143 |
+
|
| 144 |
+
if (!isPasswordValid) {
|
| 145 |
+
throw new UnauthorizedException('Invalid credentials');
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// Generate tokens
|
| 149 |
+
const { accessToken, refreshToken } = await this.getNewToken(superAdmin);
|
| 150 |
+
|
| 151 |
+
// Get client info
|
| 152 |
+
const clientInfo = this.commonService.getClientInfo(request);
|
| 153 |
+
|
| 154 |
+
// Create session
|
| 155 |
+
await this.superAdminSessionCoreService.create({
|
| 156 |
+
data: {
|
| 157 |
+
superAdminId: superAdmin.id,
|
| 158 |
+
accessToken,
|
| 159 |
+
refreshToken,
|
| 160 |
+
ipAddress: clientInfo.ipAddress,
|
| 161 |
+
userAgent: clientInfo.userAgent,
|
| 162 |
+
geoIpCountry: clientInfo.geoLocation?.country || '',
|
| 163 |
+
city: clientInfo.geoLocation?.city || '',
|
| 164 |
+
state: clientInfo.geoLocation?.region || '',
|
| 165 |
+
latitude: clientInfo.geoLocation?.ll?.[0] || null,
|
| 166 |
+
longitude: clientInfo.geoLocation?.ll?.[1] || null,
|
| 167 |
+
isFromAdmin: true,
|
| 168 |
+
status: SUPER_ADMIN_SESSION_STATUS.CURRENT,
|
| 169 |
+
loginAt: new Date(),
|
| 170 |
+
},
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
return {
|
| 174 |
+
superAdmin,
|
| 175 |
+
accessToken,
|
| 176 |
+
refreshToken,
|
| 177 |
+
};
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
async logout(
|
| 181 |
+
sessionData: SuperAdminSessionType,
|
| 182 |
+
): Promise<{ message: string }> {
|
| 183 |
+
const { session } = sessionData;
|
| 184 |
+
|
| 185 |
+
// Update session to expired
|
| 186 |
+
await this.superAdminSessionCoreService.update({
|
| 187 |
+
where: { id: session.id },
|
| 188 |
+
data: {
|
| 189 |
+
status: SUPER_ADMIN_SESSION_STATUS.EXPIRED,
|
| 190 |
+
expiredAt: new Date(),
|
| 191 |
+
},
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
return { message: AuthMessages.LOGOUT_SUCCESS };
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
async refreshToken(refreshTokenDto: SuperAdminRefreshTokenDto): Promise<{
|
| 198 |
+
accessToken: string;
|
| 199 |
+
refreshToken: string;
|
| 200 |
+
}> {
|
| 201 |
+
const { refreshToken } = refreshTokenDto;
|
| 202 |
+
|
| 203 |
+
let validateRefreshToken: any = null;
|
| 204 |
+
|
| 205 |
+
try {
|
| 206 |
+
validateRefreshToken = await this.jwtService.verifyAsync(
|
| 207 |
+
refreshToken,
|
| 208 |
+
refreshTokenVerifySettings,
|
| 209 |
+
);
|
| 210 |
+
} catch {
|
| 211 |
+
throw new UnauthorizedException('Invalid refresh token');
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// Verify user type
|
| 215 |
+
if (validateRefreshToken.userType !== TOKEN_USER_TYPE.SUPER_ADMIN) {
|
| 216 |
+
throw new UnauthorizedException('Invalid token type');
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
const superAdmin = await this.superAdminCoreService.findUnique({
|
| 220 |
+
where: { id: validateRefreshToken.id, isDeleted: false },
|
| 221 |
+
});
|
| 222 |
+
|
| 223 |
+
if (!superAdmin) {
|
| 224 |
+
throw new UnauthorizedException('Super admin not found');
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Generate new tokens
|
| 228 |
+
const newTokens = await this.getNewToken(superAdmin);
|
| 229 |
+
|
| 230 |
+
// Update session with new tokens
|
| 231 |
+
await this.superAdminSessionCoreService.updateMany({
|
| 232 |
+
where: {
|
| 233 |
+
superAdminId: superAdmin.id,
|
| 234 |
+
refreshToken,
|
| 235 |
+
status: SUPER_ADMIN_SESSION_STATUS.CURRENT,
|
| 236 |
+
},
|
| 237 |
+
data: {
|
| 238 |
+
accessToken: newTokens.accessToken,
|
| 239 |
+
refreshToken: newTokens.refreshToken,
|
| 240 |
+
},
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
return newTokens;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
private async getNewToken(superAdmin: SuperAdmin): Promise<{
|
| 247 |
+
accessToken: string;
|
| 248 |
+
refreshToken: string;
|
| 249 |
+
}> {
|
| 250 |
+
const payload = {
|
| 251 |
+
id: superAdmin.id,
|
| 252 |
+
email: superAdmin.email,
|
| 253 |
+
userType: TOKEN_USER_TYPE.SUPER_ADMIN,
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
const accessToken = await this.jwtService.signAsync(
|
| 257 |
+
{ ...payload, type: TOKEN_TYPE.ACCESS },
|
| 258 |
+
accessTokenSignSettings,
|
| 259 |
+
);
|
| 260 |
+
|
| 261 |
+
const refreshToken = await this.jwtService.signAsync(
|
| 262 |
+
{ ...payload, type: TOKEN_TYPE.REFRESH },
|
| 263 |
+
refreshTokenSignSettings,
|
| 264 |
+
);
|
| 265 |
+
|
| 266 |
+
return { accessToken, refreshToken };
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
async performJWTStrategy({
|
| 270 |
+
request,
|
| 271 |
+
superAdmin,
|
| 272 |
+
}: {
|
| 273 |
+
request: Request & { headers: { authorization: string } };
|
| 274 |
+
superAdmin: SuperAdmin;
|
| 275 |
+
}): Promise<SuperAdminSessionType> {
|
| 276 |
+
const accessToken = request.headers.authorization?.replace('Bearer ', '');
|
| 277 |
+
|
| 278 |
+
if (!accessToken) {
|
| 279 |
+
throw new UnauthorizedException('Invalid token');
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// Get full super admin object
|
| 283 |
+
const fullSuperAdmin = await this.superAdminCoreService.findUnique({
|
| 284 |
+
where: { id: superAdmin.id, isDeleted: false },
|
| 285 |
+
});
|
| 286 |
+
|
| 287 |
+
if (!fullSuperAdmin) {
|
| 288 |
+
throw new UnauthorizedException('Super admin not found');
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// Find active session
|
| 292 |
+
const session = await this.superAdminSessionCoreService.findFirst({
|
| 293 |
+
where: {
|
| 294 |
+
superAdminId: superAdmin.id,
|
| 295 |
+
accessToken,
|
| 296 |
+
status: SUPER_ADMIN_SESSION_STATUS.CURRENT,
|
| 297 |
+
isDeleted: false,
|
| 298 |
+
},
|
| 299 |
+
});
|
| 300 |
+
|
| 301 |
+
if (!session) {
|
| 302 |
+
throw new UnauthorizedException('Session not found or expired');
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
return {
|
| 306 |
+
superAdmin: fullSuperAdmin,
|
| 307 |
+
session,
|
| 308 |
+
};
|
| 309 |
+
}
|
| 310 |
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
| 3 |
+
|
| 4 |
+
export class SuperAdminLoginDto {
|
| 5 |
+
@ApiProperty({ example: 'admin@example.com' })
|
| 6 |
+
@IsEmail()
|
| 7 |
+
@IsNotEmpty()
|
| 8 |
+
email: string;
|
| 9 |
+
|
| 10 |
+
@ApiProperty({ example: 'password123' })
|
| 11 |
+
@IsString()
|
| 12 |
+
@IsNotEmpty()
|
| 13 |
+
@MinLength(6)
|
| 14 |
+
password: string;
|
| 15 |
+
}
|
|
@@ -1,12 +1,9 @@
|
|
| 1 |
-
import { IsString } from 'class-validator';
|
| 2 |
import { ApiProperty } from '@nestjs/swagger';
|
|
|
|
| 3 |
|
| 4 |
-
export class
|
| 5 |
-
@ApiProperty()
|
| 6 |
-
@IsString()
|
| 7 |
-
accessToken: string;
|
| 8 |
-
|
| 9 |
@ApiProperty()
|
| 10 |
@IsString()
|
|
|
|
| 11 |
refreshToken: string;
|
| 12 |
}
|
|
|
|
|
|
|
| 1 |
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { IsNotEmpty, IsString } from 'class-validator';
|
| 3 |
|
| 4 |
+
export class SuperAdminRefreshTokenDto {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
@ApiProperty()
|
| 6 |
@IsString()
|
| 7 |
+
@IsNotEmpty()
|
| 8 |
refreshToken: string;
|
| 9 |
}
|
|
@@ -1,24 +1,31 @@
|
|
| 1 |
-
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
| 2 |
import { ApiProperty } from '@nestjs/swagger';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
export class
|
| 5 |
-
@ApiProperty({ example: '
|
| 6 |
-
@IsOptional()
|
| 7 |
@IsEmail()
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
@ApiProperty({ example: '
|
| 11 |
-
@IsOptional()
|
| 12 |
@IsString()
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
@ApiProperty({ example: '
|
| 16 |
-
@IsOptional()
|
| 17 |
@IsString()
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
@ApiProperty({ example: '
|
| 21 |
@IsString()
|
| 22 |
-
@
|
| 23 |
-
|
| 24 |
}
|
|
|
|
|
|
|
| 1 |
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import {
|
| 3 |
+
IsEmail,
|
| 4 |
+
IsNotEmpty,
|
| 5 |
+
IsOptional,
|
| 6 |
+
IsString,
|
| 7 |
+
MinLength,
|
| 8 |
+
} from 'class-validator';
|
| 9 |
|
| 10 |
+
export class SuperAdminRegisterDto {
|
| 11 |
+
@ApiProperty({ example: 'admin@example.com' })
|
|
|
|
| 12 |
@IsEmail()
|
| 13 |
+
@IsNotEmpty()
|
| 14 |
+
email: string;
|
| 15 |
|
| 16 |
+
@ApiProperty({ example: 'password123' })
|
|
|
|
| 17 |
@IsString()
|
| 18 |
+
@IsNotEmpty()
|
| 19 |
+
@MinLength(6)
|
| 20 |
+
password: string;
|
| 21 |
|
| 22 |
+
@ApiProperty({ example: 'Admin Name' })
|
|
|
|
| 23 |
@IsString()
|
| 24 |
+
@IsNotEmpty()
|
| 25 |
+
name: string;
|
| 26 |
|
| 27 |
+
@ApiProperty({ example: 'https://example.com/profile.jpg', required: false })
|
| 28 |
@IsString()
|
| 29 |
+
@IsOptional()
|
| 30 |
+
profileImage?: string;
|
| 31 |
}
|
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, ExecutionContext } from '@nestjs/common';
|
| 2 |
+
import { Reflector } from '@nestjs/core';
|
| 3 |
+
import { AuthGuard } from '@nestjs/passport';
|
| 4 |
+
import { Observable } from 'rxjs';
|
| 5 |
+
import { IS_PUBLIC_KEY } from 'src/shared/decorators/public.decorator';
|
| 6 |
+
|
| 7 |
+
@Injectable()
|
| 8 |
+
export class SuperAdminAuthGuard extends AuthGuard('super-admin-jwt') {
|
| 9 |
+
constructor(private reflector: Reflector) {
|
| 10 |
+
super();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
canActivate(
|
| 14 |
+
context: ExecutionContext,
|
| 15 |
+
): boolean | Promise<boolean> | Observable<boolean> {
|
| 16 |
+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
| 17 |
+
context.getHandler(),
|
| 18 |
+
context.getClass(),
|
| 19 |
+
]);
|
| 20 |
+
|
| 21 |
+
if (isPublic) {
|
| 22 |
+
return true;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
return super.canActivate(context);
|
| 26 |
+
}
|
| 27 |
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
| 2 |
+
import { PassportStrategy } from '@nestjs/passport';
|
| 3 |
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
| 4 |
+
import { SuperAdminCoreService } from 'src/core/super-admin-core/super-admin-core.service';
|
| 5 |
+
import { SuperAdminAuthService } from '../auth.service';
|
| 6 |
+
import { Request } from 'express';
|
| 7 |
+
import { jwtAuthConstants, TOKEN_USER_TYPE } from 'src/shared/keys/auth.keys';
|
| 8 |
+
import { SuperAdmin } from '@prisma/client';
|
| 9 |
+
|
| 10 |
+
@Injectable()
|
| 11 |
+
export class SuperAdminJwtStrategy extends PassportStrategy(
|
| 12 |
+
Strategy,
|
| 13 |
+
'super-admin-jwt',
|
| 14 |
+
) {
|
| 15 |
+
constructor(
|
| 16 |
+
private readonly superAdminCoreService: SuperAdminCoreService,
|
| 17 |
+
private readonly superAdminAuthService: SuperAdminAuthService,
|
| 18 |
+
) {
|
| 19 |
+
super({
|
| 20 |
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
| 21 |
+
ignoreExpiration: false,
|
| 22 |
+
secretOrKey: jwtAuthConstants.accessTokenSecret,
|
| 23 |
+
passReqToCallback: true,
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async validate(
|
| 28 |
+
request: Request & { headers: { authorization: string } },
|
| 29 |
+
payload: {
|
| 30 |
+
id: string;
|
| 31 |
+
email: string;
|
| 32 |
+
userType: TOKEN_USER_TYPE;
|
| 33 |
+
},
|
| 34 |
+
) {
|
| 35 |
+
// Verify the user type
|
| 36 |
+
if (payload.userType !== TOKEN_USER_TYPE.SUPER_ADMIN) {
|
| 37 |
+
throw new UnauthorizedException('Invalid token: Not a super admin token');
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const superAdmin = await this.superAdminCoreService.findUnique({
|
| 41 |
+
where: { id: payload.id },
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
if (!superAdmin || superAdmin.isDeleted) {
|
| 45 |
+
throw new UnauthorizedException('Super admin not found');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Get full session data
|
| 49 |
+
const sessionData = await this.superAdminAuthService.performJWTStrategy({
|
| 50 |
+
request: request as any,
|
| 51 |
+
superAdmin: {
|
| 52 |
+
id: payload.id,
|
| 53 |
+
email: payload.email,
|
| 54 |
+
} as SuperAdmin,
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
return sessionData;
|
| 58 |
+
}
|
| 59 |
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { SuperAdminAuthModule } from './auth/auth.module';
|
| 3 |
+
|
| 4 |
+
@Module({
|
| 5 |
+
imports: [SuperAdminAuthModule],
|
| 6 |
+
exports: [SuperAdminAuthModule],
|
| 7 |
+
})
|
| 8 |
+
export class SuperAdminModule {}
|
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Post, Body, Req, UseGuards } from '@nestjs/common';
|
| 2 |
+
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
| 3 |
+
import { UserAuthService } from './auth.service';
|
| 4 |
+
import { UserRegisterDto } from './dto/register.dto';
|
| 5 |
+
import { UserLoginDto } from './dto/login.dto';
|
| 6 |
+
import { UserRefreshTokenDto } from './dto/refresh-token.dto';
|
| 7 |
+
import { Public } from 'src/shared/decorators/public.decorator';
|
| 8 |
+
import { GetUser } from 'src/shared/decorators/get-user.decorator';
|
| 9 |
+
import type { UserSessionType } from 'src/shared/types/user-session.type';
|
| 10 |
+
import { UserAuthGuard } from './guards/user-auth.guard';
|
| 11 |
+
|
| 12 |
+
@ApiTags('User: Authentication')
|
| 13 |
+
@Controller('user/auth')
|
| 14 |
+
@UseGuards(UserAuthGuard)
|
| 15 |
+
export class UserAuthController {
|
| 16 |
+
constructor(private readonly userAuthService: UserAuthService) {}
|
| 17 |
+
|
| 18 |
+
@Public()
|
| 19 |
+
@Post('register')
|
| 20 |
+
@ApiOperation({ summary: 'Register a new user' })
|
| 21 |
+
async register(@Body() registerDto: UserRegisterDto, @Req() request: any) {
|
| 22 |
+
return this.userAuthService.register(registerDto, request);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
@Public()
|
| 26 |
+
@Post('login')
|
| 27 |
+
@ApiOperation({ summary: 'Login user' })
|
| 28 |
+
async login(@Body() loginDto: UserLoginDto, @Req() request: any) {
|
| 29 |
+
return this.userAuthService.login(loginDto, request);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
@ApiBearerAuth()
|
| 33 |
+
@Post('logout')
|
| 34 |
+
@ApiOperation({ summary: 'Logout user' })
|
| 35 |
+
async logout(@GetUser() sessionData: UserSessionType) {
|
| 36 |
+
return this.userAuthService.logout(sessionData);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
@Public()
|
| 40 |
+
@Post('refresh-token')
|
| 41 |
+
@ApiOperation({ summary: 'Refresh access token' })
|
| 42 |
+
async refreshToken(@Body() refreshTokenDto: UserRefreshTokenDto) {
|
| 43 |
+
return this.userAuthService.refreshToken(refreshTokenDto);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
@ApiBearerAuth()
|
| 47 |
+
@Post('profile')
|
| 48 |
+
@ApiOperation({ summary: 'Get user profile' })
|
| 49 |
+
async getProfile(@GetUser() sessionData: UserSessionType) {
|
| 50 |
+
return { user: sessionData.user };
|
| 51 |
+
}
|
| 52 |
+
}
|
|
@@ -1,29 +1,29 @@
|
|
| 1 |
import { Module } from '@nestjs/common';
|
| 2 |
import { JwtModule } from '@nestjs/jwt';
|
| 3 |
import { PassportModule } from '@nestjs/passport';
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
import {
|
| 7 |
import { UserCoreModule } from 'src/core/user-core/user-core.module';
|
| 8 |
-
import { CommonModule } from 'src/shared/modules/common/common.module';
|
| 9 |
-
import { jwtAuthConstants } from 'src/shared/keys/auth.keys';
|
| 10 |
import { UserCredentialCoreModule } from 'src/core/user-credential-core/user-credential-core.module';
|
| 11 |
import { UserSessionCoreModule } from 'src/core/user-session-core/user-session-core.module';
|
|
|
|
|
|
|
| 12 |
|
| 13 |
@Module({
|
| 14 |
imports: [
|
| 15 |
-
UserCoreModule,
|
| 16 |
-
UserCredentialCoreModule,
|
| 17 |
-
UserSessionCoreModule,
|
| 18 |
-
CommonModule,
|
| 19 |
PassportModule,
|
| 20 |
JwtModule.register({
|
| 21 |
secret: jwtAuthConstants.accessTokenSecret,
|
| 22 |
signOptions: { expiresIn: jwtAuthConstants.accessTokenExpiresIn },
|
| 23 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
],
|
| 25 |
-
controllers: [
|
| 26 |
-
providers: [
|
| 27 |
-
exports: [
|
| 28 |
})
|
| 29 |
-
export class
|
|
|
|
| 1 |
import { Module } from '@nestjs/common';
|
| 2 |
import { JwtModule } from '@nestjs/jwt';
|
| 3 |
import { PassportModule } from '@nestjs/passport';
|
| 4 |
+
import { UserAuthController } from './auth.controller';
|
| 5 |
+
import { UserAuthService } from './auth.service';
|
| 6 |
+
import { UserJwtStrategy } from './strategies/user-jwt.strategy';
|
| 7 |
import { UserCoreModule } from 'src/core/user-core/user-core.module';
|
|
|
|
|
|
|
| 8 |
import { UserCredentialCoreModule } from 'src/core/user-credential-core/user-credential-core.module';
|
| 9 |
import { UserSessionCoreModule } from 'src/core/user-session-core/user-session-core.module';
|
| 10 |
+
import { CommonModule } from 'src/shared/modules/common/common.module';
|
| 11 |
+
import { jwtAuthConstants } from 'src/shared/keys/auth.keys';
|
| 12 |
|
| 13 |
@Module({
|
| 14 |
imports: [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
PassportModule,
|
| 16 |
JwtModule.register({
|
| 17 |
secret: jwtAuthConstants.accessTokenSecret,
|
| 18 |
signOptions: { expiresIn: jwtAuthConstants.accessTokenExpiresIn },
|
| 19 |
}),
|
| 20 |
+
UserCoreModule,
|
| 21 |
+
UserCredentialCoreModule,
|
| 22 |
+
UserSessionCoreModule,
|
| 23 |
+
CommonModule,
|
| 24 |
],
|
| 25 |
+
controllers: [UserAuthController],
|
| 26 |
+
providers: [UserAuthService, UserJwtStrategy],
|
| 27 |
+
exports: [UserAuthService],
|
| 28 |
})
|
| 29 |
+
export class UserAuthModule {}
|
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import {
|
| 2 |
Injectable,
|
| 3 |
UnauthorizedException,
|
| 4 |
-
BadRequestException,
|
| 5 |
ConflictException,
|
| 6 |
} from '@nestjs/common';
|
| 7 |
import { JwtService } from '@nestjs/jwt';
|
|
@@ -10,26 +9,23 @@ import { UserCoreService } from 'src/core/user-core/user-core.service';
|
|
| 10 |
import { UserCredentialCoreService } from 'src/core/user-credential-core/user-credential-core.service';
|
| 11 |
import { UserSessionCoreService } from 'src/core/user-session-core/user-session-core.service';
|
| 12 |
import { CommonService } from 'src/shared/modules/common/common.service';
|
| 13 |
-
import {
|
| 14 |
-
import {
|
| 15 |
-
import {
|
| 16 |
import { UserSessionType } from 'src/shared/types/user-session.type';
|
| 17 |
-
import {
|
| 18 |
-
getUniqueId,
|
| 19 |
-
generateClientId,
|
| 20 |
-
} from 'src/shared/modules/common/common.helper';
|
| 21 |
-
import { UNIQUE_ID_ENUM, USER_STATUS_ENUM } from 'src/keys';
|
| 22 |
import {
|
| 23 |
accessTokenSignSettings,
|
| 24 |
-
accessTokenVerifySettings,
|
| 25 |
refreshTokenSignSettings,
|
| 26 |
refreshTokenVerifySettings,
|
| 27 |
TOKEN_TYPE,
|
| 28 |
TOKEN_USER_TYPE,
|
|
|
|
| 29 |
} from 'src/shared/keys/auth.keys';
|
|
|
|
|
|
|
| 30 |
|
| 31 |
@Injectable()
|
| 32 |
-
export class
|
| 33 |
constructor(
|
| 34 |
private readonly jwtService: JwtService,
|
| 35 |
private readonly userCoreService: UserCoreService,
|
|
@@ -38,45 +34,44 @@ export class AuthService {
|
|
| 38 |
private readonly commonService: CommonService,
|
| 39 |
) {}
|
| 40 |
|
| 41 |
-
async register(
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
// Check if user already exists
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
const existingUser = await this.userCoreService.findByPhone(phone, code);
|
| 54 |
-
if (existingUser) {
|
| 55 |
-
throw new ConflictException('User with this phone already exists');
|
| 56 |
-
}
|
| 57 |
}
|
| 58 |
|
| 59 |
-
// Hash password
|
| 60 |
-
const hashedPassword = await this.commonService.hashPassword(password);
|
| 61 |
-
|
| 62 |
// Create user
|
| 63 |
const user = await this.userCoreService.create({
|
| 64 |
data: {
|
| 65 |
uniqueId: getUniqueId(UNIQUE_ID_ENUM.User),
|
| 66 |
email,
|
| 67 |
-
phone,
|
| 68 |
-
code,
|
| 69 |
firstName,
|
| 70 |
lastName,
|
|
|
|
|
|
|
| 71 |
status: USER_STATUS_ENUM.ENABLED,
|
| 72 |
emailVerified: false,
|
| 73 |
phoneVerified: false,
|
| 74 |
-
createdAt: new Date(),
|
| 75 |
-
updatedAt: new Date(),
|
| 76 |
},
|
| 77 |
});
|
| 78 |
|
| 79 |
-
//
|
|
|
|
| 80 |
await this.userCredentialCoreService.create({
|
| 81 |
data: {
|
| 82 |
userId: user.id,
|
|
@@ -84,28 +79,59 @@ export class AuthService {
|
|
| 84 |
},
|
| 85 |
});
|
| 86 |
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
| 89 |
|
| 90 |
-
async login(
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
// Find user
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
}
|
| 100 |
|
| 101 |
if (!user) {
|
| 102 |
throw new UnauthorizedException('Invalid credentials');
|
| 103 |
}
|
| 104 |
|
| 105 |
// Get user credential
|
| 106 |
-
const credential = await this.userCredentialCoreService
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
| 109 |
|
| 110 |
if (!credential) {
|
| 111 |
throw new UnauthorizedException('Invalid credentials');
|
|
@@ -128,15 +154,14 @@ export class AuthService {
|
|
| 128 |
const clientInfo = this.commonService.getClientInfo(request);
|
| 129 |
|
| 130 |
// Create session
|
| 131 |
-
|
| 132 |
data: {
|
| 133 |
userId: user.id,
|
| 134 |
accessToken,
|
| 135 |
refreshToken,
|
| 136 |
ipAddress: clientInfo.ipAddress,
|
| 137 |
userAgent: clientInfo.userAgent,
|
| 138 |
-
geoIpCountry: clientInfo.geoLocation?.country,
|
| 139 |
-
clientId: generateClientId(),
|
| 140 |
loginAt: new Date(),
|
| 141 |
},
|
| 142 |
});
|
|
@@ -144,77 +169,74 @@ export class AuthService {
|
|
| 144 |
// Update last login
|
| 145 |
await this.userCoreService.update({
|
| 146 |
where: { id: user.id },
|
| 147 |
-
data: {
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
});
|
| 152 |
|
| 153 |
-
return {
|
| 154 |
}
|
| 155 |
|
| 156 |
-
async refreshToken(refreshTokenDto:
|
| 157 |
accessToken: string;
|
| 158 |
refreshToken: string;
|
| 159 |
}> {
|
| 160 |
-
const {
|
| 161 |
|
| 162 |
-
let validateAccessToken: any = null;
|
| 163 |
let validateRefreshToken: any = null;
|
| 164 |
|
| 165 |
try {
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
);
|
| 170 |
-
} catch {
|
| 171 |
-
|
| 172 |
-
if (!validateAccessToken) {
|
| 173 |
-
try {
|
| 174 |
-
validateRefreshToken = await this.jwtService.verifyAsync(
|
| 175 |
-
refreshToken,
|
| 176 |
-
refreshTokenVerifySettings,
|
| 177 |
-
);
|
| 178 |
-
} catch {
|
| 179 |
-
throw new UnauthorizedException('Invalid refresh token');
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
const user = await this.userCoreService.findUnique({
|
| 183 |
-
where: { id: validateRefreshToken.id },
|
| 184 |
-
});
|
| 185 |
-
|
| 186 |
-
if (!user) {
|
| 187 |
-
throw new UnauthorizedException('User not found');
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
const tokens = await this.getNewToken(user);
|
| 191 |
-
return tokens;
|
| 192 |
}
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
|
|
|
|
| 204 |
|
| 205 |
-
|
|
|
|
| 206 |
where: {
|
| 207 |
userId: user.id,
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
| 210 |
},
|
| 211 |
});
|
| 212 |
|
| 213 |
-
|
| 214 |
-
throw new UnauthorizedException('Invalid session');
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
return { user, session };
|
| 218 |
}
|
| 219 |
|
| 220 |
private async getNewToken(user: User): Promise<{
|
|
@@ -224,26 +246,60 @@ export class AuthService {
|
|
| 224 |
const payload = {
|
| 225 |
id: user.id,
|
| 226 |
email: user.email,
|
| 227 |
-
type: TOKEN_TYPE.ACCESS,
|
| 228 |
-
userType: TOKEN_USER_TYPE.USER,
|
| 229 |
-
};
|
| 230 |
-
|
| 231 |
-
const refreshPayload = {
|
| 232 |
-
id: user.id,
|
| 233 |
-
type: TOKEN_TYPE.REFRESH,
|
| 234 |
userType: TOKEN_USER_TYPE.USER,
|
| 235 |
};
|
| 236 |
|
| 237 |
const accessToken = await this.jwtService.signAsync(
|
| 238 |
-
payload,
|
| 239 |
accessTokenSignSettings,
|
| 240 |
);
|
| 241 |
|
| 242 |
const refreshToken = await this.jwtService.signAsync(
|
| 243 |
-
|
| 244 |
refreshTokenSignSettings,
|
| 245 |
);
|
| 246 |
|
| 247 |
return { accessToken, refreshToken };
|
| 248 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
}
|
|
|
|
| 1 |
import {
|
| 2 |
Injectable,
|
| 3 |
UnauthorizedException,
|
|
|
|
| 4 |
ConflictException,
|
| 5 |
} from '@nestjs/common';
|
| 6 |
import { JwtService } from '@nestjs/jwt';
|
|
|
|
| 9 |
import { UserCredentialCoreService } from 'src/core/user-credential-core/user-credential-core.service';
|
| 10 |
import { UserSessionCoreService } from 'src/core/user-session-core/user-session-core.service';
|
| 11 |
import { CommonService } from 'src/shared/modules/common/common.service';
|
| 12 |
+
import { UserRegisterDto } from './dto/register.dto';
|
| 13 |
+
import { UserLoginDto } from './dto/login.dto';
|
| 14 |
+
import { UserRefreshTokenDto } from './dto/refresh-token.dto';
|
| 15 |
import { UserSessionType } from 'src/shared/types/user-session.type';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
import {
|
| 17 |
accessTokenSignSettings,
|
|
|
|
| 18 |
refreshTokenSignSettings,
|
| 19 |
refreshTokenVerifySettings,
|
| 20 |
TOKEN_TYPE,
|
| 21 |
TOKEN_USER_TYPE,
|
| 22 |
+
AuthMessages,
|
| 23 |
} from 'src/shared/keys/auth.keys';
|
| 24 |
+
import { getUniqueId } from 'src/shared/modules/common/common.helper';
|
| 25 |
+
import { UNIQUE_ID_ENUM, USER_STATUS_ENUM } from 'src/keys';
|
| 26 |
|
| 27 |
@Injectable()
|
| 28 |
+
export class UserAuthService {
|
| 29 |
constructor(
|
| 30 |
private readonly jwtService: JwtService,
|
| 31 |
private readonly userCoreService: UserCoreService,
|
|
|
|
| 34 |
private readonly commonService: CommonService,
|
| 35 |
) {}
|
| 36 |
|
| 37 |
+
async register(
|
| 38 |
+
registerDto: UserRegisterDto,
|
| 39 |
+
request: any,
|
| 40 |
+
): Promise<{
|
| 41 |
+
user: User;
|
| 42 |
+
accessToken: string;
|
| 43 |
+
refreshToken: string;
|
| 44 |
+
}> {
|
| 45 |
+
const { email, password, firstName, lastName, phone, code } = registerDto;
|
| 46 |
|
| 47 |
// Check if user already exists
|
| 48 |
+
const existingUser = await this.userCoreService
|
| 49 |
+
.findFirst({
|
| 50 |
+
where: { email, isDeleted: false },
|
| 51 |
+
})
|
| 52 |
+
.catch(() => null);
|
| 53 |
+
|
| 54 |
+
if (existingUser) {
|
| 55 |
+
throw new ConflictException('User with this email already exists');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
|
|
|
|
|
|
|
|
|
|
| 58 |
// Create user
|
| 59 |
const user = await this.userCoreService.create({
|
| 60 |
data: {
|
| 61 |
uniqueId: getUniqueId(UNIQUE_ID_ENUM.User),
|
| 62 |
email,
|
|
|
|
|
|
|
| 63 |
firstName,
|
| 64 |
lastName,
|
| 65 |
+
phone,
|
| 66 |
+
code,
|
| 67 |
status: USER_STATUS_ENUM.ENABLED,
|
| 68 |
emailVerified: false,
|
| 69 |
phoneVerified: false,
|
|
|
|
|
|
|
| 70 |
},
|
| 71 |
});
|
| 72 |
|
| 73 |
+
// Hash and store password
|
| 74 |
+
const hashedPassword = await this.commonService.hashPassword(password);
|
| 75 |
await this.userCredentialCoreService.create({
|
| 76 |
data: {
|
| 77 |
userId: user.id,
|
|
|
|
| 79 |
},
|
| 80 |
});
|
| 81 |
|
| 82 |
+
// Generate tokens
|
| 83 |
+
const { accessToken, refreshToken } = await this.getNewToken(user);
|
| 84 |
+
|
| 85 |
+
// Get client info
|
| 86 |
+
const clientInfo = this.commonService.getClientInfo(request);
|
| 87 |
+
|
| 88 |
+
// Create session
|
| 89 |
+
await this.userSessionCoreService.create({
|
| 90 |
+
data: {
|
| 91 |
+
userId: user.id,
|
| 92 |
+
accessToken,
|
| 93 |
+
refreshToken,
|
| 94 |
+
ipAddress: clientInfo.ipAddress,
|
| 95 |
+
userAgent: clientInfo.userAgent,
|
| 96 |
+
geoIpCountry: clientInfo.geoLocation?.country || '',
|
| 97 |
+
loginAt: new Date(),
|
| 98 |
+
},
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
user,
|
| 103 |
+
accessToken,
|
| 104 |
+
refreshToken,
|
| 105 |
+
};
|
| 106 |
}
|
| 107 |
|
| 108 |
+
async login(
|
| 109 |
+
loginDto: UserLoginDto,
|
| 110 |
+
request: any,
|
| 111 |
+
): Promise<{
|
| 112 |
+
user: User;
|
| 113 |
+
accessToken: string;
|
| 114 |
+
refreshToken: string;
|
| 115 |
+
}> {
|
| 116 |
+
const { email, password } = loginDto;
|
| 117 |
|
| 118 |
// Find user
|
| 119 |
+
const user = await this.userCoreService
|
| 120 |
+
.findFirst({
|
| 121 |
+
where: { email, isDeleted: false },
|
| 122 |
+
})
|
| 123 |
+
.catch(() => null);
|
|
|
|
| 124 |
|
| 125 |
if (!user) {
|
| 126 |
throw new UnauthorizedException('Invalid credentials');
|
| 127 |
}
|
| 128 |
|
| 129 |
// Get user credential
|
| 130 |
+
const credential = await this.userCredentialCoreService
|
| 131 |
+
.findFirst({
|
| 132 |
+
where: { userId: user.id, isDeleted: false },
|
| 133 |
+
})
|
| 134 |
+
.catch(() => null);
|
| 135 |
|
| 136 |
if (!credential) {
|
| 137 |
throw new UnauthorizedException('Invalid credentials');
|
|
|
|
| 154 |
const clientInfo = this.commonService.getClientInfo(request);
|
| 155 |
|
| 156 |
// Create session
|
| 157 |
+
await this.userSessionCoreService.create({
|
| 158 |
data: {
|
| 159 |
userId: user.id,
|
| 160 |
accessToken,
|
| 161 |
refreshToken,
|
| 162 |
ipAddress: clientInfo.ipAddress,
|
| 163 |
userAgent: clientInfo.userAgent,
|
| 164 |
+
geoIpCountry: clientInfo.geoLocation?.country || '',
|
|
|
|
| 165 |
loginAt: new Date(),
|
| 166 |
},
|
| 167 |
});
|
|
|
|
| 169 |
// Update last login
|
| 170 |
await this.userCoreService.update({
|
| 171 |
where: { id: user.id },
|
| 172 |
+
data: { lastLogin: new Date() },
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
return {
|
| 176 |
+
user,
|
| 177 |
+
accessToken,
|
| 178 |
+
refreshToken,
|
| 179 |
+
};
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
async logout(sessionData: UserSessionType): Promise<{ message: string }> {
|
| 183 |
+
const { session } = sessionData;
|
| 184 |
+
|
| 185 |
+
// Update session to mark as logged out
|
| 186 |
+
await this.userSessionCoreService.update({
|
| 187 |
+
where: { id: session.id },
|
| 188 |
+
data: { logoutAt: new Date() },
|
| 189 |
});
|
| 190 |
|
| 191 |
+
return { message: AuthMessages.LOGOUT_SUCCESS };
|
| 192 |
}
|
| 193 |
|
| 194 |
+
async refreshToken(refreshTokenDto: UserRefreshTokenDto): Promise<{
|
| 195 |
accessToken: string;
|
| 196 |
refreshToken: string;
|
| 197 |
}> {
|
| 198 |
+
const { refreshToken } = refreshTokenDto;
|
| 199 |
|
|
|
|
| 200 |
let validateRefreshToken: any = null;
|
| 201 |
|
| 202 |
try {
|
| 203 |
+
validateRefreshToken = await this.jwtService.verifyAsync(
|
| 204 |
+
refreshToken,
|
| 205 |
+
refreshTokenVerifySettings,
|
| 206 |
);
|
| 207 |
+
} catch {
|
| 208 |
+
throw new UnauthorizedException('Invalid refresh token');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
}
|
| 210 |
|
| 211 |
+
// Verify user type
|
| 212 |
+
if (validateRefreshToken.userType !== TOKEN_USER_TYPE.USER) {
|
| 213 |
+
throw new UnauthorizedException('Invalid token type');
|
| 214 |
+
}
|
| 215 |
|
| 216 |
+
const user = await this.userCoreService.findUnique({
|
| 217 |
+
where: { id: validateRefreshToken.id, isDeleted: false },
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
if (!user) {
|
| 221 |
+
throw new UnauthorizedException('User not found');
|
| 222 |
+
}
|
| 223 |
|
| 224 |
+
// Generate new tokens
|
| 225 |
+
const newTokens = await this.getNewToken(user);
|
| 226 |
|
| 227 |
+
// Update session with new tokens
|
| 228 |
+
await this.userSessionCoreService.updateMany({
|
| 229 |
where: {
|
| 230 |
userId: user.id,
|
| 231 |
+
refreshToken,
|
| 232 |
+
},
|
| 233 |
+
data: {
|
| 234 |
+
accessToken: newTokens.accessToken,
|
| 235 |
+
refreshToken: newTokens.refreshToken,
|
| 236 |
},
|
| 237 |
});
|
| 238 |
|
| 239 |
+
return newTokens;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
private async getNewToken(user: User): Promise<{
|
|
|
|
| 246 |
const payload = {
|
| 247 |
id: user.id,
|
| 248 |
email: user.email,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
userType: TOKEN_USER_TYPE.USER,
|
| 250 |
};
|
| 251 |
|
| 252 |
const accessToken = await this.jwtService.signAsync(
|
| 253 |
+
{ ...payload, type: TOKEN_TYPE.ACCESS },
|
| 254 |
accessTokenSignSettings,
|
| 255 |
);
|
| 256 |
|
| 257 |
const refreshToken = await this.jwtService.signAsync(
|
| 258 |
+
{ ...payload, type: TOKEN_TYPE.REFRESH },
|
| 259 |
refreshTokenSignSettings,
|
| 260 |
);
|
| 261 |
|
| 262 |
return { accessToken, refreshToken };
|
| 263 |
}
|
| 264 |
+
|
| 265 |
+
async performJWTStrategy({
|
| 266 |
+
request,
|
| 267 |
+
user,
|
| 268 |
+
}: {
|
| 269 |
+
request: Request & { headers: { authorization: string } };
|
| 270 |
+
user: User;
|
| 271 |
+
}): Promise<UserSessionType> {
|
| 272 |
+
const accessToken = request.headers.authorization?.replace('Bearer ', '');
|
| 273 |
+
|
| 274 |
+
if (!accessToken) {
|
| 275 |
+
throw new UnauthorizedException('Invalid token');
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// Get full user object
|
| 279 |
+
const fullUser = await this.userCoreService.findUnique({
|
| 280 |
+
where: { id: user.id, isDeleted: false },
|
| 281 |
+
});
|
| 282 |
+
|
| 283 |
+
if (!fullUser) {
|
| 284 |
+
throw new UnauthorizedException('User not found');
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Find active session
|
| 288 |
+
const session = await this.userSessionCoreService.findFirst({
|
| 289 |
+
where: {
|
| 290 |
+
userId: user.id,
|
| 291 |
+
accessToken,
|
| 292 |
+
isDeleted: false,
|
| 293 |
+
},
|
| 294 |
+
});
|
| 295 |
+
|
| 296 |
+
if (!session) {
|
| 297 |
+
throw new UnauthorizedException('Session not found');
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return {
|
| 301 |
+
user: fullUser,
|
| 302 |
+
session,
|
| 303 |
+
};
|
| 304 |
+
}
|
| 305 |
}
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
| 3 |
+
|
| 4 |
+
export class UserLoginDto {
|
| 5 |
+
@ApiProperty({ example: 'user@example.com' })
|
| 6 |
+
@IsEmail()
|
| 7 |
+
@IsNotEmpty()
|
| 8 |
+
email: string;
|
| 9 |
+
|
| 10 |
+
@ApiProperty({ example: 'password123' })
|
| 11 |
+
@IsString()
|
| 12 |
+
@IsNotEmpty()
|
| 13 |
+
@MinLength(6)
|
| 14 |
+
password: string;
|
| 15 |
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { IsNotEmpty, IsString } from 'class-validator';
|
| 3 |
+
|
| 4 |
+
export class UserRefreshTokenDto {
|
| 5 |
+
@ApiProperty()
|
| 6 |
+
@IsString()
|
| 7 |
+
@IsNotEmpty()
|
| 8 |
+
refreshToken: string;
|
| 9 |
+
}
|
|
@@ -1,34 +1,41 @@
|
|
| 1 |
-
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
| 2 |
import { ApiProperty } from '@nestjs/swagger';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
export class
|
| 5 |
-
@ApiProperty({ example: 'user@example.com'
|
| 6 |
-
@IsOptional()
|
| 7 |
@IsEmail()
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
@ApiProperty({ example: '
|
| 11 |
-
@IsOptional()
|
| 12 |
@IsString()
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
@ApiProperty({ example: '
|
| 16 |
-
@IsOptional()
|
| 17 |
@IsString()
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
@ApiProperty({ example: '
|
| 21 |
@IsString()
|
| 22 |
-
@
|
| 23 |
-
|
| 24 |
|
| 25 |
-
@ApiProperty({ example: '
|
| 26 |
@IsString()
|
| 27 |
-
@
|
| 28 |
-
|
| 29 |
|
| 30 |
-
@ApiProperty({ example: '
|
| 31 |
@IsString()
|
| 32 |
-
@
|
| 33 |
-
|
| 34 |
}
|
|
|
|
|
|
|
| 1 |
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import {
|
| 3 |
+
IsEmail,
|
| 4 |
+
IsNotEmpty,
|
| 5 |
+
IsOptional,
|
| 6 |
+
IsString,
|
| 7 |
+
MinLength,
|
| 8 |
+
} from 'class-validator';
|
| 9 |
|
| 10 |
+
export class UserRegisterDto {
|
| 11 |
+
@ApiProperty({ example: 'user@example.com' })
|
|
|
|
| 12 |
@IsEmail()
|
| 13 |
+
@IsNotEmpty()
|
| 14 |
+
email: string;
|
| 15 |
|
| 16 |
+
@ApiProperty({ example: 'password123' })
|
|
|
|
| 17 |
@IsString()
|
| 18 |
+
@IsNotEmpty()
|
| 19 |
+
@MinLength(6)
|
| 20 |
+
password: string;
|
| 21 |
|
| 22 |
+
@ApiProperty({ example: 'John', required: false })
|
|
|
|
| 23 |
@IsString()
|
| 24 |
+
@IsOptional()
|
| 25 |
+
firstName?: string;
|
| 26 |
|
| 27 |
+
@ApiProperty({ example: 'Doe', required: false })
|
| 28 |
@IsString()
|
| 29 |
+
@IsOptional()
|
| 30 |
+
lastName?: string;
|
| 31 |
|
| 32 |
+
@ApiProperty({ example: '+1234567890', required: false })
|
| 33 |
@IsString()
|
| 34 |
+
@IsOptional()
|
| 35 |
+
phone?: string;
|
| 36 |
|
| 37 |
+
@ApiProperty({ example: '+1', required: false })
|
| 38 |
@IsString()
|
| 39 |
+
@IsOptional()
|
| 40 |
+
code?: string;
|
| 41 |
}
|
|
@@ -1,15 +1,18 @@
|
|
| 1 |
-
import {
|
| 2 |
import { Reflector } from '@nestjs/core';
|
| 3 |
import { AuthGuard } from '@nestjs/passport';
|
|
|
|
| 4 |
import { IS_PUBLIC_KEY } from 'src/shared/decorators/public.decorator';
|
| 5 |
|
| 6 |
@Injectable()
|
| 7 |
-
export class
|
| 8 |
-
constructor(private
|
| 9 |
super();
|
| 10 |
}
|
| 11 |
|
| 12 |
-
canActivate(
|
|
|
|
|
|
|
| 13 |
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
| 14 |
context.getHandler(),
|
| 15 |
context.getClass(),
|
|
|
|
| 1 |
+
import { Injectable, ExecutionContext } from '@nestjs/common';
|
| 2 |
import { Reflector } from '@nestjs/core';
|
| 3 |
import { AuthGuard } from '@nestjs/passport';
|
| 4 |
+
import { Observable } from 'rxjs';
|
| 5 |
import { IS_PUBLIC_KEY } from 'src/shared/decorators/public.decorator';
|
| 6 |
|
| 7 |
@Injectable()
|
| 8 |
+
export class UserAuthGuard extends AuthGuard('user-jwt') {
|
| 9 |
+
constructor(private reflector: Reflector) {
|
| 10 |
super();
|
| 11 |
}
|
| 12 |
|
| 13 |
+
canActivate(
|
| 14 |
+
context: ExecutionContext,
|
| 15 |
+
): boolean | Promise<boolean> | Observable<boolean> {
|
| 16 |
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
| 17 |
context.getHandler(),
|
| 18 |
context.getClass(),
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
| 2 |
+
import { PassportStrategy } from '@nestjs/passport';
|
| 3 |
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
| 4 |
+
import { UserCoreService } from 'src/core/user-core/user-core.service';
|
| 5 |
+
import { UserAuthService } from '../auth.service';
|
| 6 |
+
import { Request } from 'express';
|
| 7 |
+
import { jwtAuthConstants, TOKEN_USER_TYPE } from 'src/shared/keys/auth.keys';
|
| 8 |
+
import { User } from '@prisma/client';
|
| 9 |
+
|
| 10 |
+
@Injectable()
|
| 11 |
+
export class UserJwtStrategy extends PassportStrategy(Strategy, 'user-jwt') {
|
| 12 |
+
constructor(
|
| 13 |
+
private readonly userCoreService: UserCoreService,
|
| 14 |
+
private readonly userAuthService: UserAuthService,
|
| 15 |
+
) {
|
| 16 |
+
super({
|
| 17 |
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
| 18 |
+
ignoreExpiration: false,
|
| 19 |
+
secretOrKey: jwtAuthConstants.accessTokenSecret,
|
| 20 |
+
passReqToCallback: true,
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
async validate(
|
| 25 |
+
request: Request & { headers: { authorization: string } },
|
| 26 |
+
payload: {
|
| 27 |
+
id: string;
|
| 28 |
+
email: string;
|
| 29 |
+
userType: TOKEN_USER_TYPE;
|
| 30 |
+
},
|
| 31 |
+
) {
|
| 32 |
+
// Verify the user type
|
| 33 |
+
if (payload.userType !== TOKEN_USER_TYPE.USER) {
|
| 34 |
+
throw new UnauthorizedException('Invalid token: Not a user token');
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const user = await this.userCoreService.findUnique({
|
| 38 |
+
where: { id: payload.id },
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
if (!user || user.isDeleted) {
|
| 42 |
+
throw new UnauthorizedException('User not found');
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Get full session data
|
| 46 |
+
const sessionData = await this.userAuthService.performJWTStrategy({
|
| 47 |
+
request: request as any,
|
| 48 |
+
user: {
|
| 49 |
+
id: payload.id,
|
| 50 |
+
email: payload.email,
|
| 51 |
+
} as User,
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
return sessionData;
|
| 55 |
+
}
|
| 56 |
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { UserAuthModule } from './auth/auth.module';
|
| 3 |
+
|
| 4 |
+
@Module({
|
| 5 |
+
imports: [UserAuthModule],
|
| 6 |
+
exports: [UserAuthModule],
|
| 7 |
+
})
|
| 8 |
+
export class UserModule {}
|
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
| 2 |
+
|
| 3 |
+
export const GetSession = createParamDecorator(
|
| 4 |
+
(data: unknown, ctx: ExecutionContext) => {
|
| 5 |
+
const request = ctx.switchToHttp().getRequest();
|
| 6 |
+
return request.user;
|
| 7 |
+
},
|
| 8 |
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
| 2 |
+
import { SuperAdminSessionType } from '../types/super-admin-session.type';
|
| 3 |
+
|
| 4 |
+
export const GetSuperAdmin = createParamDecorator(
|
| 5 |
+
(data: any, ctx: ExecutionContext): SuperAdminSessionType => {
|
| 6 |
+
const request = ctx.switchToHttp().getRequest();
|
| 7 |
+
return request.user;
|
| 8 |
+
},
|
| 9 |
+
);
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
import {
|
|
|
|
| 2 |
|
| 3 |
export enum TOKEN_TYPE {
|
| 4 |
REFRESH = 'REFRESH',
|
|
@@ -7,22 +8,25 @@ export enum TOKEN_TYPE {
|
|
| 7 |
|
| 8 |
export enum TOKEN_USER_TYPE {
|
| 9 |
USER = 'USER',
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
export const jwtAuthConstants = {
|
| 13 |
accessTokenSecret: process.env.JWT_ACCESS_SECRET || 'default-access-secret',
|
| 14 |
refreshTokenSecret:
|
| 15 |
process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
|
| 16 |
-
accessTokenExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ||
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
-
export const refreshTokenSignSettings
|
| 21 |
secret: jwtAuthConstants.refreshTokenSecret,
|
| 22 |
expiresIn: jwtAuthConstants.refreshTokenExpiresIn,
|
| 23 |
};
|
| 24 |
|
| 25 |
-
export const accessTokenSignSettings
|
| 26 |
secret: jwtAuthConstants.accessTokenSecret,
|
| 27 |
expiresIn: jwtAuthConstants.accessTokenExpiresIn,
|
| 28 |
};
|
|
|
|
| 1 |
+
import { JwtVerifyOptions } from '@nestjs/jwt';
|
| 2 |
+
import type { StringValue } from 'ms';
|
| 3 |
|
| 4 |
export enum TOKEN_TYPE {
|
| 5 |
REFRESH = 'REFRESH',
|
|
|
|
| 8 |
|
| 9 |
export enum TOKEN_USER_TYPE {
|
| 10 |
USER = 'USER',
|
| 11 |
+
SUPER_ADMIN = 'SUPER_ADMIN',
|
| 12 |
}
|
| 13 |
|
| 14 |
export const jwtAuthConstants = {
|
| 15 |
accessTokenSecret: process.env.JWT_ACCESS_SECRET || 'default-access-secret',
|
| 16 |
refreshTokenSecret:
|
| 17 |
process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
|
| 18 |
+
accessTokenExpiresIn: (process.env.JWT_ACCESS_EXPIRES_IN ||
|
| 19 |
+
'24h') as StringValue,
|
| 20 |
+
refreshTokenExpiresIn: (process.env.JWT_REFRESH_EXPIRES_IN ||
|
| 21 |
+
'7d') as StringValue,
|
| 22 |
};
|
| 23 |
|
| 24 |
+
export const refreshTokenSignSettings = {
|
| 25 |
secret: jwtAuthConstants.refreshTokenSecret,
|
| 26 |
expiresIn: jwtAuthConstants.refreshTokenExpiresIn,
|
| 27 |
};
|
| 28 |
|
| 29 |
+
export const accessTokenSignSettings = {
|
| 30 |
secret: jwtAuthConstants.accessTokenSecret,
|
| 31 |
expiresIn: jwtAuthConstants.accessTokenExpiresIn,
|
| 32 |
};
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const SuperAdminCredentialMessages = {
|
| 2 |
+
NOT_FOUND: 'Super Admin Credential not found',
|
| 3 |
+
DELETED: 'Super Admin Credential deleted successfully',
|
| 4 |
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const SuperAdminSessionMessages = {
|
| 2 |
+
NOT_FOUND: 'Super Admin Session not found',
|
| 3 |
+
DELETED: 'Super Admin Session deleted successfully',
|
| 4 |
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const SuperAdminMessages = {
|
| 2 |
+
NOT_FOUND: 'Super Admin not found',
|
| 3 |
+
DELETED: 'Super Admin deleted successfully',
|
| 4 |
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Security helper to filter includes based on whitelisted relations
|
| 3 |
+
* Prevents users from accessing sensitive data through unauthorized relation includes
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Filters the include array to only allow whitelisted relations
|
| 8 |
+
* Silently ignores unauthorized includes without throwing errors
|
| 9 |
+
*
|
| 10 |
+
* @param includes - Array of include strings from query parameter (or single string)
|
| 11 |
+
* @param whitelist - Array of allowed relation names
|
| 12 |
+
* @returns Filtered array containing only whitelisted includes
|
| 13 |
+
*/
|
| 14 |
+
export function filterAllowedIncludes(
|
| 15 |
+
includes: string[] | string | undefined,
|
| 16 |
+
whitelist: string[],
|
| 17 |
+
): string[] {
|
| 18 |
+
if (!includes) {
|
| 19 |
+
return [];
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Normalize to array (handle single string case)
|
| 23 |
+
const includesArray = Array.isArray(includes) ? includes : [includes];
|
| 24 |
+
|
| 25 |
+
if (includesArray.length === 0) {
|
| 26 |
+
return [];
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Filter includes to only allow whitelisted relations
|
| 30 |
+
return includesArray.filter((include) => whitelist.includes(include));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Creates a safe query DTO with only whitelisted includes
|
| 35 |
+
* Preserves all other query parameters (skip, take, orderBy, search, etc.)
|
| 36 |
+
*
|
| 37 |
+
* @param query - Original query DTO from request (BaseQueryCoreDto or CoreIncludesDto)
|
| 38 |
+
* @param whitelist - Array of allowed relation names
|
| 39 |
+
* @returns New query DTO with filtered includes and all other params preserved
|
| 40 |
+
*/
|
| 41 |
+
export function createSafeIncludesDto(query: any, whitelist: string[]): any {
|
| 42 |
+
if (!query) {
|
| 43 |
+
return {};
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const filteredIncludes = filterAllowedIncludes(query?.include, whitelist);
|
| 47 |
+
|
| 48 |
+
// Create a new object with all properties except include
|
| 49 |
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
| 50 |
+
const { include, ...otherParams } = query;
|
| 51 |
+
|
| 52 |
+
// Only add include if there are filtered includes
|
| 53 |
+
if (filteredIncludes.length > 0) {
|
| 54 |
+
return {
|
| 55 |
+
...otherParams,
|
| 56 |
+
include: filteredIncludes,
|
| 57 |
+
};
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Return without include field if no filtered includes
|
| 61 |
+
return otherParams;
|
| 62 |
+
}
|
|
@@ -28,6 +28,11 @@ export abstract class PrismaBaseRepository<
|
|
| 28 |
DeleteArgs extends { where: Record<string, any> },
|
| 29 |
DeleteManyArgs extends { where?: Record<string, any> },
|
| 30 |
CountArgs extends { where?: Record<string, any> },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
> {
|
| 32 |
constructor(
|
| 33 |
private readonly repo: any,
|
|
@@ -169,6 +174,14 @@ export abstract class PrismaBaseRepository<
|
|
| 169 |
query: CoreIncludesDto,
|
| 170 |
): Promise<ModelEntity> {
|
| 171 |
const updatedQuery = this.baseQueryCoreService.generatePrismaQuery(query);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
const returnData = await safePrismaCall(() =>
|
| 173 |
this.repo.findUnique({ ...params, ...updatedQuery }),
|
| 174 |
).catch(() => {
|
|
@@ -180,11 +193,69 @@ export abstract class PrismaBaseRepository<
|
|
| 180 |
return returnData;
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
async findFirstIncludes(
|
| 184 |
params: FindFirstArgs,
|
| 185 |
query: CoreIncludesDto,
|
| 186 |
): Promise<ModelEntity> {
|
| 187 |
const updatedQuery = this.baseQueryCoreService.generatePrismaQuery(query);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
const returnData = await safePrismaCall(() =>
|
| 189 |
this.repo.findFirst({ ...params, ...updatedQuery }),
|
| 190 |
).catch(() => {
|
|
@@ -195,4 +266,18 @@ export abstract class PrismaBaseRepository<
|
|
| 195 |
}
|
| 196 |
return returnData;
|
| 197 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
|
|
|
| 28 |
DeleteArgs extends { where: Record<string, any> },
|
| 29 |
DeleteManyArgs extends { where?: Record<string, any> },
|
| 30 |
CountArgs extends { where?: Record<string, any> },
|
| 31 |
+
UpsertArgs extends {
|
| 32 |
+
where: Record<string, any>;
|
| 33 |
+
create: Record<string, any>;
|
| 34 |
+
update: Record<string, any>;
|
| 35 |
+
},
|
| 36 |
> {
|
| 37 |
constructor(
|
| 38 |
private readonly repo: any,
|
|
|
|
| 174 |
query: CoreIncludesDto,
|
| 175 |
): Promise<ModelEntity> {
|
| 176 |
const updatedQuery = this.baseQueryCoreService.generatePrismaQuery(query);
|
| 177 |
+
|
| 178 |
+
// Automatically add isDeleted: false filter to related entities
|
| 179 |
+
if (updatedQuery.include) {
|
| 180 |
+
updatedQuery.include = this.addDeletedFilterToIncludes(
|
| 181 |
+
updatedQuery.include,
|
| 182 |
+
);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
const returnData = await safePrismaCall(() =>
|
| 186 |
this.repo.findUnique({ ...params, ...updatedQuery }),
|
| 187 |
).catch(() => {
|
|
|
|
| 193 |
return returnData;
|
| 194 |
}
|
| 195 |
|
| 196 |
+
/**
|
| 197 |
+
* Helper method to automatically add isDeleted: false filter to related entities
|
| 198 |
+
*/
|
| 199 |
+
private addDeletedFilterToIncludes(include: any): any {
|
| 200 |
+
if (!include) return include;
|
| 201 |
+
|
| 202 |
+
const result = { ...include };
|
| 203 |
+
|
| 204 |
+
// List of relations that have isDeleted field
|
| 205 |
+
const relationsWithDeletedField = [
|
| 206 |
+
'sessions',
|
| 207 |
+
'credentials',
|
| 208 |
+
'user',
|
| 209 |
+
'superAdmin',
|
| 210 |
+
];
|
| 211 |
+
|
| 212 |
+
for (const key in result) {
|
| 213 |
+
if (result.hasOwnProperty(key)) {
|
| 214 |
+
if (relationsWithDeletedField.includes(key)) {
|
| 215 |
+
// Add isDeleted: false filter for this relation
|
| 216 |
+
if (result[key] === true) {
|
| 217 |
+
result[key] = {
|
| 218 |
+
where: {
|
| 219 |
+
isDeleted: false,
|
| 220 |
+
},
|
| 221 |
+
};
|
| 222 |
+
} else if (typeof result[key] === 'object' && result[key] !== null) {
|
| 223 |
+
// If it's already an object, merge the where condition
|
| 224 |
+
if (!result[key].where) {
|
| 225 |
+
result[key].where = {};
|
| 226 |
+
}
|
| 227 |
+
result[key].where.isDeleted = false;
|
| 228 |
+
|
| 229 |
+
// Recursively apply to nested includes
|
| 230 |
+
if (result[key].include) {
|
| 231 |
+
result[key].include = this.addDeletedFilterToIncludes(
|
| 232 |
+
result[key].include,
|
| 233 |
+
);
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
} else if (typeof result[key] === 'object' && result[key] !== null) {
|
| 237 |
+
// Recursively apply to nested includes
|
| 238 |
+
result[key] = this.addDeletedFilterToIncludes(result[key]);
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return result;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
async findFirstIncludes(
|
| 247 |
params: FindFirstArgs,
|
| 248 |
query: CoreIncludesDto,
|
| 249 |
): Promise<ModelEntity> {
|
| 250 |
const updatedQuery = this.baseQueryCoreService.generatePrismaQuery(query);
|
| 251 |
+
|
| 252 |
+
// Automatically add isDeleted: false filter to related entities
|
| 253 |
+
if (updatedQuery.include) {
|
| 254 |
+
updatedQuery.include = this.addDeletedFilterToIncludes(
|
| 255 |
+
updatedQuery.include,
|
| 256 |
+
);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
const returnData = await safePrismaCall(() =>
|
| 260 |
this.repo.findFirst({ ...params, ...updatedQuery }),
|
| 261 |
).catch(() => {
|
|
|
|
| 266 |
}
|
| 267 |
return returnData;
|
| 268 |
}
|
| 269 |
+
|
| 270 |
+
async findManyIncludes(
|
| 271 |
+
params: FindManyArgs,
|
| 272 |
+
query: CoreIncludesDto,
|
| 273 |
+
): Promise<ModelEntity[]> {
|
| 274 |
+
const updatedQuery = this.baseQueryCoreService.generatePrismaQuery(query);
|
| 275 |
+
return safePrismaCall(() =>
|
| 276 |
+
this.repo.findMany({ ...params, ...updatedQuery }),
|
| 277 |
+
);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
async upsert(params: UpsertArgs): Promise<ModelEntity> {
|
| 281 |
+
return safePrismaCall(() => this.repo.upsert(params));
|
| 282 |
+
}
|
| 283 |
}
|
|
@@ -1,18 +1,25 @@
|
|
| 1 |
import * as os from 'os';
|
| 2 |
-
const pLimit = require('p-limit');
|
| 3 |
|
| 4 |
// Auto-scale concurrency limit: e.g., 10 Prisma queries per CPU core
|
| 5 |
const cpuCores = os.cpus().length;
|
| 6 |
-
const
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
export async function safePrismaCall<T = any>(
|
| 15 |
fn: () => Promise<T>,
|
| 16 |
): Promise<T> {
|
|
|
|
|
|
|
| 17 |
return prismaConcurrencyLimit(fn);
|
| 18 |
}
|
|
|
|
| 1 |
import * as os from 'os';
|
|
|
|
| 2 |
|
| 3 |
// Auto-scale concurrency limit: e.g., 10 Prisma queries per CPU core
|
| 4 |
const cpuCores = os.cpus().length;
|
| 5 |
+
const concurrencyLimit = cpuCores * 10;
|
| 6 |
|
| 7 |
+
// Dynamic import for ESM-only p-limit package
|
| 8 |
+
let prismaConcurrencyLimit: <T>(fn: () => Promise<T>) => Promise<T>;
|
| 9 |
+
|
| 10 |
+
// Initialize p-limit with dynamic import
|
| 11 |
+
const initPLimit = (async () => {
|
| 12 |
+
const pLimit = (await import('p-limit')).default;
|
| 13 |
+
prismaConcurrencyLimit = pLimit(concurrencyLimit);
|
| 14 |
+
console.log(
|
| 15 |
+
`[safePrismaCall] Limiting Prisma concurrency to ${concurrencyLimit} (based on ${cpuCores} CPU cores)`,
|
| 16 |
+
);
|
| 17 |
+
})();
|
| 18 |
|
| 19 |
export async function safePrismaCall<T = any>(
|
| 20 |
fn: () => Promise<T>,
|
| 21 |
): Promise<T> {
|
| 22 |
+
// Ensure p-limit is initialized before first use
|
| 23 |
+
await initPLimit;
|
| 24 |
return prismaConcurrencyLimit(fn);
|
| 25 |
}
|
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SuperAdmin, SuperAdminSession } from '@prisma/client';
|
| 2 |
+
|
| 3 |
+
export type SuperAdminSessionType = {
|
| 4 |
+
superAdmin: SuperAdmin;
|
| 5 |
+
session: SuperAdminSession;
|
| 6 |
+
};
|
|
@@ -1,38 +1,42 @@
|
|
| 1 |
import { INestApplication } from '@nestjs/common';
|
| 2 |
import { ConfigService } from '@nestjs/config';
|
| 3 |
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
|
|
| 4 |
const basicAuth = require('express-basic-auth');
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
export const setupSwagger = (app: INestApplication) => {
|
| 7 |
-
const
|
| 8 |
-
const swaggerPassword = configService.get('SWAGGER_PASSWORD');
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
);
|
| 18 |
-
}
|
| 19 |
|
| 20 |
const config = new DocumentBuilder()
|
| 21 |
-
.
|
| 22 |
-
.
|
| 23 |
-
.
|
| 24 |
-
.
|
| 25 |
-
type: 'http',
|
| 26 |
-
scheme: 'bearer',
|
| 27 |
-
bearerFormat: 'JWT',
|
| 28 |
-
name: 'JWT',
|
| 29 |
-
description: 'Enter JWT token',
|
| 30 |
-
in: 'header',
|
| 31 |
-
})
|
| 32 |
.build();
|
| 33 |
|
| 34 |
const document = SwaggerModule.createDocument(app, config);
|
| 35 |
SwaggerModule.setup('api', app, document, {
|
|
|
|
| 36 |
swaggerOptions: {
|
| 37 |
persistAuthorization: true,
|
| 38 |
},
|
|
|
|
| 1 |
import { INestApplication } from '@nestjs/common';
|
| 2 |
import { ConfigService } from '@nestjs/config';
|
| 3 |
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
| 4 |
+
import { execSync } from 'child_process';
|
| 5 |
const basicAuth = require('express-basic-auth');
|
| 6 |
|
| 7 |
+
const getBranchName = (): string => {
|
| 8 |
+
try {
|
| 9 |
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
| 10 |
+
encoding: 'utf8',
|
| 11 |
+
cwd: process.cwd(),
|
| 12 |
+
}).trim();
|
| 13 |
+
} catch (error: any) {
|
| 14 |
+
console.warn('Could not get git branch name:', error.message);
|
| 15 |
+
return 'unknown';
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
export const setupSwagger = (app: INestApplication) => {
|
| 20 |
+
const swaggerPassword = app.get(ConfigService).get('SWAGGER_PASSWORD');
|
|
|
|
| 21 |
|
| 22 |
+
app.use(
|
| 23 |
+
['/api'],
|
| 24 |
+
basicAuth({
|
| 25 |
+
challenge: true,
|
| 26 |
+
users: { admin: swaggerPassword },
|
| 27 |
+
}),
|
| 28 |
+
);
|
|
|
|
|
|
|
| 29 |
|
| 30 |
const config = new DocumentBuilder()
|
| 31 |
+
.addBearerAuth()
|
| 32 |
+
.setTitle(`StreamFlix API: ${getBranchName()}`)
|
| 33 |
+
.setDescription('The StreamFlix API documentation')
|
| 34 |
+
.setVersion('1.0.0')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
.build();
|
| 36 |
|
| 37 |
const document = SwaggerModule.createDocument(app, config);
|
| 38 |
SwaggerModule.setup('api', app, document, {
|
| 39 |
+
customCss: '.swagger-ui { background: #E6F3FF; }',
|
| 40 |
swaggerOptions: {
|
| 41 |
persistAuthorization: true,
|
| 42 |
},
|