@byline/admin 2.4.0 → 2.4.2

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 (177) hide show
  1. package/dist/abilities.js +5 -24
  2. package/dist/index.js +8 -30
  3. package/dist/lib/assert-admin-actor.js +13 -74
  4. package/dist/lib/create-command.js +6 -16
  5. package/dist/modules/admin-account/commands.js +35 -24
  6. package/dist/modules/admin-account/components/change-password.d.ts +8 -0
  7. package/dist/modules/admin-account/components/change-password.js +192 -0
  8. package/dist/modules/admin-account/components/change-password.module.js +8 -0
  9. package/dist/modules/admin-account/components/change-password_module.css +27 -0
  10. package/dist/modules/admin-account/components/container.d.ts +29 -0
  11. package/dist/modules/admin-account/components/container.js +298 -0
  12. package/dist/modules/admin-account/components/container.module.js +28 -0
  13. package/dist/modules/admin-account/components/container_module.css +106 -0
  14. package/dist/modules/admin-account/components/update.d.ts +8 -0
  15. package/dist/modules/admin-account/components/update.js +207 -0
  16. package/dist/modules/admin-account/components/update.module.js +8 -0
  17. package/dist/modules/admin-account/components/update_module.css +27 -0
  18. package/dist/modules/admin-account/errors.js +14 -45
  19. package/dist/modules/admin-account/index.js +4 -34
  20. package/dist/modules/admin-account/schemas.js +25 -59
  21. package/dist/modules/admin-account/service.js +56 -61
  22. package/dist/modules/admin-permissions/abilities.js +6 -24
  23. package/dist/modules/admin-permissions/commands.js +42 -28
  24. package/dist/modules/admin-permissions/components/inspector.d.ts +4 -0
  25. package/dist/modules/admin-permissions/components/inspector.js +284 -0
  26. package/dist/modules/admin-permissions/components/inspector.module.js +56 -0
  27. package/dist/modules/admin-permissions/components/inspector_module.css +238 -0
  28. package/dist/modules/admin-permissions/dto.js +3 -16
  29. package/dist/modules/admin-permissions/errors.js +14 -27
  30. package/dist/modules/admin-permissions/index.js +6 -26
  31. package/dist/modules/admin-permissions/repository.js +1 -8
  32. package/dist/modules/admin-permissions/schemas.js +33 -70
  33. package/dist/modules/admin-permissions/service.js +88 -92
  34. package/dist/modules/admin-roles/abilities.js +8 -30
  35. package/dist/modules/admin-roles/commands.js +89 -55
  36. package/dist/modules/admin-roles/components/create.d.ts +7 -0
  37. package/dist/modules/admin-roles/components/create.js +177 -0
  38. package/dist/modules/admin-roles/components/create.module.js +8 -0
  39. package/dist/modules/admin-roles/components/create_module.css +27 -0
  40. package/dist/modules/admin-roles/components/permissions.d.ts +10 -0
  41. package/dist/modules/admin-roles/components/permissions.js +303 -0
  42. package/dist/modules/admin-roles/components/permissions.module.js +44 -0
  43. package/dist/modules/admin-roles/components/permissions_module.css +192 -0
  44. package/dist/modules/admin-roles/components/update.d.ts +8 -0
  45. package/dist/modules/admin-roles/components/update.js +166 -0
  46. package/dist/modules/admin-roles/components/update.module.js +8 -0
  47. package/dist/modules/admin-roles/components/update_module.css +27 -0
  48. package/dist/modules/admin-roles/dto.js +3 -16
  49. package/dist/modules/admin-roles/errors.js +16 -40
  50. package/dist/modules/admin-roles/index.js +6 -26
  51. package/dist/modules/admin-roles/repository.js +1 -8
  52. package/dist/modules/admin-roles/schemas.js +41 -71
  53. package/dist/modules/admin-roles/service.js +79 -82
  54. package/dist/modules/admin-users/abilities.js +9 -38
  55. package/dist/modules/admin-users/commands.js +92 -50
  56. package/dist/modules/admin-users/components/create.d.ts +8 -0
  57. package/dist/modules/admin-users/components/create.js +268 -0
  58. package/dist/modules/admin-users/components/create.module.js +10 -0
  59. package/dist/modules/admin-users/components/create_module.css +45 -0
  60. package/dist/modules/admin-users/components/roles.d.ts +11 -0
  61. package/dist/modules/admin-users/components/roles.js +148 -0
  62. package/dist/modules/admin-users/components/roles.module.js +18 -0
  63. package/dist/modules/admin-users/components/roles_module.css +75 -0
  64. package/dist/modules/admin-users/components/set-password.d.ts +8 -0
  65. package/dist/modules/admin-users/components/set-password.js +170 -0
  66. package/dist/modules/admin-users/components/set-password.module.js +9 -0
  67. package/dist/modules/admin-users/components/set-password_module.css +31 -0
  68. package/dist/modules/admin-users/components/update.d.ts +8 -0
  69. package/dist/modules/admin-users/components/update.js +254 -0
  70. package/dist/modules/admin-users/components/update.module.js +9 -0
  71. package/dist/modules/admin-users/components/update_module.css +34 -0
  72. package/dist/modules/admin-users/dto.js +3 -18
  73. package/dist/modules/admin-users/errors.js +17 -43
  74. package/dist/modules/admin-users/index.js +7 -27
  75. package/dist/modules/admin-users/repository.js +1 -8
  76. package/dist/modules/admin-users/schemas.js +44 -75
  77. package/dist/modules/admin-users/seed-super-admin.js +9 -34
  78. package/dist/modules/admin-users/service.js +76 -91
  79. package/dist/modules/auth/components/sign-in-form.d.ts +12 -0
  80. package/dist/modules/auth/components/sign-in-form.js +115 -0
  81. package/dist/modules/auth/components/sign-in-form.module.js +12 -0
  82. package/dist/modules/auth/components/sign-in-form_module.css +41 -0
  83. package/dist/modules/auth/index.js +3 -24
  84. package/dist/modules/auth/jwt-session-provider.js +179 -149
  85. package/dist/modules/auth/password.js +11 -53
  86. package/dist/modules/auth/phc.js +21 -54
  87. package/dist/modules/auth/refresh-tokens-repository.js +1 -8
  88. package/dist/modules/auth/resolve-actor.js +6 -28
  89. package/dist/services/admin-services-context.d.ts +16 -0
  90. package/dist/services/admin-services-context.js +13 -0
  91. package/dist/services/admin-services-types.d.ts +129 -0
  92. package/dist/services/admin-services-types.js +1 -0
  93. package/dist/store.js +1 -8
  94. package/dist/vendor/noble-argon2/_blake.js +277 -45
  95. package/dist/vendor/noble-argon2/_md.js +81 -136
  96. package/dist/vendor/noble-argon2/_u64.js +65 -67
  97. package/dist/vendor/noble-argon2/argon2.js +181 -342
  98. package/dist/vendor/noble-argon2/blake2.js +252 -327
  99. package/dist/vendor/noble-argon2/utils.js +110 -490
  100. package/dist/vendor/noble-argon2/utils.js.LICENSE.txt +1 -0
  101. package/package.json +89 -10
  102. package/src/abilities.ts +32 -0
  103. package/src/declarations.d.ts +4 -0
  104. package/src/index.ts +39 -0
  105. package/src/lib/assert-admin-actor.ts +90 -0
  106. package/src/lib/create-command.ts +109 -0
  107. package/src/modules/admin-account/commands.ts +76 -0
  108. package/src/modules/admin-account/components/change-password.module.css +40 -0
  109. package/src/modules/admin-account/components/change-password.tsx +232 -0
  110. package/src/modules/admin-account/components/container.module.css +158 -0
  111. package/src/modules/admin-account/components/container.tsx +229 -0
  112. package/src/modules/admin-account/components/update.module.css +40 -0
  113. package/src/modules/admin-account/components/update.tsx +263 -0
  114. package/src/modules/admin-account/errors.ts +75 -0
  115. package/src/modules/admin-account/index.ts +60 -0
  116. package/src/modules/admin-account/schemas.ts +84 -0
  117. package/src/modules/admin-account/service.ts +92 -0
  118. package/src/modules/admin-permissions/abilities.ts +46 -0
  119. package/src/modules/admin-permissions/commands.ts +103 -0
  120. package/src/modules/admin-permissions/components/inspector.module.css +326 -0
  121. package/src/modules/admin-permissions/components/inspector.tsx +298 -0
  122. package/src/modules/admin-permissions/dto.ts +28 -0
  123. package/src/modules/admin-permissions/errors.ts +57 -0
  124. package/src/modules/admin-permissions/index.ts +72 -0
  125. package/src/modules/admin-permissions/repository.ts +49 -0
  126. package/src/modules/admin-permissions/schemas.ts +128 -0
  127. package/src/modules/admin-permissions/service.ts +137 -0
  128. package/src/modules/admin-roles/abilities.ts +62 -0
  129. package/src/modules/admin-roles/commands.ts +161 -0
  130. package/src/modules/admin-roles/components/create.module.css +40 -0
  131. package/src/modules/admin-roles/components/create.tsx +218 -0
  132. package/src/modules/admin-roles/components/permissions.module.css +279 -0
  133. package/src/modules/admin-roles/components/permissions.tsx +396 -0
  134. package/src/modules/admin-roles/components/update.module.css +40 -0
  135. package/src/modules/admin-roles/components/update.tsx +218 -0
  136. package/src/modules/admin-roles/dto.ts +30 -0
  137. package/src/modules/admin-roles/errors.ts +76 -0
  138. package/src/modules/admin-roles/index.ts +81 -0
  139. package/src/modules/admin-roles/repository.ts +96 -0
  140. package/src/modules/admin-roles/schemas.ts +139 -0
  141. package/src/modules/admin-roles/service.ts +136 -0
  142. package/src/modules/admin-users/abilities.ts +76 -0
  143. package/src/modules/admin-users/commands.ts +157 -0
  144. package/src/modules/admin-users/components/create.module.css +63 -0
  145. package/src/modules/admin-users/components/create.tsx +323 -0
  146. package/src/modules/admin-users/components/roles.module.css +119 -0
  147. package/src/modules/admin-users/components/roles.tsx +172 -0
  148. package/src/modules/admin-users/components/set-password.module.css +46 -0
  149. package/src/modules/admin-users/components/set-password.tsx +199 -0
  150. package/src/modules/admin-users/components/update.module.css +49 -0
  151. package/src/modules/admin-users/components/update.tsx +328 -0
  152. package/src/modules/admin-users/dto.ts +39 -0
  153. package/src/modules/admin-users/errors.ts +84 -0
  154. package/src/modules/admin-users/index.ts +91 -0
  155. package/src/modules/admin-users/repository.ts +161 -0
  156. package/src/modules/admin-users/schemas.ts +168 -0
  157. package/src/modules/admin-users/seed-super-admin.ts +102 -0
  158. package/src/modules/admin-users/service.ts +166 -0
  159. package/src/modules/auth/components/sign-in-form.module.css +62 -0
  160. package/src/modules/auth/components/sign-in-form.tsx +132 -0
  161. package/src/modules/auth/index.ts +31 -0
  162. package/src/modules/auth/jwt-session-provider.ts +301 -0
  163. package/src/modules/auth/password.ts +94 -0
  164. package/src/modules/auth/phc.ts +121 -0
  165. package/src/modules/auth/refresh-tokens-repository.ts +74 -0
  166. package/src/modules/auth/resolve-actor.ts +42 -0
  167. package/src/services/admin-services-context.tsx +52 -0
  168. package/src/services/admin-services-types.ts +177 -0
  169. package/src/store.ts +32 -0
  170. package/src/vendor/noble-argon2/LICENSE +21 -0
  171. package/src/vendor/noble-argon2/README.md +87 -0
  172. package/src/vendor/noble-argon2/_blake.ts +58 -0
  173. package/src/vendor/noble-argon2/_md.ts +223 -0
  174. package/src/vendor/noble-argon2/_u64.ts +118 -0
  175. package/src/vendor/noble-argon2/argon2.ts +668 -0
  176. package/src/vendor/noble-argon2/blake2.ts +583 -0
  177. package/src/vendor/noble-argon2/utils.ts +849 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { AbilityRegistry } from '@byline/auth'
