@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
@@ -0,0 +1,398 @@
1
+ import type { NextFunction, Request, RequestHandler, Response } from "express";
2
+ import type { AuthConfig } from "../auth.config.js";
3
+ import { AuthService } from "../services/auth.service.js";
4
+ import { HttpError } from "../http-error.js";
5
+ import {
6
+ authenticated,
7
+ createTieredRouter,
8
+ publicRoute,
9
+ type TierMiddleware,
10
+ } from "../route-tiers.js";
11
+ import "../request-context.js";
12
+
13
+ function requireString(value: unknown, field: string): string {
14
+ if (typeof value !== "string" || value.length === 0)
15
+ throw new HttpError(400, `${field} is required`);
16
+ return value;
17
+ }
18
+
19
+ const optionalString = (value: unknown): string | undefined =>
20
+ typeof value === "string" ? value : undefined;
21
+
22
+ export interface AuthRouterDeps extends TierMiddleware {
23
+ auth: AuthService;
24
+ config: AuthConfig;
25
+ }
26
+
27
+ /**
28
+ * Identity endpoints — everything that is about *a user*, never about a group of them.
29
+ * Administration lives in admin.router.ts.
30
+ *
31
+ * Replaces the reference combo's `AuthController` (a Nest `@Controller`). Same endpoints, same
32
+ * validation, same status codes — just an Express `Router()` instead of a decorated class, and
33
+ * middleware passed in where the controller lists `@UseGuards(...)`. Every handler is wrapped in
34
+ * try/catch + `next(err)` so a thrown `HttpError`/`AuthCoreError` reaches the error-handling
35
+ * middleware mounted in create-auth-app.ts instead of leaking a bare 500.
36
+ */
37
+ export function createAuthRouter(deps: AuthRouterDeps): RequestHandler {
38
+ const { auth, config, authentication, authorization } = deps;
39
+ const router = createTieredRouter({ authentication, authorization });
40
+
41
+ router.route(
42
+ "post",
43
+ "/signup",
44
+ publicRoute(),
45
+ async (req: Request, res: Response, next: NextFunction) => {
46
+ try {
47
+ const body = req.body as Record<string, unknown>;
48
+ const tokens = await auth.signup({
49
+ email: requireString(body.email, "email"),
50
+ password: requireString(body.password, "password"),
51
+ firstName: optionalString(body.firstName),
52
+ lastName: optionalString(body.lastName),
53
+ displayName: optionalString(body.displayName),
54
+ phone: optionalString(body.phone),
55
+ username: optionalString(body.username),
56
+ userAgent: req.headers["user-agent"],
57
+ ip: req.ip,
58
+ });
59
+ res.status(201).json(tokens);
60
+ } catch (err) {
61
+ next(err);
62
+ }
63
+ },
64
+ );
65
+
66
+ router.route(
67
+ "post",
68
+ "/login",
69
+ publicRoute(),
70
+ async (req: Request, res: Response, next: NextFunction) => {
71
+ try {
72
+ const body = req.body as Record<string, unknown>;
73
+ const result = await auth.login({
74
+ identifier: requireString(body.identifier, "identifier"),
75
+ password: requireString(body.password, "password"),
76
+ userAgent: req.headers["user-agent"],
77
+ ip: req.ip,
78
+ });
79
+ res.status(200).json(result);
80
+ } catch (err) {
81
+ next(err);
82
+ }
83
+ },
84
+ );
85
+
86
+ router.route(
87
+ "post",
88
+ "/login/2fa",
89
+ publicRoute(),
90
+ async (req: Request, res: Response, next: NextFunction) => {
91
+ try {
92
+ const body = req.body as Record<string, unknown>;
93
+ const tokens = await auth.loginTwoFactor({
94
+ challengeToken: requireString(body.challengeToken, "challengeToken"),
95
+ code: requireString(body.code, "code"),
96
+ userAgent: req.headers["user-agent"],
97
+ ip: req.ip,
98
+ });
99
+ res.status(200).json(tokens);
100
+ } catch (err) {
101
+ next(err);
102
+ }
103
+ },
104
+ );
105
+
106
+ router.route(
107
+ "post",
108
+ "/refresh",
109
+ publicRoute(),
110
+ async (req: Request, res: Response, next: NextFunction) => {
111
+ try {
112
+ const body = req.body as Record<string, unknown>;
113
+ const tokens = await auth.refresh(
114
+ requireString(body.refreshToken, "refreshToken"),
115
+ );
116
+ res.status(200).json(tokens);
117
+ } catch (err) {
118
+ next(err);
119
+ }
120
+ },
121
+ );
122
+
123
+ // `roles`/`permissions` are whatever applies to this request — see authz.middleware.ts for how
124
+ // they are resolved. A request that names no authorization scope gets empty arrays.
125
+ //
126
+ // `permissions` is the caller's whole authorization: `defineAbilitiesFor(permissions)` from
127
+ // ability.ts rebuilds, in the client, the identical CASL ability the server's own route check
128
+ // just answered with, so the UI cannot disagree with what the API will allow. The slugs go on
129
+ // the wire rather than a serialised ability, so the payload is something a human can read in a
130
+ // log — and it is the same `req.authz` the ability was built from, not a re-resolution.
131
+ router.route(
132
+ "get",
133
+ "/me",
134
+ authenticated(),
135
+ async (req: Request, res: Response, next: NextFunction) => {
136
+ try {
137
+ const { sub, sessionId } = req.auth!;
138
+ const [{ twoFactorEnabled }, profile] = await Promise.all([
139
+ auth.getTwoFactorStatus(sub),
140
+ auth.getProfile(sub),
141
+ ]);
142
+ res.status(200).json({
143
+ sub,
144
+ sessionId,
145
+ roles: req.authz?.roles ?? [],
146
+ permissions: req.authz?.permissions ?? [],
147
+ twoFactorEnabled,
148
+ email: profile.email,
149
+ firstName: profile.firstName,
150
+ lastName: profile.lastName,
151
+ displayName: profile.displayName,
152
+ phone: profile.phone,
153
+ username: profile.username,
154
+ photo: profile.photo,
155
+ });
156
+ } catch (err) {
157
+ next(err);
158
+ }
159
+ },
160
+ );
161
+
162
+ router.route(
163
+ "get",
164
+ "/sessions",
165
+ authenticated(),
166
+ async (req: Request, res: Response, next: NextFunction) => {
167
+ try {
168
+ const sessions = await auth.listActiveSessions(req.auth!.sub);
169
+ res.status(200).json(sessions);
170
+ } catch (err) {
171
+ next(err);
172
+ }
173
+ },
174
+ );
175
+
176
+ router.route(
177
+ "post",
178
+ "/logout",
179
+ authenticated(),
180
+ async (req: Request, res: Response, next: NextFunction) => {
181
+ try {
182
+ const { sessionId, jti, sub } = req.auth!;
183
+ // Recorded so the row can later be told apart from a session an administrator ended —
184
+ // see `sessions.revoked_by`.
185
+ await auth.logout(sessionId, jti, config.accessTokenTtlSeconds, {
186
+ userId: sub,
187
+ ip: req.ip,
188
+ });
189
+ res.status(200).json({ ok: true });
190
+ } catch (err) {
191
+ next(err);
192
+ }
193
+ },
194
+ );
195
+
196
+ router.route(
197
+ "post",
198
+ "/logout-all",
199
+ authenticated(),
200
+ async (req: Request, res: Response, next: NextFunction) => {
201
+ try {
202
+ await auth.logoutAll(req.auth!.sub, {
203
+ userId: req.auth!.sub,
204
+ ip: req.ip,
205
+ });
206
+ res.status(200).json({ ok: true });
207
+ } catch (err) {
208
+ next(err);
209
+ }
210
+ },
211
+ );
212
+
213
+ router.route(
214
+ "post",
215
+ "/logout-others",
216
+ authenticated(),
217
+ async (req: Request, res: Response, next: NextFunction) => {
218
+ try {
219
+ await auth.logoutOthers(req.auth!.sub, req.auth!.sessionId, {
220
+ userId: req.auth!.sub,
221
+ ip: req.ip,
222
+ });
223
+ res.status(200).json({ ok: true });
224
+ } catch (err) {
225
+ next(err);
226
+ }
227
+ },
228
+ );
229
+
230
+ router.route(
231
+ "post",
232
+ "/password/change",
233
+ authenticated(),
234
+ async (req: Request, res: Response, next: NextFunction) => {
235
+ try {
236
+ const body = req.body as Record<string, unknown>;
237
+ await auth.changePassword(req.auth!.sub, req.auth!.sessionId, {
238
+ currentPassword: requireString(
239
+ body.currentPassword,
240
+ "currentPassword",
241
+ ),
242
+ newPassword: requireString(body.newPassword, "newPassword"),
243
+ });
244
+ res.status(200).json({ ok: true });
245
+ } catch (err) {
246
+ next(err);
247
+ }
248
+ },
249
+ );
250
+
251
+ // Self-service — no admin permission required. Updates the caller's own row directly; unrelated
252
+ // to any workspace. Same null-vs-undefined field parsing every PATCH handler in this codebase
253
+ // uses — see admin.router.ts's PATCH /users/:userId.
254
+ router.route(
255
+ "patch",
256
+ "/me",
257
+ authenticated(),
258
+ async (req: Request, res: Response, next: NextFunction) => {
259
+ try {
260
+ const body = req.body as Record<string, unknown>;
261
+ const profile = await auth.updateProfile(req.auth!.sub, {
262
+ firstName:
263
+ body.firstName === null ? null : optionalString(body.firstName),
264
+ lastName:
265
+ body.lastName === null ? null : optionalString(body.lastName),
266
+ displayName:
267
+ body.displayName === null ? null : optionalString(body.displayName),
268
+ phone: body.phone === null ? null : optionalString(body.phone),
269
+ username:
270
+ body.username === null ? null : optionalString(body.username),
271
+ photo: body.photo === null ? null : optionalString(body.photo),
272
+ });
273
+ res.status(200).json(profile);
274
+ } catch (err) {
275
+ next(err);
276
+ }
277
+ },
278
+ );
279
+
280
+ router.route(
281
+ "post",
282
+ "/2fa/enroll",
283
+ authenticated(),
284
+ async (req: Request, res: Response, next: NextFunction) => {
285
+ try {
286
+ const result = await auth.enrollTwoFactor(req.auth!.sub);
287
+ res.status(200).json(result);
288
+ } catch (err) {
289
+ next(err);
290
+ }
291
+ },
292
+ );
293
+
294
+ router.route(
295
+ "post",
296
+ "/2fa/confirm",
297
+ authenticated(),
298
+ async (req: Request, res: Response, next: NextFunction) => {
299
+ try {
300
+ const body = req.body as Record<string, unknown>;
301
+ const result = await auth.confirmTwoFactor(
302
+ req.auth!.sub,
303
+ requireString(body.code, "code"),
304
+ );
305
+ res.status(200).json(result);
306
+ } catch (err) {
307
+ next(err);
308
+ }
309
+ },
310
+ );
311
+
312
+ router.route(
313
+ "post",
314
+ "/2fa/disable",
315
+ authenticated(),
316
+ async (req: Request, res: Response, next: NextFunction) => {
317
+ try {
318
+ const body = req.body as Record<string, unknown>;
319
+ await auth.disableTwoFactor(
320
+ req.auth!.sub,
321
+ requireString(body.code, "code"),
322
+ );
323
+ res.status(200).json({ ok: true });
324
+ } catch (err) {
325
+ next(err);
326
+ }
327
+ },
328
+ );
329
+
330
+ router.route(
331
+ "post",
332
+ "/password/forgot",
333
+ publicRoute(),
334
+ async (req: Request, res: Response, next: NextFunction) => {
335
+ try {
336
+ const body = req.body as Record<string, unknown>;
337
+ await auth.requestPasswordReset(requireString(body.email, "email"));
338
+ res.status(200).json({ ok: true }); // always 200 — never reveal whether the email exists
339
+ } catch (err) {
340
+ next(err);
341
+ }
342
+ },
343
+ );
344
+
345
+ router.route(
346
+ "post",
347
+ "/password/reset",
348
+ publicRoute(),
349
+ async (req: Request, res: Response, next: NextFunction) => {
350
+ try {
351
+ const body = req.body as Record<string, unknown>;
352
+ await auth.resetPassword(
353
+ requireString(body.token, "token"),
354
+ requireString(body.newPassword, "newPassword"),
355
+ );
356
+ res.status(200).json({ ok: true });
357
+ } catch (err) {
358
+ next(err);
359
+ }
360
+ },
361
+ );
362
+
363
+ router.route(
364
+ "get",
365
+ "/oauth/:provider/start",
366
+ publicRoute(),
367
+ async (req: Request, res: Response, next: NextFunction) => {
368
+ try {
369
+ const result = await auth.startOAuth(
370
+ requireString(req.params.provider, "provider"),
371
+ );
372
+ res.status(200).json(result);
373
+ } catch (err) {
374
+ next(err);
375
+ }
376
+ },
377
+ );
378
+
379
+ router.route(
380
+ "get",
381
+ "/oauth/:provider/callback",
382
+ publicRoute(),
383
+ async (req: Request, res: Response, next: NextFunction) => {
384
+ try {
385
+ const tokens = await auth.completeOAuthCallback(
386
+ requireString(req.params.provider, "provider"),
387
+ requireString(req.query.code, "code"),
388
+ requireString(req.query.state, "state"),
389
+ );
390
+ res.status(200).json(tokens);
391
+ } catch (err) {
392
+ next(err);
393
+ }
394
+ },
395
+ );
396
+
397
+ return router.handler;
398
+ }
@@ -2,25 +2,28 @@ import { PrismaPg } from "@prisma/adapter-pg";
2
2
  import express, { type Express } from "express";
