Akshar2325 commited on
Commit
f3abb0d
·
1 Parent(s): 3f1a51c

✨ 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

Files changed (50) hide show
  1. .vscode/settings.json +3 -0
  2. package-lock.json +126 -11
  3. package.json +1 -0
  4. prisma/schema.prisma +62 -1
  5. src/app.module.ts +5 -10
  6. src/core/base-query-core/base-query-core.service.ts +185 -8
  7. src/core/base-query-core/dto/base-query-core.dto.ts +19 -0
  8. src/core/super-admin-core/dto/super-admin-core.dto.ts +10 -0
  9. src/core/super-admin-core/super-admin-core.module.ts +9 -0
  10. src/core/super-admin-core/super-admin-core.service.ts +29 -0
  11. src/core/super-admin-credential-core/dto/super-admin-credential-core.dto.ts +10 -0
  12. src/core/super-admin-credential-core/super-admin-credential-core.module.ts +9 -0
  13. src/core/super-admin-credential-core/super-admin-credential-core.service.ts +29 -0
  14. src/core/super-admin-session-core/dto/super-admin-session-core.dto.ts +10 -0
  15. src/core/super-admin-session-core/super-admin-session-core.module.ts +9 -0
  16. src/core/super-admin-session-core/super-admin-session-core.service.ts +29 -0
  17. src/core/user-core/user-core.service.ts +2 -1
  18. src/core/user-credential-core/user-credential-core.service.ts +2 -1
  19. src/core/user-session-core/user-session-core.service.ts +2 -1
  20. src/modules/auth/auth.controller.ts +0 -52
  21. src/modules/auth/strategies/jwt.strategy.ts +0 -28
  22. src/modules/super-admin/auth/auth.controller.ts +55 -0
  23. src/modules/super-admin/auth/auth.module.ts +29 -0
  24. src/modules/super-admin/auth/auth.service.ts +310 -0
  25. src/modules/super-admin/auth/dto/login.dto.ts +15 -0
  26. src/modules/{auth → super-admin/auth}/dto/refresh-token.dto.ts +3 -6
  27. src/modules/{auth/dto/login.dto.ts → super-admin/auth/dto/register.dto.ts} +21 -14
  28. src/modules/super-admin/auth/guards/super-admin-auth.guard.ts +27 -0
  29. src/modules/super-admin/auth/strategies/super-admin-jwt.strategy.ts +59 -0
  30. src/modules/super-admin/super-admin.module.ts +8 -0
  31. src/modules/user/auth/auth.controller.ts +52 -0
  32. src/modules/{auth → user/auth}/auth.module.ts +13 -13
  33. src/modules/{auth → user/auth}/auth.service.ts +162 -106
  34. src/modules/user/auth/dto/login.dto.ts +15 -0
  35. src/modules/user/auth/dto/refresh-token.dto.ts +9 -0
  36. src/modules/{auth → user/auth}/dto/register.dto.ts +27 -20
  37. src/modules/{auth/guards/jwt-auth.guard.ts → user/auth/guards/user-auth.guard.ts} +7 -4
  38. src/modules/user/auth/strategies/user-jwt.strategy.ts +56 -0
  39. src/modules/user/user.module.ts +8 -0
  40. src/shared/decorators/get-session.decorator.ts +8 -0
  41. src/shared/decorators/get-super-admin.decorator.ts +9 -0
  42. src/shared/keys/auth.keys.ts +9 -5
  43. src/shared/keys/super-admin-credential.keys.ts +4 -0
  44. src/shared/keys/super-admin-session.keys.ts +4 -0
  45. src/shared/keys/super-admin.keys.ts +4 -0
  46. src/shared/libs/include-filter.helper.ts +62 -0
  47. src/shared/libs/prisma-base.repository.ts +85 -0
  48. src/shared/modules/prisma/safe-prisma-call.ts +14 -7
  49. src/shared/types/super-admin-session.type.ts +6 -0
  50. src/swagger-setup.ts +26 -22
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "typescript.tsdk": "node_modules\\typescript\\lib"
3
+ }
package-lock.json CHANGED
@@ -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": "3.1.0",
9141
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
9142
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
9143
- "dev": true,
9144
  "license": "MIT",
9145
  "dependencies": {
9146
- "yocto-queue": "^0.1.0"
9147
  },
