@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,534 @@
1
+ import type { NextFunction, Request, RequestHandler, Response } from "express";
2
+ import { hashPassword } from "@/lib/auth/core/crypto.js";
3
+ import { AuthService } from "../services/auth.service.js";
4
+ import { HttpError } from "../http-error.js";
5
+ import { ability, createTieredRouter } from "../route-tiers.js";
6
+ import { WorkspaceRepository } from "../repositories/workspace.repository.js";
7
+ import "../request-context.js";
8
+
9
+ function requireString(value: unknown, field: string): string {
10
+ if (typeof value !== "string" || value.length === 0)
11
+ throw new HttpError(400, `${field} is required`);
12
+ return value;
13
+ }
14
+
15
+ const optionalString = (value: unknown): string | undefined =>
16
+ typeof value === "string" ? value : undefined;
17
+
18
+ export interface AdminRouterDeps {
19
+ auth: AuthService;
20
+ /** Creating a member directly writes through the repository, not `AuthService` — see the `POST /users` route below. */
21
+ workspaces: WorkspaceRepository;
22
+ authentication: RequestHandler;
23
+ /** The mandatory workspace resolution — every route here acts inside one. See authz.middleware.ts. */
24
+ workspaceScope: RequestHandler;
25
+ }
26
+
27
+ /**
28
+ * Administration *of one workspace*, named by the `X-Workspace-Id` header. Authority is a
29
+ * permission, never a role name: the workspace middleware resolves the caller's membership and the
30
+ * roles and permissions it carries **in that workspace**, builds the CASL ability from them, and
31
+ * each route's `ability(...)` tier checks its slugs against it. There is no role-based bypass —
32
+ * holding a role called "admin" that carries no permissions gets exactly the same 403 as holding no
33
+ * role at all, which is why `npm run seed` (which provisions the catalog and the roles that carry
34
+ * it) is a prerequisite for this router doing anything.
35
+ *
36
+ * Every handler passes `req.authz` down to the service, and every service method reaches the
37
+ * database through a workspace-scoped query. That is the security property this model rests on: an
38
+ * admin of one workspace has no expressible way to name a row in another.
39
+ *
40
+ * Each route says what it demands; the rows say who is granted it. The slugs below are checked at
41
+ * compile time against `PERMISSION_CATALOG` in `rbac.defaults.ts`, while who holds them — and
42
+ * whether the permission is active at all — is edited through this very router and takes effect on
43
+ * the next request. `GET /auth/me` returns the caller's slugs, and a console rebuilds the same
44
+ * ability from them with the same function the middleware used.
45
+ *
46
+ * Replaces the reference combo's `AdminController`. Mounted under `/auth/admin` by
47
+ * create-auth-app.ts; `createTieredRouter` stands in for its
48
+ * `@UseGuards(AuthGuard, AuthzGuard, AbilityGuard)` — see route-tiers.ts for why the tier is a
49
+ * required argument rather than a decorator.
50
+ */
51
+ export function createAdminRouter(deps: AdminRouterDeps): RequestHandler {
52
+ const { auth, workspaces, authentication, workspaceScope } = deps;
53
+ const admin = createTieredRouter({
54
+ authentication,
55
+ authorization: workspaceScope,
56
+ });
57
+
58
+ admin.route(
59
+ "get",
60
+ "/users",
61
+ ability("users:read"),
62
+ async (req: Request, res: Response, next: NextFunction) => {
63
+ try {
64
+ const { search, page, limit } = req.query as Record<
65
+ string,
66
+ string | undefined
67
+ >;
68
+ res
69
+ .status(200)
70
+ .json(
71
+ await auth.listUsers(req.authz!, {
72
+ search,
73
+ page: page ? Number(page) : undefined,
74
+ limit: limit ? Number(limit) : undefined,
75
+ }),
76
+ );
77
+ } catch (err) {
78
+ next(err);
79
+ }
80
+ },
81
+ );
82
+
83
+ admin.route(
84
+ "post",
85
+ "/users",
86
+ ability("users:manage"),
87
+ async (req: Request, res: Response, next: NextFunction) => {
88
+ try {
89
+ const body = req.body as Record<string, unknown>;
90
+ const passwordHash = await hashPassword(
91
+ requireString(body.password, "password"),
92
+ );
93
+ const member = await workspaces.createMember(
94
+ req.authz!.workspaceId,
95
+ {
96
+ email: requireString(body.email, "email"),
97
+ passwordHash,
98
+ firstName: optionalString(body.firstName),
99
+ lastName: optionalString(body.lastName),
100
+ displayName: optionalString(body.displayName),
101
+ phone: optionalString(body.phone),
102
+ username: optionalString(body.username),
103
+ roles: Array.isArray(body.roles)
104
+ ? body.roles.filter(
105
+ (role): role is string => typeof role === "string",
106
+ )
107
+ : undefined,
108
+ },
109
+ req.auth!.sub,
110
+ );
111
+ res.status(201).json(await auth.getUser(req.authz!, member.userId));
112
+ } catch (err) {
113
+ next(err);
114
+ }
115
+ },
116
+ );
117
+
118
+ admin.route(
119
+ "get",
120
+ "/users/:userId",
121
+ ability("users:read"),
122
+ async (req: Request, res: Response, next: NextFunction) => {
123
+ try {
124
+ res
125
+ .status(200)
126
+ .json(
127
+ await auth.getUser(
128
+ req.authz!,
129
+ requireString(req.params.userId, "userId"),
130
+ ),
131
+ );
132
+ } catch (err) {
133
+ next(err);
134
+ }
135
+ },
136
+ );
137
+
138
+ admin.route(
139
+ "patch",
140
+ "/users/:userId",
141
+ ability("users:manage"),
142
+ async (req: Request, res: Response, next: NextFunction) => {
143
+ try {
144
+ const body = req.body as Record<string, unknown>;
145
+ res.status(200).json(
146
+ await auth.updateUser(
147
+ req.authz!,
148
+ requireString(req.params.userId, "userId"),
149
+ {
150
+ firstName:
151
+ body.firstName === null ? null : optionalString(body.firstName),
152
+ lastName:
153
+ body.lastName === null ? null : optionalString(body.lastName),
154
+ displayName:
155
+ body.displayName === null
156
+ ? null
157
+ : optionalString(body.displayName),
158
+ phone: body.phone === null ? null : optionalString(body.phone),
159
+ username:
160
+ body.username === null ? null : optionalString(body.username),
161
+ photo: body.photo === null ? null : optionalString(body.photo),
162
+ },
163
+ req.auth!.sub,
164
+ ),
165
+ );
166
+ } catch (err) {
167
+ next(err);
168
+ }
169
+ },
170
+ );
171
+
172
+ admin.route(
173
+ "delete",
174
+ "/users/:userId",
175
+ ability("users:manage"),
176
+ async (req: Request, res: Response, next: NextFunction) => {
177
+ try {
178
+ const userId = requireString(req.params.userId, "userId");
179
+ if (userId === req.auth!.sub)
180
+ throw new HttpError(403, "cannot delete your own account");
181
+ const body = (req.body ?? {}) as Record<string, unknown>;
182
+ await auth.deleteUser(
183
+ req.authz!,
184
+ userId,
185
+ req.auth!.sub,
186
+ optionalString(body.reason),
187
+ );
188
+ res.status(200).json({ ok: true });
189
+ } catch (err) {
190
+ next(err);
191
+ }
192
+ },
193
+ );
194
+
195
+ admin.route(
196
+ "get",
197
+ "/audit-log",
198
+ ability("audit-log:read"),
199
+ async (req: Request, res: Response, next: NextFunction) => {
200
+ try {
201
+ const { userId, action, since, until, page, limit } =
202
+ req.query as Record<string, string | undefined>;
203
+ res
204
+ .status(200)
205
+ .json(
206
+ await auth.listAuditLog(req.authz!, {
207
+ userId,
208
+ action,
209
+ since,
210
+ until,
211
+ page: page ? Number(page) : undefined,
212
+ limit: limit ? Number(limit) : undefined,
213
+ }),
214
+ );
215
+ } catch (err) {
216
+ next(err);
217
+ }
218
+ },
219
+ );
220
+
221
+ admin.route(
222
+ "get",
223
+ "/permissions",
224
+ ability("permissions:read"),
225
+ async (_req: Request, res: Response, next: NextFunction) => {
226
+ try {
227
+ res.status(200).json(await auth.listPermissions());
228
+ } catch (err) {
229
+ next(err);
230
+ }
231
+ },
232
+ );
233
+
234
+ admin.route(
235
+ "post",
236
+ "/permissions",
237
+ ability("permissions:define"),
238
+ async (req: Request, res: Response, next: NextFunction) => {
239
+ try {
240
+ const body = req.body as Record<string, unknown>;
241
+ res.status(201).json(
242
+ await auth.definePermission(
243
+ {
244
+ slug: requireString(body.slug, "slug"),
245
+ name: optionalString(body.name),
246
+ displayName: optionalString(body.displayName),
247
+ description:
248
+ body.description === null
249
+ ? null
250
+ : optionalString(body.description),
251
+ group: optionalString(body.group),
252
+ groupOrder:
253
+ typeof body.groupOrder === "number"
254
+ ? body.groupOrder
255
+ : undefined,
256
+ order: typeof body.order === "number" ? body.order : undefined,
257
+ isActive:
258
+ typeof body.isActive === "boolean" ? body.isActive : undefined,
259
+ },
260
+ req.auth!.sub,
261
+ ),
262
+ );
263
+ } catch (err) {
264
+ next(err);
265
+ }
266
+ },
267
+ );
268
+
269
+ admin.route(
270
+ "get",
271
+ "/roles",
272
+ ability("roles:manage"),
273
+ async (req: Request, res: Response, next: NextFunction) => {
274
+ try {
275
+ res.status(200).json(await auth.listRoles(req.authz!));
276
+ } catch (err) {
277
+ next(err);
278
+ }
279
+ },
280
+ );
281
+
282
+ admin.route(
283
+ "post",
284
+ "/roles",
285
+ ability("roles:manage"),
286
+ async (req: Request, res: Response, next: NextFunction) => {
287
+ try {
288
+ const body = req.body as Record<string, unknown>;
289
+ res.status(201).json(
290
+ await auth.createRole(
291
+ req.authz!,
292
+ {
293
+ slug: requireString(body.slug, "slug"),
294
+ name: optionalString(body.name),
295
+ displayName: optionalString(body.displayName),
296
+ description: optionalString(body.description) ?? null,
297
+ },
298
+ req.auth!.sub,
299
+ ),
300
+ );
301
+ } catch (err) {
302
+ next(err);
303
+ }
304
+ },
305
+ );
306
+
307
+ admin.route(
308
+ "patch",
309
+ "/roles/:roleId",
310
+ ability("roles:manage"),
311
+ async (req: Request, res: Response, next: NextFunction) => {
312
+ try {
313
+ const body = req.body as Record<string, unknown>;
314
+ res.status(200).json(
315
+ await auth.updateRole(
316
+ req.authz!,
317
+ requireString(req.params.roleId, "roleId"),
318
+ {
319
+ name: optionalString(body.name),
320
+ displayName: optionalString(body.displayName),
321
+ description:
322
+ body.description === null
323
+ ? null
324
+ : optionalString(body.description),
325
+ isActive:
326
+ typeof body.isActive === "boolean" ? body.isActive : undefined,
327
+ },
328
+ req.auth!.sub,
329
+ ),
330
+ );
331
+ } catch (err) {
332
+ next(err);
333
+ }
334
+ },
335
+ );
336
+
337
+ admin.route(
338
+ "delete",
339
+ "/roles/:roleId",
340
+ ability("roles:manage"),
341
+ async (req: Request, res: Response, next: NextFunction) => {
342
+ try {
343
+ const body = (req.body ?? {}) as Record<string, unknown>;
344
+ await auth.deleteRole(
345
+ req.authz!,
346
+ requireString(req.params.roleId, "roleId"),
347
+ req.auth!.sub,
348
+ optionalString(body.reason),
349
+ );
350
+ res.status(200).json({ ok: true });
351
+ } catch (err) {
352
+ next(err);
353
+ }
354
+ },
355
+ );
356
+
357
+ admin.route(
358
+ "post",
359
+ "/roles/:roleId/permissions",
360
+ ability("roles:manage"),
361
+ async (req: Request, res: Response, next: NextFunction) => {
362
+ try {
363
+ const body = req.body as Record<string, unknown>;
364
+ await auth.attachPermissionToRole(
365
+ req.authz!,
366
+ requireString(req.params.roleId, "roleId"),
367
+ requireString(body.permission, "permission"),
368
+ req.auth!.sub,
369
+ );
370
+ res.status(201).json({ ok: true });
371
+ } catch (err) {
372
+ next(err);
373
+ }
374
+ },
375
+ );
376
+
377
+ admin.route(
378
+ "post",
379
+ "/users/:userId/roles",
380
+ ability("roles:assign"),
381
+ async (req: Request, res: Response, next: NextFunction) => {
382
+ try {
383
+ const body = req.body as Record<string, unknown>;
384
+ await auth.assignRole(
385
+ req.authz!,
386
+ requireString(req.params.userId, "userId"),
387
+ requireString(body.role, "role"),
388
+ );
389
+ res.status(201).json({ ok: true });
390
+ } catch (err) {
391
+ next(err);
392
+ }
393
+ },
394
+ );
395
+
396
+ admin.route(
397
+ "post",
398
+ "/users/:userId/roles/:roleSlug/revoke",
399
+ ability("roles:assign"),
400
+ async (req: Request, res: Response, next: NextFunction) => {
401
+ try {
402
+ const userId = requireString(req.params.userId, "userId");
403
+ // Revoking your own `admin` would strip the very permission that let you call this,
404
+ // with no route back in. Assigning to yourself is fine — it can't lock anyone out.
405
+ if (userId === req.auth!.sub)
406
+ throw new HttpError(403, "cannot change your own roles");
407
+ await auth.revokeRole(
408
+ req.authz!,
409
+ userId,
410
+ requireString(req.params.roleSlug, "roleSlug"),
411
+ );
412
+ res.status(201).json({ ok: true });
413
+ } catch (err) {
414
+ next(err);
415
+ }
416
+ },
417
+ );
418
+
419
+ admin.route(
420
+ "post",
421
+ "/users/:userId/permissions",
422
+ ability("permissions:grant"),
423
+ async (req: Request, res: Response, next: NextFunction) => {
424
+ try {
425
+ const body = req.body as Record<string, unknown>;
426
+ await auth.grantPermission(
427
+ req.authz!,
428
+ requireString(req.params.userId, "userId"),
429
+ requireString(body.permission, "permission"),
430
+ req.auth!.sub,
431
+ );
432
+ res.status(201).json({ ok: true });
433
+ } catch (err) {
434
+ next(err);
435
+ }
436
+ },
437
+ );
438
+
439
+ admin.route(
440
+ "post",
441
+ "/users/:userId/permissions/:permissionSlug/revoke",
442
+ ability("permissions:grant"),
443
+ async (req: Request, res: Response, next: NextFunction) => {
444
+ try {
445
+ await auth.revokePermission(
446
+ req.authz!,
447
+ requireString(req.params.userId, "userId"),
448
+ requireString(req.params.permissionSlug, "permissionSlug"),
449
+ );
450
+ res.status(201).json({ ok: true });
451
+ } catch (err) {
452
+ next(err);
453
+ }
454
+ },
455
+ );
456
+
457
+ admin.route(
458
+ "post",
459
+ "/users/:userId/block",
460
+ ability("users:block"),
461
+ async (req: Request, res: Response, next: NextFunction) => {
462
+ try {
463
+ const userId = requireString(req.params.userId, "userId");
464
+ if (userId === req.auth!.sub)
465
+ throw new HttpError(403, "cannot block your own account");
466
+ await auth.block(req.authz!, userId, {
467
+ userId: req.auth!.sub,
468
+ ip: req.ip,
469
+ });
470
+ res.status(201).json({ ok: true });
471
+ } catch (err) {
472
+ next(err);
473
+ }
474
+ },
475
+ );
476
+
477
+ admin.route(
478
+ "post",
479
+ "/users/:userId/unblock",
480
+ ability("users:block"),
481
+ async (req: Request, res: Response, next: NextFunction) => {
482
+ try {
483
+ await auth.unblock(
484
+ req.authz!,
485
+ requireString(req.params.userId, "userId"),
486
+ { userId: req.auth!.sub, ip: req.ip },
487
+ );
488
+ res.status(201).json({ ok: true });
489
+ } catch (err) {
490
+ next(err);
491
+ }
492
+ },
493
+ );
494
+
495
+ admin.route(
496
+ "post",
497
+ "/users/:userId/deactivate",
498
+ ability("users:block"),
499
+ async (req: Request, res: Response, next: NextFunction) => {
500
+ try {
501
+ const userId = requireString(req.params.userId, "userId");
502
+ if (userId === req.auth!.sub)
503
+ throw new HttpError(403, "cannot deactivate your own account");
504
+ await auth.deactivate(req.authz!, userId, {
505
+ userId: req.auth!.sub,
506
+ ip: req.ip,
507
+ });
508
+ res.status(201).json({ ok: true });
509
+ } catch (err) {
510
+ next(err);
511
+ }
512
+ },
513
+ );
514
+
515
+ admin.route(
516
+ "post",
517
+ "/users/:userId/activate",
518
+ ability("users:block"),
519
+ async (req: Request, res: Response, next: NextFunction) => {
520
+ try {
521
+ await auth.activate(
522
+ req.authz!,
523
+ requireString(req.params.userId, "userId"),
524
+ { userId: req.auth!.sub, ip: req.ip },
525
+ );
526
+ res.status(201).json({ ok: true });
527
+ } catch (err) {
528
+ next(err);
529
+ }
530
+ },
531
+ );
532
+
533
+ return admin.handler;
534
+ }