@jini-ai/user-management 0.6.15-rc.2 → 0.6.16
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/README.md +2 -0
- package/dist/admin/adapters/http.js +15 -5
- package/dist/admin/adapters/memory.js +103 -38
- package/dist/admin/conformance/index.js +48 -30
- package/dist/admin/controllers/controller-store.js +18 -6
- package/dist/admin/controllers/members.controller.js +48 -21
- package/dist/admin/controllers/roles.controller.js +98 -49
- package/dist/admin/controllers/users.controller.js +22 -11
- package/dist/admin/error-copy.js +3 -1
- package/dist/admin/ports.d.ts +11 -10
- package/dist/admin/react/components/PermissionPanel.js +3 -2
- package/dist/admin/react/components/PolicyRow.js +3 -2
- package/dist/admin/react/components/RoleRow.js +3 -2
- package/dist/admin/react/components/UsersConfirmation.js +1 -2
- package/dist/admin/react/hooks/AuthScope.hooks.js +2 -1
- package/dist/admin/react/hooks/MembersPage.hooks.js +23 -10
- package/dist/admin/react/hooks/MembersScope.hooks.js +2 -1
- package/dist/admin/react/hooks/UsersPage.hooks.js +13 -5
- package/dist/admin/react/hooks/UsersScope.hooks.js +2 -1
- package/dist/admin/react/index.d.ts +6 -6
- package/dist/admin/rules.js +15 -7
- package/dist/core/ports.d.ts +6 -0
- package/dist/react/errors.js +3 -1
- package/dist/react/users/Users.js +2 -1
- package/dist/react/users/rules.js +9 -5
- package/dist/server/admin-crud-service.js +17 -13
- package/dist/server/agent-tool-registrations.js +18 -15
- package/dist/server/agent-tools.js +38 -31
- package/dist/server/auth-challenge.js +4 -3
- package/dist/server/hasher.js +2 -1
- package/dist/server/repo.memory-transactions.js +13 -9
- package/dist/server/repo.memory.d.ts +3 -0
- package/dist/server/repo.memory.js +8 -0
- package/dist/server/seed.d.ts +17 -1
- package/dist/server/seed.js +25 -2
- package/package.json +41 -32
|
@@ -17,17 +17,20 @@ export function createRolesController({ api }, { permissions = [] } = {}) {
|
|
|
17
17
|
const abort = new AbortController();
|
|
18
18
|
const call = { signal: abort.signal };
|
|
19
19
|
const set = (patch) => {
|
|
20
|
-
if (disposed)
|
|
20
|
+
if (disposed) {
|
|
21
21
|
return;
|
|
22
|
+
}
|
|
22
23
|
const immutablePatch = patch.permissionRows ? { ...patch, permissionRows: Object.freeze([...patch.permissionRows]) } : patch;
|
|
23
24
|
state = Object.freeze({ ...state, ...immutablePatch });
|
|
24
|
-
for (const listener of listeners)
|
|
25
|
+
for (const listener of listeners) {
|
|
25
26
|
listener();
|
|
27
|
+
}
|
|
26
28
|
};
|
|
27
29
|
const report = (error, fallback) => describeRolesError({ error, fallback });
|
|
28
30
|
const gate = () => {
|
|
29
|
-
if (disposed || state.confirming)
|
|
31
|
+
if (disposed || state.confirming) {
|
|
30
32
|
return false;
|
|
33
|
+
}
|
|
31
34
|
if (!canManageRoles({ permissions: grants })) {
|
|
32
35
|
set({ rowError: m.forbidden });
|
|
33
36
|
return false;
|
|
@@ -68,147 +71,188 @@ export function createRolesController({ api }, { permissions = [] } = {}) {
|
|
|
68
71
|
set({ rowError: report(error, fallback) });
|
|
69
72
|
}
|
|
70
73
|
finally {
|
|
71
|
-
if (state.rowSavingId === id)
|
|
74
|
+
if (state.rowSavingId === id) {
|
|
72
75
|
set({ rowSavingId: null });
|
|
76
|
+
}
|
|
73
77
|
}
|
|
74
78
|
return saved;
|
|
75
79
|
}
|
|
76
80
|
async function load(_required, _optional = {}) {
|
|
77
|
-
if (disposed)
|
|
81
|
+
if (disposed) {
|
|
78
82
|
return;
|
|
79
|
-
|
|
83
|
+
}
|
|
84
|
+
++listGeneration;
|
|
85
|
+
const generation = listGeneration;
|
|
80
86
|
set({ error: null });
|
|
81
87
|
try {
|
|
82
88
|
const [r, p] = await Promise.all([api.listRoles({}, call), api.listPolicies({}, call)]);
|
|
83
|
-
if (generation === listGeneration)
|
|
89
|
+
if (generation === listGeneration) {
|
|
84
90
|
set({ roles: rows(r.roles), policies: rows(p.policies) });
|
|
91
|
+
}
|
|
85
92
|
}
|
|
86
93
|
catch (error) {
|
|
87
|
-
if (generation === listGeneration)
|
|
94
|
+
if (generation === listGeneration) {
|
|
88
95
|
set({ error: report(error, 'Failed to load roles/policies.') });
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
async function loadPermissions({ policyId }, _optional = {}) {
|
|
92
|
-
if (disposed || state.permissionPolicyId !== policyId)
|
|
100
|
+
if (disposed || state.permissionPolicyId !== policyId) {
|
|
93
101
|
return;
|
|
102
|
+
}
|
|
94
103
|
// Start order, rather than completion order, owns rows AND failures. Closing also invalidates.
|
|
95
|
-
|
|
104
|
+
++permissionGeneration;
|
|
105
|
+
const generation = permissionGeneration;
|
|
96
106
|
set({ permissionsLoading: true });
|
|
97
107
|
try {
|
|
98
108
|
const result = await api.listPolicyPermissions({ policyId }, call);
|
|
99
|
-
if (generation === permissionGeneration && state.permissionPolicyId === policyId)
|
|
109
|
+
if (generation === permissionGeneration && state.permissionPolicyId === policyId) {
|
|
100
110
|
set({ permissionRows: rows(result.policyPermissions) });
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
catch (error) {
|
|
103
|
-
if (generation === permissionGeneration && state.permissionPolicyId === policyId)
|
|
114
|
+
if (generation === permissionGeneration && state.permissionPolicyId === policyId) {
|
|
104
115
|
set({ permissionRows: [], rowError: report(error, 'Failed to load permissions.') });
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
finally {
|
|
107
|
-
if (generation === permissionGeneration)
|
|
119
|
+
if (generation === permissionGeneration) {
|
|
108
120
|
set({ permissionsLoading: false });
|
|
121
|
+
}
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
async function togglePermissions({ policyId }, _optional = {}) {
|
|
112
|
-
if (disposed || !policy(policyId))
|
|
125
|
+
if (disposed || !policy(policyId)) {
|
|
113
126
|
return;
|
|
127
|
+
}
|
|
114
128
|
const next = state.permissionPolicyId === policyId ? null : policyId;
|
|
115
129
|
++panelGeneration;
|
|
116
130
|
++permissionGeneration;
|
|
117
131
|
// Clear first so reopening cannot flash the previous policy's permissions.
|
|
118
132
|
set({ permissionPolicyId: next, permissionRows: [], permissionsLoading: false, permissionInput: '', resourceTypeInput: '', rowError: null });
|
|
119
|
-
if (next)
|
|
133
|
+
if (next) {
|
|
120
134
|
await loadPermissions({ policyId: next });
|
|
135
|
+
}
|
|
121
136
|
}
|
|
122
137
|
async function createRole(_required, _optional = {}) {
|
|
123
|
-
if (!gate() || state.roleSaving)
|
|
138
|
+
if (!gate() || state.roleSaving) {
|
|
124
139
|
return false;
|
|
140
|
+
}
|
|
125
141
|
const name = state.roleName;
|
|
126
|
-
return formWrite({
|
|
142
|
+
return formWrite({
|
|
143
|
+
saving: 'roleSaving', error: 'roleError', fallback: 'Failed to create role.', run: async () => {
|
|
127
144
|
await api.createRole({ name }, call);
|
|
128
|
-
if (state.roleName === name)
|
|
145
|
+
if (state.roleName === name) {
|
|
129
146
|
set({ roleName: '' });
|
|
130
|
-
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
131
150
|
}
|
|
132
151
|
async function createPolicy(_required, _optional = {}) {
|
|
133
|
-
if (!gate() || state.policySaving)
|
|
152
|
+
if (!gate() || state.policySaving) {
|
|
134
153
|
return false;
|
|
154
|
+
}
|
|
135
155
|
const name = state.policyName, description = state.policyDescription;
|
|
136
|
-
return formWrite({
|
|
156
|
+
return formWrite({
|
|
157
|
+
saving: 'policySaving', error: 'policyError', fallback: 'Failed to create policy.', run: async () => {
|
|
137
158
|
await api.createPolicy({ name }, { ...call, ...(description ? { description } : {}) });
|
|
138
|
-
if (state.policyName === name && state.policyDescription === description)
|
|
159
|
+
if (state.policyName === name && state.policyDescription === description) {
|
|
139
160
|
set({ policyName: '', policyDescription: '' });
|
|
140
|
-
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
141
164
|
}
|
|
142
165
|
function editRole({ role: r }, _optional = {}) {
|
|
143
|
-
if (roleGate(role(r.id)))
|
|
166
|
+
if (roleGate(role(r.id))) {
|
|
144
167
|
set({ editingRoleId: r.id, editingRoleName: r.name, rowError: null });
|
|
168
|
+
}
|
|
145
169
|
}
|
|
146
170
|
function editPolicy({ policy: p }, _optional = {}) {
|
|
147
|
-
if (policyGate(policy(p.id)))
|
|
171
|
+
if (policyGate(policy(p.id))) {
|
|
148
172
|
set({ editingPolicyId: p.id, editingPolicyName: p.name, editingPolicyDescription: p.description ?? '', rowError: null });
|
|
173
|
+
}
|
|
149
174
|
}
|
|
150
175
|
async function saveRole(_required, _optional = {}) {
|
|
151
176
|
const id = state.editingRoleId, name = state.editingRoleName;
|
|
152
|
-
if (!id || !roleGate(role(id)) || state.rowSavingId)
|
|
177
|
+
if (!id || !roleGate(role(id)) || state.rowSavingId) {
|
|
153
178
|
return false;
|
|
154
|
-
|
|
179
|
+
}
|
|
180
|
+
return rowWrite({
|
|
181
|
+
id, fallback: 'Failed to rename role.', run: async () => {
|
|
155
182
|
await api.updateRole({ roleId: id, name }, call);
|
|
156
183
|
// A late save must not close another row's newer edit.
|
|
157
|
-
if (state.editingRoleId === id && state.editingRoleName === name)
|
|
184
|
+
if (state.editingRoleId === id && state.editingRoleName === name) {
|
|
158
185
|
set({ editingRoleId: null });
|
|
186
|
+
}
|
|
159
187
|
await load({});
|
|
160
|
-
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
161
190
|
}
|
|
162
191
|
async function savePolicy(_required, _optional = {}) {
|
|
163
192
|
const id = state.editingPolicyId, name = state.editingPolicyName, description = state.editingPolicyDescription;
|
|
164
|
-
if (!id || !policyGate(policy(id)) || state.rowSavingId)
|
|
193
|
+
if (!id || !policyGate(policy(id)) || state.rowSavingId) {
|
|
165
194
|
return false;
|
|
166
|
-
|
|
195
|
+
}
|
|
196
|
+
return rowWrite({
|
|
197
|
+
id, fallback: 'Failed to update policy.', run: async () => {
|
|
167
198
|
await api.updatePolicy({ policyId: id }, { ...call, name, description });
|
|
168
|
-
if (state.editingPolicyId === id && state.editingPolicyName === name && state.editingPolicyDescription === description)
|
|
199
|
+
if (state.editingPolicyId === id && state.editingPolicyName === name && state.editingPolicyDescription === description) {
|
|
169
200
|
set({ editingPolicyId: null });
|
|
201
|
+
}
|
|
170
202
|
await load({});
|
|
171
|
-
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
172
205
|
}
|
|
173
206
|
async function writePermission(_required, _optional = {}) {
|
|
174
207
|
const id = state.permissionPolicyId, permission = state.permissionInput, resourceType = state.resourceTypeInput;
|
|
175
|
-
if (!id || !permission || !policyGate(policy(id)) || state.rowSavingId)
|
|
208
|
+
if (!id || !permission || !policyGate(policy(id)) || state.rowSavingId) {
|
|
176
209
|
return false;
|
|
210
|
+
}
|
|
177
211
|
const generation = panelGeneration;
|
|
178
|
-
return rowWrite({
|
|
212
|
+
return rowWrite({
|
|
213
|
+
id, fallback: 'Failed to add permission.', run: async () => {
|
|
179
214
|
await api.writePolicyPermission({ policyId: id, permission }, { ...call, ...(resourceType ? { resourceType } : {}) });
|
|
180
215
|
// Ordering alone cannot protect a trailing refresh: it always mints the newest generation.
|
|
181
216
|
// Check the CURRENT panel identity, and preserve a newer draft even on the same panel.
|
|
182
|
-
if (generation === panelGeneration && state.permissionInput === permission && state.resourceTypeInput === resourceType)
|
|
217
|
+
if (generation === panelGeneration && state.permissionInput === permission && state.resourceTypeInput === resourceType) {
|
|
183
218
|
set({ permissionInput: '', resourceTypeInput: '' });
|
|
184
|
-
|
|
219
|
+
}
|
|
220
|
+
if (state.permissionPolicyId === id) {
|
|
185
221
|
await loadPermissions({ policyId: id });
|
|
186
|
-
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
187
225
|
}
|
|
188
226
|
function targetGate(target) {
|
|
189
|
-
if (target.kind === 'role')
|
|
227
|
+
if (target.kind === 'role') {
|
|
190
228
|
return roleGate(role(target.role.id));
|
|
229
|
+
}
|
|
191
230
|
return policyGate(policy(target.kind === 'policy' ? target.policy.id : target.policyId));
|
|
192
231
|
}
|
|
193
232
|
function capture(target) {
|
|
194
|
-
if (target.kind === 'role')
|
|
233
|
+
if (target.kind === 'role') {
|
|
195
234
|
return Object.freeze({ kind: 'role', role: Object.freeze({ ...target.role }) });
|
|
196
|
-
|
|
235
|
+
}
|
|
236
|
+
if (target.kind === 'policy') {
|
|
197
237
|
return Object.freeze({ kind: 'policy', policy: Object.freeze({ ...target.policy }) });
|
|
238
|
+
}
|
|
198
239
|
return Object.freeze({ kind: 'permission', policyId: target.policyId, row: Object.freeze({ ...target.row }) });
|
|
199
240
|
}
|
|
200
241
|
function requestDestructive({ target }, _optional = {}) {
|
|
201
|
-
if (state.confirming || !targetGate(target))
|
|
242
|
+
if (state.confirming || !targetGate(target)) {
|
|
202
243
|
return;
|
|
203
|
-
|
|
244
|
+
}
|
|
245
|
+
if (target.kind === 'permission' && (state.permissionPolicyId !== target.policyId || !state.permissionRows.some(r => r.id === target.row.id))) {
|
|
204
246
|
return;
|
|
247
|
+
}
|
|
205
248
|
// The menu closes after one click; shell confirmation survives that closure. Revoke also
|
|
206
249
|
// needs confirmation: removing from an attached policy immediately changes live authority.
|
|
207
250
|
set({ pending: capture(target), rowError: null });
|
|
208
251
|
}
|
|
209
252
|
function cancelDestructive(_required, _optional = {}) {
|
|
210
|
-
if (!state.confirming)
|
|
253
|
+
if (!state.confirming) {
|
|
211
254
|
set({ pending: null });
|
|
255
|
+
}
|
|
212
256
|
}
|
|
213
257
|
async function destroy(target) {
|
|
214
258
|
if (target.kind === 'role') {
|
|
@@ -227,14 +271,16 @@ export function createRolesController({ api }, { permissions = [] } = {}) {
|
|
|
227
271
|
else {
|
|
228
272
|
set({ removingPermissionId: target.row.id });
|
|
229
273
|
await api.removePolicyPermission({ policyId: target.policyId, policyPermissionId: target.row.id }, call);
|
|
230
|
-
if (state.permissionPolicyId === target.policyId)
|
|
274
|
+
if (state.permissionPolicyId === target.policyId) {
|
|
231
275
|
await loadPermissions({ policyId: target.policyId });
|
|
276
|
+
}
|
|
232
277
|
}
|
|
233
278
|
}
|
|
234
279
|
async function confirmDestructive(_required, _optional = {}) {
|
|
235
280
|
const target = state.pending;
|
|
236
|
-
if (!target || state.confirming || state.rowSavingId || !targetGate(target))
|
|
281
|
+
if (!target || state.confirming || state.rowSavingId || !targetGate(target)) {
|
|
237
282
|
return false;
|
|
283
|
+
}
|
|
238
284
|
set({ confirming: true, rowError: null });
|
|
239
285
|
let destroyed = false;
|
|
240
286
|
try {
|
|
@@ -245,8 +291,9 @@ export function createRolesController({ api }, { permissions = [] } = {}) {
|
|
|
245
291
|
set({ rowError: report(error, target.kind === 'permission' ? 'Failed to remove permission.' : `Failed to delete ${target.kind}.`) });
|
|
246
292
|
}
|
|
247
293
|
finally {
|
|
248
|
-
if (state.pending === target)
|
|
294
|
+
if (state.pending === target) {
|
|
249
295
|
set({ pending: null });
|
|
296
|
+
}
|
|
250
297
|
set({ confirming: false, removingPermissionId: null });
|
|
251
298
|
}
|
|
252
299
|
return destroyed;
|
|
@@ -254,13 +301,15 @@ export function createRolesController({ api }, { permissions = [] } = {}) {
|
|
|
254
301
|
return {
|
|
255
302
|
getSnapshot(_required = {}, _optional = {}) { return state; },
|
|
256
303
|
subscribe({ listener }, _optional = {}) {
|
|
257
|
-
if (!disposed)
|
|
304
|
+
if (!disposed) {
|
|
258
305
|
listeners.add(listener);
|
|
306
|
+
}
|
|
259
307
|
return () => { listeners.delete(listener); };
|
|
260
308
|
},
|
|
261
309
|
dispose(_required = {}, _optional = {}) {
|
|
262
|
-
if (disposed)
|
|
310
|
+
if (disposed) {
|
|
263
311
|
return;
|
|
312
|
+
}
|
|
264
313
|
disposed = true;
|
|
265
314
|
abort.abort();
|
|
266
315
|
++listGeneration;
|
|
@@ -37,8 +37,10 @@ export function createUsersController({ api, safety }, { permissions = [], openO
|
|
|
37
37
|
if (read({}).rowSavingId || !gate(principalId, kind))
|
|
38
38
|
return;
|
|
39
39
|
const user = row(principalId);
|
|
40
|
-
set({
|
|
41
|
-
|
|
40
|
+
set({
|
|
41
|
+
pending: Object.freeze({ kind, user: snapshotUser(user) }), rowError: null, passwordError: null,
|
|
42
|
+
newPassword: '', confirmPassword: '', showNewPassword: false, showConfirmPassword: false
|
|
43
|
+
});
|
|
42
44
|
}
|
|
43
45
|
function maybeDeepLink() {
|
|
44
46
|
const s = read({});
|
|
@@ -63,7 +65,8 @@ export function createUsersController({ api, safety }, { permissions = [], openO
|
|
|
63
65
|
async function load(_required, _optional = {}) {
|
|
64
66
|
if (!store.active({}))
|
|
65
67
|
return;
|
|
66
|
-
|
|
68
|
+
++listGeneration;
|
|
69
|
+
const generation = listGeneration;
|
|
67
70
|
set({ error: null });
|
|
68
71
|
try {
|
|
69
72
|
const [u, r, p] = await Promise.all([api.listUsers({}, call), api.listRoles({}, call), api.listPolicies({}, call)]);
|
|
@@ -86,8 +89,10 @@ export function createUsersController({ api, safety }, { permissions = [], openO
|
|
|
86
89
|
const caller = await api.me({}, call);
|
|
87
90
|
if (!store.active({}) || !caller.user || typeof caller.user.id !== 'string' || !caller.user.id)
|
|
88
91
|
return;
|
|
89
|
-
const safeCaller = Object.freeze({
|
|
90
|
-
|
|
92
|
+
const safeCaller = Object.freeze({
|
|
93
|
+
user: Object.freeze({ ...caller.user }), canManageUserTrash: caller.canManageUserTrash === true,
|
|
94
|
+
...(caller.effectivePermissions ? { effectivePermissions: Object.freeze([...caller.effectivePermissions]) } : {})
|
|
95
|
+
});
|
|
91
96
|
set({ caller: safeCaller });
|
|
92
97
|
maybeDeepLink();
|
|
93
98
|
}
|
|
@@ -127,12 +132,18 @@ export function createUsersController({ api, safety }, { permissions = [], openO
|
|
|
127
132
|
/** Per-kind panel writes: the authority checked, the selection that must be present, the call,
|
|
128
133
|
* and the selection cleared on success (email keeps its field, but still notifies). */
|
|
129
134
|
const panelWrites = {
|
|
130
|
-
email: {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
email: {
|
|
136
|
+
action: 'email', fallback: 'failed to update email', cleared: {}, missing: () => false,
|
|
137
|
+
write: (principalId, s) => api.updateUser({ principalId }, { ...call, email: s.editEmail })
|
|
138
|
+
},
|
|
139
|
+
role: {
|
|
140
|
+
action: 'grants', fallback: 'failed to assign role', cleared: { pendingRoleId: '' }, missing: s => !s.pendingRoleId,
|
|
141
|
+
write: (principalId, s) => api.assignRole({ principalId, roleId: s.pendingRoleId }, call)
|
|
142
|
+
},
|
|
143
|
+
policy: {
|
|
144
|
+
action: 'grants', fallback: 'failed to attach policy', cleared: { pendingPolicyId: '' }, missing: s => !s.pendingPolicyId,
|
|
145
|
+
write: (principalId, s) => api.attachPolicy({ principalId, policyId: s.pendingPolicyId }, call)
|
|
146
|
+
},
|
|
136
147
|
};
|
|
137
148
|
/** Busy, expanded-row and authority checks run before the selection check, so only a refusal of authority reports. */
|
|
138
149
|
function panelSavable(s, principalId, panel) {
|
package/dist/admin/error-copy.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Shared pieces of the People and Roles error-copy mappers (`describePeopleError`, `describeRolesError`). */
|
|
2
2
|
/** The string `code` of a structured error, or undefined when there is none. */
|
|
3
3
|
export function errorCode({ error }, _optional = {}) {
|
|
4
|
-
|
|
4
|
+
if (!error || typeof error !== 'object' || !('code' in error))
|
|
5
|
+
return undefined;
|
|
6
|
+
return typeof error.code === 'string' ? error.code : undefined;
|
|
5
7
|
}
|
|
6
8
|
/** A real Error's non-empty message, else the given fallback copy. */
|
|
7
9
|
export function errorMessageOr({ error, fallback }, _optional = {}) {
|
package/dist/admin/ports.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AdminOperator, CallerInfo, AdminMember, AdminAccount } from './models.js';
|
|
1
2
|
import type { RoleRecord, PolicyRecord, PolicyPermissionRecord } from './models.js';
|
|
2
3
|
export interface RolesCallOptions {
|
|
3
4
|
readonly signal?: AbortSignal;
|
|
@@ -76,7 +77,7 @@ export declare class RolesApiError extends Error {
|
|
|
76
77
|
}
|
|
77
78
|
export interface UsersApiPort {
|
|
78
79
|
listUsers(required: Record<string, never>, optional?: RolesCallOptions): Promise<{
|
|
79
|
-
users:
|
|
80
|
+
users: AdminOperator[];
|
|
80
81
|
}>;
|
|
81
82
|
listRoles(required: Record<string, never>, optional?: RolesCallOptions): Promise<{
|
|
82
83
|
roles: RoleRecord[];
|
|
@@ -84,31 +85,31 @@ export interface UsersApiPort {
|
|
|
84
85
|
listPolicies(required: Record<string, never>, optional?: RolesCallOptions): Promise<{
|
|
85
86
|
policies: PolicyRecord[];
|
|
86
87
|
}>;
|
|
87
|
-
me(required: Record<string, never>, optional?: RolesCallOptions): Promise<
|
|
88
|
+
me(required: Record<string, never>, optional?: RolesCallOptions): Promise<CallerInfo>;
|
|
88
89
|
createUser(required: {
|
|
89
90
|
username: string;
|
|
90
91
|
password: string;
|
|
91
92
|
}, optional?: RolesCallOptions & {
|
|
92
93
|
email?: string;
|
|
93
94
|
}): Promise<{
|
|
94
|
-
user:
|
|
95
|
+
user: AdminOperator;
|
|
95
96
|
}>;
|
|
96
97
|
updateUser(required: {
|
|
97
98
|
principalId: string;
|
|
98
99
|
}, optional?: RolesCallOptions & {
|
|
99
100
|
email?: string;
|
|
100
101
|
}): Promise<{
|
|
101
|
-
user:
|
|
102
|
+
user: AdminOperator;
|
|
102
103
|
}>;
|
|
103
104
|
disableUser(required: {
|
|
104
105
|
principalId: string;
|
|
105
106
|
}, optional?: RolesCallOptions): Promise<{
|
|
106
|
-
user:
|
|
107
|
+
user: AdminOperator;
|
|
107
108
|
}>;
|
|
108
109
|
enableUser(required: {
|
|
109
110
|
principalId: string;
|
|
110
111
|
}, optional?: RolesCallOptions): Promise<{
|
|
111
|
-
user:
|
|
112
|
+
user: AdminOperator;
|
|
112
113
|
}>;
|
|
113
114
|
resetUserPassword(required: {
|
|
114
115
|
principalId: string;
|
|
@@ -143,17 +144,17 @@ export interface MembersApiPort {
|
|
|
143
144
|
afterId?: string;
|
|
144
145
|
limit?: number;
|
|
145
146
|
}): Promise<{
|
|
146
|
-
members:
|
|
147
|
+
members: AdminMember[];
|
|
147
148
|
}>;
|
|
148
149
|
getMember(required: {
|
|
149
150
|
id: string;
|
|
150
151
|
}, optional?: RolesCallOptions): Promise<{
|
|
151
|
-
member:
|
|
152
|
+
member: AdminMember;
|
|
152
153
|
}>;
|
|
153
154
|
disableMember(required: {
|
|
154
155
|
id: string;
|
|
155
156
|
}, optional?: RolesCallOptions): Promise<{
|
|
156
|
-
member:
|
|
157
|
+
member: AdminMember;
|
|
157
158
|
}>;
|
|
158
159
|
requestMemberMagicLink(required: {
|
|
159
160
|
email: string;
|
|
@@ -174,7 +175,7 @@ export interface AuthApiPort {
|
|
|
174
175
|
username: string;
|
|
175
176
|
password: string;
|
|
176
177
|
}, optional?: RolesCallOptions): Promise<{
|
|
177
|
-
user:
|
|
178
|
+
user: AdminAccount;
|
|
178
179
|
}>;
|
|
179
180
|
}
|
|
180
181
|
export declare class PeopleApiError extends Error {
|
|
@@ -3,7 +3,8 @@ import { Button, TextField, Spinner } from '@jini-ai/ui-kit/react';
|
|
|
3
3
|
import { usePermissionPanel } from '../hooks/PermissionPanel.hooks.js';
|
|
4
4
|
export function PermissionPanel(props, _optional = {}) {
|
|
5
5
|
const v = usePermissionPanel(props);
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const permissions = v.empty ? _jsx("p", { children: "No permissions yet." }) :
|
|
7
|
+
_jsx("ul", { children: v.rows.map(row => _jsxs("li", { children: [_jsx("code", { children: row.permission }), row.resourceType ? _jsxs("span", { children: [" (", row.resourceType, ")"] }) : null, v.editable ? _jsx(Button, { onPress: row.requestRemove, pending: row.pending, attrs: { 'aria-label': row.removeLabel }, children: "Remove" }) : null] }, row.id)) });
|
|
8
|
+
return _jsxs("div", { children: [_jsx("h3", { children: "Current permissions" }), v.loading ? _jsx(Spinner, { label: "Loading permissions\u2026" }) : permissions, v.editable ? _jsxs("form", { onSubmit: v.submit, children: [_jsx(TextField, { label: "Permission", value: v.permission, onValueChange: v.changePermission, placeholder: "e.g. content.write" }), _jsx(TextField, { label: "Resource type (optional)", value: v.resourceType, onValueChange: v.changeResourceType }), _jsx(Button, { type: "submit", pending: v.pending, disabled: v.addDisabled, children: "Add" })] }) : null] });
|
|
8
9
|
}
|
|
9
10
|
//# sourceMappingURL=PermissionPanel.js.map
|
|
@@ -4,8 +4,9 @@ import { usePolicyRow } from '../hooks/PoliciesTab.hooks.js';
|
|
|
4
4
|
import { PermissionPanel } from './PermissionPanel.js';
|
|
5
5
|
export function PolicyRow(props, _optional = {}) {
|
|
6
6
|
const v = usePolicyRow(props);
|
|
7
|
+
const editActions = v.editing ? _jsxs(_Fragment, { children: [_jsx(Button, { onPress: v.save, pending: v.pending, disabled: v.saveDisabled, children: "Save" }), _jsx(Button, { onPress: v.cancel, children: "Cancel" })] }) :
|
|
8
|
+
_jsx(Menu, { label: v.menuLabel, items: v.menu });
|
|
7
9
|
return _jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: v.editing ? _jsx(TextField, { label: "Policy new name", value: v.draft, onValueChange: v.changeName }) : v.name }), _jsx("td", { children: v.editing ? _jsx(TextField, { label: "Policy new description", value: v.draftDescription, onValueChange: v.changeDescription }) : v.description }), _jsx("td", { children: _jsx(Badge, { children: v.type }) }), _jsx("td", { children: v.editable ?
|
|
8
|
-
|
|
9
|
-
_jsx(Menu, { label: v.menuLabel, items: v.menu }) : _jsx(Button, { onPress: v.viewPermissions, children: v.readLabel }) })] }), v.open ? _jsx("tr", { children: _jsx("td", { colSpan: 4, children: _jsx(PermissionPanel, { policy: props.policy }) }) }) : null] });
|
|
10
|
+
editActions : _jsx(Button, { onPress: v.viewPermissions, children: v.readLabel }) })] }), v.open ? _jsx("tr", { children: _jsx("td", { colSpan: 4, children: _jsx(PermissionPanel, { policy: props.policy }) }) }) : null] });
|
|
10
11
|
}
|
|
11
12
|
//# sourceMappingURL=PolicyRow.js.map
|
|
@@ -3,8 +3,9 @@ import { Button, TextField, Menu, Badge } from '@jini-ai/ui-kit/react';
|
|
|
3
3
|
import { useRoleRow } from '../hooks/RolesTab.hooks.js';
|
|
4
4
|
export function RoleRow(props, _optional = {}) {
|
|
5
5
|
const v = useRoleRow(props);
|
|
6
|
+
const editActions = v.editing ? _jsxs(_Fragment, { children: [_jsx(Button, { onPress: v.save, pending: v.pending, disabled: v.saveDisabled, children: "Save" }), _jsx(Button, { onPress: v.cancel, children: "Cancel" })] }) :
|
|
7
|
+
_jsx(Menu, { label: v.menuLabel, items: v.menu });
|
|
6
8
|
return _jsxs("tr", { children: [_jsx("td", { children: v.editing ? _jsx(TextField, { label: "Role new name", value: v.draft, onValueChange: v.changeName }) : v.name }), _jsx("td", { children: _jsx(Badge, { children: v.type }) }), _jsx("td", { children: v.editable ?
|
|
7
|
-
|
|
8
|
-
_jsx(Menu, { label: v.menuLabel, items: v.menu }) : _jsx("span", { children: "\u2014" }) })] });
|
|
9
|
+
editActions : _jsx("span", { children: "\u2014" }) })] });
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=RoleRow.js.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ConfirmDialog, Notice } from '@jini-ai/ui-kit/react';
|
|
3
|
-
import { SecretInput } from '@jini-ai/ui';
|
|
2
|
+
import { ConfirmDialog, Notice, SecretInput } from '@jini-ai/ui-kit/react';
|
|
4
3
|
import { useId } from 'react';
|
|
5
4
|
import { useUsersConfirmation, newPasswordAttrs } from '../hooks/UsersTab.hooks.js';
|
|
6
5
|
/** Kept mounted independently of row menus. Confirmation and cancellation share ui-kit's guards. */
|
|
@@ -3,8 +3,9 @@ import { createContext, useContext } from 'react';
|
|
|
3
3
|
export const ScopeContext = createContext(null);
|
|
4
4
|
export function useAuthScope(_required = {}, _optional = {}) {
|
|
5
5
|
const scope = useContext(ScopeContext);
|
|
6
|
-
if (!scope)
|
|
6
|
+
if (!scope) {
|
|
7
7
|
throw new Error('Auth admin scope is unavailable.');
|
|
8
|
+
}
|
|
8
9
|
return scope;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=AuthScope.hooks.js.map
|
|
@@ -4,35 +4,48 @@ import { hasAdminGrant } from '../../people.rules.js';
|
|
|
4
4
|
import { useMembersScope } from './MembersScope.hooks.js';
|
|
5
5
|
import { usePeopleController } from './PeopleController.hooks.js';
|
|
6
6
|
export const MembersViewContext = createContext(null);
|
|
7
|
-
export function useMembersView(_required = {}, _optional = {}) {
|
|
8
|
-
|
|
7
|
+
export function useMembersView(_required = {}, _optional = {}) {
|
|
8
|
+
const v = useContext(MembersViewContext);
|
|
9
|
+
if (!v) {
|
|
10
|
+
throw new Error('Members controller unavailable.');
|
|
11
|
+
}
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
9
14
|
export function useMembersPage(props, _optional = {}) {
|
|
10
15
|
const { api, permissions } = useMembersScope({});
|
|
11
16
|
const scope = useMemo(() => ({ api, refresh: props.refresh }), [api, props.refresh]);
|
|
12
17
|
const key = JSON.stringify(permissions);
|
|
13
|
-
const { controller, state } = usePeopleController({
|
|
18
|
+
const { controller, state } = usePeopleController({
|
|
19
|
+
scope, key,
|
|
14
20
|
create: () => createMembersController({ api }, { permissions, ...(props.refresh ? { refresh: props.refresh } : {}) }), start: c => { void c.load({}); },
|
|
15
21
|
});
|
|
16
22
|
const canManage = hasAdminGrant({ permissions, permission: 'member.manage' });
|
|
17
|
-
return {
|
|
18
|
-
|
|
23
|
+
return {
|
|
24
|
+
content: props.tabs.members ? createElement(props.tabs.members, {}) : null, state, controller, canManage, visible: props.description.visible && props.description.tabs.some(t => t.visible),
|
|
25
|
+
view: state && controller ? { state, controller, canManage } : null, retry: () => { void controller?.load({}); }
|
|
26
|
+
};
|
|
19
27
|
}
|
|
20
28
|
export function useMemberRow({ member }, _optional = {}) {
|
|
21
29
|
const { state: s, controller: c, canManage } = useMembersView({}), id = member.id;
|
|
22
30
|
const rs = c.stateFor({ id }), items = [];
|
|
23
31
|
if (canManage) {
|
|
24
32
|
items.push({ id: 'resend', label: 'Resend sign-in link', disabled: rs.resending, onPress: () => { void c.resendSignInLink({ id }); } });
|
|
25
|
-
if (member.status !== 'disabled')
|
|
33
|
+
if (member.status !== 'disabled') {
|
|
26
34
|
items.push({ id: 'disable', label: 'Disable', disabled: rs.disabling, onPress: () => c.requestDestructive({ id }) });
|
|
35
|
+
}
|
|
27
36
|
}
|
|
28
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
s, rs, items, menuLabel: `Actions for member "${member.email}"`, expanded: s.expandedId === id,
|
|
29
39
|
detail: s.detailById[id], loading: s.detailLoadingId === id, toggle: () => { void c.toggleDetail({ id }); },
|
|
30
|
-
verified: s.detailById[id]?.emailVerifiedAt ?? 'not verified', created: member.createdAt,
|
|
40
|
+
verified: s.detailById[id]?.emailVerifiedAt ?? 'not verified', created: member.createdAt,
|
|
41
|
+
};
|
|
31
42
|
}
|
|
32
43
|
export function useMembersConfirmation(_required = {}, _optional = {}) {
|
|
33
44
|
const { state: s, controller: c } = useMembersView({});
|
|
34
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
open: s.pending !== null, title: 'Disable this member?', body: s.pending ? `Disable "${s.pending.email}"? They will no longer be able to sign in.` : '',
|
|
35
47
|
confirmLabel: 'Disable', tone: 'warning', consequence: 'revokes sign-in access', pending: s.confirming, agentMayConfirm: false,
|
|
36
|
-
onConfirm: async () => { await c.confirmDestructive({}); }, onCancel: () => c.cancelDestructive({})
|
|
48
|
+
onConfirm: async () => { await c.confirmDestructive({}); }, onCancel: () => c.cancelDestructive({})
|
|
49
|
+
};
|
|
37
50
|
}
|
|
38
51
|
//# sourceMappingURL=MembersPage.hooks.js.map
|
|
@@ -3,8 +3,9 @@ import { createContext, useContext } from 'react';
|
|
|
3
3
|
export const ScopeContext = createContext(null);
|
|
4
4
|
export function useMembersScope(_required = {}, _optional = {}) {
|
|
5
5
|
const scope = useContext(ScopeContext);
|
|
6
|
-
if (!scope)
|
|
6
|
+
if (!scope) {
|
|
7
7
|
throw new Error('Members admin scope is unavailable.');
|
|
8
|
+
}
|
|
8
9
|
return scope;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=MembersScope.hooks.js.map
|
|
@@ -4,19 +4,27 @@ import { hasAdminGrant } from '../../people.rules.js';
|
|
|
4
4
|
import { useUsersScope } from './UsersScope.hooks.js';
|
|
5
5
|
import { usePeopleController } from './PeopleController.hooks.js';
|
|
6
6
|
export const UsersViewContext = createContext(null);
|
|
7
|
-
export function useUsersView(_required = {}, _optional = {}) {
|
|
8
|
-
|
|
7
|
+
export function useUsersView(_required = {}, _optional = {}) {
|
|
8
|
+
const v = useContext(UsersViewContext);
|
|
9
|
+
if (!v) {
|
|
10
|
+
throw new Error('Users controller unavailable.');
|
|
11
|
+
}
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
9
14
|
export function useUsersPage(props, _optional = {}) {
|
|
10
15
|
const { api, safety, permissions } = useUsersScope({});
|
|
11
16
|
const scope = useMemo(() => ({ api, safety, closed: props.onOwnPasswordResetClosed }), [api, safety, props.onOwnPasswordResetClosed]);
|
|
12
17
|
const key = JSON.stringify([permissions, props.openOwnPasswordReset]);
|
|
13
|
-
const { controller, state } = usePeopleController({
|
|
18
|
+
const { controller, state } = usePeopleController({
|
|
19
|
+
scope, key,
|
|
14
20
|
create: () => createUsersController({ api, safety }, { permissions, ...(props.openOwnPasswordReset !== undefined ? { openOwnPasswordReset: props.openOwnPasswordReset } : {}), ...(props.onOwnPasswordResetClosed ? { onOwnPasswordResetClosed: props.onOwnPasswordResetClosed } : {}) }),
|
|
15
21
|
start: c => { void c.load({}); void c.loadCaller({}); },
|
|
16
22
|
});
|
|
17
23
|
const visible = props.description.visible && props.description.tabs.some(t => t.visible);
|
|
18
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
content: props.tabs.users ? createElement(props.tabs.users, {}) : null, controller, state, visible, view: controller && state ? { controller, state, permissions } : null,
|
|
19
26
|
readOnly: !hasAdminGrant({ permissions, permission: 'user.manage' }) && !hasAdminGrant({ permissions, permission: 'role.manage' }),
|
|
20
|
-
retry: () => { void controller?.load({}); }
|
|
27
|
+
retry: () => { void controller?.load({}); }
|
|
28
|
+
};
|
|
21
29
|
}
|
|
22
30
|
//# sourceMappingURL=UsersPage.hooks.js.map
|
|
@@ -3,8 +3,9 @@ import { createContext, useContext } from 'react';
|
|
|
3
3
|
export const ScopeContext = createContext(null);
|
|
4
4
|
export function useUsersScope(_required = {}, _optional = {}) {
|
|
5
5
|
const scope = useContext(ScopeContext);
|
|
6
|
-
if (!scope)
|
|
6
|
+
if (!scope) {
|
|
7
7
|
throw new Error('Users admin scope is unavailable.');
|
|
8
|
+
}
|
|
8
9
|
return scope;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=UsersScope.hooks.js.map
|