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