@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
@@ -1,314 +0,0 @@
1
- import type { NextFunction, Request, RequestHandler, Response } from "express";
2
- import { AuthService } from "./auth.service.js";
3
- import { HttpError } from "./http-error.js";
4
- import { ability, createTieredRouter, type TierMiddleware } from "./route-tiers.js";
5
- import "./request-context.js";
6
-
7
- function requireString(value: unknown, field: string): string {
8
- if (typeof value !== "string" || value.length === 0) throw new HttpError(400, `${field} is required`);
9
- return value;
10
- }
11
-
12
- const optionalString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
13
-
14
- export interface AdminRouterDeps extends TierMiddleware {
15
- auth: AuthService;
16
- }
17
-
18
- /**
19
- * Administration of the whole deployment. Authority is a permission, never a role name: the authz
20
- * middleware resolves the caller's roles and permissions from the database on this request, builds
21
- * the CASL ability from them, and each route's `ability(...)` tier checks its slugs against it.
22
- * There is no role-based bypass — holding a role called "admin" that carries no permissions gets
23
- * exactly the same 403 as holding no role at all, which is why `npm run seed` (which provisions the
24
- * catalog and the roles that carry it) is a prerequisite for this router doing anything.
25
- *
26
- * Each route says what it demands; the rows say who is granted it. The slugs below are checked at
27
- * compile time against `PERMISSION_CATALOG` in `rbac.defaults.ts`, while who holds them — and
28
- * whether the permission is active at all — is edited through this very router and takes effect on
29
- * the next request. `GET /auth/me` returns the caller's slugs, and a console rebuilds the same
30
- * ability from them with the same function the middleware used.
31
- *
32
- * Replaces the reference combo's `AdminController`. Mounted under `/auth/admin` by
33
- * create-auth-app.ts; `createTieredRouter` stands in for its
34
- * `@UseGuards(AuthGuard, AuthzGuard, AbilityGuard)` — see route-tiers.ts for why the tier is a
35
- * required argument rather than a decorator.
36
- */
37
- export function createAdminRouter(deps: AdminRouterDeps): RequestHandler {
38
- const { auth, authentication, authorization } = deps;
39
- const admin = createTieredRouter({ authentication, authorization });
40
-
41
- admin.route("get", "/users", ability("users:read"), async (req: Request, res: Response, next: NextFunction) => {
42
- try {
43
- const { search, page, limit } = req.query as Record<string, string | undefined>;
44
- res.status(200).json(await auth.listUsers({ search, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined }));
45
- } catch (err) {
46
- next(err);
47
- }
48
- });
49
-
50
- // No invitation email — the account is usable immediately with the password given here.
51
- admin.route("post", "/users", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
52
- try {
53
- const body = req.body as Record<string, unknown>;
54
- res.status(201).json(
55
- await auth.createUser(
56
- {
57
- email: requireString(body.email, "email"),
58
- password: requireString(body.password, "password"),
59
- firstName: optionalString(body.firstName),
60
- lastName: optionalString(body.lastName),
61
- displayName: optionalString(body.displayName),
62
- phone: optionalString(body.phone),
63
- username: optionalString(body.username),
64
- roles: Array.isArray(body.roles) ? body.roles.filter((role): role is string => typeof role === "string") : undefined,
65
- },
66
- req.auth!.sub,
67
- ),
68
- );
69
- } catch (err) {
70
- next(err);
71
- }
72
- });
73
-
74
- admin.route("get", "/users/:userId", ability("users:read"), async (req: Request, res: Response, next: NextFunction) => {
75
- try {
76
- res.status(200).json(await auth.getUser(requireString(req.params.userId, "userId")));
77
- } catch (err) {
78
- next(err);
79
- }
80
- });
81
-
82
- // Profile fields only — email is the login identifier and is not editable here.
83
- admin.route("patch", "/users/:userId", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
84
- try {
85
- const userId = requireString(req.params.userId, "userId");
86
- const body = req.body as Record<string, unknown>;
87
- res.status(200).json(
88
- await auth.updateUser(
89
- userId,
90
- {
91
- firstName: body.firstName === null ? null : optionalString(body.firstName),
92
- lastName: body.lastName === null ? null : optionalString(body.lastName),
93
- displayName: body.displayName === null ? null : optionalString(body.displayName),
94
- phone: body.phone === null ? null : optionalString(body.phone),
95
- username: body.username === null ? null : optionalString(body.username),
96
- photo: body.photo === null ? null : optionalString(body.photo),
97
- },
98
- req.auth!.sub,
99
- ),
100
- );
101
- } catch (err) {
102
- next(err);
103
- }
104
- });
105
-
106
- // Soft-delete: the row survives for audit purposes, stops appearing in listings, and can no
107
- // longer authenticate.
108
- admin.route("delete", "/users/:userId", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
109
- try {
110
- const userId = requireString(req.params.userId, "userId");
111
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot delete your own account");
112
- await auth.deleteUser(userId, req.auth!.sub, optionalString((req.body as Record<string, unknown> | undefined)?.reason));
113
- res.status(200).json({ ok: true });
114
- } catch (err) {
115
- next(err);
116
- }
117
- });
118
-
119
- admin.route("get", "/audit-log", ability("audit-log:read"), async (req: Request, res: Response, next: NextFunction) => {
120
- try {
121
- const { userId, action, since, until, page, limit } = req.query as Record<string, string | undefined>;
122
- res.status(200).json(
123
- await auth.listAuditLog({ userId, action, since, until, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined }),
124
- );
125
- } catch (err) {
126
- next(err);
127
- }
128
- });
129
-
130
- admin.route("get", "/permissions", ability("permissions:read"), async (_req: Request, res: Response, next: NextFunction) => {
131
- try {
132
- res.status(200).json(await auth.listPermissions());
133
- } catch (err) {
134
- next(err);
135
- }
136
- });
137
-
138
- admin.route("post", "/permissions", ability("permissions:define"), async (req: Request, res: Response, next: NextFunction) => {
139
- try {
140
- const body = req.body as Record<string, unknown>;
141
- res.status(201).json(
142
- await auth.definePermission(
143
- {
144
- slug: requireString(body.slug, "slug"),
145
- name: optionalString(body.name),
146
- displayName: optionalString(body.displayName),
147
- description: body.description === null ? null : optionalString(body.description),
148
- group: optionalString(body.group),
149
- groupOrder: typeof body.groupOrder === "number" ? body.groupOrder : undefined,
150
- order: typeof body.order === "number" ? body.order : undefined,
151
- isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
152
- },
153
- req.auth!.sub,
154
- ),
155
- );
156
- } catch (err) {
157
- next(err);
158
- }
159
- });
160
-
161
- admin.route("get", "/roles", ability("roles:manage"), async (_req: Request, res: Response, next: NextFunction) => {
162
- try {
163
- res.status(200).json(await auth.listRoles());
164
- } catch (err) {
165
- next(err);
166
- }
167
- });
168
-
169
- admin.route("post", "/roles", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
170
- try {
171
- const body = req.body as Record<string, unknown>;
172
- res.status(201).json(
173
- await auth.createRole(
174
- {
175
- slug: requireString(body.slug, "slug"),
176
- name: optionalString(body.name),
177
- displayName: optionalString(body.displayName),
178
- description: optionalString(body.description) ?? null,
179
- },
180
- req.auth!.sub,
181
- ),
182
- );
183
- } catch (err) {
184
- next(err);
185
- }
186
- });
187
-
188
- admin.route("patch", "/roles/:roleId", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
189
- try {
190
- const roleId = requireString(req.params.roleId, "roleId");
191
- const body = req.body as Record<string, unknown>;
192
- res.status(200).json(
193
- await auth.updateRole(
194
- roleId,
195
- {
196
- name: optionalString(body.name),
197
- displayName: optionalString(body.displayName),
198
- description: body.description === null ? null : optionalString(body.description),
199
- isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
200
- },
201
- req.auth!.sub,
202
- ),
203
- );
204
- } catch (err) {
205
- next(err);
206
- }
207
- });
208
-
209
- // Soft-delete: existing assignments are left in place rather than cascade-deleted, and the role
210
- // simply stops being resolved.
211
- admin.route("delete", "/roles/:roleId", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
212
- try {
213
- const roleId = requireString(req.params.roleId, "roleId");
214
- await auth.deleteRole(roleId, req.auth!.sub, optionalString((req.body as Record<string, unknown> | undefined)?.reason));
215
- res.status(200).json({ ok: true });
216
- } catch (err) {
217
- next(err);
218
- }
219
- });
220
-
221
- admin.route("post", "/roles/:roleId/permissions", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
222
- try {
223
- const body = req.body as Record<string, unknown>;
224
- await auth.attachPermissionToRole(requireString(req.params.roleId, "roleId"), requireString(body.permission, "permission"), req.auth!.sub);
225
- res.status(201).json({ ok: true });
226
- } catch (err) {
227
- next(err);
228
- }
229
- });
230
-
231
- admin.route("post", "/users/:userId/roles", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
232
- try {
233
- const body = req.body as Record<string, unknown>;
234
- await auth.assignRole(requireString(req.params.userId, "userId"), requireString(body.role, "role"));
235
- res.status(201).json({ ok: true });
236
- } catch (err) {
237
- next(err);
238
- }
239
- });
240
-
241
- admin.route("post", "/users/:userId/roles/:roleSlug/revoke", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
242
- try {
243
- const userId = requireString(req.params.userId, "userId");
244
- // Revoking your own `admin` would strip the very permission that let you call this,
245
- // with no route back in. Assigning to yourself is fine — it can't lock anyone out.
246
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot change your own roles");
247
- await auth.revokeRole(userId, requireString(req.params.roleSlug, "roleSlug"));
248
- res.status(201).json({ ok: true });
249
- } catch (err) {
250
- next(err);
251
- }
252
- });
253
-
254
- admin.route("post", "/users/:userId/permissions", ability("permissions:grant"), async (req: Request, res: Response, next: NextFunction) => {
255
- try {
256
- const body = req.body as Record<string, unknown>;
257
- await auth.grantPermission(requireString(req.params.userId, "userId"), requireString(body.permission, "permission"), req.auth!.sub);
258
- res.status(201).json({ ok: true });
259
- } catch (err) {
260
- next(err);
261
- }
262
- });
263
-
264
- admin.route("post", "/users/:userId/permissions/:permissionSlug/revoke", ability("permissions:grant"), async (req: Request, res: Response, next: NextFunction) => {
265
- try {
266
- await auth.revokePermission(requireString(req.params.userId, "userId"), requireString(req.params.permissionSlug, "permissionSlug"));
267
- res.status(201).json({ ok: true });
268
- } catch (err) {
269
- next(err);
270
- }
271
- });
272
-
273
- admin.route("post", "/users/:userId/block", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
274
- try {
275
- const userId = requireString(req.params.userId, "userId");
276
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot block your own account");
277
- await auth.block(userId, { userId: req.auth!.sub, ip: req.ip });
278
- res.status(201).json({ ok: true });
279
- } catch (err) {
280
- next(err);
281
- }
282
- });
283
-
284
- admin.route("post", "/users/:userId/unblock", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
285
- try {
286
- await auth.unblock(requireString(req.params.userId, "userId"), { userId: req.auth!.sub, ip: req.ip });
287
- res.status(201).json({ ok: true });
288
- } catch (err) {
289
- next(err);
290
- }
291
- });
292
-
293
- admin.route("post", "/users/:userId/deactivate", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
294
- try {
295
- const userId = requireString(req.params.userId, "userId");
296
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot deactivate your own account");
297
- await auth.deactivate(userId, { userId: req.auth!.sub, ip: req.ip });
298
- res.status(201).json({ ok: true });
299
- } catch (err) {
300
- next(err);
301
- }
302
- });
303
-
304
- admin.route("post", "/users/:userId/activate", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
305
- try {
306
- await auth.activate(requireString(req.params.userId, "userId"), { userId: req.auth!.sub, ip: req.ip });
307
- res.status(201).json({ ok: true });
308
- } catch (err) {
309
- next(err);
310
- }
311
- });
312
-
313
- return admin.handler;
314
- }
@@ -1,330 +0,0 @@
1
- import type { NextFunction, Request, RequestHandler, Response } from "express";
2
- import { AuthService } from "./auth.service.js";
3
- import { HttpError } from "./http-error.js";
4
- import { ability, createTieredRouter } from "./route-tiers.js";
5
- import { WorkspaceRepository } from "./workspace.repository.js";
6
- import { hashPassword } from "@/lib/auth/core/crypto.js";
7
- import "./request-context.js";
8
-
9
- function requireString(value: unknown, field: string): string {
10
- if (typeof value !== "string" || value.length === 0) throw new HttpError(400, `${field} is required`);
11
- return value;
12
- }
13
-
14
- const optionalString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
15
-
16
- export interface AdminRouterDeps {
17
- auth: AuthService;
18
- workspaces: WorkspaceRepository;
19
- authentication: RequestHandler;
20
- /** The mandatory workspace resolution — every route here acts inside one. See authz.middleware.ts. */
21
- workspaceScope: RequestHandler;
22
- }
23
-
24
- /**
25
- * Administration *of one workspace*, named by the `X-Workspace-Id` header. Authority is a
26
- * permission, never a role name: the workspace middleware resolves the caller's membership and the
27
- * roles and permissions it carries **in that workspace**, builds the CASL ability from them, and
28
- * each route's `ability(...)` tier checks its slugs against it. There is no role-based bypass —
29
- * holding a role called "admin" that carries no permissions gets exactly the same 403 as holding no
30
- * role at all, which is why `npm run seed` (which provisions the catalog and the roles that carry
31
- * it) is a prerequisite for this router doing anything.
32
- *
33
- * Every handler passes `req.authz` down to the service, and every service method reaches the
34
- * database through a workspace-scoped query. That is the security property this model rests on: an
35
- * admin of one workspace has no expressible way to name a row in another.
36
- *
37
- * Each route says what it demands; the rows say who is granted it. The slugs below are checked at
38
- * compile time against `PERMISSION_CATALOG` in `rbac.defaults.ts`, while who holds them — and
39
- * whether the permission is active at all — is edited through this very router and takes effect on
40
- * the next request. `GET /auth/me` returns the caller's slugs, and a console rebuilds the same
41
- * ability from them with the same function the middleware used.
42
- *
43
- * Replaces the reference combo's `AdminController`. Mounted under `/auth/admin` by
44
- * create-auth-app.ts; `createTieredRouter` stands in for its
45
- * `@UseGuards(AuthGuard, AuthzGuard, AbilityGuard)` — see route-tiers.ts for why the tier is a
46
- * required argument rather than a decorator.
47
- */
48
- export function createAdminRouter(deps: AdminRouterDeps): RequestHandler {
49
- const { auth, workspaces, authentication, workspaceScope } = deps;
50
- const admin = createTieredRouter({ authentication, authorization: workspaceScope });
51
-
52
- admin.route("get", "/users", ability("users:read"), async (req: Request, res: Response, next: NextFunction) => {
53
- try {
54
- const { search, page, limit } = req.query as Record<string, string | undefined>;
55
- res.status(200).json(await auth.listUsers(req.authz!, { search, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined }));
56
- } catch (err) {
57
- next(err);
58
- }
59
- });
60
-
61
- // No invitation email — the account is usable immediately with the password given here.
62
- admin.route("post", "/users", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
63
- try {
64
- const body = req.body as Record<string, unknown>;
65
- const passwordHash = await hashPassword(requireString(body.password, "password"));
66
- const member = await workspaces.createMember(
67
- req.authz!.workspaceId,
68
- {
69
- email: requireString(body.email, "email"),
70
- passwordHash,
71
- firstName: optionalString(body.firstName),
72
- lastName: optionalString(body.lastName),
73
- displayName: optionalString(body.displayName),
74
- phone: optionalString(body.phone),
75
- username: optionalString(body.username),
76
- roles: Array.isArray(body.roles) ? body.roles.filter((role): role is string => typeof role === "string") : undefined,
77
- },
78
- req.auth!.sub,
79
- );
80
- res.status(201).json(await auth.getUser(req.authz!, member.userId));
81
- } catch (err) {
82
- next(err);
83
- }
84
- });
85
-
86
- admin.route("get", "/users/:userId", ability("users:read"), async (req: Request, res: Response, next: NextFunction) => {
87
- try {
88
- res.status(200).json(await auth.getUser(req.authz!, requireString(req.params.userId, "userId")));
89
- } catch (err) {
90
- next(err);
91
- }
92
- });
93
-
94
- // Profile fields only — email is the login identifier and is not editable here.
95
- admin.route("patch", "/users/:userId", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
96
- try {
97
- const userId = requireString(req.params.userId, "userId");
98
- const body = req.body as Record<string, unknown>;
99
- res.status(200).json(
100
- await auth.updateUser(
101
- req.authz!,
102
- userId,
103
- {
104
- firstName: body.firstName === null ? null : optionalString(body.firstName),
105
- lastName: body.lastName === null ? null : optionalString(body.lastName),
106
- displayName: body.displayName === null ? null : optionalString(body.displayName),
107
- phone: body.phone === null ? null : optionalString(body.phone),
108
- username: body.username === null ? null : optionalString(body.username),
109
- photo: body.photo === null ? null : optionalString(body.photo),
110
- },
111
- req.auth!.sub,
112
- ),
113
- );
114
- } catch (err) {
115
- next(err);
116
- }
117
- });
118
-
119
- // Soft-delete: the row survives for audit purposes, stops appearing in listings, and can no
120
- // longer authenticate. This disables the account across every workspace it belongs to, not
121
- // just this one — the same reach `block` already has.
122
- admin.route("delete", "/users/:userId", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
123
- try {
124
- const userId = requireString(req.params.userId, "userId");
125
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot delete your own account");
126
- await auth.deleteUser(req.authz!, userId, req.auth!.sub, optionalString((req.body as Record<string, unknown> | undefined)?.reason));
127
- res.status(200).json({ ok: true });
128
- } catch (err) {
129
- next(err);
130
- }
131
- });
132
-
133
- admin.route("get", "/audit-log", ability("audit-log:read"), async (req: Request, res: Response, next: NextFunction) => {
134
- try {
135
- const { userId, action, since, until, page, limit } = req.query as Record<string, string | undefined>;
136
- res.status(200).json(
137
- await auth.listAuditLog(req.authz!, { userId, action, since, until, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined }),
138
- );
139
- } catch (err) {
140
- next(err);
141
- }
142
- });
143
-
144
- admin.route("get", "/permissions", ability("permissions:read"), async (_req: Request, res: Response, next: NextFunction) => {
145
- try {
146
- res.status(200).json(await auth.listPermissions());
147
- } catch (err) {
148
- next(err);
149
- }
150
- });
151
-
152
- admin.route("post", "/permissions", ability("permissions:define"), async (req: Request, res: Response, next: NextFunction) => {
153
- try {
154
- const body = req.body as Record<string, unknown>;
155
- res.status(201).json(
156
- await auth.definePermission(
157
- {
158
- slug: requireString(body.slug, "slug"),
159
- name: optionalString(body.name),
160
- displayName: optionalString(body.displayName),
161
- description: body.description === null ? null : optionalString(body.description),
162
- group: optionalString(body.group),
163
- groupOrder: typeof body.groupOrder === "number" ? body.groupOrder : undefined,
164
- order: typeof body.order === "number" ? body.order : undefined,
165
- isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
166
- },
167
- req.auth!.sub,
168
- ),
169
- );
170
- } catch (err) {
171
- next(err);
172
- }
173
- });
174
-
175
- admin.route("get", "/roles", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
176
- try {
177
- res.status(200).json(await auth.listRoles(req.authz!));
178
- } catch (err) {
179
- next(err);
180
- }
181
- });
182
-
183
- admin.route("post", "/roles", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
184
- try {
185
- const body = req.body as Record<string, unknown>;
186
- res.status(201).json(
187
- await auth.createRole(
188
- req.authz!,
189
- {
190
- slug: requireString(body.slug, "slug"),
191
- name: optionalString(body.name),
192
- displayName: optionalString(body.displayName),
193
- description: optionalString(body.description) ?? null,
194
- },
195
- req.auth!.sub,
196
- ),
197
- );
198
- } catch (err) {
199
- next(err);
200
- }
201
- });
202
-
203
- admin.route("patch", "/roles/:roleId", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
204
- try {
205
- const roleId = requireString(req.params.roleId, "roleId");
206
- const body = req.body as Record<string, unknown>;
207
- res.status(200).json(
208
- await auth.updateRole(
209
- req.authz!,
210
- roleId,
211
- {
212
- name: optionalString(body.name),
213
- displayName: optionalString(body.displayName),
214
- description: body.description === null ? null : optionalString(body.description),
215
- isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
216
- },
217
- req.auth!.sub,
218
- ),
219
- );
220
- } catch (err) {
221
- next(err);
222
- }
223
- });
224
-
225
- // Soft-delete: existing assignments are left in place rather than cascade-deleted, and the role
226
- // simply stops being resolved.
227
- admin.route("delete", "/roles/:roleId", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
228
- try {
229
- const roleId = requireString(req.params.roleId, "roleId");
230
- await auth.deleteRole(req.authz!, roleId, req.auth!.sub, optionalString((req.body as Record<string, unknown> | undefined)?.reason));
231
- res.status(200).json({ ok: true });
232
- } catch (err) {
233
- next(err);
234
- }
235
- });
236
-
237
- admin.route("post", "/roles/:roleId/permissions", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
238
- try {
239
- const body = req.body as Record<string, unknown>;
240
- await auth.attachPermissionToRole(req.authz!, requireString(req.params.roleId, "roleId"), requireString(body.permission, "permission"), req.auth!.sub);
241
- res.status(201).json({ ok: true });
242
- } catch (err) {
243
- next(err);
244
- }
245
- });
246
-
247
- admin.route("post", "/users/:userId/roles", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
248
- try {
249
- const body = req.body as Record<string, unknown>;
250
- await auth.assignRole(req.authz!, requireString(req.params.userId, "userId"), requireString(body.role, "role"));
251
- res.status(201).json({ ok: true });
252
- } catch (err) {
253
- next(err);
254
- }
255
- });
256
-
257
- admin.route("post", "/users/:userId/roles/:roleSlug/revoke", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
258
- try {
259
- const userId = requireString(req.params.userId, "userId");
260
- // Revoking your own `admin` would strip the very permission that let you call this,
261
- // with no route back in. Assigning to yourself is fine — it can't lock anyone out.
262
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot change your own roles");
263
- await auth.revokeRole(req.authz!, userId, requireString(req.params.roleSlug, "roleSlug"));
264
- res.status(201).json({ ok: true });
265
- } catch (err) {
266
- next(err);
267
- }
268
- });
269
-
270
- admin.route("post", "/users/:userId/permissions", ability("permissions:grant"), async (req: Request, res: Response, next: NextFunction) => {
271
- try {
272
- const body = req.body as Record<string, unknown>;
273
- await auth.grantPermission(req.authz!, requireString(req.params.userId, "userId"), requireString(body.permission, "permission"), req.auth!.sub);
274
- res.status(201).json({ ok: true });
275
- } catch (err) {
276
- next(err);
277
- }
278
- });
279
-
280
- admin.route("post", "/users/:userId/permissions/:permissionSlug/revoke", ability("permissions:grant"), async (req: Request, res: Response, next: NextFunction) => {
281
- try {
282
- await auth.revokePermission(req.authz!, requireString(req.params.userId, "userId"), requireString(req.params.permissionSlug, "permissionSlug"));
283
- res.status(201).json({ ok: true });
284
- } catch (err) {
285
- next(err);
286
- }
287
- });
288
-
289
- admin.route("post", "/users/:userId/block", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
290
- try {
291
- const userId = requireString(req.params.userId, "userId");
292
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot block your own account");
293
- await auth.block(req.authz!, userId, { userId: req.auth!.sub, ip: req.ip });
294
- res.status(201).json({ ok: true });
295
- } catch (err) {
296
- next(err);
297
- }
298
- });
299
-
300
- admin.route("post", "/users/:userId/unblock", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
301
- try {
302
- await auth.unblock(req.authz!, requireString(req.params.userId, "userId"), { userId: req.auth!.sub, ip: req.ip });
303
- res.status(201).json({ ok: true });
304
- } catch (err) {
305
- next(err);
306
- }
307
- });
308
-
309
- admin.route("post", "/users/:userId/deactivate", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
310
- try {
311
- const userId = requireString(req.params.userId, "userId");
312
- if (userId === req.auth!.sub) throw new HttpError(403, "cannot deactivate your own account");
313
- await auth.deactivate(req.authz!, userId, { userId: req.auth!.sub, ip: req.ip });
314
- res.status(201).json({ ok: true });
315
- } catch (err) {
316
- next(err);
317
- }
318
- });
319
-
320
- admin.route("post", "/users/:userId/activate", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
321
- try {
322
- await auth.activate(req.authz!, requireString(req.params.userId, "userId"), { userId: req.auth!.sub, ip: req.ip });
323
- res.status(201).json({ ok: true });
324
- } catch (err) {
325
- next(err);
326
- }
327
- });
328
-
329
- return admin.handler;
330
- }