@simple-auth-kit/cli 1.4.3 → 1.5.1

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 (134) hide show
  1. package/lib/copy.ts +1 -1
  2. package/package.json +1 -1
  3. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  4. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  5. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  6. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  7. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  8. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  9. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  10. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  11. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  12. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  13. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  14. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  15. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  16. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  17. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  18. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  19. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  20. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  23. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  26. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  27. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  28. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  29. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  30. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  31. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  32. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  33. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  34. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  35. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  36. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
  37. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  38. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
  39. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  40. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  41. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  42. package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
  43. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  44. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
  45. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  46. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  47. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
  48. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  49. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  50. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  53. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
  54. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  56. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  57. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  58. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  59. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  62. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  63. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  64. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  65. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  66. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  71. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  79. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  80. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  81. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  82. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  83. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  84. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  85. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  86. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  87. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  88. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  89. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  90. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  91. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
  92. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  93. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  94. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  95. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
  96. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  97. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  98. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  99. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  100. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  101. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
  102. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
  115. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  116. package/registry.json +4 -4
  117. package/simple-auth-kit.ts +42 -11
  118. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  119. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  120. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  121. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  122. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  123. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  124. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  125. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  126. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  127. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  128. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  129. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  130. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  131. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  132. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  134. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -0,0 +1,395 @@
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
+ router.route(
252
+ "patch",
253
+ "/me",
254
+ authenticated(),
255
+ async (req: Request, res: Response, next: NextFunction) => {
256
+ try {
257
+ const body = req.body as Record<string, unknown>;
258
+ const profile = await auth.updateProfile(req.auth!.sub, {
259
+ firstName:
260
+ body.firstName === null ? null : optionalString(body.firstName),
261
+ lastName:
262
+ body.lastName === null ? null : optionalString(body.lastName),
263
+ displayName:
264
+ body.displayName === null ? null : optionalString(body.displayName),
265
+ phone: body.phone === null ? null : optionalString(body.phone),
266
+ username:
267
+ body.username === null ? null : optionalString(body.username),
268
+ photo: body.photo === null ? null : optionalString(body.photo),
269
+ });
270
+ res.status(200).json(profile);
271
+ } catch (err) {
272
+ next(err);
273
+ }
274
+ },
275
+ );
276
+
277
+ router.route(
278
+ "post",
279
+ "/2fa/enroll",
280
+ authenticated(),
281
+ async (req: Request, res: Response, next: NextFunction) => {
282
+ try {
283
+ const result = await auth.enrollTwoFactor(req.auth!.sub);
284
+ res.status(200).json(result);
285
+ } catch (err) {
286
+ next(err);
287
+ }
288
+ },
289
+ );
290
+
291
+ router.route(
292
+ "post",
293
+ "/2fa/confirm",
294
+ authenticated(),
295
+ async (req: Request, res: Response, next: NextFunction) => {
296
+ try {
297
+ const body = req.body as Record<string, unknown>;
298
+ const result = await auth.confirmTwoFactor(
299
+ req.auth!.sub,
300
+ requireString(body.code, "code"),
301
+ );
302
+ res.status(200).json(result);
303
+ } catch (err) {
304
+ next(err);
305
+ }
306
+ },
307
+ );
308
+
309
+ router.route(
310
+ "post",
311
+ "/2fa/disable",
312
+ authenticated(),
313
+ async (req: Request, res: Response, next: NextFunction) => {
314
+ try {
315
+ const body = req.body as Record<string, unknown>;
316
+ await auth.disableTwoFactor(
317
+ req.auth!.sub,
318
+ requireString(body.code, "code"),
319
+ );
320
+ res.status(200).json({ ok: true });
321
+ } catch (err) {
322
+ next(err);
323
+ }
324
+ },
325
+ );
326
+
327
+ router.route(
328
+ "post",
329
+ "/password/forgot",
330
+ publicRoute(),
331
+ async (req: Request, res: Response, next: NextFunction) => {
332
+ try {
333
+ const body = req.body as Record<string, unknown>;
334
+ await auth.requestPasswordReset(requireString(body.email, "email"));
335
+ res.status(200).json({ ok: true }); // always 200 — never reveal whether the email exists
336
+ } catch (err) {
337
+ next(err);
338
+ }
339
+ },
340
+ );
341
+
342
+ router.route(
343
+ "post",
344
+ "/password/reset",
345
+ publicRoute(),
346
+ async (req: Request, res: Response, next: NextFunction) => {
347
+ try {
348
+ const body = req.body as Record<string, unknown>;
349
+ await auth.resetPassword(
350
+ requireString(body.token, "token"),
351
+ requireString(body.newPassword, "newPassword"),
352
+ );
353
+ res.status(200).json({ ok: true });
354
+ } catch (err) {
355
+ next(err);
356
+ }
357
+ },
358
+ );
359
+
360
+ router.route(
361
+ "get",
362
+ "/oauth/:provider/start",
363
+ publicRoute(),
364
+ async (req: Request, res: Response, next: NextFunction) => {
365
+ try {
366
+ const result = await auth.startOAuth(
367
+ requireString(req.params.provider, "provider"),
368
+ );
369
+ res.status(200).json(result);
370
+ } catch (err) {
371
+ next(err);
372
+ }
373
+ },
374
+ );
375
+
376
+ router.route(
377
+ "get",
378
+ "/oauth/:provider/callback",
379
+ publicRoute(),
380
+ async (req: Request, res: Response, next: NextFunction) => {
381
+ try {
382
+ const tokens = await auth.completeOAuthCallback(
383
+ requireString(req.params.provider, "provider"),
384
+ requireString(req.query.code, "code"),
385
+ requireString(req.query.state, "state"),
386
+ );
387
+ res.status(200).json(tokens);
388
+ } catch (err) {
389
+ next(err);
390
+ }
391
+ },
392
+ );
393
+
394
+ return router.handler;
395
+ }
@@ -3,25 +3,28 @@ import swaggerUi from "swagger-ui-express";
3
3
  import { drizzle } from "drizzle-orm/node-postgres";