10
+
11
+ import { toAbilityDescriptor } from './dto.js'
12
+ import {
13
+ ERR_ADMIN_PERMISSIONS_ABILITY_UNREGISTERED,
14
+ ERR_ADMIN_PERMISSIONS_ROLE_NOT_FOUND,
15
+ } from './errors.js'
16
+ import type { AdminStore } from '../../store.js'
17
+ import type {
18
+ GetRoleAbilitiesRequest,
19
+ GetRoleAbilitiesResponse,
20
+ ListRegisteredAbilitiesResponse,
21
+ SetRoleAbilitiesRequest,
22
+ SetRoleAbilitiesResponse,
23
+ WhoHasAbilityRequest,
24
+ WhoHasAbilityResponse,
25
+ } from './schemas.js'
26
+
27
+ /**
28
+ * Read-only inspector service for admin-permissions.
29
+ *
30
+ * Two responsibilities:
31
+ *
32
+ * 1. **Enumerate registered abilities.** Pure registry read — no DB
33
+ * access. The registry is populated at `initBylineCore()` time
34
+ * by collection auto-registration plus subsystem registrars
35
+ * (`registerAdminAbilities`).
36
+ * 2. **Resolve the who-has matrix.** For a given ability key, list
37
+ * the roles that grant it and the distinct admin users
38
+ * transitively holding it. Backed by two single-query joins on
39
+ * the permissions repository, then resolved against the roles
40
+ * and users repositories so the inspector can render names
41
+ * without further round-trips.
42
+ *
43
+ * The editor surface (`getRoleAbilities` / `setRoleAbilities`) is
44
+ * deliberately not on this service yet — it lands with Phase B and
45
+ * will live alongside these methods.
46
+ */
47
+ export class AdminPermissionsService {
48
+ readonly #store: AdminStore
49
+ readonly #abilities: AbilityRegistry
50
+
51
+ constructor(deps: { store: AdminStore; abilities: AbilityRegistry }) {
52
+ this.#store = deps.store
53
+ this.#abilities = deps.abilities
54
+ }
55
+
56
+ listRegisteredAbilities(): ListRegisteredAbilitiesResponse {
57
+ const flat = this.#abilities.list().map(toAbilityDescriptor)
58
+ // Re-bucket from the same shaped descriptors so flat and groups
59
+ // stay byte-identical apart from grouping. Iteration order matches
60
+ // registration order — the registry's `byGroup` already preserves
61
+ // insertion order.
62
+ const grouped = this.#abilities.byGroup()
63
+ const groups = Array.from(grouped.entries(), ([group, abilities]) => ({
64
+ group,
65
+ abilities: abilities.map(toAbilityDescriptor),
66
+ }))
67
+ return {
68
+ abilities: flat,
69
+ groups,
70
+ total: flat.length,
71
+ }
72
+ }
73
+
74
+ async getRoleAbilities(request: GetRoleAbilitiesRequest): Promise<GetRoleAbilitiesResponse> {
75
+ const role = await this.#store.adminRoles.getById(request.id)
76
+ if (!role) throw ERR_ADMIN_PERMISSIONS_ROLE_NOT_FOUND()
77
+ const abilities = await this.#store.adminPermissions.listAbilities(request.id)
78
+ return { roleId: request.id, abilities }
79
+ }
80
+
81
+ async setRoleAbilities(request: SetRoleAbilitiesRequest): Promise<SetRoleAbilitiesResponse> {
82
+ const role = await this.#store.adminRoles.getById(request.id)
83
+ if (!role) throw ERR_ADMIN_PERMISSIONS_ROLE_NOT_FOUND()
84
+
85
+ // Reject any ability that is not in the registry — guards against
86
+ // typos, stale UI state, and a since-removed plugin's keys lingering
87
+ // in someone's draft. The registry was populated at init time so
88
+ // this is an in-memory check.
89
+ const unknown = request.abilities.filter((key) => !this.#abilities.has(key))
90
+ if (unknown.length > 0) {
91
+ throw ERR_ADMIN_PERMISSIONS_ABILITY_UNREGISTERED({
92
+ message: `Unregistered abilities: ${unknown.join(', ')}`,
93
+ })
94
+ }
95
+
96
+ // Wholesale-replace inside a transaction (handled by the repo).
97
+ await this.#store.adminPermissions.setAbilities(request.id, request.abilities)
98
+ // Return the freshly-stored set so the client can reset its dirty
99
+ // state without a second round-trip — also defends against drift if
100
+ // the repo dedupes or reorders.
101
+ const stored = await this.#store.adminPermissions.listAbilities(request.id)
102
+ return { roleId: request.id, abilities: stored }
103
+ }
104
+
105
+ async whoHasAbility(request: WhoHasAbilityRequest): Promise<WhoHasAbilityResponse> {
106
+ // Run the two inverse joins in parallel — they read the same table
107
+ // through different join paths but neither blocks the other.
108
+ const [roleIds, userIds] = await Promise.all([
109
+ this.#store.adminPermissions.listRolesForAbility(request.ability),
110
+ this.#store.adminPermissions.listUsersForAbility(request.ability),
111
+ ])
112
+
113
+ // Resolve role + user metadata in parallel batches. We accept the
114
+ // N round-trips here because admin role and user counts are small
115
+ // by design; if they grow we add `getByIds(ids[])` repo methods
116
+ // later.
117
+ const [roles, users] = await Promise.all([
118
+ Promise.all(roleIds.map((id) => this.#store.adminRoles.getById(id))),
119
+ Promise.all(userIds.map((id) => this.#store.adminUsers.getById(id))),
120
+ ])
121
+
122
+ return {
123
+ ability: request.ability,
124
+ roles: roles
125
+ .filter((r): r is NonNullable<typeof r> => r != null)
126
+ .map((r) => ({ id: r.id, name: r.name, machine_name: r.machine_name })),
127
+ users: users
128
+ .filter((u): u is NonNullable<typeof u> => u != null)
129
+ .map((u) => ({
130
+ id: u.id,
131
+ email: u.email,
132
+ given_name: u.given_name,
133
+ family_name: u.family_name,
134
+ })),
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { AbilityRegistry } from '@byline/auth'
10
+
11
+ /**
12
+ * Ability keys for the admin-roles module.
13
+ *
14
+ * Reorder is intentionally **rolled into `update`** — same trust level
15
+ * (mutating role identity), and splitting it would force a redundant
16
+ * `reorder` permission alongside `update` for every role-managing role.
17
+ *
18
+ * Per-role ability grants are managed by the sibling
19
+ * `@byline/admin/admin-permissions` module and have their own ability
20
+ * keys there.
21
+ */
22
+ export const ADMIN_ROLES_ABILITIES = {
23
+ read: 'admin.roles.read',
24
+ create: 'admin.roles.create',
25
+ update: 'admin.roles.update',
26
+ delete: 'admin.roles.delete',
27
+ } as const
28
+
29
+ export type AdminRolesAbilityKey =
30
+ (typeof ADMIN_ROLES_ABILITIES)[keyof typeof ADMIN_ROLES_ABILITIES]
31
+
32
+ /**
33
+ * Register every admin-roles ability with the framework's `AbilityRegistry`.
34
+ * Called from `registerAdminAbilities(registry)` at package level, which
35
+ * the webapp wires into `initBylineCore()`.
36
+ */
37
+ export function registerAdminRolesAbilities(registry: AbilityRegistry): void {
38
+ registry.register({
39
+ key: ADMIN_ROLES_ABILITIES.read,
40
+ label: 'Read admin roles',
41
+ group: 'admin.roles',
42
+ source: 'admin',
43
+ })
44
+ registry.register({
45
+ key: ADMIN_ROLES_ABILITIES.create,
46
+ label: 'Create admin roles',
47
+ group: 'admin.roles',
48
+ source: 'admin',
49
+ })
50
+ registry.register({
51
+ key: ADMIN_ROLES_ABILITIES.update,
52
+ label: 'Update or reorder admin roles',
53
+ group: 'admin.roles',
54
+ source: 'admin',
55
+ })
56
+ registry.register({
57
+ key: ADMIN_ROLES_ABILITIES.delete,
58
+ label: 'Delete admin roles',
59
+ group: 'admin.roles',
60
+ source: 'admin',
61
+ })
62
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { type Command, createCommand } from '../../lib/create-command.js'
10
+ import { ADMIN_USERS_ABILITIES } from '../admin-users/abilities.js'
11
+ import { ADMIN_ROLES_ABILITIES } from './abilities.js'
12
+ import {
13
+ adminRoleListResponseSchema,
14
+ adminRoleResponseSchema,
15
+ createAdminRoleRequestSchema,
16
+ deleteAdminRoleRequestSchema,
17
+ getAdminRoleRequestSchema,
18
+ getRolesForUserRequestSchema,
19
+ listAdminRolesRequestSchema,
20
+ okResponseSchema,
21
+ reorderAdminRolesRequestSchema,
22
+ setRolesForUserRequestSchema,
23
+ updateAdminRoleRequestSchema,
24
+ userRolesResponseSchema,
25
+ } from './schemas.js'
26
+ import { AdminRolesService } from './service.js'
27
+ import type { AdminStore } from '../../store.js'
28
+ import type {
29
+ AdminRoleListResponse,
30
+ AdminRoleResponse,
31
+ CreateAdminRoleRequest,
32
+ DeleteAdminRoleRequest,
33
+ GetAdminRoleRequest,
34
+ GetRolesForUserRequest,
35
+ ListAdminRolesRequest,
36
+ OkResponse,
37
+ ReorderAdminRolesRequest,
38
+ SetRolesForUserRequest,
39
+ UpdateAdminRoleRequest,
40
+ UserRolesResponse,
41
+ } from './schemas.js'
42
+
43
+ /**
44
+ * Transport-agnostic commands for the admin-roles module.
45
+ *
46
+ * Every command goes through `createCommand`, which folds the four
47
+ * standard steps (validate → assert admin actor + ability → invoke
48
+ * service → validate output) into one declaration.
49
+ *
50
+ * Reorder uses the `update` ability — see `abilities.ts` for the
51
+ * rationale (same trust level as content updates; splitting it would
52
+ * force a redundant key on every role-managing role).
53
+ */
54
+
55
+ export interface AdminRolesCommandDeps {
56
+ store: AdminStore
57
+ }
58
+
59
+ function serviceOf(deps: AdminRolesCommandDeps): AdminRolesService {
60
+ return new AdminRolesService({ store: deps.store })
61
+ }
62
+
63
+ export const listAdminRolesCommand: Command<
64
+ ListAdminRolesRequest,
65
+ AdminRoleListResponse,
66
+ AdminRolesCommandDeps
67
+ > = createCommand({
68
+ method: 'listAdminRoles',
69
+ auth: { ability: ADMIN_ROLES_ABILITIES.read },
70
+ schemas: { input: listAdminRolesRequestSchema, output: adminRoleListResponseSchema },
71
+ handler: ({ deps }) => serviceOf(deps).listRoles(),
72
+ })
73
+
74
+ export const getAdminRoleCommand: Command<
75
+ GetAdminRoleRequest,
76
+ AdminRoleResponse,
77
+ AdminRolesCommandDeps
78
+ > = createCommand({
79
+ method: 'getAdminRole',
80
+ auth: { ability: ADMIN_ROLES_ABILITIES.read },
81
+ schemas: { input: getAdminRoleRequestSchema, output: adminRoleResponseSchema },
82
+ handler: ({ input, deps }) => serviceOf(deps).getRole(input),
83
+ })
84
+
85
+ export const createAdminRoleCommand: Command<
86
+ CreateAdminRoleRequest,
87
+ AdminRoleResponse,
88
+ AdminRolesCommandDeps
89
+ > = createCommand({
90
+ method: 'createAdminRole',
91
+ auth: { ability: ADMIN_ROLES_ABILITIES.create },
92
+ schemas: { input: createAdminRoleRequestSchema, output: adminRoleResponseSchema },
93
+ handler: ({ input, deps }) => serviceOf(deps).createRole(input),
94
+ })
95
+
96
+ export const updateAdminRoleCommand: Command<
97
+ UpdateAdminRoleRequest,
98
+ AdminRoleResponse,
99
+ AdminRolesCommandDeps
100
+ > = createCommand({
101
+ method: 'updateAdminRole',
102
+ auth: { ability: ADMIN_ROLES_ABILITIES.update },
103
+ schemas: { input: updateAdminRoleRequestSchema, output: adminRoleResponseSchema },
104
+ handler: ({ input, deps }) => serviceOf(deps).updateRole(input),
105
+ })
106
+
107
+ export const deleteAdminRoleCommand: Command<
108
+ DeleteAdminRoleRequest,
109
+ OkResponse,
110
+ AdminRolesCommandDeps
111
+ > = createCommand({
112
+ method: 'deleteAdminRole',
113
+ auth: { ability: ADMIN_ROLES_ABILITIES.delete },
114
+ schemas: { input: deleteAdminRoleRequestSchema, output: okResponseSchema },
115
+ handler: async ({ input, deps }) => {
116
+ await serviceOf(deps).deleteRole(input)
117
+ return { ok: true } as const
118
+ },
119
+ })
120
+
121
+ export const reorderAdminRolesCommand: Command<
122
+ ReorderAdminRolesRequest,
123
+ OkResponse,
124
+ AdminRolesCommandDeps
125
+ > = createCommand({
126
+ method: 'reorderAdminRoles',
127
+ auth: { ability: ADMIN_ROLES_ABILITIES.update },
128
+ schemas: { input: reorderAdminRolesRequestSchema, output: okResponseSchema },
129
+ handler: async ({ input, deps }) => {
130
+ await serviceOf(deps).reorderRoles(input)
131
+ return { ok: true } as const
132
+ },
133
+ })
134
+
135
+ export const getRolesForUserCommand: Command<
136
+ GetRolesForUserRequest,
137
+ UserRolesResponse,
138
+ AdminRolesCommandDeps
139
+ > = createCommand({
140
+ method: 'getRolesForUser',
141
+ // Reading a user's role assignments requires read access to admin
142
+ // users — the data is fundamentally about that user.
143
+ auth: { ability: ADMIN_USERS_ABILITIES.read },
144
+ schemas: { input: getRolesForUserRequestSchema, output: userRolesResponseSchema },
145
+ handler: ({ input, deps }) => serviceOf(deps).getRolesForUser(input),
146
+ })
147
+
148
+ export const setRolesForUserCommand: Command<
149
+ SetRolesForUserRequest,
150
+ UserRolesResponse,
151
+ AdminRolesCommandDeps
152
+ > = createCommand({
153
+ method: 'setRolesForUser',
154
+ // Editing a user's role-set is at the same trust level as updating
155
+ // their other admin fields. Roll into `admin.users.update` rather
156
+ // than minting a separate `admin.users.assignRoles` key — the role
157
+ // editor's checkbox tree would otherwise need both.
158
+ auth: { ability: ADMIN_USERS_ABILITIES.update },
159
+ schemas: { input: setRolesForUserRequestSchema, output: userRolesResponseSchema },
160
+ handler: ({ input, deps }) => serviceOf(deps).setRolesForUser(input),
161
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CreateAdminRole — drawer form for creating a new role.
3
+ *
4
+ * Override handles:
5
+ * .byline-role-create-wrap — outer container
6
+ * .byline-role-create-form — vertical-stack form element
7
+ * .byline-role-create-actions — Cancel/Save row
8
+ * .byline-role-create-action — buttons in the actions row
9
+ */
10
+
11
+ .wrap,
12
+ :global(.byline-role-create-wrap) {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--spacing-8);
16
+ padding: var(--spacing-4);
17
+ margin-top: var(--spacing-4);
18
+ }
19
+
20
+ .form,
21
+ :global(.byline-role-create-form) {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--spacing-16);
25
+ padding-top: var(--spacing-8);
26
+ }
27
+
28
+ .actions,
29
+ :global(.byline-role-create-actions) {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: flex-end;
33
+ gap: var(--spacing-8);
34
+ margin-top: var(--spacing-16);
35
+ }
36
+
37
+ .action,
38
+ :global(.byline-role-create-action) {
39
+ min-width: 4rem;
40
+ }
@@ -0,0 +1,218 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * This Source Code is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ *
8
+ * Copyright (c) Infonomic Company Limited
9
+ */
10
+
11
+ /**
12
+ * Create-admin-role drawer form.
13
+ *
14
+ * Same TanStack Form + Zod shape as the admin-users equivalent. The
15
+ * `machine_name` field is captured at create time only — it is the
16
+ * stable code-side handle for the role and is immutable thereafter
17
+ * (see the repository contract).
18
+ */
19
+
20
+ import { useState } from 'react'
21
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start'
22
+
23
+ import { Alert, Button, Input, LoaderEllipsis, TextArea } from '@byline/ui/react'
24
+ import cx from 'classnames'
25
+ import { z } from 'zod'
26
+
27
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
28
+ import styles from './create.module.css'
29
+ import type { AdminRoleResponse } from '../index.js'
30
+
31
+ const createAdminRoleFormSchema = z.object({
32
+ name: z.string().min(1, 'Name is required').max(128, 'Name must not exceed 128 characters'),
33
+ machine_name: z
34
+ .string()
35
+ .min(1, 'Machine name is required')
36
+ .max(128, 'Machine name must not exceed 128 characters')
37
+ .regex(/^[a-z0-9][a-z0-9_-]*$/, {
38
+ message: 'Lowercase letters, numbers, hyphens, and underscores only',
39
+ }),
40
+ description: z.string().max(2000, 'Description must not exceed 2000 characters'),
41
+ })
42
+
43
+ type CreateAdminRoleValues = z.infer<typeof createAdminRoleFormSchema>
44
+
45
+ const initialValues: CreateAdminRoleValues = {
46
+ name: '',
47
+ machine_name: '',
48
+ description: '',
49
+ }
50
+
51
+ function normaliseText(value: string): string | null {
52
+ return value.trim().length > 0 ? value : null
53
+ }
54
+
55
+ interface CreateAdminRoleProps {
56
+ onClose?: () => void
57
+ onSuccess?: (role: AdminRoleResponse) => void
58
+ }
59
+
60
+ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
61
+ const { createAdminRole } = useBylineAdminServices()
62
+ const [formError, setFormError] = useState<string | null>(null)
63
+
64
+ const form = useForm({
65
+ defaultValues: initialValues,
66
+ validationLogic: revalidateLogic({
67
+ mode: 'blur',
68
+ modeAfterSubmission: 'change',
69
+ }),
70
+ validators: {
71
+ onDynamic: createAdminRoleFormSchema,
72
+ },
73
+ onSubmit: async ({ value }) => {
74
+ setFormError(null)
75
+ try {
76
+ const created = await createAdminRole({
77
+ data: {
78
+ name: value.name.trim(),
79
+ machine_name: value.machine_name.trim(),
80
+ description: normaliseText(value.description),
81
+ },
82
+ })
83
+ form.reset(initialValues)
84
+ onSuccess?.(created)
85
+ } catch (err) {
86
+ const code = getErrorCode(err)
87
+ if (code === 'admin.roles.machineNameInUse') {
88
+ form.setFieldMeta('machine_name', (meta) => ({
89
+ ...meta,
90
+ errorMap: { ...meta.errorMap, onServer: 'This machine name is already in use.' },
91
+ errors: ['This machine name is already in use.'],
92
+ }))
93
+ return
94
+ }
95
+ setFormError('Could not create this admin role. Please try again.')
96
+ }
97
+ },
98
+ })
99
+
100
+ return (
101
+ <div className={cx('byline-role-create-wrap', styles.wrap)}>
102
+ <form
103
+ noValidate
104
+ onSubmit={(event) => {
105
+ event.preventDefault()
106
+ event.stopPropagation()
107
+ void form.handleSubmit()
108
+ }}
109
+ className={cx('byline-role-create-form', styles.form)}
110
+ >
111
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
112
+
113
+ <form.Field name="name">
114
+ {(field) => (
115
+ <Input
116
+ label="Name"
117
+ id="new-role-name"
118
+ name={field.name}
119
+ value={field.state.value}
120
+ onBlur={field.handleBlur}
121
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
122
+ error={field.state.meta.errors.length > 0}
123
+ errorText={firstError(field.state.meta.errors)}
124
+ helpText="Human-readable label, e.g. 'Editor'."
125
+ required
126
+ />
127
+ )}
128
+ </form.Field>
129
+
130
+ <form.Field name="machine_name">
131
+ {(field) => (
132
+ <Input
133
+ label="Machine name"
134
+ id="new-role-machine-name"
135
+ name={field.name}
136
+ value={field.state.value}
137
+ onBlur={field.handleBlur}
138
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
139
+ error={field.state.meta.errors.length > 0}
140
+ errorText={firstError(field.state.meta.errors)}
141
+ helpText="Stable code-side handle, e.g. 'editor'. Cannot be changed later."
142
+ required
143
+ />
144
+ )}
145
+ </form.Field>
146
+
147
+ <form.Field name="description">
148
+ {(field) => (
149
+ <TextArea
150
+ label="Description"
151
+ id="new-role-description"
152
+ name={field.name}
153
+ value={field.state.value}
154
+ onBlur={field.handleBlur}
155
+ onChange={(e) => field.handleChange(e.currentTarget.value)}
156
+ error={field.state.meta.errors.length > 0}
157
+ errorText={firstError(field.state.meta.errors)}
158
+ rows={3}
159
+ />
160
+ )}
161
+ </form.Field>
162
+
163
+ <div className={cx('byline-role-create-actions', styles.actions)}>
164
+ <Button
165
+ type="button"
166
+ intent="secondary"
167
+ size="sm"
168
+ onClick={onClose}
169
+ className={cx('byline-role-create-action', styles.action)}
170
+ >
171
+ Cancel
172
+ </Button>
173
+ <form.Subscribe
174
+ selector={(state) => ({
175
+ canSubmit: state.canSubmit,
176
+ isSubmitting: state.isSubmitting,
177
+ })}
178
+ >
179
+ {({ canSubmit, isSubmitting }) => (
180
+ <Button
181
+ size="sm"
182
+ intent="primary"
183
+ type="submit"
184
+ disabled={!canSubmit || isSubmitting}
185
+ className={cx('byline-role-create-action', styles.action)}
186
+ >
187
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
188
+ </Button>
189
+ )}
190
+ </form.Subscribe>
191
+ </div>
192
+ </form>
193
+ </div>
194
+ )
195
+ }
196
+
197
+ function firstError(errors: readonly unknown[]): string | undefined {
198
+ for (const err of errors) {
199
+ if (typeof err === 'string') return err
200
+ if (err && typeof err === 'object' && 'message' in err) {
201
+ const msg = (err as { message?: unknown }).message
202
+ if (typeof msg === 'string') return msg
203
+ }
204
+ }
205
+ return undefined
206
+ }
207
+
208
+ function getErrorCode(err: unknown): string | null {
209
+ if (err && typeof err === 'object') {
210
+ const e = err as { code?: unknown; cause?: unknown }
211
+ if (typeof e.code === 'string') return e.code
212
+ if (e.cause && typeof e.cause === 'object' && 'code' in e.cause) {
213
+ const cause = e.cause as { code?: unknown }
214
+ if (typeof cause.code === 'string') return cause.code
215
+ }
216
+ }
217
+ return null
218
+ }