@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,298 @@
|
|
|
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
|
+
* Read-only abilities inspector — see docs/AUTHN-AUTHZ.md.
|
|
13
|
+
*
|
|
14
|
+
* Top level: a collapsible group per ability source (collections.docs,
|
|
15
|
+
* admin.users, etc.), each containing the abilities that group
|
|
16
|
+
* registered. Group buckets and ordering come straight from the
|
|
17
|
+
* `AbilityRegistry.byGroup()` shape (registration order preserved).
|
|
18
|
+
*
|
|
19
|
+
* Per-ability: an inline-expandable row showing the roles that grant
|
|
20
|
+
* the ability and the distinct admin users who hold it transitively.
|
|
21
|
+
* The matrix is fetched lazily on first expand and cached for the
|
|
22
|
+
* lifetime of the page — the registry is small (~40 keys) but the
|
|
23
|
+
* matrix queries are not free, and most visitors only inspect a few
|
|
24
|
+
* keys.
|
|
25
|
+
*
|
|
26
|
+
* Stable override handles: see `inspector.module.css`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useState } from 'react'
|
|
30
|
+
|
|
31
|
+
import { Button, Container, LoaderRing, Section } from '@byline/ui/react'
|
|
32
|
+
import cx from 'classnames'
|
|
33
|
+
|
|
34
|
+
import { useBylineAdminServices } from '../../../services/admin-services-context.js'
|
|
35
|
+
import styles from './inspector.module.css'
|
|
36
|
+
import type {
|
|
37
|
+
AbilityDescriptorResponse,
|
|
38
|
+
AbilityGroupResponse,
|
|
39
|
+
ListRegisteredAbilitiesResponse,
|
|
40
|
+
WhoHasAbilityResponse,
|
|
41
|
+
} from '../index.js'
|
|
42
|
+
|
|
43
|
+
// --- helpers ---------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function sourceVariant(source: AbilityDescriptorResponse['source']) {
|
|
46
|
+
switch (source) {
|
|
47
|
+
case 'collection':
|
|
48
|
+
return {
|
|
49
|
+
global: 'byline-inspector-row-source-collection',
|
|
50
|
+
local: styles['row-source-collection'],
|
|
51
|
+
}
|
|
52
|
+
case 'admin':
|
|
53
|
+
return {
|
|
54
|
+
global: 'byline-inspector-row-source-admin',
|
|
55
|
+
local: styles['row-source-admin'],
|
|
56
|
+
}
|
|
57
|
+
case 'plugin':
|
|
58
|
+
return {
|
|
59
|
+
global: 'byline-inspector-row-source-plugin',
|
|
60
|
+
local: styles['row-source-plugin'],
|
|
61
|
+
}
|
|
62
|
+
case 'core':
|
|
63
|
+
return {
|
|
64
|
+
global: 'byline-inspector-row-source-core',
|
|
65
|
+
local: styles['row-source-core'],
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
return {
|
|
69
|
+
global: 'byline-inspector-row-source-unknown',
|
|
70
|
+
local: styles['row-source-unknown'],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function displayUser(user: WhoHasAbilityResponse['users'][number]): string {
|
|
76
|
+
const parts = [user.given_name, user.family_name].filter(
|
|
77
|
+
(p): p is string => typeof p === 'string' && p.length > 0
|
|
78
|
+
)
|
|
79
|
+
return parts.length > 0 ? `${parts.join(' ')} (${user.email})` : user.email
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- expandable matrix row ------------------------------------------------
|
|
83
|
+
|
|
84
|
+
function MatrixPanel({ matrix }: { matrix: WhoHasAbilityResponse }) {
|
|
85
|
+
return (
|
|
86
|
+
<div className={cx('byline-inspector-matrix', styles.matrix)}>
|
|
87
|
+
<div>
|
|
88
|
+
<h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
|
|
89
|
+
Roles ({matrix.roles.length})
|
|
90
|
+
</h4>
|
|
91
|
+
{matrix.roles.length === 0 ? (
|
|
92
|
+
<p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
|
|
93
|
+
No role grants this ability.
|
|
94
|
+
</p>
|
|
95
|
+
) : (
|
|
96
|
+
<ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
|
|
97
|
+
{matrix.roles.map((role) => (
|
|
98
|
+
<li
|
|
99
|
+
key={role.id}
|
|
100
|
+
className={cx('byline-inspector-matrix-item', styles['matrix-item'])}
|
|
101
|
+
>
|
|
102
|
+
<span className={cx('byline-inspector-matrix-name', styles['matrix-name'])}>
|
|
103
|
+
{role.name}
|
|
104
|
+
</span>
|
|
105
|
+
<span className="muted"> · {role.machine_name}</span>
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
|
|
113
|
+
Admin users ({matrix.users.length})
|
|
114
|
+
</h4>
|
|
115
|
+
{matrix.users.length === 0 ? (
|
|
116
|
+
<p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
|
|
117
|
+
No admin user holds this ability.
|
|
118
|
+
</p>
|
|
119
|
+
) : (
|
|
120
|
+
<ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
|
|
121
|
+
{matrix.users.map((user) => (
|
|
122
|
+
<li
|
|
123
|
+
key={user.id}
|
|
124
|
+
className={cx('byline-inspector-matrix-item', styles['matrix-item'])}
|
|
125
|
+
>
|
|
126
|
+
{displayUser(user)}
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface AbilityRowProps {
|
|
137
|
+
ability: AbilityDescriptorResponse
|
|
138
|
+
matrix: WhoHasAbilityResponse | undefined
|
|
139
|
+
loading: boolean
|
|
140
|
+
onToggle: () => void
|
|
141
|
+
expanded: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRowProps) {
|
|
145
|
+
const sv = sourceVariant(ability.source)
|
|
146
|
+
return (
|
|
147
|
+
<div className={cx('byline-inspector-row', styles.row)}>
|
|
148
|
+
<div className={cx('byline-inspector-row-head', styles['row-head'])}>
|
|
149
|
+
<div className={cx('byline-inspector-row-info', styles['row-info'])}>
|
|
150
|
+
<div className={cx('byline-inspector-row-meta', styles['row-meta'])}>
|
|
151
|
+
<code className={cx('byline-inspector-row-key', styles['row-key'])}>{ability.key}</code>
|
|
152
|
+
<span
|
|
153
|
+
className={cx(
|
|
154
|
+
'byline-inspector-row-source',
|
|
155
|
+
styles['row-source'],
|
|
156
|
+
sv.global,
|
|
157
|
+
sv.local
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{ability.source ?? 'unknown'}
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
<p className={cx('byline-inspector-row-label', styles['row-label'])}>{ability.label}</p>
|
|
164
|
+
{ability.description ? (
|
|
165
|
+
<p
|
|
166
|
+
className={cx('muted', 'byline-inspector-row-description', styles['row-description'])}
|
|
167
|
+
>
|
|
168
|
+
{ability.description}
|
|
169
|
+
</p>
|
|
170
|
+
) : null}
|
|
171
|
+
</div>
|
|
172
|
+
<Button size="xs" intent="secondary" onClick={onToggle}>
|
|
173
|
+
{expanded ? 'Hide' : 'Holders'}
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
{expanded ? (
|
|
177
|
+
loading ? (
|
|
178
|
+
<div className={cx('byline-inspector-loader', styles.loader)}>
|
|
179
|
+
<LoaderRing size={20} color="#888" />
|
|
180
|
+
<span className="muted">Loading…</span>
|
|
181
|
+
</div>
|
|
182
|
+
) : matrix ? (
|
|
183
|
+
<MatrixPanel matrix={matrix} />
|
|
184
|
+
) : null
|
|
185
|
+
) : null}
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- group section --------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
interface GroupSectionProps {
|
|
193
|
+
group: AbilityGroupResponse
|
|
194
|
+
matrices: Record<string, WhoHasAbilityResponse>
|
|
195
|
+
loading: Set<string>
|
|
196
|
+
expanded: Set<string>
|
|
197
|
+
onToggle: (abilityKey: string) => void
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSectionProps) {
|
|
201
|
+
return (
|
|
202
|
+
<details open className={cx('byline-inspector-group', styles.group)}>
|
|
203
|
+
<summary className={cx('byline-inspector-group-summary', styles['group-summary'])}>
|
|
204
|
+
<span className={cx('byline-inspector-group-name', styles['group-name'])}>
|
|
205
|
+
{group.group}
|
|
206
|
+
</span>
|
|
207
|
+
<span className={cx('muted', 'byline-inspector-group-count', styles['group-count'])}>
|
|
208
|
+
{group.abilities.length} abilities
|
|
209
|
+
</span>
|
|
210
|
+
</summary>
|
|
211
|
+
<div className={cx('byline-inspector-group-body', styles['group-body'])}>
|
|
212
|
+
{group.abilities.map((ability) => (
|
|
213
|
+
<AbilityRow
|
|
214
|
+
key={ability.key}
|
|
215
|
+
ability={ability}
|
|
216
|
+
matrix={matrices[ability.key]}
|
|
217
|
+
loading={loading.has(ability.key)}
|
|
218
|
+
expanded={expanded.has(ability.key)}
|
|
219
|
+
onToggle={() => onToggle(ability.key)}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</details>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- top level ------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
export function AbilitiesInspector({ data }: { data: ListRegisteredAbilitiesResponse }) {
|
|
230
|
+
const { whoHasAbility } = useBylineAdminServices()
|
|
231
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
|
232
|
+
const [loading, setLoading] = useState<Set<string>>(new Set())
|
|
233
|
+
const [matrices, setMatrices] = useState<Record<string, WhoHasAbilityResponse>>({})
|
|
234
|
+
|
|
235
|
+
async function handleToggle(abilityKey: string): Promise<void> {
|
|
236
|
+
setExpanded((current) => {
|
|
237
|
+
const next = new Set(current)
|
|
238
|
+
if (next.has(abilityKey)) {
|
|
239
|
+
next.delete(abilityKey)
|
|
240
|
+
} else {
|
|
241
|
+
next.add(abilityKey)
|
|
242
|
+
}
|
|
243
|
+
return next
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Lazy-load the matrix the first time a row expands. Cached for
|
|
247
|
+
// the lifetime of the page after that.
|
|
248
|
+
if (!matrices[abilityKey] && !loading.has(abilityKey)) {
|
|
249
|
+
setLoading((current) => new Set(current).add(abilityKey))
|
|
250
|
+
try {
|
|
251
|
+
const result = await whoHasAbility({ data: { ability: abilityKey } })
|
|
252
|
+
setMatrices((current) => ({ ...current, [abilityKey]: result }))
|
|
253
|
+
} finally {
|
|
254
|
+
setLoading((current) => {
|
|
255
|
+
const next = new Set(current)
|
|
256
|
+
next.delete(abilityKey)
|
|
257
|
+
return next
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<Section>
|
|
265
|
+
<Container>
|
|
266
|
+
<div className={cx('byline-inspector-head', styles.head)}>
|
|
267
|
+
<h1 className={cx('byline-inspector-title', styles.title)}>Abilities Inspector</h1>
|
|
268
|
+
<span className={cx('byline-inspector-count-pill', styles['count-pill'])}>
|
|
269
|
+
{data.total} registered
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
<p className={cx('muted', 'byline-inspector-lead', styles.lead)}>
|
|
273
|
+
Read-only view of every ability registered through <code>bylineCore.abilities</code>.
|
|
274
|
+
Collections auto-register CRUD + workflow abilities; admin subsystems contribute their own
|
|
275
|
+
keys at composition root via <code>registerAdminAbilities</code>.
|
|
276
|
+
</p>
|
|
277
|
+
{data.groups.length === 0 ? (
|
|
278
|
+
<p className={cx('muted', 'byline-inspector-empty', styles.empty)}>
|
|
279
|
+
No abilities are registered.
|
|
280
|
+
</p>
|
|
281
|
+
) : (
|
|
282
|
+
<div className={cx('byline-inspector-groups', styles.groups)}>
|
|
283
|
+
{data.groups.map((group) => (
|
|
284
|
+
<GroupSection
|
|
285
|
+
key={group.group}
|
|
286
|
+
group={group}
|
|
287
|
+
matrices={matrices}
|
|
288
|
+
loading={loading}
|
|
289
|
+
expanded={expanded}
|
|
290
|
+
onToggle={(key) => void handleToggle(key)}
|
|
291
|
+
/>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</Container>
|
|
296
|
+
</Section>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { AbilityDescriptor } from '@byline/auth'
|
|
10
|
+
|
|
11
|
+
import type { AbilityDescriptorResponse } from './schemas.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shape an `AbilityDescriptor` from the registry into its public
|
|
15
|
+
* response form. Identity-shaped today — the indirection exists so
|
|
16
|
+
* that future internal-only fields on `AbilityDescriptor` (e.g. a
|
|
17
|
+
* registration timestamp) stay opted out of the public shape by
|
|
18
|
+
* default.
|
|
19
|
+
*/
|
|
20
|
+
export function toAbilityDescriptor(descriptor: AbilityDescriptor): AbilityDescriptorResponse {
|
|
21
|
+
return {
|
|
22
|
+
key: descriptor.key,
|
|
23
|
+
label: descriptor.label,
|
|
24
|
+
description: descriptor.description ?? null,
|
|
25
|
+
group: descriptor.group,
|
|
26
|
+
source: descriptor.source ?? null,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Module-local error codes for admin-permissions.
|
|
11
|
+
*
|
|
12
|
+
* `ROLE_NOT_FOUND` covers the editor (and future grant/revoke) paths;
|
|
13
|
+
* `ABILITY_UNREGISTERED` is reserved for the editor too — when a client
|
|
14
|
+
* tries to grant an ability key that no subsystem has registered. The
|
|
15
|
+
* inspector is read-only and never throws either of these.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const AdminPermissionsErrorCodes = {
|
|
19
|
+
ROLE_NOT_FOUND: 'admin.permissions.roleNotFound',
|
|
20
|
+
ABILITY_UNREGISTERED: 'admin.permissions.abilityUnregistered',
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
export type AdminPermissionsErrorCode =
|
|
24
|
+
(typeof AdminPermissionsErrorCodes)[keyof typeof AdminPermissionsErrorCodes]
|
|
25
|
+
|
|
26
|
+
export interface AdminPermissionsErrorOptions {
|
|
27
|
+
message?: string
|
|
28
|
+
cause?: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AdminPermissionsError extends Error {
|
|
32
|
+
public readonly code: AdminPermissionsErrorCode
|
|
33
|
+
|
|
34
|
+
constructor(code: AdminPermissionsErrorCode, options: { message: string; cause?: unknown }) {
|
|
35
|
+
super(options.message, options.cause != null ? { cause: options.cause } : undefined)
|
|
36
|
+
this.name = 'AdminPermissionsError'
|
|
37
|
+
this.code = code
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const make =
|
|
42
|
+
(code: AdminPermissionsErrorCode, defaultMessage: string) =>
|
|
43
|
+
(options?: AdminPermissionsErrorOptions): AdminPermissionsError =>
|
|
44
|
+
new AdminPermissionsError(code, {
|
|
45
|
+
message: options?.message ?? defaultMessage,
|
|
46
|
+
cause: options?.cause,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const ERR_ADMIN_PERMISSIONS_ROLE_NOT_FOUND = make(
|
|
50
|
+
AdminPermissionsErrorCodes.ROLE_NOT_FOUND,
|
|
51
|
+
'admin role not found'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
export const ERR_ADMIN_PERMISSIONS_ABILITY_UNREGISTERED = make(
|
|
55
|
+
AdminPermissionsErrorCodes.ABILITY_UNREGISTERED,
|
|
56
|
+
'one or more abilities are not registered'
|
|
57
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* `@byline/admin/admin-permissions` — ability grants against roles plus
|
|
11
|
+
* the read-only inspector view.
|
|
12
|
+
*
|
|
13
|
+
* Backs the `byline_admin_permissions` table. Ability keys are
|
|
14
|
+
* registered at `initBylineCore()` time through the `AbilityRegistry`
|
|
15
|
+
* from `@byline/auth`; this module owns the per-role grant data and the
|
|
16
|
+
* inspector that surfaces it.
|
|
17
|
+
*
|
|
18
|
+
* The editor surface (`getRoleAbilities` / `setRoleAbilities`) is
|
|
19
|
+
* deliberately out of scope on this first ship — it lands with Phase B
|
|
20
|
+
* and mounts on the admin-roles role detail page.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
ADMIN_PERMISSIONS_ABILITIES,
|
|
25
|
+
type AdminPermissionsAbilityKey,
|
|
26
|
+
registerAdminPermissionsAbilities,
|
|
27
|
+
} from './abilities.js'
|
|
28
|
+
export {
|
|
29
|
+
getRoleAbilitiesCommand,
|
|
30
|
+
listRegisteredAbilitiesCommand,
|
|
31
|
+
setRoleAbilitiesCommand,
|
|
32
|
+
whoHasAbilityCommand,
|
|
33
|
+
} from './commands.js'
|
|
34
|
+
export { toAbilityDescriptor } from './dto.js'
|
|
35
|
+
export {
|
|
36
|
+
AdminPermissionsError,
|
|
37
|
+
type AdminPermissionsErrorCode,
|
|
38
|
+
AdminPermissionsErrorCodes,
|
|
39
|
+
ERR_ADMIN_PERMISSIONS_ABILITY_UNREGISTERED,
|
|
40
|
+
ERR_ADMIN_PERMISSIONS_ROLE_NOT_FOUND,
|
|
41
|
+
} from './errors.js'
|
|
42
|
+
export {
|
|
43
|
+
abilityDescriptorResponseSchema,
|
|
44
|
+
abilityGroupResponseSchema,
|
|
45
|
+
abilityHolderRoleSchema,
|
|
46
|
+
abilityHolderUserSchema,
|
|
47
|
+
getRoleAbilitiesRequestSchema,
|
|
48
|
+
getRoleAbilitiesResponseSchema,
|
|
49
|
+
listRegisteredAbilitiesRequestSchema,
|
|
50
|
+
listRegisteredAbilitiesResponseSchema,
|
|
51
|
+
setRoleAbilitiesRequestSchema,
|
|
52
|
+
setRoleAbilitiesResponseSchema,
|
|
53
|
+
whoHasAbilityRequestSchema,
|
|
54
|
+
whoHasAbilityResponseSchema,
|
|
55
|
+
} from './schemas.js'
|
|
56
|
+
export { AdminPermissionsService } from './service.js'
|
|
57
|
+
export type { AdminPermissionsCommandDeps } from './commands.js'
|
|
58
|
+
export type { AdminPermissionsRepository } from './repository.js'
|
|
59
|
+
export type {
|
|
60
|
+
AbilityDescriptorResponse,
|
|
61
|
+
AbilityGroupResponse,
|
|
62
|
+
AbilityHolderRole,
|
|
63
|
+
AbilityHolderUser,
|
|
64
|
+
GetRoleAbilitiesRequest,
|
|
65
|
+
GetRoleAbilitiesResponse,
|
|
66
|
+
ListRegisteredAbilitiesRequest,
|
|
67
|
+
ListRegisteredAbilitiesResponse,
|
|
68
|
+
SetRoleAbilitiesRequest,
|
|
69
|
+
SetRoleAbilitiesResponse,
|
|
70
|
+
WhoHasAbilityRequest,
|
|
71
|
+
WhoHasAbilityResponse,
|
|
72
|
+
} from './schemas.js'
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* `AdminPermissionsRepository` — ability grants against roles.
|
|
11
|
+
*
|
|
12
|
+
* Backs the `byline_admin_permissions` table — one row per (role, ability)
|
|
13
|
+
* grant. `setAbilities` is the wholesale-replace operation the role-ability
|
|
14
|
+
* editor in the admin UI will drive; `grantAbility` / `revokeAbility` are
|
|
15
|
+
* the incremental operations for programmatic callers.
|
|
16
|
+
*
|
|
17
|
+
* `listAbilitiesForUser` is the join used by `resolveActor` to build an
|
|
18
|
+
* `AdminAuth` — distinct abilities across every role the user holds.
|
|
19
|
+
*
|
|
20
|
+
* `listRolesForAbility` and `listUsersForAbility` are the inverse joins
|
|
21
|
+
* driving the admin-permissions inspector view (which roles grant a given
|
|
22
|
+
* ability, and which admin users hold those roles transitively).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface AdminPermissionsRepository {
|
|
26
|
+
/** Grant an ability to a role. Idempotent via the unique constraint. */
|
|
27
|
+
grantAbility(roleId: string, ability: string): Promise<void>
|
|
28
|
+
revokeAbility(roleId: string, ability: string): Promise<void>
|
|
29
|
+
listAbilities(roleId: string): Promise<string[]>
|
|
30
|
+
/** Replace the ability set for a role wholesale. Runs inside a transaction. */
|
|
31
|
+
setAbilities(roleId: string, abilities: readonly string[]): Promise<void>
|
|
32
|
+
/**
|
|
33
|
+
* Distinct abilities granted to a user via every role they hold. Used by
|
|
34
|
+
* `resolveActor()` to build the ability set on an `AdminAuth`.
|
|
35
|
+
*/
|
|
36
|
+
listAbilitiesForUser(userId: string): Promise<string[]>
|
|
37
|
+
/**
|
|
38
|
+
* Role ids that grant the given ability. Used by the inspector to render
|
|
39
|
+
* the per-ability "granted by these roles" list.
|
|
40
|
+
*/
|
|
41
|
+
listRolesForAbility(ability: string): Promise<string[]>
|
|
42
|
+
/**
|
|
43
|
+
* Distinct admin user ids that hold a role granting the given ability.
|
|
44
|
+
* Single-query join through `byline_admin_role_admin_user` — preferred
|
|
45
|
+
* over chaining `listRolesForAbility` + `listUsersForRole` so the
|
|
46
|
+
* inspector stays O(1) queries per ability.
|
|
47
|
+
*/
|
|
48
|
+
listUsersForAbility(ability: string): Promise<string[]>
|
|
49
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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 { uuidSchema } from '@byline/core/validation'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Zod request/response schemas for the admin-permissions inspector.
|
|
14
|
+
*
|
|
15
|
+
* The inspector ships two endpoints:
|
|
16
|
+
*
|
|
17
|
+
* - `listRegisteredAbilities` — flat list + grouped buckets straight
|
|
18
|
+
* out of the `AbilityRegistry`. No DB read.
|
|
19
|
+
* - `whoHasAbility` — for a given ability key, the list of roles that
|
|
20
|
+
* grant it and the distinct list of admin users transitively
|
|
21
|
+
* holding it. Two DB joins.
|
|
22
|
+
*
|
|
23
|
+
* Phase B will add `getRoleAbilities` / `setRoleAbilities` for the
|
|
24
|
+
* per-role editor on the admin-roles detail page; both are deliberately
|
|
25
|
+
* out of scope here.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const abilityKeySchema = z.string().min(1).max(128)
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Requests
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export const listRegisteredAbilitiesRequestSchema = z.object({}).optional()
|
|
35
|
+
export type ListRegisteredAbilitiesRequest = z.infer<typeof listRegisteredAbilitiesRequestSchema>
|
|
36
|
+
|
|
37
|
+
export const whoHasAbilityRequestSchema = z.object({
|
|
38
|
+
ability: abilityKeySchema,
|
|
39
|
+
})
|
|
40
|
+
export type WhoHasAbilityRequest = z.infer<typeof whoHasAbilityRequestSchema>
|
|
41
|
+
|
|
42
|
+
export const getRoleAbilitiesRequestSchema = z.object({
|
|
43
|
+
id: uuidSchema,
|
|
44
|
+
})
|
|
45
|
+
export type GetRoleAbilitiesRequest = z.infer<typeof getRoleAbilitiesRequestSchema>
|
|
46
|
+
|
|
47
|
+
export const setRoleAbilitiesRequestSchema = z.object({
|
|
48
|
+
id: uuidSchema,
|
|
49
|
+
abilities: z.array(abilityKeySchema),
|
|
50
|
+
})
|
|
51
|
+
export type SetRoleAbilitiesRequest = z.infer<typeof setRoleAbilitiesRequestSchema>
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Responses
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
const abilitySourceSchema = z.enum(['collection', 'plugin', 'core', 'admin']).nullable()
|
|
58
|
+
|
|
59
|
+
export const abilityDescriptorResponseSchema = z.object({
|
|
60
|
+
key: z.string(),
|
|
61
|
+
label: z.string(),
|
|
62
|
+
description: z.string().nullable(),
|
|
63
|
+
group: z.string(),
|
|
64
|
+
source: abilitySourceSchema,
|
|
65
|
+
})
|
|
66
|
+
export type AbilityDescriptorResponse = z.infer<typeof abilityDescriptorResponseSchema>
|
|
67
|
+
|
|
68
|
+
export const abilityGroupResponseSchema = z.object({
|
|
69
|
+
group: z.string(),
|
|
70
|
+
abilities: z.array(abilityDescriptorResponseSchema),
|
|
71
|
+
})
|
|
72
|
+
export type AbilityGroupResponse = z.infer<typeof abilityGroupResponseSchema>
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Inspector list payload. Returns both the flat list and the grouped
|
|
76
|
+
* buckets so the UI can render either shape without re-bucketing.
|
|
77
|
+
*/
|
|
78
|
+
export const listRegisteredAbilitiesResponseSchema = z.object({
|
|
79
|
+
abilities: z.array(abilityDescriptorResponseSchema),
|
|
80
|
+
groups: z.array(abilityGroupResponseSchema),
|
|
81
|
+
total: z.number().int().min(0),
|
|
82
|
+
})
|
|
83
|
+
export type ListRegisteredAbilitiesResponse = z.infer<typeof listRegisteredAbilitiesResponseSchema>
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Who-has-ability matrix entry. Roles and users are surfaced in the
|
|
87
|
+
* same response so the inline-expand row in the inspector renders in
|
|
88
|
+
* one round-trip.
|
|
89
|
+
*/
|
|
90
|
+
export const abilityHolderRoleSchema = z.object({
|
|
91
|
+
id: z.string(),
|
|
92
|
+
name: z.string(),
|
|
93
|
+
machine_name: z.string(),
|
|
94
|
+
})
|
|
95
|
+
export type AbilityHolderRole = z.infer<typeof abilityHolderRoleSchema>
|
|
96
|
+
|
|
97
|
+
export const abilityHolderUserSchema = z.object({
|
|
98
|
+
id: z.string(),
|
|
99
|
+
email: z.string(),
|
|
100
|
+
given_name: z.string().nullable(),
|
|
101
|
+
family_name: z.string().nullable(),
|
|
102
|
+
})
|
|
103
|
+
export type AbilityHolderUser = z.infer<typeof abilityHolderUserSchema>
|
|
104
|
+
|
|
105
|
+
export const whoHasAbilityResponseSchema = z.object({
|
|
106
|
+
ability: z.string(),
|
|
107
|
+
roles: z.array(abilityHolderRoleSchema),
|
|
108
|
+
users: z.array(abilityHolderUserSchema),
|
|
109
|
+
})
|
|
110
|
+
export type WhoHasAbilityResponse = z.infer<typeof whoHasAbilityResponseSchema>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Editor payloads. `roleId` is echoed back on both responses so the
|
|
114
|
+
* caller can match async writes against the role they were editing
|
|
115
|
+
* without holding the id separately. `abilities` is the authoritative
|
|
116
|
+
* stored set after the write.
|
|
117
|
+
*/
|
|
118
|
+
export const getRoleAbilitiesResponseSchema = z.object({
|
|
119
|
+
roleId: z.string(),
|
|
120
|
+
abilities: z.array(z.string()),
|
|
121
|
+
})
|
|
122
|
+
export type GetRoleAbilitiesResponse = z.infer<typeof getRoleAbilitiesResponseSchema>
|
|
123
|
+
|
|
124
|
+
export const setRoleAbilitiesResponseSchema = z.object({
|
|
125
|
+
roleId: z.string(),
|
|
126
|
+
abilities: z.array(z.string()),
|
|
127
|
+
})
|
|
128
|
+
export type SetRoleAbilitiesResponse = z.infer<typeof setRoleAbilitiesResponseSchema>
|