@simple-auth-kit/cli 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  3. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  4. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  5. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  6. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  7. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  8. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  9. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  10. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  11. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  12. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  13. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  14. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  15. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  16. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  17. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  18. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  19. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  20. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  23. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  26. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  27. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  28. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  29. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  30. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  31. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  32. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  33. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  34. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  35. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
  36. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  37. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
  38. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  39. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  40. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  41. package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
  42. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  43. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
  44. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  45. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  46. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
  47. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  48. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  49. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  50. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
  53. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  54. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  56. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  57. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  58. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  59. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  62. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  63. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  64. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  65. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  66. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  71. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  79. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  80. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  81. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  82. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  83. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  84. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  85. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  86. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  87. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  88. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  89. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  90. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
  91. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  92. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  93. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  94. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
  95. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  96. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  97. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  98. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  99. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  100. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
  101. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  102. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  115. package/registry.json +6 -4
  116. package/simple-auth-kit.ts +582 -124
  117. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  118. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  119. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  120. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  121. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  122. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  123. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  124. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  125. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  126. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  127. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  128. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  130. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  131. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  132. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -0,0 +1,1064 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ BadRequestException,
4
+ ConflictException,
5
+ HttpException,
6
+ HttpStatus,
7
+ Inject,
8
+ Injectable,
9
+ UnauthorizedException,
10
+ } from "@nestjs/common";
11
+ import { hashPassword, verifyPassword } from "@/lib/auth/core/crypto.js";
12
+ import {
13
+ buildAuthorizationUrl,
14
+ APPLE_OIDC_PROVIDER,
15
+ completeOAuthLogin,
16
+ exchangeCodeForTokens,
17
+ GOOGLE_OIDC_PROVIDER,
18
+ OAuthProviderDescriptor,
19
+ signAppleClientSecret,
20
+ verifyIdTokenAndExtractProfile,
21
+ } from "@/lib/auth/core/oauth.js";
22
+ import {
23
+ requestPasswordReset as coreRequestPasswordReset,
24
+ resetPassword as coreResetPassword,
25
+ } from "@/lib/auth/core/password-reset.js";
26
+ import {
27
+ checkRateLimit,
28
+ type RateLimitDeps,
29
+ } from "@/lib/auth/core/rate-limit.js";
30
+ import {
31
+ blockUser,
32
+ createSession,
33
+ deactivateUser,
34
+ revokeAccessToken,
35
+ revokeAllSessionsForUser,
36
+ revokeOtherSessionsForUser,
37
+ revokeSession,
38
+ rotateRefreshToken,
39
+ } from "@/lib/auth/core/session-policy.js";
40
+ import {
41
+ signAccessToken,
42
+ signRefreshToken,
43
+ signTwoFactorChallengeToken,
44
+ verifyRefreshToken,
45
+ verifyTwoFactorChallengeToken,
46
+ } from "@/lib/auth/core/token-service.js";
47
+ import {
48
+ buildTotpProvisioningUri,
49
+ generateBackupCodes,
50
+ generateTotpSecret,
51
+ verifyTotpCode,
52
+ } from "@/lib/auth/core/two-factor.js";
53
+ import type { Revoker } from "@/lib/auth/core/types.js";
54
+ import { AUTH_CONFIG, AuthConfig } from "../auth.config.js";
55
+ import {
56
+ AuditLogEntry,
57
+ AuditLogListFilter,
58
+ AuditLogRepository,
59
+ toAuditLogEntry,
60
+ } from "../repositories/audit-log.repository.js";
61
+ import { PrismaClient } from "../../generated/prisma/client.js";
62
+ import {
63
+ CountryInput,
64
+ CountryListFilter,
65
+ CountryListResult,
66
+ CountryRepository,
67
+ CountrySummary,
68
+ } from "../repositories/country.repository.js";
69
+ import {
70
+ CustomerInput,
71
+ CustomerListFilter,
72
+ CustomerListResult,
73
+ CustomerRepository,
74
+ CustomerSummary,
75
+ } from "../repositories/customer.repository.js";
76
+ import { KeyProviderService } from "../key-provider.js";
77
+ import {
78
+ LanguageInput,
79
+ LanguageListFilter,
80
+ LanguageListResult,
81
+ LanguageRepository,
82
+ LanguageSummary,
83
+ } from "../repositories/language.repository.js";
84
+ import { OAuthRepository } from "../repositories/oauth.repository.js";
85
+ import { PasswordResetRepository } from "../repositories/password-reset.repository.js";
86
+ import type { Paginated } from "../pagination.js";
87
+ import { RATE_LIMIT_STORE } from "../rate-limit.store.js";
88
+ import {
89
+ PermissionInput,
90
+ PermissionSummary,
91
+ RbacRepository,
92
+ RoleSummary,
93
+ toUserSummary,
94
+ UserListFilter,
95
+ UserListResult,
96
+ UserSummary,
97
+ } from "../repositories/rbac.repository.js";
98
+ import { SessionRepository } from "../repositories/session.repository.js";
99
+ import { TwoFactorRepository } from "../repositories/two-factor.repository.js";
100
+ import { toId, toIdOrNull } from "../id.helper.js";
101
+
102
+ export interface AuthTokens {
103
+ accessToken: string;
104
+ refreshToken: string;
105
+ sessionId: string;
106
+ }
107
+
108
+ export interface TwoFactorChallenge {
109
+ twoFactorRequired: true;
110
+ challengeToken: string;
111
+ }
112
+
113
+ /** `PATCH /auth/me`'s return shape — the same across every combo, workspace-scoped or not, since a profile isn't a workspace concept. */
114
+ export interface SelfProfile {
115
+ id: string;
116
+ email: string;
117
+ firstName: string | null;
118
+ lastName: string | null;
119
+ displayName: string | null;
120
+ phone: string | null;
121
+ username: string | null;
122
+ photo: string | null;
123
+ createdAt: string;
124
+ }
125
+
126
+ @Injectable()
127
+ export class AuthService {
128
+ constructor(
129
+ @Inject(PrismaClient) private readonly prisma: PrismaClient,
130
+ @Inject(SessionRepository) private readonly sessions: SessionRepository,
131
+ @Inject(KeyProviderService) private readonly keys: KeyProviderService,
132
+ @Inject(RATE_LIMIT_STORE) private readonly rateLimit: RateLimitDeps,
133
+ @Inject(AuditLogRepository) private readonly auditLog: AuditLogRepository,
134
+ @Inject(RbacRepository) private readonly rbac: RbacRepository,
135
+ @Inject(TwoFactorRepository)
136
+ private readonly twoFactor: TwoFactorRepository,
137
+ @Inject(OAuthRepository) private readonly oauth: OAuthRepository,
138
+ @Inject(PasswordResetRepository)
139
+ private readonly passwordReset: PasswordResetRepository,
140
+ @Inject(CountryRepository) private readonly countries: CountryRepository,
141
+ @Inject(LanguageRepository) private readonly languages: LanguageRepository,
142
+ @Inject(CustomerRepository) private readonly customers: CustomerRepository,
143
+ @Inject(AUTH_CONFIG) private readonly config: AuthConfig,
144
+ ) {}
145
+
146
+ async signup(input: {
147
+ email: string;
148
+ password: string;
149
+ firstName?: string;
150
+ lastName?: string;
151
+ displayName?: string;
152
+ phone?: string;
153
+ username?: string;
154
+ userAgent?: string;
155
+ ip?: string;
156
+ }): Promise<AuthTokens> {
157
+ const existing = await this.prisma.user.findUnique({
158
+ where: { email: input.email },
159
+ });
160
+ if (existing) throw new ConflictException("email already registered");
161
+
162
+ const passwordHash = await hashPassword(input.password);
163
+ const user = await this.prisma.user.create({
164
+ data: {
165
+ email: input.email,
166
+ passwordHash,
167
+ firstName: input.firstName,
168
+ lastName: input.lastName,
169
+ displayName: input.displayName,
170
+ phone: input.phone,
171
+ username: input.username,
172
+ },
173
+ });
174
+ // Signup default = whichever roles are flagged `isDefault` in the database, not a code literal.
175
+ await this.rbac.assignDefaultRoles(user.id);
176
+ return this.issueSessionTokens(user, {
177
+ userAgent: input.userAgent,
178
+ ip: input.ip,
179
+ });
180
+ }
181
+
182
+ async login(input: {
183
+ identifier: string;
184
+ password: string;
185
+ userAgent?: string;
186
+ ip?: string;
187
+ }): Promise<AuthTokens | TwoFactorChallenge> {
188
+ const { allowed } = await checkRateLimit(
189
+ this.rateLimit,
190
+ "login",
191
+ input.identifier,
192
+ );
193
+ if (!allowed)
194
+ throw new HttpException(
195
+ "too many login attempts",
196
+ HttpStatus.TOO_MANY_REQUESTS,
197
+ );
198
+
199
+ const user = await this.prisma.user.findFirst({
200
+ where: {
201
+ OR: [
202
+ { email: input.identifier },
203
+ { username: input.identifier },
204
+ { phone: input.identifier },
205
+ ],
206
+ },
207
+ });
208
+ if (
209
+ !user ||
210
+ user.blocked ||
211
+ !user.isActive ||
212
+ user.isDeleted ||
213
+ !user.passwordHash
214
+ )
215
+ throw new UnauthorizedException("invalid credentials");
216
+
217
+ const valid = await verifyPassword(user.passwordHash, input.password);
218
+ if (!valid) throw new UnauthorizedException("invalid credentials");
219
+
220
+ if (user.twoFactorEnabled) {
221
+ const key = await this.keys.getActiveKey();
222
+ const { token } = await signTwoFactorChallengeToken(
223
+ { activeKey: key },
224
+ user.id.toString(),
225
+ ); // core-facing: sub must be string
226
+ return { twoFactorRequired: true, challengeToken: token };
227
+ }
228
+
229
+ return this.issueSessionTokens(user, {
230
+ userAgent: input.userAgent,
231
+ ip: input.ip,
232
+ });
233
+ }
234
+
235
+ async loginTwoFactor(input: {
236
+ challengeToken: string;
237
+ code: string;
238
+ userAgent?: string;
239
+ ip?: string;
240
+ }): Promise<AuthTokens> {
241
+ const { sub } = await verifyTwoFactorChallengeToken(
242
+ { secret: this.keys.secret },
243
+ input.challengeToken,
244
+ );
245
+
246
+ const user = await this.prisma.user.findUnique({
247
+ where: { id: toId(sub) },
248
+ });
249
+ if (
250
+ !user ||
251
+ user.blocked ||
252
+ !user.isActive ||
253
+ user.isDeleted ||
254
+ !user.twoFactorEnabled ||
255
+ !user.twoFactorSecret
256
+ )
257
+ throw new UnauthorizedException("invalid credentials");
258
+
259
+ const validTotp = verifyTotpCode(user.twoFactorSecret, input.code);
260
+ const validBackup =
261
+ !validTotp &&
262
+ (await this.twoFactor.consumeBackupCode(user.id, input.code));
263
+ if (!validTotp && !validBackup) {
264
+ await this.sessions.appendAuditEvent({
265
+ type: "two_factor_challenge_failed",
266
+ userId: user.id.toString(),
267
+ }); // core-facing: string
268
+ throw new UnauthorizedException("invalid two-factor code");
269
+ }
270
+
271
+ return this.issueSessionTokens(user, {
272
+ userAgent: input.userAgent,
273
+ ip: input.ip,
274
+ });
275
+ }
276
+
277
+ async enrollTwoFactor(
278
+ userId: string,
279
+ ): Promise<{ secret: string; provisioningUri: string }> {
280
+ const idBig = toId(userId);
281
+ const user = await this.prisma.user.findUniqueOrThrow({
282
+ where: { id: idBig },
283
+ });
284
+ const secret = generateTotpSecret();
285
+ // Not enabled yet — confirmTwoFactor() flips that.
286
+ await this.prisma.user.update({
287
+ where: { id: idBig },
288
+ data: { twoFactorSecret: secret, twoFactorEnabled: false },
289
+ });
290
+ return {
291
+ secret,
292
+ provisioningUri: buildTotpProvisioningUri({
293
+ secret,
294
+ accountName: user.email,
295
+ issuer: this.config.twoFactorIssuer,
296
+ }),
297
+ };
298
+ }
299
+
300
+ async confirmTwoFactor(
301
+ userId: string,
302
+ code: string,
303
+ ): Promise<{ backupCodes: string[] }> {
304
+ const idBig = toId(userId);
305
+ const user = await this.prisma.user.findUniqueOrThrow({
306
+ where: { id: idBig },
307
+ });
308
+ if (!user.twoFactorSecret)
309
+ throw new BadRequestException("call enrollTwoFactor first");
310
+ if (!verifyTotpCode(user.twoFactorSecret, code))
311
+ throw new UnauthorizedException("invalid two-factor code");
312
+
313
+ const backupCodes = generateBackupCodes();
314
+ await this.twoFactor.saveBackupCodes(
315
+ idBig,
316
+ backupCodes.map((c) => c.hash),
317
+ );
318
+ await this.prisma.user.update({
319
+ where: { id: idBig },
320
+ data: { twoFactorEnabled: true },
321
+ });
322
+ await this.sessions.appendAuditEvent({
323
+ type: "two_factor_enabled",
324
+ userId,
325
+ });
326
+ return { backupCodes: backupCodes.map((c) => c.code) };
327
+ }
328
+
329
+ async disableTwoFactor(userId: string, code: string): Promise<void> {
330
+ const idBig = toId(userId);
331
+ const user = await this.prisma.user.findUniqueOrThrow({
332
+ where: { id: idBig },
333
+ });
334
+ if (!user.twoFactorEnabled || !user.twoFactorSecret)
335
+ throw new BadRequestException("two-factor is not enabled");
336
+
337
+ const validTotp = verifyTotpCode(user.twoFactorSecret, code);
338
+ const validBackup =
339
+ !validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
340
+ if (!validTotp && !validBackup)
341
+ throw new UnauthorizedException("invalid two-factor code");
342
+
343
+ await this.prisma.user.update({
344
+ where: { id: idBig },
345
+ data: { twoFactorEnabled: false, twoFactorSecret: null },
346
+ });
347
+ await this.twoFactor.clearBackupCodes(idBig);
348
+ await this.sessions.appendAuditEvent({
349
+ type: "two_factor_disabled",
350
+ userId,
351
+ });
352
+ }
353
+
354
+ // Always succeeds from the caller's point of view — an unknown email or a throttled request
355
+ // looks identical, to avoid enumeration.
356
+ async requestPasswordReset(email: string): Promise<void> {
357
+ const user = await this.prisma.user.findUnique({ where: { email } });
358
+ if (!user || user.isDeleted) return;
359
+
360
+ const { allowed } = await checkRateLimit(
361
+ this.rateLimit,
362
+ "password-reset",
363
+ email,
364
+ );
365
+ if (!allowed) return;
366
+
367
+ const { token } = await coreRequestPasswordReset(
368
+ this.passwordReset,
369
+ user.id.toString(),
370
+ );
371
+ if (this.config.sendPasswordResetEmail)
372
+ await this.config.sendPasswordResetEmail(email, token);
373
+ }
374
+
375
+ async resetPassword(token: string, newPassword: string): Promise<void> {
376
+ const { userId } = await coreResetPassword(
377
+ this.passwordReset,
378
+ token,
379
+ newPassword,
380
+ );
381
+ // The old password is no longer trusted, so neither are its sessions.
382
+ await revokeAllSessionsForUser(this.sessions, userId, { userId });
383
+ }
384
+
385
+ async startOAuth(provider: string): Promise<{ url: string }> {
386
+ const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
387
+ "base64url",
388
+ );
389
+
390
+ if (provider === "google") {
391
+ const creds = this.config.oauthProviders.google;
392
+ if (!creds)
393
+ throw new BadRequestException("google OAuth is not configured");
394
+ return {
395
+ url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, {
396
+ clientId: creds.clientId,
397
+ redirectUri: creds.redirectUri,
398
+ state,
399
+ }),
400
+ };
401
+ }
402
+ if (provider === "apple") {
403
+ const creds = this.config.oauthProviders.apple;
404
+ if (!creds)
405
+ throw new BadRequestException("apple OAuth is not configured");
406
+ return {
407
+ url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, {
408
+ clientId: creds.clientId,
409
+ redirectUri: creds.redirectUri,
410
+ state,
411
+ }),
412
+ };
413
+ }
414
+ throw new BadRequestException(`unknown OAuth provider "${provider}"`);
415
+ }
416
+
417
+ async completeOAuthCallback(
418
+ provider: string,
419
+ code: string,
420
+ _state: string,
421
+ ): Promise<AuthTokens> {
422
+ let providerDescriptor: OAuthProviderDescriptor;
423
+ let clientId: string;
424
+ let idToken: string;
425
+
426
+ if (provider === "google") {
427
+ const creds = this.config.oauthProviders.google;
428
+ if (!creds)
429
+ throw new BadRequestException("google OAuth is not configured");
430
+ providerDescriptor = GOOGLE_OIDC_PROVIDER;
431
+ clientId = creds.clientId;
432
+ idToken = (
433
+ await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
434
+ clientId: creds.clientId,
435
+ clientSecret: creds.clientSecret,
436
+ redirectUri: creds.redirectUri,
437
+ code,
438
+ })
439
+ ).idToken;
440
+ } else if (provider === "apple") {
441
+ const creds = this.config.oauthProviders.apple;
442
+ if (!creds)
443
+ throw new BadRequestException("apple OAuth is not configured");
444
+ providerDescriptor = APPLE_OIDC_PROVIDER;
445
+ clientId = creds.clientId;
446
+ const clientSecret = await signAppleClientSecret({
447
+ teamId: creds.teamId,
448
+ clientId: creds.clientId,
449
+ keyId: creds.keyId,
450
+ privateKeyPem: creds.privateKey,
451
+ });
452
+ idToken = (
453
+ await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, {
454
+ clientId: creds.clientId,
455
+ clientSecret,
456
+ redirectUri: creds.redirectUri,
457
+ code,
458
+ })
459
+ ).idToken;
460
+ } else {
461
+ throw new BadRequestException(`unknown OAuth provider "${provider}"`);
462
+ }
463
+
464
+ const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
465
+ clientId,
466
+ idToken,
467
+ });
468
+ const { userId } = await completeOAuthLogin(this.oauth, {
469
+ provider,
470
+ profile,
471
+ });
472
+
473
+ const user = await this.prisma.user.findUniqueOrThrow({
474
+ where: { id: toId(userId) },
475
+ });
476
+ if (user.blocked || !user.isActive || user.isDeleted)
477
+ throw new UnauthorizedException("account is blocked");
478
+ // Guarded on "holds no roles" rather than "was just created", so this can never re-add a
479
+ // role an administrator revoked from an existing user.
480
+ if (!(await this.rbac.hasAnyRole(user.id)))
481
+ await this.rbac.assignDefaultRoles(user.id);
482
+ return this.issueSessionTokens(user, { provider });
483
+ }
484
+
485
+ async listAuditLog(
486
+ filter: AuditLogListFilter,
487
+ ): Promise<Paginated<AuditLogEntry>> {
488
+ const { items, meta } = await this.auditLog.list(filter);
489
+ return { items: items.map(toAuditLogEntry), meta };
490
+ }
491
+
492
+ async getTwoFactorStatus(
493
+ userId: string,
494
+ ): Promise<{ twoFactorEnabled: boolean }> {
495
+ const user = await this.prisma.user.findUniqueOrThrow({
496
+ where: { id: toId(userId) },
497
+ select: { twoFactorEnabled: true },
498
+ });
499
+ return { twoFactorEnabled: user.twoFactorEnabled };
500
+ }
501
+
502
+ async listActiveSessions(
503
+ userId: string,
504
+ ): Promise<
505
+ Array<{
506
+ id: string;
507
+ createdAt: string;
508
+ expiresAt: string;
509
+ provider?: string;
510
+ userAgent?: string;
511
+ ip?: string;
512
+ }>
513
+ > {
514
+ const rows = await this.prisma.session.findMany({
515
+ where: {
516
+ userId: toId(userId),
517
+ isRevoked: false,
518
+ expiresAt: { gt: new Date() },
519
+ },
520
+ orderBy: { createdAt: "desc" },
521
+ });
522
+ return rows.map((row) => ({
523
+ id: row.id.toString(),
524
+ createdAt: row.createdAt.toISOString(),
525
+ expiresAt: row.expiresAt.toISOString(),
526
+ provider: row.provider ?? undefined,
527
+ userAgent: row.userAgent ?? undefined,
528
+ ip: row.ip ?? undefined,
529
+ }));
530
+ }
531
+
532
+ async listUsers(filter: UserListFilter): Promise<UserListResult> {
533
+ const { items, meta } = await this.rbac.listUsers(filter);
534
+ return { items: items.map(toUserSummary), meta };
535
+ }
536
+
537
+ async getUser(userId: string): Promise<UserSummary> {
538
+ return this.rbac.getUser(userId);
539
+ }
540
+
541
+ async createUser(
542
+ input: {
543
+ email: string;
544
+ password: string;
545
+ firstName?: string;
546
+ lastName?: string;
547
+ displayName?: string;
548
+ phone?: string;
549
+ username?: string;
550
+ photo?: string;
551
+ dob?: string;
552
+ gender?: string;
553
+ joinedDate?: string;
554
+ isActive?: boolean;
555
+ roles?: string[];
556
+ },
557
+ actorUserId: string | null,
558
+ ): Promise<UserSummary> {
559
+ const passwordHash = await hashPassword(input.password);
560
+ return this.rbac.createUser({ ...input, passwordHash }, actorUserId);
561
+ }
562
+
563
+ async updateUser(
564
+ userId: string,
565
+ input: {
566
+ firstName?: string | null;
567
+ lastName?: string | null;
568
+ displayName?: string | null;
569
+ phone?: string | null;
570
+ username?: string | null;
571
+ photo?: string | null;
572
+ dob?: string | null;
573
+ gender?: string | null;
574
+ joinedDate?: string;
575
+ },
576
+ actorUserId: string | null,
577
+ ): Promise<UserSummary> {
578
+ return this.rbac.updateUser(userId, input, actorUserId);
579
+ }
580
+
581
+ async deleteUser(
582
+ userId: string,
583
+ actorUserId: string | null,
584
+ reason?: string,
585
+ ): Promise<void> {
586
+ await this.rbac.deleteUser(userId, actorUserId, reason);
587
+ }
588
+
589
+ async listPermissions(
590
+ activeOnly?: boolean,
591
+ ): Promise<{ permissions: PermissionSummary[] }> {
592
+ return { permissions: await this.rbac.listPermissions(activeOnly) };
593
+ }
594
+
595
+ async definePermission(
596
+ input: PermissionInput,
597
+ actorUserId: string | null,
598
+ ): Promise<PermissionSummary> {
599
+ return this.rbac.upsertPermission(input, actorUserId);
600
+ }
601
+
602
+ async listRoles(activeOnly?: boolean): Promise<{ roles: RoleSummary[] }> {
603
+ return { roles: await this.rbac.listRoles(activeOnly) };
604
+ }
605
+
606
+ async createRole(
607
+ input: {
608
+ slug: string;
609
+ name?: string;
610
+ displayName?: string;
611
+ description?: string | null;
612
+ isDefault?: boolean;
613
+ isActive?: boolean;
614
+ },
615
+ actorUserId: string | null,
616
+ ): Promise<RoleSummary> {
617
+ return this.rbac.createRole(input, actorUserId);
618
+ }
619
+
620
+ async updateRole(
621
+ roleId: string,
622
+ input: {
623
+ name?: string;
624
+ displayName?: string;
625
+ description?: string | null;
626
+ isDefault?: boolean;
627
+ isActive?: boolean;
628
+ },
629
+ actorUserId: string | null,
630
+ ): Promise<RoleSummary> {
631
+ return this.rbac.updateRole(roleId, input, actorUserId);
632
+ }
633
+
634
+ async deleteRole(
635
+ roleId: string,
636
+ actorUserId: string | null,
637
+ reason?: string,
638
+ ): Promise<void> {
639
+ await this.rbac.deleteRole(roleId, actorUserId, reason);
640
+ }
641
+
642
+ async attachPermissionToRole(
643
+ roleId: string,
644
+ permissionSlug: string,
645
+ actorUserId: string | null,
646
+ ): Promise<void> {
647
+ await this.rbac.attachPermissionToRole(roleId, permissionSlug, actorUserId);
648
+ }
649
+
650
+ async detachPermissionFromRole(
651
+ roleId: string,
652
+ permissionSlug: string,
653
+ ): Promise<void> {
654
+ await this.rbac.detachPermissionFromRole(roleId, permissionSlug);
655
+ }
656
+
657
+ async assignRole(userId: string, roleSlug: string): Promise<void> {
658
+ await this.rbac.assignRoleToUser(userId, roleSlug);
659
+ await this.sessions.appendAuditEvent({
660
+ type: "role_assigned",
661
+ userId,
662
+ role: roleSlug,
663
+ });
664
+ }
665
+
666
+ async revokeRole(userId: string, roleSlug: string): Promise<void> {
667
+ await this.rbac.revokeRoleFromUser(userId, roleSlug);
668
+ await this.sessions.appendAuditEvent({
669
+ type: "role_revoked",
670
+ userId,
671
+ role: roleSlug,
672
+ });
673
+ }
674
+
675
+ async grantPermission(
676
+ userId: string,
677
+ permissionSlug: string,
678
+ actorUserId: string | null,
679
+ ): Promise<void> {
680
+ await this.rbac.grantPermissionToUser(userId, permissionSlug, actorUserId);
681
+ await this.sessions.appendAuditEvent({
682
+ type: "permission_granted",
683
+ userId,
684
+ permission: permissionSlug,
685
+ });
686
+ }
687
+
688
+ async revokePermission(
689
+ userId: string,
690
+ permissionSlug: string,
691
+ ): Promise<void> {
692
+ await this.rbac.revokePermissionFromUser(userId, permissionSlug);
693
+ await this.sessions.appendAuditEvent({
694
+ type: "permission_revoked",
695
+ userId,
696
+ permission: permissionSlug,
697
+ });
698
+ }
699
+
700
+ async refresh(refreshToken: string): Promise<Omit<AuthTokens, "sessionId">> {
701
+ const key = await this.keys.getActiveKey();
702
+ const presented = await verifyRefreshToken(
703
+ { secret: this.keys.secret },
704
+ refreshToken,
705
+ );
706
+ const { session, nextJti } = await rotateRefreshToken(
707
+ this.sessions,
708
+ presented,
709
+ );
710
+
711
+ const user = await this.prisma.user.findUnique({
712
+ where: { id: toId(session.userId) },
713
+ });
714
+ if (!user || user.blocked || !user.isActive || user.isDeleted)
715
+ throw new UnauthorizedException("invalid credentials");
716
+
717
+ // Identity only. Authorization is resolved from the database on every request (AuthzGuard),
718
+ // so a token issued before a grant is as authoritative as one issued after it.
719
+ const access = await signAccessToken(
720
+ { activeKey: key },
721
+ { sub: user.id.toString(), sessionId: session.id },
722
+ { ttlSeconds: this.config.accessTokenTtlSeconds },
723
+ );
724
+ const refresh = await signRefreshToken(
725
+ { activeKey: key },
726
+ {
727
+ sub: user.id.toString(),
728
+ sessionId: session.id,
729
+ sv: session.sessionVersion,
730
+ },
731
+ { jti: nextJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
732
+ );
733
+ return { accessToken: access.token, refreshToken: refresh.token };
734
+ }
735
+
736
+ async logout(
737
+ sessionId: string,
738
+ accessJti: string,
739
+ accessRemainingTtlSeconds: number,
740
+ revoker?: Revoker,
741
+ ): Promise<void> {
742
+ await revokeSession(this.sessions, sessionId, revoker);
743
+ await revokeAccessToken(
744
+ this.sessions,
745
+ accessJti,
746
+ accessRemainingTtlSeconds,
747
+ );
748
+ }
749
+
750
+ async logoutAll(userId: string, revoker?: Revoker): Promise<void> {
751
+ await revokeAllSessionsForUser(this.sessions, userId, revoker);
752
+ }
753
+
754
+ async logoutOthers(
755
+ userId: string,
756
+ keepSessionId: string,
757
+ revoker?: Revoker,
758
+ ): Promise<void> {
759
+ await revokeOtherSessionsForUser(
760
+ this.sessions,
761
+ userId,
762
+ keepSessionId,
763
+ revoker,
764
+ );
765
+ }
766
+
767
+ async changePassword(
768
+ userId: string,
769
+ currentSessionId: string,
770
+ input: { currentPassword: string; newPassword: string },
771
+ ): Promise<void> {
772
+ const userIdBig = toId(userId);
773
+ const user = await this.prisma.user.findUnique({
774
+ where: { id: userIdBig },
775
+ });
776
+ if (
777
+ !user ||
778
+ user.blocked ||
779
+ !user.isActive ||
780
+ user.isDeleted ||
781
+ !user.passwordHash
782
+ )
783
+ throw new UnauthorizedException("invalid credentials");
784
+
785
+ const valid = await verifyPassword(
786
+ user.passwordHash,
787
+ input.currentPassword,
788
+ );
789
+ if (!valid) throw new UnauthorizedException("invalid credentials");
790
+
791
+ const passwordHash = await hashPassword(input.newPassword);
792
+ await this.prisma.user.update({
793
+ where: { id: userIdBig },
794
+ data: { passwordHash, updatedBy: userIdBig },
795
+ });
796
+ // The old password is no longer trusted everywhere else it's signed in — but leave the
797
+ // session making this very call alone, the same courtesy `logoutOthers` extends.
798
+ await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
799
+ userId,
800
+ });
801
+ }
802
+
803
+ /** Backs both `GET`- and `PATCH /auth/me` — roles/blocked/lastLogin are an admin's business, not this endpoint's. */
804
+ private toSelfProfile(user: UserSummary): SelfProfile {
805
+ return {
806
+ id: user.id,
807
+ email: user.email,
808
+ firstName: user.firstName,
809
+ lastName: user.lastName,
810
+ displayName: user.displayName,
811
+ phone: user.phone,
812
+ username: user.username,
813
+ photo: user.photo,
814
+ createdAt: user.createdAt,
815
+ };
816
+ }
817
+
818
+ /** Self-service — no `users:manage` permission required, callable by anyone on their own row. */
819
+ async getProfile(userId: string): Promise<SelfProfile> {
820
+ return this.toSelfProfile(await this.rbac.getUser(userId));
821
+ }
822
+
823
+ /**
824
+ * Self-service — no `users:manage` permission required, the caller's own row only. Reuses the
825
+ * admin update path (the actor named is the caller themselves), then trims the response to
826
+ * `SelfProfile`: roles/blocked/lastLogin are an admin's business, not this endpoint's.
827
+ */
828
+ async updateProfile(
829
+ userId: string,
830
+ input: {
831
+ firstName?: string | null;
832
+ lastName?: string | null;
833
+ displayName?: string | null;
834
+ phone?: string | null;
835
+ username?: string | null;
836
+ photo?: string | null;
837
+ },
838
+ ): Promise<SelfProfile> {
839
+ return this.toSelfProfile(
840
+ await this.rbac.updateUser(userId, input, userId),
841
+ );
842
+ }
843
+
844
+ async block(userId: string, revoker?: Revoker): Promise<void> {
845
+ await this.prisma.user.update({
846
+ where: { id: toId(userId) },
847
+ data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) },
848
+ });
849
+ await blockUser(this.sessions, userId, revoker);
850
+ // Belt and braces: the block is already enforced on the authentication path, but drop the
851
+ // cache entry anyway so nothing about a blocked account is served from memory.
852
+ await this.rbac.invalidateUser(userId);
853
+ }
854
+
855
+ async unblock(userId: string, revoker?: Revoker): Promise<void> {
856
+ await this.prisma.user.update({
857
+ where: { id: toId(userId) },
858
+ data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) },
859
+ });
860
+ }
861
+
862
+ /**
863
+ * A routine administrative on/off toggle — distinct from `block`/`unblock`, which is a
864
+ * security/moderation action. Both independently deny login; see the note on the `User` model.
865
+ */
866
+ async deactivate(userId: string, revoker?: Revoker): Promise<void> {
867
+ await this.prisma.user.update({
868
+ where: { id: toId(userId) },
869
+ data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) },
870
+ });
871
+ await deactivateUser(this.sessions, userId, revoker);
872
+ await this.rbac.invalidateUser(userId);
873
+ }
874
+
875
+ async activate(userId: string, revoker?: Revoker): Promise<void> {
876
+ await this.prisma.user.update({
877
+ where: { id: toId(userId) },
878
+ data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) },
879
+ });
880
+ }
881
+
882
+ // ---- countries ----
883
+
884
+ async listCountries(filter: CountryListFilter): Promise<CountryListResult> {
885
+ return this.countries.list(filter);
886
+ }
887
+
888
+ async getCountry(countryId: string): Promise<CountrySummary> {
889
+ return this.countries.get(countryId);
890
+ }
891
+
892
+ async createCountry(
893
+ input: CountryInput,
894
+ actorUserId: string | null,
895
+ ): Promise<CountrySummary> {
896
+ return this.countries.create(input, actorUserId);
897
+ }
898
+
899
+ async updateCountry(
900
+ countryId: string,
901
+ input: Partial<CountryInput>,
902
+ actorUserId: string | null,
903
+ ): Promise<CountrySummary> {
904
+ return this.countries.update(countryId, input, actorUserId);
905
+ }
906
+
907
+ async deleteCountry(
908
+ countryId: string,
909
+ actorUserId: string | null,
910
+ reason?: string,
911
+ ): Promise<void> {
912
+ await this.countries.delete(countryId, actorUserId, reason);
913
+ }
914
+
915
+ async activateCountry(
916
+ countryId: string,
917
+ actorUserId: string | null,
918
+ ): Promise<void> {
919
+ await this.countries.setActive(countryId, true, actorUserId);
920
+ }
921
+
922
+ async deactivateCountry(
923
+ countryId: string,
924
+ actorUserId: string | null,
925
+ ): Promise<void> {
926
+ await this.countries.setActive(countryId, false, actorUserId);
927
+ }
928
+
929
+ // ---- languages ----
930
+
931
+ async listLanguages(filter: LanguageListFilter): Promise<LanguageListResult> {
932
+ return this.languages.list(filter);
933
+ }
934
+
935
+ async getLanguage(languageId: string): Promise<LanguageSummary> {
936
+ return this.languages.get(languageId);
937
+ }
938
+
939
+ async createLanguage(
940
+ input: LanguageInput,
941
+ actorUserId: string | null,
942
+ ): Promise<LanguageSummary> {
943
+ return this.languages.create(input, actorUserId);
944
+ }
945
+
946
+ async updateLanguage(
947
+ languageId: string,
948
+ input: Partial<LanguageInput>,
949
+ actorUserId: string | null,
950
+ ): Promise<LanguageSummary> {
951
+ return this.languages.update(languageId, input, actorUserId);
952
+ }
953
+
954
+ async deleteLanguage(
955
+ languageId: string,
956
+ actorUserId: string | null,
957
+ reason?: string,
958
+ ): Promise<void> {
959
+ await this.languages.delete(languageId, actorUserId, reason);
960
+ }
961
+
962
+ async activateLanguage(
963
+ languageId: string,
964
+ actorUserId: string | null,
965
+ ): Promise<void> {
966
+ await this.languages.setActive(languageId, true, actorUserId);
967
+ }
968
+
969
+ async deactivateLanguage(
970
+ languageId: string,
971
+ actorUserId: string | null,
972
+ ): Promise<void> {
973
+ await this.languages.setActive(languageId, false, actorUserId);
974
+ }
975
+
976
+ // ---- customers ----
977
+ // End-users managed by admins — no login capability, not related to the RBAC `User` model above.
978
+
979
+ async listCustomers(filter: CustomerListFilter): Promise<CustomerListResult> {
980
+ return this.customers.list(filter);
981
+ }
982
+
983
+ async getCustomer(customerId: string): Promise<CustomerSummary> {
984
+ return this.customers.get(customerId);
985
+ }
986
+
987
+ async createCustomer(
988
+ input: CustomerInput,
989
+ actorUserId: string | null,
990
+ ): Promise<CustomerSummary> {
991
+ return this.customers.create(input, actorUserId);
992
+ }
993
+
994
+ async updateCustomer(
995
+ customerId: string,
996
+ input: Partial<CustomerInput>,
997
+ actorUserId: string | null,
998
+ ): Promise<CustomerSummary> {
999
+ return this.customers.update(customerId, input, actorUserId);
1000
+ }
1001
+
1002
+ async deleteCustomer(
1003
+ customerId: string,
1004
+ actorUserId: string | null,
1005
+ reason?: string,
1006
+ ): Promise<void> {
1007
+ await this.customers.delete(customerId, actorUserId, reason);
1008
+ }
1009
+
1010
+ async activateCustomer(
1011
+ customerId: string,
1012
+ actorUserId: string | null,
1013
+ ): Promise<void> {
1014
+ await this.customers.setActive(customerId, true, actorUserId);
1015
+ }
1016
+
1017
+ async deactivateCustomer(
1018
+ customerId: string,
1019
+ actorUserId: string | null,
1020
+ ): Promise<void> {
1021
+ await this.customers.setActive(customerId, false, actorUserId);
1022
+ }
1023
+
1024
+ // refresh() does not call this, so a token rotation never counts as a fresh login.
1025
+ // Takes the raw Prisma user row (bigint id) — every call site already has one in hand.
1026
+ private async issueSessionTokens(
1027
+ user: { id: bigint },
1028
+ meta: { userAgent?: string; ip?: string; provider?: string },
1029
+ ): Promise<AuthTokens> {
1030
+ await this.prisma.user.update({
1031
+ where: { id: user.id },
1032
+ data: { lastLogin: new Date() },
1033
+ });
1034
+ const session = await createSession(this.sessions, {
1035
+ userId: user.id.toString(),
1036
+ ...meta,
1037
+ });
1038
+ const key = await this.keys.getActiveKey();
1039
+
1040
+ const access = await signAccessToken(
1041
+ { activeKey: key },
1042
+ { sub: user.id.toString(), sessionId: session.id },
1043
+ { ttlSeconds: this.config.accessTokenTtlSeconds },
1044
+ );
1045
+ const refresh = await signRefreshToken(
1046
+ { activeKey: key },
1047
+ {
1048
+ sub: user.id.toString(),
1049
+ sessionId: session.id,
1050
+ sv: session.sessionVersion,
1051
+ },
1052
+ {
1053
+ jti: session.currentRefreshJti,
1054
+ ttlSeconds: this.config.refreshTokenTtlSeconds,
1055
+ },
1056
+ );
1057
+
1058
+ return {
1059
+ accessToken: access.token,
1060
+ refreshToken: refresh.token,
1061
+ sessionId: session.id,
1062
+ };
1063
+ }
1064
+ }