3
3
  import swaggerUi from "swagger-ui-express";
4
4
  import type { RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
5
- import { createAdminRouter } from "./admin.router.js";
6
- import { AuditLogRepository } from "./audit-log.repository.js";
7
- import { authCoreErrorMiddleware } from "./auth-core-error.middleware.js";
5
+ import { createAdminRouter } from "./routers/admin.router.js";
6
+ import { AuditLogRepository } from "./repositories/audit-log.repository.js";
7
+ import { authCoreErrorMiddleware } from "./middleware/auth-core-error.middleware.js";
8
8
  import { AuthConfig, defaultAuthConfig } from "./auth.config.js";
9
- import { createAuthMiddleware } from "./auth.middleware.js";
10
- import { createAuthRouter } from "./auth.router.js";
11
- import { AuthService } from "./auth.service.js";
12
- import { createAuthzMiddleware } from "./authz.middleware.js";
13
- import { PrismaClient } from "../generated/prisma/client.js";
9
+ import { createAuthMiddleware } from "./middleware/auth.middleware.js";
10
+ import { createAuthRouter } from "./routers/auth.router.js";
11
+ import { AuthService } from "./services/auth.service.js";
12
+ import { createAuthzMiddleware } from "./middleware/authz.middleware.js";
13
+ import { PrismaClient } from "./../generated/prisma/client.js";
14
14
  import { KeyProviderService } from "./key-provider.js";
15
- import { OAuthRepository } from "./oauth.repository.js";
15
+ import { OAuthRepository } from "./repositories/oauth.repository.js";
16
16
  import { openApiSpec } from "./openapi-spec.js";
17
- import { PasswordResetRepository } from "./password-reset.repository.js";
18
- import { InMemoryPermissionCacheStore, PermissionCache } from "./permission-cache.js";
17
+ import { PasswordResetRepository } from "./repositories/password-reset.repository.js";
18
+ import {
19
+ InMemoryPermissionCacheStore,
20
+ PermissionCache,
21
+ } from "./permission-cache.js";
19
22
  import { InMemoryRateLimitStore } from "./rate-limit.store.js";
20
- import { RbacRepository } from "./rbac.repository.js";
21
- import { responseEnvelope } from "./response-envelope.middleware.js";
22
- import { SessionRepository } from "./session.repository.js";
23
- import { TwoFactorRepository } from "./two-factor.repository.js";
23
+ import { RbacRepository } from "./repositories/rbac.repository.js";
24
+ import { responseEnvelope } from "./middleware/response-envelope.middleware.js";
25
+ import { SessionRepository } from "./repositories/session.repository.js";
26
+ import { TwoFactorRepository } from "./repositories/two-factor.repository.js";
24
27
 
25
28
  export interface CreateAuthAppOptions {
26
29
  /** Overrides merged on top of `defaultAuthConfig`, same shape as the reference combo's `AuthModule.forRoot(config)`. */
@@ -39,7 +42,9 @@ export interface CreateAuthAppOptions {
39
42
  export function createAuthApp(options: CreateAuthAppOptions = {}): Express {
40
43
  const config: AuthConfig = { ...defaultAuthConfig, ...options.config };
41
44
 
42
- const adapter = new PrismaPg({ connectionString: process.env["DATABASE_URL"] });
45
+ const adapter = new PrismaPg({
46
+ connectionString: process.env["DATABASE_URL"],
47
+ });
43
48
  const prisma = new PrismaClient({ adapter });
44
49
 
45
50
  const auditLog = new AuditLogRepository(prisma);
@@ -47,15 +52,30 @@ export function createAuthApp(options: CreateAuthAppOptions = {}): Express {
47
52
  const keys = new KeyProviderService();
48
53
  // Swap the store for a Redis-backed one by passing `rateLimitStore` in `config` — nothing in
49
54
  // this library's source changes.
50
- const rateLimit: RateLimitDeps = config.rateLimitStore ?? new InMemoryRateLimitStore();
55
+ const rateLimit: RateLimitDeps =
56
+ config.rateLimitStore ?? new InMemoryRateLimitStore();
51
57
  // The cache seam. Swap the store for a Redis-backed one by passing `permissionCacheStore` in
52
58
  // `config` — nothing in this library's source changes. Keys are namespaced simpleauthkit:authz:*.
53
- const permissionCache = new PermissionCache(config.permissionCacheStore ?? new InMemoryPermissionCacheStore(), config);
59
+ const permissionCache = new PermissionCache(
60
+ config.permissionCacheStore ?? new InMemoryPermissionCacheStore(),
61
+ config,
62
+ );
54
63
  const rbac = new RbacRepository(prisma, permissionCache);
55
64
  const twoFactor = new TwoFactorRepository(prisma);
56
65
  const oauth = new OAuthRepository(prisma);
57
66
  const passwordReset = new PasswordResetRepository(prisma);
58
- const authService = new AuthService(prisma, sessions, keys, rateLimit, auditLog, rbac, twoFactor, oauth, passwordReset, config);
67
+ const authService = new AuthService(
68
+ prisma,
69
+ sessions,
70
+ keys,
71
+ rateLimit,
72
+ auditLog,
73
+ rbac,
74
+ twoFactor,
75
+ oauth,
76
+ passwordReset,
77
+ config,
78
+ );
59
79
 
60
80
  const authentication = createAuthMiddleware({ keys, sessions });
61
81
  // Roles are global here, but they are read from the database on the request that uses them —
@@ -79,13 +99,28 @@ export function createAuthApp(options: CreateAuthAppOptions = {}): Express {
79
99
  // slash) a plain 200 instead of swagger-ui-express's default 301 to "/docs/" — the
80
100
  // underlying static asset middleware would otherwise treat the mount root as a directory
81
101
  // listing and redirect.
82
- app.use("/docs", swaggerUi.serveWithOptions({ redirect: false }), swaggerUi.setup(openApiSpec));
102
+ app.use(
103
+ "/docs",
104
+ swaggerUi.serveWithOptions({ redirect: false }),
105
+ swaggerUi.setup(openApiSpec),
106
+ );
83
107
  app.get("/docs-json", (_req, res) => res.json(openApiSpec));
84
108
  app.use(responseEnvelope());
85
109
  // Mounted before /auth so `/auth/admin/*` reaches the admin router rather than falling
86
110
  // through to the identity router's 404.
87
- app.use("/auth/admin", createAdminRouter({ auth: authService, authentication, authorization }));
88
- app.use("/auth", createAuthRouter({ auth: authService, config, authentication, authorization }));
111
+ app.use(
112
+ "/auth/admin",
113
+ createAdminRouter({ auth: authService, authentication, authorization }),
114
+ );
115
+ app.use(
116
+ "/auth",
117
+ createAuthRouter({
118
+ auth: authService,
119
+ config,
120
+ authentication,
121
+ authorization,
122
+ }),
123
+ );
89
124
  // Mounted last so it catches errors forwarded via `next(err)` from every route above.
90
125
  app.use(authCoreErrorMiddleware);
91
126
 
@@ -1,8 +1,8 @@
1
1
  import type { NextFunction, Request, RequestHandler, Response } from "express";
2
- import { defineAbilitiesFor } from "./ability.js";
3
- import type { PermissionCache } from "./permission-cache.js";
4
- import type { RbacRepository } from "./rbac.repository.js";
5
- import "./request-context.js";
2
+ import { defineAbilitiesFor } from "../ability.js";
3
+ import type { PermissionCache } from "../permission-cache.js";
4
+ import type { RbacRepository } from "../repositories/rbac.repository.js";
5
+ import "../request-context.js";
6
6
 
7
7
  /**
8
8
  * Roles and permissions are global to this deployment: a user has one set, and it applies
@@ -41,13 +41,21 @@ export interface AuthzMiddlewareDeps {
41
41
  *
42
42
  * Replaces the reference combo's `AuthzGuard`.
43
43
  */
44
- export function createAuthzMiddleware(deps: AuthzMiddlewareDeps): RequestHandler {
44
+ export function createAuthzMiddleware(
45
+ deps: AuthzMiddlewareDeps,
46
+ ): RequestHandler {
45
47
  const { rbac, cache } = deps;
46
- return async function authzMiddleware(req: Request, _res: Response, next: NextFunction): Promise<void> {
48
+ return async function authzMiddleware(
49
+ req: Request,
50
+ _res: Response,
51
+ next: NextFunction,
52
+ ): Promise<void> {
47
53
  try {
48
54
  if (req.auth) {
49
55
  const userId = req.auth.sub;
50
- req.authz = (await cache.resolve<AuthzContext>(userId, () => rbac.resolveAuthzContext(userId))) ?? { roles: [], permissions: [] };
56
+ req.authz = (await cache.resolve<AuthzContext>(userId, () =>
57
+ rbac.resolveAuthzContext(userId),
58
+ )) ?? { roles: [], permissions: [] };
51
59
  req.ability = defineAbilitiesFor(req.authz.permissions);
52
60
  }
53
61
  next();