4
4
  import { Pool } from "pg";
5
5
  import type { RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
6
- import { createAdminRouter } from "./admin.router.js";
7
- import { AuditLogRepository } from "./audit-log.repository.js";
8
- import { authCoreErrorMiddleware } from "./auth-core-error.middleware.js";
6
+ import { createAdminRouter } from "./routers/admin.router.js";
7
+ import { AuditLogRepository } from "./repositories/audit-log.repository.js";
8
+ import { authCoreErrorMiddleware } from "./middleware/auth-core-error.middleware.js";
9
9
  import { AuthConfig, defaultAuthConfig } from "./auth.config.js";
10
- import { createAuthMiddleware } from "./auth.middleware.js";
11
- import { createAuthRouter } from "./auth.router.js";
12
- import { AuthService } from "./auth.service.js";
13
- import { createAuthzMiddleware } from "./authz.middleware.js";
10
+ import { createAuthMiddleware } from "./middleware/auth.middleware.js";
11
+ import { createAuthRouter } from "./routers/auth.router.js";
12
+ import { AuthService } from "./services/auth.service.js";
13
+ import { createAuthzMiddleware } from "./middleware/authz.middleware.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";
23
+ import { RbacRepository } from "./repositories/rbac.repository.js";
24
+ import { responseEnvelope } from "./middleware/response-envelope.middleware.js";
22
25
  import * as schema from "./schema.js";
23
- import { SessionRepository } from "./session.repository.js";
24
- import { TwoFactorRepository } from "./two-factor.repository.js";
26
+ import { SessionRepository } from "./repositories/session.repository.js";
27
+ import { TwoFactorRepository } from "./repositories/two-factor.repository.js";
25
28
 
26
29
  /**
27
30
  * Factory wiring a Drizzle `NodePgDatabase` (from a `pg.Pool`) plus all the plain classes,
@@ -39,15 +42,30 @@ export function createAuthApp(config: Partial<AuthConfig> = {}): Express {
39
42
  const keys = new KeyProviderService();
40
43
  // Swap the store for a Redis-backed one by passing `rateLimitStore` in `config` — nothing in
41
44
  // this library's source changes.
42
- const rateLimit: RateLimitDeps = resolvedConfig.rateLimitStore ?? new InMemoryRateLimitStore();
45
+ const rateLimit: RateLimitDeps =
46
+ resolvedConfig.rateLimitStore ?? new InMemoryRateLimitStore();
43
47
  // The cache seam. Swap the store for a Redis-backed one by passing `permissionCacheStore` in
44
48
  // `config` — nothing in this library's source changes. Keys are namespaced simpleauthkit:authz:*.
45
- const permissionCache = new PermissionCache(resolvedConfig.permissionCacheStore ?? new InMemoryPermissionCacheStore(), resolvedConfig);
49
+ const permissionCache = new PermissionCache(
50
+ resolvedConfig.permissionCacheStore ?? new InMemoryPermissionCacheStore(),
51
+ resolvedConfig,
52
+ );
46
53
  const rbac = new RbacRepository(db, permissionCache);
47
54
  const twoFactor = new TwoFactorRepository(db);
48
55
  const oauth = new OAuthRepository(db);
49
56
  const passwordReset = new PasswordResetRepository(db);
50
- const authService = new AuthService(db, sessions, keys, rateLimit, auditLog, rbac, twoFactor, oauth, passwordReset, resolvedConfig);
57
+ const authService = new AuthService(
58
+ db,
59
+ sessions,
60
+ keys,
61
+ rateLimit,
62
+ auditLog,
63
+ rbac,
64
+ twoFactor,
65
+ oauth,
66
+ passwordReset,
67
+ resolvedConfig,
68
+ );
51
69
 
52
70
  const requireAuth = createAuthMiddleware(keys, sessions);
53
71
  // Roles are global here, but they are read from the database on the request that uses them —
@@ -66,8 +84,23 @@ export function createAuthApp(config: Partial<AuthConfig> = {}): Express {
66
84
  const app = express();
67
85
  app.use(express.json());
68
86
  app.use(responseEnvelope());
69
- app.use("/auth/admin", createAdminRouter({ auth: authService, authentication: requireAuth, authorization: requireAuthz }));
70
- app.use("/auth", createAuthRouter({ auth: authService, config: resolvedConfig, authentication: requireAuth, authorization: requireAuthz }));
87
+ app.use(
88
+ "/auth/admin",
89
+ createAdminRouter({
90
+ auth: authService,
91
+ authentication: requireAuth,
92
+ authorization: requireAuthz,
93
+ }),
94
+ );
95
+ app.use(
96
+ "/auth",
97
+ createAuthRouter({
98
+ auth: authService,
99
+ config: resolvedConfig,
100
+ authentication: requireAuth,
101
+ authorization: requireAuthz,
102
+ }),
103
+ );
71
104
  // Hand-authored OpenAPI spec (see openapi-spec.ts) — this combo has no NestJS decorators to
72
105
  // generate one from, so it's served as static JSON, same as the nestjs-* combos' /docs-json.
73
106
  app.use("/docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
@@ -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();
@@ -1,9 +1,14 @@
1
1
  import { and, count, desc, eq, gte, lte, type SQL } from "drizzle-orm";
2
2
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import type { AuditEvent } from "@/lib/auth/core/types.js";
4
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
5
- import * as schema from "./schema.js";
6
- import { toIdOrNull, toIdOrUndefined } from "./id.helper.js";
4
+ import {
5
+ buildPageMeta,
6
+ normalizeLimit,
7
+ normalizePage,
8
+ type Paginated,
9
+ } from "../pagination.js";
10
+ import * as schema from "../schema.js";
11
+ import { toIdOrNull, toIdOrUndefined } from "../id.helper.js";
7
12
 
8
13
  export interface AuditLogEntry {
9
14
  id: string;
@@ -45,7 +50,10 @@ export function toAuditLogEntry(row: AuditLogRow): AuditLogEntry {
45
50
  /** "role_assigned" -> "Role assigned". Derived rather than passed in, so core never has to carry display text. */
46
51
  export function humanizeAction(action: string): string {
47
52
  const words = action.split("_");
48
- return [words[0].charAt(0).toUpperCase() + words[0].slice(1), ...words.slice(1)].join(" ");
53
+ return [
54
+ words[0].charAt(0).toUpperCase() + words[0].slice(1),
55
+ ...words.slice(1),
56
+ ].join(" ");
49
57
  }
50
58
 
51
59
  /**
@@ -56,7 +64,10 @@ export function humanizeAction(action: string): string {
56
64
  export class AuditLogRepository {
57
65
  constructor(private readonly db: NodePgDatabase<typeof schema>) {}
58
66
 
59
- async append(event: AuditEvent, opts: { remarks?: string } = {}): Promise<void> {
67
+ async append(
68
+ event: AuditEvent,
69
+ opts: { remarks?: string } = {},
70
+ ): Promise<void> {
60
71
  await this.db.insert(schema.auditLogs).values({
61
72
  action: event.type,
62
73
  name: humanizeAction(event.type),
@@ -73,10 +84,14 @@ export class AuditLogRepository {
73
84
  const conditions: SQL[] = [];
74
85
 
75
86
  const userIdBig = toIdOrUndefined(filter.userId);
76
- if (userIdBig !== undefined) conditions.push(eq(schema.auditLogs.userId, userIdBig));
77
- if (filter.action) conditions.push(eq(schema.auditLogs.action, filter.action));
78
- if (filter.since) conditions.push(gte(schema.auditLogs.createdAt, new Date(filter.since)));
79
- if (filter.until) conditions.push(lte(schema.auditLogs.createdAt, new Date(filter.until)));
87
+ if (userIdBig !== undefined)
88
+ conditions.push(eq(schema.auditLogs.userId, userIdBig));
89
+ if (filter.action)
90
+ conditions.push(eq(schema.auditLogs.action, filter.action));
91
+ if (filter.since)
92
+ conditions.push(gte(schema.auditLogs.createdAt, new Date(filter.since)));
93
+ if (filter.until)
94
+ conditions.push(lte(schema.auditLogs.createdAt, new Date(filter.until)));
80
95
  const where = conditions.length ? and(...conditions) : undefined;
81
96
 
82
97
  const rows = await this.db
@@ -86,7 +101,10 @@ export class AuditLogRepository {
86
101
  .orderBy(desc(schema.auditLogs.createdAt), desc(schema.auditLogs.id))
87
102
  .limit(limit)
88
103
  .offset((page - 1) * limit);
89
- const [{ value: total }] = await this.db.select({ value: count() }).from(schema.auditLogs).where(where);
104
+ const [{ value: total }] = await this.db
105
+ .select({ value: count() })
106
+ .from(schema.auditLogs)
107
+ .where(where);
90
108
 
91
109
  return { items: rows, meta: buildPageMeta(page, limit, total) };
92
110
  }