@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
@@ -10,8 +10,14 @@ import {
10
10
  signAppleClientSecret,
11
11
  verifyIdTokenAndExtractProfile,
12
12
  } from "@/lib/auth/core/oauth.js";
13
- import { requestPasswordReset as coreRequestPasswordReset, resetPassword as coreResetPassword } from "@/lib/auth/core/password-reset.js";
14
- import { checkRateLimit, type RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
13
+ import {
14
+ requestPasswordReset as coreRequestPasswordReset,
15
+ resetPassword as coreResetPassword,
16
+ } from "@/lib/auth/core/password-reset.js";
17
+ import {
18
+ checkRateLimit,
19
+ type RateLimitDeps,
20
+ } from "@/lib/auth/core/rate-limit.js";
15
21
  import {
16
22
  blockUser,
17
23
  createSession,
@@ -22,21 +28,46 @@ import {
22
28
  revokeSession,
23
29
  rotateRefreshToken,
24
30
  } from "@/lib/auth/core/session-policy.js";
25
- import { signAccessToken, signRefreshToken, signTwoFactorChallengeToken, verifyRefreshToken, verifyTwoFactorChallengeToken } from "@/lib/auth/core/token-service.js";
26
- import { buildTotpProvisioningUri, generateBackupCodes, generateTotpSecret, verifyTotpCode } from "@/lib/auth/core/two-factor.js";
31
+ import {
32
+ signAccessToken,
33
+ signRefreshToken,
34
+ signTwoFactorChallengeToken,
35
+ verifyRefreshToken,
36
+ verifyTwoFactorChallengeToken,
37
+ } from "@/lib/auth/core/token-service.js";
38
+ import {
39
+ buildTotpProvisioningUri,
40
+ generateBackupCodes,
41
+ generateTotpSecret,
42
+ verifyTotpCode,
43
+ } from "@/lib/auth/core/two-factor.js";
27
44
  import type { Revoker } from "@/lib/auth/core/types.js";
28
- import type { AuthConfig } from "./auth.config.js";
29
- import { HttpError } from "./http-error.js";
30
- import { AuditLogEntry, AuditLogListFilter, AuditLogRepository, toAuditLogEntry } from "./audit-log.repository.js";
31
- import type { Paginated } from "./pagination.js";
32
- import { PrismaClient } from "../generated/prisma/client.js";
33
- import { KeyProviderService } from "./key-provider.js";
34
- import { OAuthRepository } from "./oauth.repository.js";
35
- import { PasswordResetRepository } from "./password-reset.repository.js";
36
- import { PermissionInput, PermissionSummary, RbacRepository, RoleSummary, toUserSummary, UserListFilter, UserListResult, UserSummary } from "./rbac.repository.js";
37
- import { SessionRepository } from "./session.repository.js";
38
- import { TwoFactorRepository } from "./two-factor.repository.js";
39
- import { toId, toIdOrNull } from "./id.helper.js";
45
+ import type { AuthConfig } from "../auth.config.js";
46
+ import { HttpError } from "../http-error.js";
47
+ import {
48
+ AuditLogEntry,
49
+ AuditLogListFilter,
50
+ AuditLogRepository,
51
+ toAuditLogEntry,
52
+ } from "../repositories/audit-log.repository.js";
53
+ import type { Paginated } from "../pagination.js";
54
+ import { PrismaClient } from "../../generated/prisma/client.js";
55
+ import { KeyProviderService } from "../key-provider.js";
56
+ import { OAuthRepository } from "../repositories/oauth.repository.js";
57
+ import { PasswordResetRepository } from "../repositories/password-reset.repository.js";
58
+ import {
59
+ PermissionInput,
60
+ PermissionSummary,
61
+ RbacRepository,
62
+ RoleSummary,
63
+ toUserSummary,
64
+ UserListFilter,
65
+ UserListResult,
66
+ UserSummary,
67
+ } from "../repositories/rbac.repository.js";
68
+ import { SessionRepository } from "../repositories/session.repository.js";
69
+ import { TwoFactorRepository } from "../repositories/two-factor.repository.js";
70
+ import { toId, toIdOrNull } from "../id.helper.js";
40
71
 
41
72
  export interface AuthTokens {
42
73
  accessToken: string;
@@ -91,7 +122,9 @@ export class AuthService {
91
122
  userAgent?: string;
92
123
  ip?: string;
93
124
  }): Promise<AuthTokens> {
94
- const existing = await this.prisma.user.findUnique({ where: { email: input.email } });
125
+ const existing = await this.prisma.user.findUnique({
126
+ where: { email: input.email },
127
+ });
95
128
  if (existing) throw new HttpError(409, "email already registered");
96
129
 
97
130
  const passwordHash = await hashPassword(input.password);
@@ -109,82 +142,174 @@ export class AuthService {
109
142
  // The signup default is whichever roles are flagged `isDefault` in the database, not a name
110
143
  // spelled in code — see rbac.defaults.ts.
111
144
  await this.rbac.assignDefaultRoles(user.id);
112
- return this.issueSessionTokens(user, { userAgent: input.userAgent, ip: input.ip });
145
+ return this.issueSessionTokens(user, {
146
+ userAgent: input.userAgent,
147
+ ip: input.ip,
148
+ });
113
149
  }
114
150
 
115
151
  /** Returns full tokens directly, or a short-lived challenge if the account has 2FA enabled — see `loginTwoFactor`. */
116
- async login(input: { identifier: string; password: string; userAgent?: string; ip?: string }): Promise<AuthTokens | TwoFactorChallenge> {
117
- const { allowed } = await checkRateLimit(this.rateLimit, "login", input.identifier);
152
+ async login(input: {
153
+ identifier: string;
154
+ password: string;
155
+ userAgent?: string;
156
+ ip?: string;
157
+ }): Promise<AuthTokens | TwoFactorChallenge> {
158
+ const { allowed } = await checkRateLimit(
159
+ this.rateLimit,
160
+ "login",
161
+ input.identifier,
162
+ );
118
163
  if (!allowed) throw new HttpError(429, "too many login attempts");
119
164
 
120
165
  const user = await this.prisma.user.findFirst({
121
- where: { OR: [{ email: input.identifier }, { username: input.identifier }, { phone: input.identifier }] },
166
+ where: {
167
+ OR: [
168
+ { email: input.identifier },
169
+ { username: input.identifier },
170
+ { phone: input.identifier },
171
+ ],
172
+ },
122
173
  });
123
- if (!user || user.blocked || !user.isActive || user.isDeleted || !user.passwordHash) throw new HttpError(401, "invalid credentials");
174
+ if (
175
+ !user ||
176
+ user.blocked ||
177
+ !user.isActive ||
178
+ user.isDeleted ||
179
+ !user.passwordHash
180
+ )
181
+ throw new HttpError(401, "invalid credentials");
124
182
 
125
183
  const valid = await verifyPassword(user.passwordHash, input.password);
126
184
  if (!valid) throw new HttpError(401, "invalid credentials");
127
185
 
128
186
  if (user.twoFactorEnabled) {
129
187
  const key = await this.keys.getActiveKey();
130
- const { token } = await signTwoFactorChallengeToken({ activeKey: key }, user.id.toString()); // core-facing: sub must be string
188
+ const { token } = await signTwoFactorChallengeToken(
189
+ { activeKey: key },
190
+ user.id.toString(),
191
+ ); // core-facing: sub must be string
131
192
  return { twoFactorRequired: true, challengeToken: token };
132
193
  }
133
194
 
134
- return this.issueSessionTokens(user, { userAgent: input.userAgent, ip: input.ip });
195
+ return this.issueSessionTokens(user, {
196
+ userAgent: input.userAgent,
197
+ ip: input.ip,
198
+ });
135
199
  }
136
200
 
137
201
  /** Completes the challenge `login()` returned when 2FA is enabled — a TOTP code or an unused backup code. */
138
- async loginTwoFactor(input: { challengeToken: string; code: string; userAgent?: string; ip?: string }): Promise<AuthTokens> {
139
- const { sub } = await verifyTwoFactorChallengeToken({ secret: this.keys.secret }, input.challengeToken);
202
+ async loginTwoFactor(input: {
203
+ challengeToken: string;
204
+ code: string;
205
+ userAgent?: string;
206
+ ip?: string;
207
+ }): Promise<AuthTokens> {
208
+ const { sub } = await verifyTwoFactorChallengeToken(
209
+ { secret: this.keys.secret },
210
+ input.challengeToken,
211
+ );
140
212
 
141
- const user = await this.prisma.user.findUnique({ where: { id: toId(sub) } });
142
- if (!user || user.blocked || !user.isActive || user.isDeleted || !user.twoFactorEnabled || !user.twoFactorSecret) throw new HttpError(401, "invalid credentials");
213
+ const user = await this.prisma.user.findUnique({
214
+ where: { id: toId(sub) },
215
+ });
216
+ if (
217
+ !user ||
218
+ user.blocked ||
219
+ !user.isActive ||
220
+ user.isDeleted ||
221
+ !user.twoFactorEnabled ||
222
+ !user.twoFactorSecret
223
+ )
224
+ throw new HttpError(401, "invalid credentials");
143
225
 
144
226
  const validTotp = verifyTotpCode(user.twoFactorSecret, input.code);
145
- const validBackup = !validTotp && (await this.twoFactor.consumeBackupCode(user.id, input.code));
227
+ const validBackup =
228
+ !validTotp &&
229
+ (await this.twoFactor.consumeBackupCode(user.id, input.code));
146
230
  if (!validTotp && !validBackup) {
147
- await this.sessions.appendAuditEvent({ type: "two_factor_challenge_failed", userId: user.id.toString() }); // core-facing: string
231
+ await this.sessions.appendAuditEvent({
232
+ type: "two_factor_challenge_failed",
233
+ userId: user.id.toString(),
234
+ }); // core-facing: string
148
235
  throw new HttpError(401, "invalid two-factor code");
149
236
  }
150
237
 
151
- return this.issueSessionTokens(user, { userAgent: input.userAgent, ip: input.ip });
238
+ return this.issueSessionTokens(user, {
239
+ userAgent: input.userAgent,
240
+ ip: input.ip,
241
+ });
152
242
  }
153
243
 
154
- async enrollTwoFactor(userId: string): Promise<{ secret: string; provisioningUri: string }> {
244
+ async enrollTwoFactor(
245
+ userId: string,
246
+ ): Promise<{ secret: string; provisioningUri: string }> {
155
247
  const idBig = toId(userId);
156
248
  const user = await this.getUserOrThrow(userId);
157
249
  const secret = generateTotpSecret();
158
250
  // Not enabled yet — confirmTwoFactor() flips that, so an abandoned enrollment never locks the account out.
159
- await this.prisma.user.update({ where: { id: idBig }, data: { twoFactorSecret: secret, twoFactorEnabled: false } });
160
- return { secret, provisioningUri: buildTotpProvisioningUri({ secret, accountName: user.email, issuer: this.config.twoFactorIssuer }) };
251
+ await this.prisma.user.update({
252
+ where: { id: idBig },
253
+ data: { twoFactorSecret: secret, twoFactorEnabled: false },
254
+ });
255
+ return {
256
+ secret,
257
+ provisioningUri: buildTotpProvisioningUri({
258
+ secret,
259
+ accountName: user.email,
260
+ issuer: this.config.twoFactorIssuer,
261
+ }),
262
+ };
161
263
  }
162
264
 
163
- async confirmTwoFactor(userId: string, code: string): Promise<{ backupCodes: string[] }> {
265
+ async confirmTwoFactor(
266
+ userId: string,
267
+ code: string,
268
+ ): Promise<{ backupCodes: string[] }> {
164
269
  const idBig = toId(userId);
165
270
  const user = await this.getUserOrThrow(userId);
166
- if (!user.twoFactorSecret) throw new HttpError(400, "call enrollTwoFactor first");
167
- if (!verifyTotpCode(user.twoFactorSecret, code)) throw new HttpError(401, "invalid two-factor code");
271
+ if (!user.twoFactorSecret)
272
+ throw new HttpError(400, "call enrollTwoFactor first");
273
+ if (!verifyTotpCode(user.twoFactorSecret, code))
274
+ throw new HttpError(401, "invalid two-factor code");
168
275
 
169
276
  const backupCodes = generateBackupCodes();
170
- await this.twoFactor.saveBackupCodes(idBig, backupCodes.map((c) => c.hash));
171
- await this.prisma.user.update({ where: { id: idBig }, data: { twoFactorEnabled: true } });
172
- await this.sessions.appendAuditEvent({ type: "two_factor_enabled", userId });
277
+ await this.twoFactor.saveBackupCodes(
278
+ idBig,
279
+ backupCodes.map((c) => c.hash),
280
+ );
281
+ await this.prisma.user.update({
282
+ where: { id: idBig },
283
+ data: { twoFactorEnabled: true },
284
+ });
285
+ await this.sessions.appendAuditEvent({
286
+ type: "two_factor_enabled",
287
+ userId,
288
+ });
173
289
  return { backupCodes: backupCodes.map((c) => c.code) };
174
290
  }
175
291
 
176
292
  async disableTwoFactor(userId: string, code: string): Promise<void> {
177
293
  const idBig = toId(userId);
178
294
  const user = await this.getUserOrThrow(userId);
179
- if (!user.twoFactorEnabled || !user.twoFactorSecret) throw new HttpError(400, "two-factor is not enabled");
295
+ if (!user.twoFactorEnabled || !user.twoFactorSecret)
296
+ throw new HttpError(400, "two-factor is not enabled");
180
297
 
181
298
  const validTotp = verifyTotpCode(user.twoFactorSecret, code);
182
- const validBackup = !validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
183
- if (!validTotp && !validBackup) throw new HttpError(401, "invalid two-factor code");
299
+ const validBackup =
300
+ !validTotp && (await this.twoFactor.consumeBackupCode(idBig, code));
301
+ if (!validTotp && !validBackup)
302
+ throw new HttpError(401, "invalid two-factor code");
184
303
 
185
- await this.prisma.user.update({ where: { id: idBig }, data: { twoFactorEnabled: false, twoFactorSecret: null } });
304
+ await this.prisma.user.update({
305
+ where: { id: idBig },
306
+ data: { twoFactorEnabled: false, twoFactorSecret: null },
307
+ });
186
308
  await this.twoFactor.clearBackupCodes(idBig);
187
- await this.sessions.appendAuditEvent({ type: "two_factor_disabled", userId });
309
+ await this.sessions.appendAuditEvent({
310
+ type: "two_factor_disabled",
311
+ userId,
312
+ });
188
313
  }
189
314
 
190
315
  /** Always succeeds from the caller's point of view — an unknown email or a throttled request looks identical, to avoid enumeration. */
@@ -192,15 +317,27 @@ export class AuthService {
192
317
  const user = await this.prisma.user.findUnique({ where: { email } });
193
318
  if (!user || user.isDeleted) return;
194
319
 
195
- const { allowed } = await checkRateLimit(this.rateLimit, "password-reset", email);
320
+ const { allowed } = await checkRateLimit(
321
+ this.rateLimit,
322
+ "password-reset",
323
+ email,
324
+ );
196
325
  if (!allowed) return;
197
326
 
198
- const { token } = await coreRequestPasswordReset(this.passwordReset, user.id.toString());
199
- if (this.config.sendPasswordResetEmail) await this.config.sendPasswordResetEmail(email, token);
327
+ const { token } = await coreRequestPasswordReset(
328
+ this.passwordReset,
329
+ user.id.toString(),
330
+ );
331
+ if (this.config.sendPasswordResetEmail)
332
+ await this.config.sendPasswordResetEmail(email, token);
200
333
  }
201
334
 
202
335
  async resetPassword(token: string, newPassword: string): Promise<void> {
203
- const { userId } = await coreResetPassword(this.passwordReset, token, newPassword);
336
+ const { userId } = await coreResetPassword(
337
+ this.passwordReset,
338
+ token,
339
+ newPassword,
340
+ );
204
341
  // The old password is no longer trusted, so neither are its sessions. The revoker is the user
205
342
  // themselves — they proved control of the account by consuming the reset token.
206
343
  await revokeAllSessionsForUser(this.sessions, userId, { userId });
@@ -208,22 +345,40 @@ export class AuthService {
208
345
 
209
346
  /** Builds the redirect URL for `provider`. `state` is an opaque anti-CSRF nonce the provider echoes back verbatim. */
210
347
  async startOAuth(provider: string): Promise<{ url: string }> {
211
- const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString("base64url");
348
+ const state = Buffer.from(JSON.stringify({ nonce: randomUUID() })).toString(
349
+ "base64url",
350
+ );
212
351
 
213
352
  if (provider === "google") {
214
353
  const creds = this.config.oauthProviders.google;
215
354
  if (!creds) throw new HttpError(400, "google OAuth is not configured");
216
- return { url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, { clientId: creds.clientId, redirectUri: creds.redirectUri, state }) };
355
+ return {
356
+ url: buildAuthorizationUrl(GOOGLE_OIDC_PROVIDER, {
357
+ clientId: creds.clientId,
358
+ redirectUri: creds.redirectUri,
359
+ state,
360
+ }),
361
+ };
217
362
  }
218
363
  if (provider === "apple") {
219
364
  const creds = this.config.oauthProviders.apple;
220
365
  if (!creds) throw new HttpError(400, "apple OAuth is not configured");
221
- return { url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, { clientId: creds.clientId, redirectUri: creds.redirectUri, state }) };
366
+ return {
367
+ url: buildAuthorizationUrl(APPLE_OIDC_PROVIDER, {
368
+ clientId: creds.clientId,
369
+ redirectUri: creds.redirectUri,
370
+ state,
371
+ }),
372
+ };
222
373
  }
223
374
  throw new HttpError(400, `unknown OAuth provider "${provider}"`);
224
375
  }
225
376
 
226
- async completeOAuthCallback(provider: string, code: string, _state: string): Promise<AuthTokens> {
377
+ async completeOAuthCallback(
378
+ provider: string,
379
+ code: string,
380
+ _state: string,
381
+ ): Promise<AuthTokens> {
227
382
  let providerDescriptor: OAuthProviderDescriptor;
228
383
  let clientId: string;
229
384
  let idToken: string;
@@ -234,47 +389,91 @@ export class AuthService {
234
389
  providerDescriptor = GOOGLE_OIDC_PROVIDER;
235
390
  clientId = creds.clientId;
236
391
  idToken = (
237
- await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, { clientId: creds.clientId, clientSecret: creds.clientSecret, redirectUri: creds.redirectUri, code })
392
+ await exchangeCodeForTokens(GOOGLE_OIDC_PROVIDER, {
393
+ clientId: creds.clientId,
394
+ clientSecret: creds.clientSecret,
395
+ redirectUri: creds.redirectUri,
396
+ code,
397
+ })
238
398
  ).idToken;
239
399
  } else if (provider === "apple") {
240
400
  const creds = this.config.oauthProviders.apple;
241
401
  if (!creds) throw new HttpError(400, "apple OAuth is not configured");
242
402
  providerDescriptor = APPLE_OIDC_PROVIDER;
243
403
  clientId = creds.clientId;
244
- const clientSecret = await signAppleClientSecret({ teamId: creds.teamId, clientId: creds.clientId, keyId: creds.keyId, privateKeyPem: creds.privateKey });
245
- idToken = (await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, { clientId: creds.clientId, clientSecret, redirectUri: creds.redirectUri, code })).idToken;
404
+ const clientSecret = await signAppleClientSecret({
405
+ teamId: creds.teamId,
406
+ clientId: creds.clientId,
407
+ keyId: creds.keyId,
408
+ privateKeyPem: creds.privateKey,
409
+ });
410
+ idToken = (
411
+ await exchangeCodeForTokens(APPLE_OIDC_PROVIDER, {
412
+ clientId: creds.clientId,
413
+ clientSecret,
414
+ redirectUri: creds.redirectUri,
415
+ code,
416
+ })
417
+ ).idToken;
246
418
  } else {
247
419
  throw new HttpError(400, `unknown OAuth provider "${provider}"`);
248
420
  }
249
421
 
250
- const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, { clientId, idToken });
251
- const { userId } = await completeOAuthLogin(this.oauth, { provider, profile });
422
+ const profile = await verifyIdTokenAndExtractProfile(providerDescriptor, {
423
+ clientId,
424
+ idToken,
425
+ });
426
+ const { userId } = await completeOAuthLogin(this.oauth, {
427
+ provider,
428
+ profile,
429
+ });
252
430
 
253
431
  const user = await this.getUserOrThrow(userId);
254
- if (user.blocked || !user.isActive || user.isDeleted) throw new HttpError(401, "account is blocked");
432
+ if (user.blocked || !user.isActive || user.isDeleted)
433
+ throw new HttpError(401, "account is blocked");
255
434
  // A user this login just created holds no roles at all; give them the defaults. Guarded on
256
435
  // "holds none" rather than "was created", so it can never re-add a role an administrator
257
436
  // revoked from someone who has been logging in for months.
258
- if (!(await this.rbac.hasAnyRole(user.id))) await this.rbac.assignDefaultRoles(user.id);
437
+ if (!(await this.rbac.hasAnyRole(user.id)))
438
+ await this.rbac.assignDefaultRoles(user.id);
259
439
  return this.issueSessionTokens(user, { provider });
260
440
  }
261
441
 
262
- async listAuditLog(filter: AuditLogListFilter): Promise<Paginated<AuditLogEntry>> {
442
+ async listAuditLog(
443
+ filter: AuditLogListFilter,
444
+ ): Promise<Paginated<AuditLogEntry>> {
263
445
  const { items, meta } = await this.auditLog.list(filter);
264
446
  return { items: items.map(toAuditLogEntry), meta };
265
447
  }
266
448
 
267
449
  /** `req.auth` (the JWT claims) never carries 2FA status, so `/auth/me` fetches it fresh — the one bit of the response that isn't just echoing the token. */
268
- async getTwoFactorStatus(userId: string): Promise<{ twoFactorEnabled: boolean }> {
450
+ async getTwoFactorStatus(
451
+ userId: string,
452
+ ): Promise<{ twoFactorEnabled: boolean }> {
269
453
  const user = await this.getUserOrThrow(userId);
270
454
  return { twoFactorEnabled: user.twoFactorEnabled };
271
455
  }
272
456
 
273
- async listActiveSessions(userId: string): Promise<Array<{ id: string; createdAt: string; expiresAt: string; provider?: string; userAgent?: string; ip?: string }>> {
457
+ async listActiveSessions(
458
+ userId: string,
459
+ ): Promise<
460
+ Array<{
461
+ id: string;
462
+ createdAt: string;
463
+ expiresAt: string;
464
+ provider?: string;
465
+ userAgent?: string;
466
+ ip?: string;
467
+ }>
468
+ > {
274
469
  // "Active" is now three conditions, not one: not revoked, and not past its own absolute
275
470
  // expiry. `sessions_user_active_idx` is (user_id, is_revoked, expires_at) for exactly this read.
276
471
  const rows = await this.prisma.session.findMany({
277
- where: { userId: toId(userId), isRevoked: false, expiresAt: { gt: new Date() } },
472
+ where: {
473
+ userId: toId(userId),
474
+ isRevoked: false,
475
+ expiresAt: { gt: new Date() },
476
+ },
278
477
  orderBy: { createdAt: "desc" },
279
478
  });
280
479
  return rows.map((row) => ({
@@ -315,13 +514,24 @@ export class AuthService {
315
514
 
316
515
  async updateUser(
317
516
  userId: string,
318
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
517
+ input: {
518
+ firstName?: string | null;
519
+ lastName?: string | null;
520
+ displayName?: string | null;
521
+ phone?: string | null;
522
+ username?: string | null;
523
+ photo?: string | null;
524
+ },
319
525
  actorUserId: string | null,
320
526
  ): Promise<UserSummary> {
321
527
  return this.rbac.updateUser(userId, input, actorUserId);
322
528
  }
323
529
 
324
- async deleteUser(userId: string, actorUserId: string | null, reason?: string): Promise<void> {
530
+ async deleteUser(
531
+ userId: string,
532
+ actorUserId: string | null,
533
+ reason?: string,
534
+ ): Promise<void> {
325
535
  await this.rbac.deleteUser(userId, actorUserId, reason);
326
536
  }
327
537
 
@@ -331,7 +541,10 @@ export class AuthService {
331
541
  }
332
542
 
333
543
  /** Defines a permission, or edits one — including deactivating it. The write path for "the catalog is editable in the database". */
334
- async definePermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
544
+ async definePermission(
545
+ input: PermissionInput,
546
+ actorUserId: string | null,
547
+ ): Promise<PermissionSummary> {
335
548
  return this.rbac.upsertPermission(input, actorUserId);
336
549
  }
337
550
 
@@ -339,53 +552,106 @@ export class AuthService {
339
552
  return { roles: await this.rbac.listRoles() };
340
553
  }
341
554
 
342
- async createRole(input: { slug: string; name?: string; displayName?: string; description?: string | null }, actorUserId: string | null): Promise<RoleSummary> {
555
+ async createRole(
556
+ input: {
557
+ slug: string;
558
+ name?: string;
559
+ displayName?: string;
560
+ description?: string | null;
561
+ },
562
+ actorUserId: string | null,
563
+ ): Promise<RoleSummary> {
343
564
  return this.rbac.createRole(input, actorUserId);
344
565
  }
345
566
 
346
567
  async updateRole(
347
568
  roleId: string,
348
- input: { name?: string; displayName?: string; description?: string | null; isActive?: boolean },
569
+ input: {
570
+ name?: string;
571
+ displayName?: string;
572
+ description?: string | null;
573
+ isActive?: boolean;
574
+ },
349
575
  actorUserId: string | null,
350
576
  ): Promise<RoleSummary> {
351
577
  return this.rbac.updateRole(roleId, input, actorUserId);
352
578
  }
353
579
 
354
- async deleteRole(roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
580
+ async deleteRole(
581
+ roleId: string,
582
+ actorUserId: string | null,
583
+ reason?: string,
584
+ ): Promise<void> {
355
585
  await this.rbac.deleteRole(roleId, actorUserId, reason);
356
586
  }
357
587
 
358
- async attachPermissionToRole(roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
588
+ async attachPermissionToRole(
589
+ roleId: string,
590
+ permissionSlug: string,
591
+ actorUserId: string | null,
592
+ ): Promise<void> {
359
593
  await this.rbac.attachPermissionToRole(roleId, permissionSlug, actorUserId);
360
594
  }
361
595
 
362
596
  async assignRole(userId: string, roleSlug: string): Promise<void> {
363
597
  await this.rbac.assignRoleToUser(userId, roleSlug);
364
- await this.sessions.appendAuditEvent({ type: "role_assigned", userId, role: roleSlug });
598
+ await this.sessions.appendAuditEvent({
599
+ type: "role_assigned",
600
+ userId,
601
+ role: roleSlug,
602
+ });
365
603
  }
366
604
 
367
605
  async revokeRole(userId: string, roleSlug: string): Promise<void> {
368
606
  await this.rbac.revokeRoleFromUser(userId, roleSlug);
369
- await this.sessions.appendAuditEvent({ type: "role_revoked", userId, role: roleSlug });
607
+ await this.sessions.appendAuditEvent({
608
+ type: "role_revoked",
609
+ userId,
610
+ role: roleSlug,
611
+ });
370
612
  }
371
613
 
372
- async grantPermission(userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
614
+ async grantPermission(
615
+ userId: string,
616
+ permissionSlug: string,
617
+ actorUserId: string | null,
618
+ ): Promise<void> {
373
619
  await this.rbac.grantPermissionToUser(userId, permissionSlug, actorUserId);
374
- await this.sessions.appendAuditEvent({ type: "permission_granted", userId, permission: permissionSlug });
620
+ await this.sessions.appendAuditEvent({
621
+ type: "permission_granted",
622
+ userId,
623
+ permission: permissionSlug,
624
+ });
375
625
  }
376
626
 
377
- async revokePermission(userId: string, permissionSlug: string): Promise<void> {
627
+ async revokePermission(
628
+ userId: string,
629
+ permissionSlug: string,
630
+ ): Promise<void> {
378
631
  await this.rbac.revokePermissionFromUser(userId, permissionSlug);
379
- await this.sessions.appendAuditEvent({ type: "permission_revoked", userId, permission: permissionSlug });
632
+ await this.sessions.appendAuditEvent({
633
+ type: "permission_revoked",
634
+ userId,
635
+ permission: permissionSlug,
636
+ });
380
637
  }
381
638
 
382
639
  async refresh(refreshToken: string): Promise<Omit<AuthTokens, "sessionId">> {
383
640
  const key = await this.keys.getActiveKey();
384
- const presented = await verifyRefreshToken({ secret: this.keys.secret }, refreshToken);
385
- const { session, nextJti } = await rotateRefreshToken(this.sessions, presented);
641
+ const presented = await verifyRefreshToken(
642
+ { secret: this.keys.secret },
643
+ refreshToken,
644
+ );
645
+ const { session, nextJti } = await rotateRefreshToken(
646
+ this.sessions,
647
+ presented,
648
+ );
386
649
 
387
- const user = await this.prisma.user.findUnique({ where: { id: toId(session.userId) } });
388
- if (!user || user.blocked || !user.isActive || user.isDeleted) throw new HttpError(401, "invalid credentials");
650
+ const user = await this.prisma.user.findUnique({
651
+ where: { id: toId(session.userId) },
652
+ });
653
+ if (!user || user.blocked || !user.isActive || user.isDeleted)
654
+ throw new HttpError(401, "invalid credentials");
389
655
 
390
656
  // Identity only. Authorization is resolved from the database on every request (the authz middleware),
391
657
  // so a token issued before a grant is as authoritative as one issued after it.
@@ -396,38 +662,81 @@ export class AuthService {
396
662
  );
397
663
  const refresh = await signRefreshToken(
398
664
  { activeKey: key },
399
- { sub: user.id.toString(), sessionId: session.id, sv: session.sessionVersion },
665
+ {
666
+ sub: user.id.toString(),
667
+ sessionId: session.id,
668
+ sv: session.sessionVersion,
669
+ },
400
670
  { jti: nextJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
401
671
  );
402
672
  return { accessToken: access.token, refreshToken: refresh.token };
403
673
  }
404
674
 
405
- async logout(sessionId: string, accessJti: string, accessRemainingTtlSeconds: number, revoker?: Revoker): Promise<void> {
675
+ async logout(
676
+ sessionId: string,
677
+ accessJti: string,
678
+ accessRemainingTtlSeconds: number,
679
+ revoker?: Revoker,
680
+ ): Promise<void> {
406
681
  await revokeSession(this.sessions, sessionId, revoker);
407
- await revokeAccessToken(this.sessions, accessJti, accessRemainingTtlSeconds);
682
+ await revokeAccessToken(
683
+ this.sessions,
684
+ accessJti,
685
+ accessRemainingTtlSeconds,
686
+ );
408
687
  }
409
688
 
410
689
  async logoutAll(userId: string, revoker?: Revoker): Promise<void> {
411
690
  await revokeAllSessionsForUser(this.sessions, userId, revoker);
412
691
  }
413
692
 
414
- async logoutOthers(userId: string, keepSessionId: string, revoker?: Revoker): Promise<void> {
415
- await revokeOtherSessionsForUser(this.sessions, userId, keepSessionId, revoker);
693
+ async logoutOthers(
694
+ userId: string,
695
+ keepSessionId: string,
696
+ revoker?: Revoker,
697
+ ): Promise<void> {
698
+ await revokeOtherSessionsForUser(
699
+ this.sessions,
700
+ userId,
701
+ keepSessionId,
702
+ revoker,
703
+ );
416
704
  }
417
705
 
418
- async changePassword(userId: string, currentSessionId: string, input: { currentPassword: string; newPassword: string }): Promise<void> {
706
+ async changePassword(
707
+ userId: string,
708
+ currentSessionId: string,
709
+ input: { currentPassword: string; newPassword: string },
710
+ ): Promise<void> {
419
711
  const userIdBig = toId(userId);
420
- const user = await this.prisma.user.findUnique({ where: { id: userIdBig } });
421
- if (!user || user.blocked || !user.isActive || user.isDeleted || !user.passwordHash) throw new HttpError(401, "invalid credentials");
422
-
423
- const valid = await verifyPassword(user.passwordHash, input.currentPassword);
712
+ const user = await this.prisma.user.findUnique({
713
+ where: { id: userIdBig },
714
+ });
715
+ if (
716
+ !user ||
717
+ user.blocked ||
718
+ !user.isActive ||
719
+ user.isDeleted ||
720
+ !user.passwordHash
721
+ )
722
+ throw new HttpError(401, "invalid credentials");
723
+
724
+ const valid = await verifyPassword(
725
+ user.passwordHash,
726
+ input.currentPassword,
727
+ );
424
728
  if (!valid) throw new HttpError(401, "invalid credentials");
425
729
 
426
730
  const passwordHash = await hashPassword(input.newPassword);
427
- await this.prisma.user.update({ where: { id: userIdBig }, data: { passwordHash, updatedBy: userIdBig } });
731
+ await this.prisma.user.update({
732
+ where: { id: userIdBig },
733
+ data: { passwordHash, updatedBy: userIdBig },
734
+ });
428
735
  // The old password is no longer trusted everywhere else it's signed in — but leave the
429
736
  // session making this very call alone, the same courtesy `logoutOthers` extends.
430
- await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, { userId });
737
+ await revokeOtherSessionsForUser(this.sessions, userId, currentSessionId, {
738
+ userId,
739
+ });
431
740
  }
432
741
 
433
742
  /** Backs both `GET`- and `PATCH /auth/me` — roles/blocked/lastLogin are an admin's business, not this endpoint's. */
@@ -457,13 +766,25 @@ export class AuthService {
457
766
  */
458
767
  async updateProfile(
459
768
  userId: string,
460
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
769
+ input: {
770
+ firstName?: string | null;
771
+ lastName?: string | null;
772
+ displayName?: string | null;
773
+ phone?: string | null;
774
+ username?: string | null;
775
+ photo?: string | null;
776
+ },
461
777
  ): Promise<SelfProfile> {
462
- return this.toSelfProfile(await this.rbac.updateUser(userId, input, userId));
778
+ return this.toSelfProfile(
779
+ await this.rbac.updateUser(userId, input, userId),
780
+ );
463
781
  }
464
782
 
465
783
  async block(userId: string, revoker?: Revoker): Promise<void> {
466
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) } });
784
+ await this.prisma.user.update({
785
+ where: { id: toId(userId) },
786
+ data: { blocked: true, updatedBy: toIdOrNull(revoker?.userId) },
787
+ });
467
788
  // The administrator, not the blocked user, is what lands in `sessions.revoked_by`.
468
789
  await blockUser(this.sessions, userId, revoker);
469
790
  // Belt and braces. The block is enforced on the authentication path — login and refresh both
@@ -474,7 +795,10 @@ export class AuthService {
474
795
  }
475
796
 
476
797
  async unblock(userId: string, revoker?: Revoker): Promise<void> {
477
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) } });
798
+ await this.prisma.user.update({
799
+ where: { id: toId(userId) },
800
+ data: { blocked: false, updatedBy: toIdOrNull(revoker?.userId) },
801
+ });
478
802
  }
479
803
 
480
804
  /**
@@ -482,18 +806,26 @@ export class AuthService {
482
806
  * security/moderation action. Both independently deny login; see the note on the `User` model.
483
807
  */
484
808
  async deactivate(userId: string, revoker?: Revoker): Promise<void> {
485
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) } });
809
+ await this.prisma.user.update({
810
+ where: { id: toId(userId) },
811
+ data: { isActive: false, updatedBy: toIdOrNull(revoker?.userId) },
812
+ });
486
813
  await deactivateUser(this.sessions, userId, revoker);
487
814
  await this.rbac.invalidateUser(userId);
488
815
  }
489
816
 
490
817
  async activate(userId: string, revoker?: Revoker): Promise<void> {
491
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) } });
818
+ await this.prisma.user.update({
819
+ where: { id: toId(userId) },
820
+ data: { isActive: true, updatedBy: toIdOrNull(revoker?.userId) },
821
+ });
492
822
  }
493
823
 
494
824
  /** Nest's filter turns Prisma's `findUniqueOrThrow` into a 404; without one, this does it by hand. */
495
825
  private async getUserOrThrow(userId: string) {
496
- const user = await this.prisma.user.findUnique({ where: { id: toId(userId) } });
826
+ const user = await this.prisma.user.findUnique({
827
+ where: { id: toId(userId) },
828
+ });
497
829
  if (!user) throw new HttpError(404, `user "${userId}" not found`);
498
830
  return user;
499
831
  }
@@ -501,8 +833,14 @@ export class AuthService {
501
833
  /** The access token identifies the user and the session and carries no authorization at all
502
834
  * — see the authz middleware. Takes the raw Prisma user row (bigint id) — every call site
503
835
  * already has one in hand. */
504
- private async issueSessionTokens(user: { id: bigint }, meta: { userAgent?: string; ip?: string; provider?: string }): Promise<AuthTokens> {
505
- const session = await createSession(this.sessions, { userId: user.id.toString(), ...meta });
836
+ private async issueSessionTokens(
837
+ user: { id: bigint },
838
+ meta: { userAgent?: string; ip?: string; provider?: string },
839
+ ): Promise<AuthTokens> {
840
+ const session = await createSession(this.sessions, {
841
+ userId: user.id.toString(),
842
+ ...meta,
843
+ });
506
844
  const key = await this.keys.getActiveKey();
507
845
 
508
846
  const access = await signAccessToken(
@@ -512,10 +850,21 @@ export class AuthService {
512
850
  );
513
851
  const refresh = await signRefreshToken(
514
852
  { activeKey: key },
515
- { sub: user.id.toString(), sessionId: session.id, sv: session.sessionVersion },
516
- { jti: session.currentRefreshJti, ttlSeconds: this.config.refreshTokenTtlSeconds },
853
+ {
854
+ sub: user.id.toString(),
855
+ sessionId: session.id,
856
+ sv: session.sessionVersion,
857
+ },
858
+ {
859
+ jti: session.currentRefreshJti,
860
+ ttlSeconds: this.config.refreshTokenTtlSeconds,
861
+ },
517
862
  );
518
863
 
519
- return { accessToken: access.token, refreshToken: refresh.token, sessionId: session.id };
864
+ return {
865
+ accessToken: access.token,
866
+ refreshToken: refresh.token,
867
+ sessionId: session.id,
868
+ };
520
869
  }
521
870
  }