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