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