9148
  "engines": {
9149
- "node": ">=10"
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": "0.1.0",
11983
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
11984
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
11985
- "dev": true,
11986
  "license": "MIT",
11987
  "engines": {
11988
- "node": ">=10"
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"
package.json CHANGED
@@ -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",
prisma/schema.prisma CHANGED
@@ -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
+ }
src/app.module.ts CHANGED
@@ -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 { AuthModule } from './modules/auth/auth.module';
 
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: 10,
22
  ttl: 60000, // 60 seconds in milliseconds
23
  },
24
  ]),
25
  CommonModule,
26
  BaseQueryCoreModule,
27
- UserCoreModule,
28
- AuthModule,
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,
src/core/base-query-core/base-query-core.service.ts CHANGED
@@ -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 returnObj = {};
84
 
85
- queryArr.map((qry, i) => {
86
- returnObj[i] = this.formatArray(qry, baseVal);
87
  });
88
 
89
- return { OR: returnObj };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
src/core/base-query-core/dto/base-query-core.dto.ts CHANGED
@@ -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 {
src/core/super-admin-core/dto/super-admin-core.dto.ts ADDED
@@ -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
+ }
src/core/super-admin-core/super-admin-core.module.ts ADDED
@@ -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 {}
src/core/super-admin-core/super-admin-core.service.ts ADDED
@@ -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
+ }
src/core/super-admin-credential-core/dto/super-admin-credential-core.dto.ts ADDED
@@ -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
+ }
src/core/super-admin-credential-core/super-admin-credential-core.module.ts ADDED
@@ -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 {}
src/core/super-admin-credential-core/super-admin-credential-core.service.ts ADDED
@@ -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
+ }
src/core/super-admin-session-core/dto/super-admin-session-core.dto.ts ADDED
@@ -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
+ }
src/core/super-admin-session-core/super-admin-session-core.module.ts ADDED
@@ -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 {}
src/core/super-admin-session-core/super-admin-session-core.service.ts ADDED
@@ -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
+ }
src/core/user-core/user-core.service.ts CHANGED
@@ -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, {
src/core/user-credential-core/user-credential-core.service.ts CHANGED
@@ -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, {
src/core/user-session-core/user-session-core.service.ts CHANGED
@@ -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, {
src/modules/auth/auth.controller.ts DELETED
@@ -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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/modules/auth/strategies/jwt.strategy.ts DELETED
@@ -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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/modules/super-admin/auth/auth.controller.ts ADDED
@@ -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
+ }
src/modules/super-admin/auth/auth.module.ts ADDED
@@ -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 {}
src/modules/super-admin/auth/auth.service.ts ADDED
@@ -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
+ }
src/modules/super-admin/auth/dto/login.dto.ts ADDED
@@ -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
+ }
src/modules/{auth → super-admin/auth}/dto/refresh-token.dto.ts RENAMED
@@ -1,12 +1,9 @@
1
- import { IsString } from 'class-validator';
2
  import { ApiProperty } from '@nestjs/swagger';
 
3
 
4
- export class RefreshTokenDto {
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
  }
src/modules/{auth/dto/login.dto.ts → super-admin/auth/dto/register.dto.ts} RENAMED
@@ -1,24 +1,31 @@
1
- import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
2
  import { ApiProperty } from '@nestjs/swagger';
 
 
 
 
 
 
 
3
 
4
- export class LoginDto {
5
- @ApiProperty({ example: 'user@example.com', required: false })
6
- @IsOptional()
7
  @IsEmail()
8
- email?: string;
 
9
 
10
- @ApiProperty({ example: '+1234567890', required: false })
11
- @IsOptional()
12
  @IsString()
13
- phone?: string;
 
 
14
 
15
- @ApiProperty({ example: '+1', required: false })
16
- @IsOptional()
17
  @IsString()
18
- code?: string;
 
19
 
20
- @ApiProperty({ example: 'password123' })
21
  @IsString()
22
- @MinLength(6)
23
- password: string;
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
  }
src/modules/super-admin/auth/guards/super-admin-auth.guard.ts ADDED
@@ -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
+ }
src/modules/super-admin/auth/strategies/super-admin-jwt.strategy.ts ADDED
@@ -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
+ }
src/modules/super-admin/super-admin.module.ts ADDED
@@ -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 {}
src/modules/user/auth/auth.controller.ts ADDED
@@ -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
+ }
src/modules/{auth → user/auth}/auth.module.ts RENAMED
@@ -1,29 +1,29 @@
1
  import { Module } from '@nestjs/common';
