@byline/admin 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abilities.js +5 -24
- package/dist/index.js +8 -30
- package/dist/lib/assert-admin-actor.js +13 -74
- package/dist/lib/create-command.js +6 -16
- package/dist/modules/admin-account/commands.js +35 -24
- package/dist/modules/admin-account/components/change-password.d.ts +8 -0
- package/dist/modules/admin-account/components/change-password.js +192 -0
- package/dist/modules/admin-account/components/change-password.module.js +8 -0
- package/dist/modules/admin-account/components/change-password_module.css +27 -0
- package/dist/modules/admin-account/components/container.d.ts +29 -0
- package/dist/modules/admin-account/components/container.js +298 -0
- package/dist/modules/admin-account/components/container.module.js +28 -0
- package/dist/modules/admin-account/components/container_module.css +106 -0
- package/dist/modules/admin-account/components/update.d.ts +8 -0
- package/dist/modules/admin-account/components/update.js +207 -0
- package/dist/modules/admin-account/components/update.module.js +8 -0
- package/dist/modules/admin-account/components/update_module.css +27 -0
- package/dist/modules/admin-account/errors.js +14 -45
- package/dist/modules/admin-account/index.js +4 -34
- package/dist/modules/admin-account/schemas.js +25 -59
- package/dist/modules/admin-account/service.js +56 -61
- package/dist/modules/admin-permissions/abilities.js +6 -24
- package/dist/modules/admin-permissions/commands.js +42 -28
- package/dist/modules/admin-permissions/components/inspector.d.ts +4 -0
- package/dist/modules/admin-permissions/components/inspector.js +284 -0
- package/dist/modules/admin-permissions/components/inspector.module.js +56 -0
- package/dist/modules/admin-permissions/components/inspector_module.css +238 -0
- package/dist/modules/admin-permissions/dto.js +3 -16
- package/dist/modules/admin-permissions/errors.js +14 -27
- package/dist/modules/admin-permissions/index.js +6 -26
- package/dist/modules/admin-permissions/repository.js +1 -8
- package/dist/modules/admin-permissions/schemas.js +33 -70
- package/dist/modules/admin-permissions/service.js +88 -92
- package/dist/modules/admin-roles/abilities.js +8 -30
- package/dist/modules/admin-roles/commands.js +89 -55
- package/dist/modules/admin-roles/components/create.d.ts +7 -0
- package/dist/modules/admin-roles/components/create.js +177 -0
- package/dist/modules/admin-roles/components/create.module.js +8 -0
- package/dist/modules/admin-roles/components/create_module.css +27 -0
- package/dist/modules/admin-roles/components/permissions.d.ts +10 -0
- package/dist/modules/admin-roles/components/permissions.js +303 -0
- package/dist/modules/admin-roles/components/permissions.module.js +44 -0
- package/dist/modules/admin-roles/components/permissions_module.css +192 -0
- package/dist/modules/admin-roles/components/update.d.ts +8 -0
- package/dist/modules/admin-roles/components/update.js +166 -0
- package/dist/modules/admin-roles/components/update.module.js +8 -0
- package/dist/modules/admin-roles/components/update_module.css +27 -0
- package/dist/modules/admin-roles/dto.js +3 -16
- package/dist/modules/admin-roles/errors.js +16 -40
- package/dist/modules/admin-roles/index.js +6 -26
- package/dist/modules/admin-roles/repository.js +1 -8
- package/dist/modules/admin-roles/schemas.js +41 -71
- package/dist/modules/admin-roles/service.js +79 -82
- package/dist/modules/admin-users/abilities.js +9 -38
- package/dist/modules/admin-users/commands.js +92 -50
- package/dist/modules/admin-users/components/create.d.ts +8 -0
- package/dist/modules/admin-users/components/create.js +268 -0
- package/dist/modules/admin-users/components/create.module.js +10 -0
- package/dist/modules/admin-users/components/create_module.css +45 -0
- package/dist/modules/admin-users/components/roles.d.ts +11 -0
- package/dist/modules/admin-users/components/roles.js +148 -0
- package/dist/modules/admin-users/components/roles.module.js +18 -0
- package/dist/modules/admin-users/components/roles_module.css +75 -0
- package/dist/modules/admin-users/components/set-password.d.ts +8 -0
- package/dist/modules/admin-users/components/set-password.js +170 -0
- package/dist/modules/admin-users/components/set-password.module.js +9 -0
- package/dist/modules/admin-users/components/set-password_module.css +31 -0
- package/dist/modules/admin-users/components/update.d.ts +8 -0
- package/dist/modules/admin-users/components/update.js +254 -0
- package/dist/modules/admin-users/components/update.module.js +9 -0
- package/dist/modules/admin-users/components/update_module.css +34 -0
- package/dist/modules/admin-users/dto.js +3 -18
- package/dist/modules/admin-users/errors.js +17 -43
- package/dist/modules/admin-users/index.js +7 -27
- package/dist/modules/admin-users/repository.js +1 -8
- package/dist/modules/admin-users/schemas.js +44 -75
- package/dist/modules/admin-users/seed-super-admin.js +9 -34
- package/dist/modules/admin-users/service.js +76 -91
- package/dist/modules/auth/components/sign-in-form.d.ts +12 -0
- package/dist/modules/auth/components/sign-in-form.js +115 -0
- package/dist/modules/auth/components/sign-in-form.module.js +12 -0
- package/dist/modules/auth/components/sign-in-form_module.css +41 -0
- package/dist/modules/auth/index.js +3 -24
- package/dist/modules/auth/jwt-session-provider.js +179 -149
- package/dist/modules/auth/password.js +11 -53
- package/dist/modules/auth/phc.js +21 -54
- package/dist/modules/auth/refresh-tokens-repository.js +1 -8
- package/dist/modules/auth/resolve-actor.js +6 -28
- package/dist/services/admin-services-context.d.ts +16 -0
- package/dist/services/admin-services-context.js +13 -0
- package/dist/services/admin-services-types.d.ts +129 -0
- package/dist/services/admin-services-types.js +1 -0
- package/dist/store.js +1 -8
- package/dist/vendor/noble-argon2/_blake.js +277 -45
- package/dist/vendor/noble-argon2/_md.js +81 -136
- package/dist/vendor/noble-argon2/_u64.js +65 -67
- package/dist/vendor/noble-argon2/argon2.js +181 -342
- package/dist/vendor/noble-argon2/blake2.js +252 -327
- package/dist/vendor/noble-argon2/utils.js +110 -490
- package/dist/vendor/noble-argon2/utils.js.LICENSE.txt +1 -0
- package/package.json +89 -10
- package/src/abilities.ts +32 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +39 -0
- package/src/lib/assert-admin-actor.ts +90 -0
- package/src/lib/create-command.ts +109 -0
- package/src/modules/admin-account/commands.ts +76 -0
- package/src/modules/admin-account/components/change-password.module.css +40 -0
- package/src/modules/admin-account/components/change-password.tsx +232 -0
- package/src/modules/admin-account/components/container.module.css +158 -0
- package/src/modules/admin-account/components/container.tsx +229 -0
- package/src/modules/admin-account/components/update.module.css +40 -0
- package/src/modules/admin-account/components/update.tsx +263 -0
- package/src/modules/admin-account/errors.ts +75 -0
- package/src/modules/admin-account/index.ts +60 -0
- package/src/modules/admin-account/schemas.ts +84 -0
- package/src/modules/admin-account/service.ts +92 -0
- package/src/modules/admin-permissions/abilities.ts +46 -0
- package/src/modules/admin-permissions/commands.ts +103 -0
- package/src/modules/admin-permissions/components/inspector.module.css +326 -0
- package/src/modules/admin-permissions/components/inspector.tsx +298 -0
- package/src/modules/admin-permissions/dto.ts +28 -0
- package/src/modules/admin-permissions/errors.ts +57 -0
- package/src/modules/admin-permissions/index.ts +72 -0
- package/src/modules/admin-permissions/repository.ts +49 -0
- package/src/modules/admin-permissions/schemas.ts +128 -0
- package/src/modules/admin-permissions/service.ts +137 -0
- package/src/modules/admin-roles/abilities.ts +62 -0
- package/src/modules/admin-roles/commands.ts +161 -0
- package/src/modules/admin-roles/components/create.module.css +40 -0
- package/src/modules/admin-roles/components/create.tsx +218 -0
- package/src/modules/admin-roles/components/permissions.module.css +279 -0
- package/src/modules/admin-roles/components/permissions.tsx +396 -0
- package/src/modules/admin-roles/components/update.module.css +40 -0
- package/src/modules/admin-roles/components/update.tsx +218 -0
- package/src/modules/admin-roles/dto.ts +30 -0
- package/src/modules/admin-roles/errors.ts +76 -0
- package/src/modules/admin-roles/index.ts +81 -0
- package/src/modules/admin-roles/repository.ts +96 -0
- package/src/modules/admin-roles/schemas.ts +139 -0
- package/src/modules/admin-roles/service.ts +136 -0
- package/src/modules/admin-users/abilities.ts +76 -0
- package/src/modules/admin-users/commands.ts +157 -0
- package/src/modules/admin-users/components/create.module.css +63 -0
- package/src/modules/admin-users/components/create.tsx +323 -0
- package/src/modules/admin-users/components/roles.module.css +119 -0
- package/src/modules/admin-users/components/roles.tsx +172 -0
- package/src/modules/admin-users/components/set-password.module.css +46 -0
- package/src/modules/admin-users/components/set-password.tsx +199 -0
- package/src/modules/admin-users/components/update.module.css +49 -0
- package/src/modules/admin-users/components/update.tsx +328 -0
- package/src/modules/admin-users/dto.ts +39 -0
- package/src/modules/admin-users/errors.ts +84 -0
- package/src/modules/admin-users/index.ts +91 -0
- package/src/modules/admin-users/repository.ts +161 -0
- package/src/modules/admin-users/schemas.ts +168 -0
- package/src/modules/admin-users/seed-super-admin.ts +102 -0
- package/src/modules/admin-users/service.ts +166 -0
- package/src/modules/auth/components/sign-in-form.module.css +62 -0
- package/src/modules/auth/components/sign-in-form.tsx +132 -0
- package/src/modules/auth/index.ts +31 -0
- package/src/modules/auth/jwt-session-provider.ts +301 -0
- package/src/modules/auth/password.ts +94 -0
- package/src/modules/auth/phc.ts +121 -0
- package/src/modules/auth/refresh-tokens-repository.ts +74 -0
- package/src/modules/auth/resolve-actor.ts +42 -0
- package/src/services/admin-services-context.tsx +52 -0
- package/src/services/admin-services-types.ts +177 -0
- package/src/store.ts +32 -0
- package/src/vendor/noble-argon2/LICENSE +21 -0
- package/src/vendor/noble-argon2/README.md +87 -0
- package/src/vendor/noble-argon2/_blake.ts +58 -0
- package/src/vendor/noble-argon2/_md.ts +223 -0
- package/src/vendor/noble-argon2/_u64.ts +118 -0
- package/src/vendor/noble-argon2/argon2.ts +668 -0
- package/src/vendor/noble-argon2/blake2.ts +583 -0
- 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
|
+
}
|