2
  import { JwtModule } from '@nestjs/jwt';
3
  import { PassportModule } from '@nestjs/passport';
4
- import { AuthService } from './auth.service';
5
- import { AuthController } from './auth.controller';
6
- import { JwtStrategy } from './strategies/jwt.strategy';
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: [AuthController],
26
- providers: [AuthService, JwtStrategy],
27
- exports: [AuthService],
28
  })
29
- export class AuthModule {}
 
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 {}
src/modules/{auth → user/auth}/auth.service.ts RENAMED
@@ -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 { LoginDto } from './dto/login.dto';
14
- import { RegisterDto } from './dto/register.dto';
15
- import { RefreshTokenDto } from './dto/refresh-token.dto';
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 AuthService {
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(registerDto: RegisterDto): Promise<{ message: string }> {
42
- const { email, phone, code, firstName, lastName, password } = registerDto;
 
 
 
 
 
 
 
43
 
44
  // Check if user already exists
45
- if (email) {
46
- const existingUser = await this.userCoreService.findByEmail(email);
47
- if (existingUser) {
48
- throw new ConflictException('User with this email already exists');
49
- }
50
- }
51
-
52
- if (phone && code) {
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
- // Create user credential
 
80
  await this.userCredentialCoreService.create({
81
  data: {
82
  userId: user.id,
@@ -84,28 +79,59 @@ export class AuthService {
84
  },
85
  });
86
 
87
- return { message: 'User registered successfully' };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
 
90
- async login(loginDto: LoginDto, request: Request): Promise<UserSessionType> {
91
- const { email, phone, code, password } = loginDto;
 
 
 
 
 
 
 
92
 
93
  // Find user
94
- let user: User | null = null;
95
- if (email) {
96
- user = await this.userCoreService.findByEmail(email);
97
- } else if (phone && code) {
98
- user = await this.userCoreService.findByPhone(phone, code);
99
- }
100
 
101
  if (!user) {
102
  throw new UnauthorizedException('Invalid credentials');
103
  }
104
 
105
  // Get user credential
106
- const credential = await this.userCredentialCoreService.findFirst({
107
- where: { userId: user.id, isDeleted: false },
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
- const session = await this.userSessionCoreService.create({
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
- lastLogin: new Date(),
149
- updatedAt: new Date(),
150
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  });
152
 
153
- return { user, session };
154
  }
155
 
156
- async refreshToken(refreshTokenDto: RefreshTokenDto): Promise<{
157
  accessToken: string;
158
  refreshToken: string;
159
  }> {
160
- const { accessToken, refreshToken } = refreshTokenDto;
161
 
162
- let validateAccessToken: any = null;
163
  let validateRefreshToken: any = null;
164
 
165
  try {
166
- validateAccessToken = await this.jwtService.verifyAsync(
167
- accessToken,
168
- accessTokenVerifySettings,
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
- throw new BadRequestException('Access token is still valid');
195
- }
 
 
196
 
197
- async performJWTStrategy(params: {
198
- request: Request & { headers: { authorization: string } };
199
- user: User;
200
- }): Promise<UserSessionType> {
201
- const { request, user } = params;
 
 
202
 
203
- const token = request.headers.authorization?.replace('Bearer ', '');
 
204
 
205
- const session = await this.userSessionCoreService.findFirst({
 
206
  where: {
207
  userId: user.id,
208
- accessToken: token,
209
- isDeleted: false,
 
 
 
210
  },
211
  });
212
 
213
- if (!session) {
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
- refreshPayload,
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
  }
src/modules/user/auth/dto/login.dto.ts ADDED
@@ -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
+ }
src/modules/user/auth/dto/refresh-token.dto.ts ADDED
@@ -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
+ }
src/modules/{auth → user/auth}/dto/register.dto.ts RENAMED
@@ -1,34 +1,41 @@
1
- import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
2
  import { ApiProperty } from '@nestjs/swagger';
 
 
 
 
 
 
 
3
 
4
- export class RegisterDto {
5
- @ApiProperty({ example: 'user@example.com', required: false })
6
- @IsOptional()
7
  @IsEmail()
8
- email?: string;
 
9
 
10
- @ApiProperty({ example: '+1234567890', required: false })
11
- @IsOptional()
12
  @IsString()
13
- phone?: string;
 
 
14
 
15
- @ApiProperty({ example: '+1', required: false })
16
- @IsOptional()
17
  @IsString()
18
- code?: string;
 
19
 
20
- @ApiProperty({ example: 'John' })
21
  @IsString()
22
- @MinLength(2)
23
- firstName: string;
24
 
25
- @ApiProperty({ example: 'Doe' })
26
  @IsString()
27
- @MinLength(2)
28
- lastName: string;
29
 
30
- @ApiProperty({ example: 'password123' })
31
  @IsString()
32
- @MinLength(6)
33
- password: string;
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
  }
src/modules/{auth/guards/jwt-auth.guard.ts → user/auth/guards/user-auth.guard.ts} RENAMED
@@ -1,15 +1,18 @@
1
- import { ExecutionContext, Injectable } from '@nestjs/common';
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 JwtAuthGuard extends AuthGuard('jwt') {
8
- constructor(private readonly reflector: Reflector) {
9
  super();
10
  }
11
 
12
- canActivate(context: ExecutionContext) {
 
 
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(),
src/modules/user/auth/strategies/user-jwt.strategy.ts ADDED
@@ -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
+ }
src/modules/user/user.module.ts ADDED
@@ -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 {}
src/shared/decorators/get-session.decorator.ts ADDED
@@ -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
+ );
src/shared/decorators/get-super-admin.decorator.ts ADDED
@@ -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
+ );
src/shared/keys/auth.keys.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { JwtSignOptions, JwtVerifyOptions } from '@nestjs/jwt';
 
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 || '24h',
17
- refreshTokenExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
 
 
18
  };
19
 
20
- export const refreshTokenSignSettings: JwtSignOptions = {
21
  secret: jwtAuthConstants.refreshTokenSecret,
22
  expiresIn: jwtAuthConstants.refreshTokenExpiresIn,
23
  };
24
 
25
- export const accessTokenSignSettings: JwtSignOptions = {
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
  };
src/shared/keys/super-admin-credential.keys.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const SuperAdminCredentialMessages = {
2
+ NOT_FOUND: 'Super Admin Credential not found',
3
+ DELETED: 'Super Admin Credential deleted successfully',
4
+ };
src/shared/keys/super-admin-session.keys.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const SuperAdminSessionMessages = {
2
+ NOT_FOUND: 'Super Admin Session not found',
3
+ DELETED: 'Super Admin Session deleted successfully',
4
+ };
src/shared/keys/super-admin.keys.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const SuperAdminMessages = {
2
+ NOT_FOUND: 'Super Admin not found',
3
+ DELETED: 'Super Admin deleted successfully',
4
+ };
src/shared/libs/include-filter.helper.ts ADDED
@@ -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
+ }
src/shared/libs/prisma-base.repository.ts CHANGED
@@ -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
  }
src/shared/modules/prisma/safe-prisma-call.ts CHANGED
@@ -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 prismaConcurrencyLimit = pLimit(cpuCores * 10);
7
 
8
- console.log(
9
- `[safePrismaCall] Limiting Prisma concurrency to ${
10
- cpuCores * 10
11
- } (based on ${cpuCores} CPU cores)`,
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
  }
src/shared/types/super-admin-session.type.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { SuperAdmin, SuperAdminSession } from '@prisma/client';
2
+
3
+ export type SuperAdminSessionType = {
4
+ superAdmin: SuperAdmin;
5
+ session: SuperAdminSession;
6
+ };
src/swagger-setup.ts CHANGED
@@ -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 configService = app.get(ConfigService);
8
- const swaggerPassword = configService.get('SWAGGER_PASSWORD');
9
 
10
- if (swaggerPassword) {
11
- app.use(
12
- ['/api'],
13
- basicAuth({
14
- challenge: true,
15
- users: { admin: swaggerPassword },
16
- }),
17
- );
18
- }
19
 
20
  const config = new DocumentBuilder()
21
- .setTitle('API Documentation')
22
- .setDescription('The API description')
23
- .setVersion('1.0')
24
- .addBearerAuth({
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
  },