@jini-ai/user-management 0.3.3 → 0.3.4
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/CHANGELOG.md +4 -0
- package/dist/admin/adapters/memory.d.ts +3 -5
- package/dist/admin/adapters/memory.js +27 -28
- package/dist/admin/conformance/index.js +1 -0
- package/dist/admin/controllers/auth.controller.js +8 -6
- package/dist/admin/controllers/members.controller.js +9 -6
- package/dist/admin/controllers/roles.controller.js +99 -105
- package/dist/admin/controllers/users.controller.js +56 -36
- package/dist/admin/error-copy.d.ts +11 -0
- package/dist/admin/error-copy.js +10 -0
- package/dist/admin/people.rules.js +37 -31
- package/dist/admin/react/components/MemberRow.d.ts +1 -1
- package/dist/admin/react/components/MemberRow.js +12 -2
- package/dist/admin/react/hooks/AuthBinding.hooks.d.ts +6 -21
- package/dist/admin/react/hooks/AuthBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/AuthScope.hooks.d.ts +22 -0
- package/dist/admin/react/hooks/AuthScope.hooks.js +10 -0
- package/dist/admin/react/hooks/LoginPage.hooks.d.ts +1 -1
- package/dist/admin/react/hooks/LoginPage.hooks.js +1 -1
- package/dist/admin/react/hooks/MembersBinding.hooks.d.ts +6 -17
- package/dist/admin/react/hooks/MembersBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/MembersPage.hooks.d.ts +1 -1
- package/dist/admin/react/hooks/MembersPage.hooks.js +1 -1
- package/dist/admin/react/hooks/MembersScope.hooks.d.ts +17 -0
- package/dist/admin/react/hooks/MembersScope.hooks.js +10 -0
- package/dist/admin/react/hooks/RolesBinding.hooks.d.ts +7 -21
- package/dist/admin/react/hooks/RolesBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/RolesController.hooks.js +1 -1
- package/dist/admin/react/hooks/RolesPage.hooks.d.ts +2 -2
- package/dist/admin/react/hooks/RolesPage.hooks.js +22 -8
- package/dist/admin/react/hooks/RolesScope.hooks.d.ts +21 -0
- package/dist/admin/react/hooks/RolesScope.hooks.js +10 -0
- package/dist/admin/react/hooks/UsersBinding.hooks.d.ts +6 -18
- package/dist/admin/react/hooks/UsersBinding.hooks.js +3 -8
- package/dist/admin/react/hooks/UsersPage.hooks.d.ts +1 -1
- package/dist/admin/react/hooks/UsersPage.hooks.js +1 -1
- package/dist/admin/react/hooks/UsersScope.hooks.d.ts +19 -0
- package/dist/admin/react/hooks/UsersScope.hooks.js +10 -0
- package/dist/admin/react/hooks/UsersTab.hooks.d.ts +6 -0
- package/dist/admin/react/hooks/UsersTab.hooks.js +34 -22
- package/dist/admin/react/index.d.ts +4 -4
- package/dist/admin/react/pages/LoginPage.d.ts +1 -1
- package/dist/admin/react/pages/MembersPage.d.ts +1 -1
- package/dist/admin/react/pages/RolesPage.d.ts +2 -2
- package/dist/admin/react/pages/RolesPage.js +6 -2
- package/dist/admin/react/pages/UsersPage.d.ts +2 -2
- package/dist/admin/react/pages/UsersPage.js +6 -2
- package/dist/admin/rules.js +14 -12
- package/dist/react/errors.js +16 -9
- package/dist/react/index.d.ts +1 -1
- package/dist/react/members/hooks/use-members.hooks.js +21 -12
- package/dist/react/testing.js +4 -6
- package/dist/react/users/UserRowParts.d.ts +53 -0
- package/dist/react/users/UserRowParts.js +39 -0
- package/dist/react/users/Users.d.ts +31 -2
- package/dist/react/users/Users.js +41 -36
- package/dist/react/users/hooks/use-users.hooks.js +57 -30
- package/dist/server/admin-crud-service.js +25 -13
- package/dist/server/agent-tool-input.js +62 -36
- package/dist/server/agent-tool-registrations.d.ts +46 -0
- package/dist/server/agent-tool-registrations.js +28 -20
- package/dist/server/auth-service.js +35 -20
- package/dist/server/authorize.js +7 -9
- package/dist/server/permission-migrations.js +35 -25
- package/dist/server/repo.memory-transactions.js +5 -6
- package/dist/server/repo.memory.js +35 -81
- package/dist/server/seed.js +100 -84
- package/package.json +8 -8
|
@@ -8,22 +8,36 @@ export function useRolesView(_required = {}, _optional = {}) {
|
|
|
8
8
|
throw new Error('Roles page controller unavailable.');
|
|
9
9
|
return view;
|
|
10
10
|
}
|
|
11
|
+
/** A described tab id is namespaced (`roles.roles.policies`); its last segment keys `props.tabs`. */
|
|
12
|
+
function tabKey({ id }, _optional = {}) {
|
|
13
|
+
return id.slice(id.lastIndexOf('.') + 1);
|
|
14
|
+
}
|
|
15
|
+
/** Tabs to offer: none on a hidden page, else every visible described tab the host supplied. */
|
|
16
|
+
function visibleTabs(props, _optional = {}) {
|
|
17
|
+
if (!props.description.visible)
|
|
18
|
+
return [];
|
|
19
|
+
return props.description.tabs.filter(t => t.visible && props.tabs[tabKey(t)]).map(t => ({ ...t, key: tabKey(t) }));
|
|
20
|
+
}
|
|
21
|
+
/** The shell's destructive dialog; before the controller attaches it is closed and inert. */
|
|
22
|
+
function rolesConfirm({ controller, state }, _optional = {}) {
|
|
23
|
+
const pending = state?.pending ?? null;
|
|
24
|
+
const copy = destructiveCopy({ target: pending });
|
|
25
|
+
return { open: pending != null, title: copy.title, body: copy.body, confirmLabel: copy.label,
|
|
26
|
+
tone: copy.tone, consequence: copy.consequence, pending: state?.confirming ?? false, agentMayConfirm: false,
|
|
27
|
+
onConfirm: async () => { await controller?.confirmDestructive({}); }, onCancel: () => controller?.cancelDestructive({}) };
|
|
28
|
+
}
|
|
11
29
|
export function useRolesPage(props, _optional = {}) {
|
|
12
30
|
const { controller, state, permissions } = useRolesController({});
|
|
13
31
|
const [selected, setSelected] = useState('roles');
|
|
14
32
|
const canManage = canManageRoles({ permissions });
|
|
15
|
-
const visible =
|
|
16
|
-
const ids = visible.map(t => t.
|
|
33
|
+
const visible = visibleTabs(props);
|
|
34
|
+
const ids = visible.map(t => t.key);
|
|
17
35
|
// A stale bookmark opens a real visible tab; omitted/forbidden tabs cannot be rendered directly.
|
|
18
36
|
const requested = props.requestedTab ?? selected;
|
|
19
37
|
const tab = ids.includes(requested) ? requested : ids[0] ?? '';
|
|
20
|
-
const items = visible.map(t => {
|
|
21
|
-
const copy = destructiveCopy({ target: state?.pending ?? null });
|
|
22
|
-
const confirm = { open: state?.pending != null, title: copy.title, body: copy.body, confirmLabel: copy.label,
|
|
23
|
-
tone: copy.tone, consequence: copy.consequence, pending: state?.confirming ?? false, agentMayConfirm: false,
|
|
24
|
-
onConfirm: async () => { await controller?.confirmDestructive({}); }, onCancel: () => controller?.cancelDestructive({}) };
|
|
38
|
+
const items = visible.map(t => ({ id: t.key, label: t.label, content: t.key === tab ? createElement(props.tabs[t.key], { params: t.params }) : null }));
|
|
25
39
|
return {
|
|
26
|
-
controller, state, canManage, items, tab, confirm,
|
|
40
|
+
controller, state, canManage, items, tab, confirm: rolesConfirm({ controller, state }),
|
|
27
41
|
hidden: !props.description.visible, noTabs: visible.length === 0,
|
|
28
42
|
view: controller && state ? { controller, state, canManage } : null,
|
|
29
43
|
changeTab: ({ value }) => { setSelected(value); props.onTabChange?.({ tab: value }); },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentType, LazyExoticComponent } from 'react';
|
|
2
|
+
import type { AdminInstance } from '@jini-ai/admin/core/module';
|
|
3
|
+
import type { RolesApiPort } from '../../ports.js';
|
|
4
|
+
export interface RolesScope {
|
|
5
|
+
readonly api: RolesApiPort;
|
|
6
|
+
readonly permissions: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export declare const ScopeContext: import("react").Context<RolesScope | null>;
|
|
9
|
+
export interface RolesTabProps {
|
|
10
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
11
|
+
}
|
|
12
|
+
export interface RolesPageProps {
|
|
13
|
+
readonly tabs: Readonly<Record<string, LazyExoticComponent<ComponentType<RolesTabProps>>>>;
|
|
14
|
+
readonly description: ReturnType<AdminInstance['describe']>['pages'][number];
|
|
15
|
+
readonly requestedTab?: string;
|
|
16
|
+
readonly onTabChange?: (required: {
|
|
17
|
+
tab: string;
|
|
18
|
+
}, optional?: Record<string, never>) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function useRolesScope(_required?: Record<string, never>, _optional?: Record<string, never>): RolesScope;
|
|
21
|
+
//# sourceMappingURL=RolesScope.hooks.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Scope and page contracts must not import the lazy page/binding assembly.
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
export const ScopeContext = createContext(null);
|
|
4
|
+
export function useRolesScope(_required = {}, _optional = {}) {
|
|
5
|
+
const scope = useContext(ScopeContext);
|
|
6
|
+
if (!scope)
|
|
7
|
+
throw new Error('Roles admin scope is unavailable.');
|
|
8
|
+
return scope;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=RolesScope.hooks.js.map
|
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type UsersScope } from './UsersScope.hooks.js';
|
|
2
|
+
export { useUsersScope } from './UsersScope.hooks.js';
|
|
3
|
+
export type { UsersScope, UsersPageProps } from './UsersScope.hooks.js';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
2
5
|
import type { AdminInstance } from '@jini-ai/admin/core/module';
|
|
3
|
-
import type { UsersApiPort, UsersSafetyPort } from '../../ports.js';
|
|
4
6
|
import { usersModule } from '../../users.module.js';
|
|
5
|
-
export interface UsersScope {
|
|
6
|
-
readonly api: UsersApiPort;
|
|
7
|
-
readonly permissions: readonly string[];
|
|
8
|
-
readonly safety: UsersSafetyPort;
|
|
9
|
-
}
|
|
10
|
-
export interface UsersPageProps {
|
|
11
|
-
readonly tabs: Readonly<Record<string, LazyExoticComponent<ComponentType<{
|
|
12
|
-
readonly params?: Readonly<Record<string, unknown>>;
|
|
13
|
-
}>>>>;
|
|
14
|
-
readonly description: ReturnType<AdminInstance['describe']>['pages'][number];
|
|
15
|
-
readonly openOwnPasswordReset?: boolean;
|
|
16
|
-
readonly onOwnPasswordResetClosed?: (required: Record<string, never>, optional?: Record<string, never>) => void;
|
|
17
|
-
}
|
|
18
|
-
export declare function useUsersScope(_required?: Record<string, never>, _optional?: Record<string, never>): UsersScope;
|
|
19
7
|
/** Descriptor identity must be the exact enriched object supplied to createAdmin. */
|
|
20
8
|
export declare function createUsersBinding({ module }: {
|
|
21
9
|
module: typeof usersModule;
|
|
22
10
|
}, _optional?: Record<string, never>): {
|
|
23
11
|
pages: {
|
|
24
12
|
users: {
|
|
25
|
-
Page: LazyExoticComponent<typeof import("../pages/UsersPage.js").default>;
|
|
13
|
+
Page: import("react").LazyExoticComponent<typeof import("../pages/UsersPage.js").default>;
|
|
26
14
|
tabs: {
|
|
27
|
-
users: LazyExoticComponent<typeof import("../tabs/UsersTab.js").default>;
|
|
15
|
+
users: import("react").LazyExoticComponent<typeof import("../tabs/UsersTab.js").default>;
|
|
28
16
|
};
|
|
29
17
|
};
|
|
30
18
|
};
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ScopeContext } from './UsersScope.hooks.js';
|
|
2
|
+
export { useUsersScope } from './UsersScope.hooks.js';
|
|
3
|
+
import { createElement, lazy } from 'react';
|
|
2
4
|
import { usersModule } from '../../users.module.js';
|
|
3
|
-
const ScopeContext = createContext(null);
|
|
4
|
-
export function useUsersScope(_required = {}, _optional = {}) {
|
|
5
|
-
const scope = useContext(ScopeContext);
|
|
6
|
-
if (!scope)
|
|
7
|
-
throw new Error('Users admin scope is unavailable.');
|
|
8
|
-
return scope;
|
|
9
|
-
}
|
|
10
5
|
/** Descriptor identity must be the exact enriched object supplied to createAdmin. */
|
|
11
6
|
export function createUsersBinding({ module }, _optional = {}) {
|
|
12
7
|
const pages = { users: { Page: lazy(() => import('../pages/UsersPage.js')), tabs: { users: lazy(() => import('../tabs/UsersTab.js')) } } };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { UsersController } from '../../controllers/users.controller.js';
|
|
2
2
|
import type { UsersState } from '../../models.js';
|
|
3
|
-
import type { UsersPageProps } from './
|
|
3
|
+
import type { UsersPageProps } from './UsersScope.hooks.js';
|
|
4
4
|
export interface UsersView {
|
|
5
5
|
controller: UsersController;
|
|
6
6
|
state: UsersState;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement, createContext, useContext, useMemo } from 'react';
|
|
2
2
|
import { createUsersController } from '../../controllers/users.controller.js';
|
|
3
3
|
import { hasAdminGrant } from '../../people.rules.js';
|
|
4
|
-
import { useUsersScope } from './
|
|
4
|
+
import { useUsersScope } from './UsersScope.hooks.js';
|
|
5
5
|
import { usePeopleController } from './PeopleController.hooks.js';
|
|
6
6
|
export const UsersViewContext = createContext(null);
|
|
7
7
|
export function useUsersView(_required = {}, _optional = {}) { const v = useContext(UsersViewContext); if (!v)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentType, LazyExoticComponent } from 'react';
|
|
2
|
+
import type { AdminInstance } from '@jini-ai/admin/core/module';
|
|
3
|
+
import type { UsersApiPort, UsersSafetyPort } from '../../ports.js';
|
|
4
|
+
export interface UsersScope {
|
|
5
|
+
readonly api: UsersApiPort;
|
|
6
|
+
readonly permissions: readonly string[];
|
|
7
|
+
readonly safety: UsersSafetyPort;
|
|
8
|
+
}
|
|
9
|
+
export declare const ScopeContext: import("react").Context<UsersScope | null>;
|
|
10
|
+
export interface UsersPageProps {
|
|
11
|
+
readonly tabs: Readonly<Record<string, LazyExoticComponent<ComponentType<{
|
|
12
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
13
|
+
}>>>>;
|
|
14
|
+
readonly description: ReturnType<AdminInstance['describe']>['pages'][number];
|
|
15
|
+
readonly openOwnPasswordReset?: boolean;
|
|
16
|
+
readonly onOwnPasswordResetClosed?: (required: Record<string, never>, optional?: Record<string, never>) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function useUsersScope(_required?: Record<string, never>, _optional?: Record<string, never>): UsersScope;
|
|
19
|
+
//# sourceMappingURL=UsersScope.hooks.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Scope and page contracts must not import the lazy page/binding assembly.
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
export const ScopeContext = createContext(null);
|
|
4
|
+
export function useUsersScope(_required = {}, _optional = {}) {
|
|
5
|
+
const scope = useContext(ScopeContext);
|
|
6
|
+
if (!scope)
|
|
7
|
+
throw new Error('Users admin scope is unavailable.');
|
|
8
|
+
return scope;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=UsersScope.hooks.js.map
|
|
@@ -73,6 +73,12 @@ export declare function useUsersConfirmation(_required?: Record<string, never>,
|
|
|
73
73
|
tone: "warning";
|
|
74
74
|
body: string;
|
|
75
75
|
consequence: string;
|
|
76
|
+
} | {
|
|
77
|
+
title: string;
|
|
78
|
+
label: string;
|
|
79
|
+
tone: "warning";
|
|
80
|
+
body: string;
|
|
81
|
+
consequence: string;
|
|
76
82
|
};
|
|
77
83
|
confirm: {
|
|
78
84
|
open: boolean;
|
|
@@ -12,42 +12,54 @@ export function useUsersTab(_required = {}, _optional = {}) {
|
|
|
12
12
|
submit: (e) => { e.preventDefault(); void c.createUser({}); },
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
/** Row actions in menu order; each is offered only when the controller allows it for this user. */
|
|
16
|
+
const rowActions = [
|
|
17
|
+
{ action: 'disable', label: 'Disable', run: (c, principalId) => c.requestDestructive({ principalId, kind: 'disable' }) },
|
|
18
|
+
{ action: 'enable', label: 'Enable', run: (c, principalId) => { void c.enableUser({ principalId }); } },
|
|
19
|
+
{ action: 'reset', label: 'Reset password', run: (c, principalId) => c.requestDestructive({ principalId, kind: 'reset' }) },
|
|
20
|
+
{ action: 'delete', label: 'Delete', run: (c, principalId) => c.requestDestructive({ principalId, kind: 'delete' }) },
|
|
21
|
+
];
|
|
22
|
+
function grantOptions({ placeholder, records }, _optional = {}) {
|
|
23
|
+
return [{ value: '', label: placeholder }, ...records.map(r => ({ value: r.id, label: `${r.name}${r.isBuiltin ? ' (built-in)' : ''}` }))];
|
|
24
|
+
}
|
|
15
25
|
export function useUserRow({ user }, _optional = {}) {
|
|
16
26
|
const { controller: c, state: s } = useUsersView({});
|
|
17
27
|
const id = user.principalId, canEmail = c.allowed({ principalId: id, action: 'email' }), canGrants = c.allowed({ principalId: id, action: 'grants' });
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (c.allowed({ principalId: id, action: 'reset' }))
|
|
26
|
-
items.push({ id: 'reset', label: 'Reset password', onPress: () => c.requestDestructive({ principalId: id, kind: 'reset' }) });
|
|
27
|
-
if (c.allowed({ principalId: id, action: 'delete' }))
|
|
28
|
-
items.push({ id: 'delete', label: 'Delete', onPress: () => c.requestDestructive({ principalId: id, kind: 'delete' }) });
|
|
29
|
-
return { s, items, canEmail, canGrants, expanded: s.expandedId === id, canExpand: canEmail || canGrants,
|
|
30
|
-
roles: grantLabel({ ids: user.roleIds, records: s.roles ?? [] }), policies: grantLabel({ ids: user.policyIds, records: s.policies ?? [] }),
|
|
28
|
+
const canExpand = canEmail || canGrants, roles = s.roles ?? [], policies = s.policies ?? [];
|
|
29
|
+
const items = canExpand ? [{ id: 'manage', label: 'Manage', onPress: () => c.toggleExpanded({ principalId: id }) }] : [];
|
|
30
|
+
for (const a of rowActions)
|
|
31
|
+
if (c.allowed({ principalId: id, action: a.action }))
|
|
32
|
+
items.push({ id: a.action, label: a.label, onPress: () => a.run(c, id) });
|
|
33
|
+
return { s, items, canEmail, canGrants, expanded: s.expandedId === id, canExpand,
|
|
34
|
+
roles: grantLabel({ ids: user.roleIds, records: roles }), policies: grantLabel({ ids: user.policyIds, records: policies }),
|
|
31
35
|
toggle: () => c.toggleExpanded({ principalId: id }), menuLabel: `Actions for user "${user.username}"`,
|
|
32
36
|
email: ({ value }) => c.setDraft({ patch: { editEmail: value } }), role: ({ value }) => c.setDraft({ patch: { pendingRoleId: value } }), policy: ({ value }) => c.setDraft({ patch: { pendingPolicyId: value } }),
|
|
33
37
|
saveEmail: () => { void c.savePanel({ principalId: id, kind: 'email' }); }, assignRole: () => { void c.savePanel({ principalId: id, kind: 'role' }); }, attachPolicy: () => { void c.savePanel({ principalId: id, kind: 'policy' }); },
|
|
34
|
-
roleOptions:
|
|
35
|
-
policyOptions:
|
|
38
|
+
roleOptions: grantOptions({ placeholder: 'Select a role…', records: roles }),
|
|
39
|
+
policyOptions: grantOptions({ placeholder: 'Select a policy…', records: policies }), };
|
|
40
|
+
}
|
|
41
|
+
/** Dialog copy per pending kind; nothing pending (a closed dialog) reads as the disable copy. */
|
|
42
|
+
const confirmationCopy = {
|
|
43
|
+
delete: { title: 'Delete this user?', label: 'Move to trash', tone: 'danger',
|
|
44
|
+
body: 'They will be signed out and moved to the Trash. You can restore them there; they are deleted permanently after 60 days.', consequence: 'moves the user to the Trash' },
|
|
45
|
+
reset: { title: 'Reset password?', label: 'Reset password', tone: 'warning', body: 'Every active session for this user will be signed out.', consequence: 'revokes every active session' },
|
|
46
|
+
disable: { title: 'Disable this user?', label: 'Disable', tone: 'warning', body: 'They will not be able to sign in until re-enabled.', consequence: 'revokes sign-in access' },
|
|
47
|
+
};
|
|
48
|
+
/** A password field's reveal state: its input type and its toggle button's label. */
|
|
49
|
+
function revealState({ shown, name }, _optional = {}) {
|
|
50
|
+
return shown ? { type: 'text', label: `Hide ${name} password` } : { type: 'password', label: `Show ${name} password` };
|
|
36
51
|
}
|
|
37
52
|
export function useUsersConfirmation(_required = {}, _optional = {}) {
|
|
38
53
|
const { state: s, controller: c } = useUsersView({});
|
|
39
|
-
const kind = s.pending?.kind, reset = kind === 'reset', mismatch = s.newPassword !== s.confirmPassword;
|
|
40
|
-
const copy = kind
|
|
41
|
-
body: 'They will be signed out and moved to the Trash. You can restore them there; they are deleted permanently after 60 days.', consequence: 'moves the user to the Trash' }
|
|
42
|
-
: reset ? { title: 'Reset password?', label: 'Reset password', tone: 'warning', body: 'Every active session for this user will be signed out.', consequence: 'revokes every active session' }
|
|
43
|
-
: { title: 'Disable this user?', label: 'Disable', tone: 'warning', body: 'They will not be able to sign in until re-enabled.', consequence: 'revokes sign-in access' };
|
|
54
|
+
const kind = s.pending?.kind ?? 'disable', reset = kind === 'reset', mismatch = s.newPassword !== s.confirmPassword;
|
|
55
|
+
const copy = confirmationCopy[kind], shownNew = revealState({ shown: s.showNewPassword, name: 'new' }), shownConfirm = revealState({ shown: s.showConfirmPassword, name: 'confirm' });
|
|
44
56
|
return { s, reset, mismatch, error: mismatch ? 'Passwords do not match.' : s.passwordError, copy,
|
|
45
57
|
confirm: { open: s.pending !== null, title: copy.title, confirmLabel: copy.label, tone: copy.tone, consequence: copy.consequence, pending: s.confirming, agentMayConfirm: false,
|
|
46
58
|
onConfirm: async () => { await c.confirmDestructive({}); }, onCancel: () => c.cancelDestructive({}) },
|
|
47
59
|
newPassword: ({ value }) => c.setDraft({ patch: { newPassword: value } }),
|
|
48
60
|
confirmPassword: ({ value }) => c.setDraft({ patch: { confirmPassword: value } }),
|
|
49
|
-
newType:
|
|
61
|
+
newType: shownNew.type, confirmType: shownConfirm.type,
|
|
50
62
|
toggleNew: () => c.togglePasswordVisible({ field: 'new' }), toggleConfirm: () => c.togglePasswordVisible({ field: 'confirm' }),
|
|
51
|
-
newLabel:
|
|
63
|
+
newLabel: shownNew.label, confirmLabel: shownConfirm.label, };
|
|
52
64
|
}
|
|
53
65
|
//# sourceMappingURL=UsersTab.hooks.js.map
|
|
@@ -54,7 +54,7 @@ export declare function roles(_required: Record<string, never>, _optional?: Reco
|
|
|
54
54
|
Provider: ({ admin, children }: {
|
|
55
55
|
admin: import("@jini-ai/admin/core/module").AdminInstance;
|
|
56
56
|
children: import("react").ReactNode;
|
|
57
|
-
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/
|
|
57
|
+
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/RolesScope.hooks.js").RolesScope | null>>;
|
|
58
58
|
};
|
|
59
59
|
kitNeeds: import("@jini-ai/ui-kit").KitNeeds<"Notice" | "Spinner" | "Tabs" | "Badge" | "Menu" | "Button" | "TextField" | "ConfirmDialog">;
|
|
60
60
|
};
|
|
@@ -107,7 +107,7 @@ export declare function users(_required: Record<string, never>, _optional?: Reco
|
|
|
107
107
|
Provider: ({ admin, children }: {
|
|
108
108
|
admin: import("@jini-ai/admin/core/module").AdminInstance;
|
|
109
109
|
children: import("react").ReactNode;
|
|
110
|
-
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/
|
|
110
|
+
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/UsersScope.hooks.js").UsersScope | null>>;
|
|
111
111
|
};
|
|
112
112
|
kitNeeds: import("@jini-ai/ui-kit").KitNeeds<"Notice" | "Spinner" | "Badge" | "Menu" | "Button" | "TextField" | "Select" | "ConfirmDialog">;
|
|
113
113
|
};
|
|
@@ -152,7 +152,7 @@ export declare function members(_required: Record<string, never>, _optional?: Re
|
|
|
152
152
|
Provider: ({ admin, children }: {
|
|
153
153
|
admin: import("@jini-ai/admin/core/module").AdminInstance;
|
|
154
154
|
children: import("react").ReactNode;
|
|
155
|
-
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/
|
|
155
|
+
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/MembersScope.hooks.js").MembersScope | null>>;
|
|
156
156
|
};
|
|
157
157
|
kitNeeds: import("@jini-ai/ui-kit").KitNeeds<"Notice" | "Spinner" | "Badge" | "Menu" | "Button" | "ConfirmDialog">;
|
|
158
158
|
};
|
|
@@ -193,7 +193,7 @@ export declare function auth(_required: Record<string, never>, _optional?: Recor
|
|
|
193
193
|
Provider: ({ admin, children }: {
|
|
194
194
|
admin: import("@jini-ai/admin/core/module").AdminInstance;
|
|
195
195
|
children: import("react").ReactNode;
|
|
196
|
-
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/
|
|
196
|
+
}, _optional?: Record<string, never>) => import("react").FunctionComponentElement<import("react").ProviderProps<import("./hooks/AuthScope.hooks.js").AuthScope | null>>;
|
|
197
197
|
};
|
|
198
198
|
kitNeeds: import("@jini-ai/ui-kit").KitNeeds<"Notice" | "Spinner" | "Button" | "TextField">;
|
|
199
199
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { AuthPageProps } from '../hooks/
|
|
1
|
+
import type { AuthPageProps } from '../hooks/AuthScope.hooks.js';
|
|
2
2
|
export default function LoginPage(props: AuthPageProps, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=LoginPage.d.ts.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { MembersPageProps } from '../hooks/
|
|
1
|
+
import type { MembersPageProps } from '../hooks/MembersScope.hooks.js';
|
|
2
2
|
export default function MembersPage(props: MembersPageProps, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=MembersPage.d.ts.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { RolesPageProps } from '../hooks/
|
|
2
|
-
export default function RolesPage(props: RolesPageProps
|
|
1
|
+
import type { RolesPageProps } from '../hooks/RolesScope.hooks.js';
|
|
2
|
+
export default function RolesPage(props: RolesPageProps): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=RolesPage.d.ts.map
|
|
@@ -4,7 +4,11 @@ import { Button, Notice, Spinner, Tabs } from '@jini-ai/ui-kit/react';
|
|
|
4
4
|
import { RolesViewContext, useRolesPage } from '../hooks/RolesPage.hooks.js';
|
|
5
5
|
import { DestructiveDialog } from '../components/DestructiveDialog.js';
|
|
6
6
|
import { rolesMessagesEn as m } from '../../messages.en.js';
|
|
7
|
-
|
|
7
|
+
/** The loaded shell; RolesPage only renders it once the page view and both lists exist. */
|
|
8
|
+
function RolesShell({ v, view }) {
|
|
9
|
+
return _jsx(RolesViewContext.Provider, { value: view, children: _jsxs("div", { className: "page", children: [_jsxs("header", { children: [_jsx("p", { children: m.people }), _jsx("h1", { children: m.title }), _jsx("p", { children: m.description })] }), !v.canManage ? _jsx(Notice, { children: m.readOnly }) : null, view.state.rowError ? _jsx(Notice, { tone: "danger", children: view.state.rowError }) : null, v.noTabs ? _jsx(Notice, { children: "No roles tabs are enabled." }) : _jsx(Suspense, { fallback: _jsx(Spinner, { label: m.loading }), children: _jsx(Tabs, { label: m.title, value: v.tab, items: v.items, onValueChange: v.changeTab }) }), _jsx(DestructiveDialog, { ...v.confirm })] }) });
|
|
10
|
+
}
|
|
11
|
+
export default function RolesPage(props) {
|
|
8
12
|
const v = useRolesPage(props);
|
|
9
13
|
if (v.hidden)
|
|
10
14
|
return _jsx(Notice, { children: "Roles administration is unavailable." });
|
|
@@ -14,6 +18,6 @@ export default function RolesPage(props, _optional = {}) {
|
|
|
14
18
|
return _jsxs(Notice, { tone: "danger", children: [v.state.error, _jsx(Button, { onPress: v.retry, children: "Retry" })] });
|
|
15
19
|
if (!v.view || !v.state.roles || !v.state.policies)
|
|
16
20
|
return _jsx(Spinner, { label: m.loading });
|
|
17
|
-
return _jsx(
|
|
21
|
+
return _jsx(RolesShell, { v: v, view: v.view });
|
|
18
22
|
}
|
|
19
23
|
//# sourceMappingURL=RolesPage.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { UsersPageProps } from '../hooks/
|
|
2
|
-
export default function UsersPage(props: UsersPageProps
|
|
1
|
+
import type { UsersPageProps } from '../hooks/UsersScope.hooks.js';
|
|
2
|
+
export default function UsersPage(props: UsersPageProps): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=UsersPage.d.ts.map
|
|
@@ -4,7 +4,11 @@ import { Button, Notice, Spinner } from '@jini-ai/ui-kit/react';
|
|
|
4
4
|
import { useUsersPage, UsersViewContext } from '../hooks/UsersPage.hooks.js';
|
|
5
5
|
import { usersMessagesEn as m } from '../../messages.en.js';
|
|
6
6
|
import { UsersConfirmation } from '../components/UsersConfirmation.js';
|
|
7
|
-
|
|
7
|
+
/** The loaded shell; UsersPage only renders it once the page view and all three lists exist. */
|
|
8
|
+
function UsersShell({ v, view }) {
|
|
9
|
+
return _jsx(UsersViewContext.Provider, { value: view, children: _jsxs("div", { className: "page", children: [_jsxs("header", { children: [_jsx("p", { children: "People" }), _jsx("h1", { children: m.title }), _jsx("p", { children: m.description }), _jsx("p", { children: m.passwordHelp })] }), v.readOnly ? _jsx(Notice, { children: m.readOnly }) : null, _jsx(Notice, { children: m.protected }), view.state.rowError ? _jsx(Notice, { tone: "danger", children: view.state.rowError }) : null, view.state.notice ? _jsx(Notice, { tone: "success", children: view.state.notice }) : null, _jsx(Suspense, { fallback: _jsx(Spinner, { label: m.loading }), children: v.content }), _jsx(UsersConfirmation, {})] }) });
|
|
10
|
+
}
|
|
11
|
+
export default function UsersPage(props) {
|
|
8
12
|
const v = useUsersPage(props);
|
|
9
13
|
if (!v.visible)
|
|
10
14
|
return _jsx(Notice, { children: "Users administration is unavailable." });
|
|
@@ -14,6 +18,6 @@ export default function UsersPage(props, _optional = {}) {
|
|
|
14
18
|
return _jsxs(Notice, { tone: "danger", children: [v.state.error, _jsx(Button, { onPress: v.retry, children: "Retry" })] });
|
|
15
19
|
if (!v.view || !v.state.users || !v.state.roles || !v.state.policies)
|
|
16
20
|
return _jsx(Spinner, { label: m.loading });
|
|
17
|
-
return _jsx(
|
|
21
|
+
return _jsx(UsersShell, { v: v, view: v.view });
|
|
18
22
|
}
|
|
19
23
|
//# sourceMappingURL=UsersPage.js.map
|
package/dist/admin/rules.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { rolesMessagesEn as m } from './messages.en.js';
|
|
2
|
+
import { hasAdminGrant } from './people.rules.js';
|
|
3
|
+
import { errorCode, errorMessageOr } from './error-copy.js';
|
|
2
4
|
/** All writes share the actual server vocabulary. A missing grant is a denial. */
|
|
3
5
|
export function canManageRoles({ permissions }, _optional = {}) {
|
|
4
|
-
return permissions
|
|
6
|
+
return hasAdminGrant({ permissions, permission: 'role.manage' });
|
|
5
7
|
}
|
|
6
8
|
export function mutableRole({ role }, _optional = {}) { return !role.isBuiltin; }
|
|
7
9
|
export function mutablePolicy({ policy }, _optional = {}) { return !policy.isBuiltin && !policy.isFrozen; }
|
|
10
|
+
const ROLES_ERROR_COPY = Object.freeze({
|
|
11
|
+
FORBIDDEN: m.forbidden, RESOURCE_CONFLICT: m.conflict, PERMISSION_UNKNOWN: m.unknownPermission,
|
|
12
|
+
GRANT_EXCEEDS_ISSUER: m.exceedsIssuer,
|
|
13
|
+
});
|
|
8
14
|
/** Conflict copy is feature-specific: references, rather than duplicate workspace slugs. */
|
|
9
15
|
export function describeRolesError({ error, fallback }, _optional = {}) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (messages[error.code])
|
|
18
|
-
return messages[error.code];
|
|
19
|
-
}
|
|
20
|
-
return error instanceof Error && error.message ? error.message : fallback;
|
|
16
|
+
const code = errorCode({ error });
|
|
17
|
+
if (code === 'VALIDATION_ERROR')
|
|
18
|
+
return errorMessageOr({ error, fallback: m.validation });
|
|
19
|
+
// Inherited keys resolve here (see the "reported, not fixed" pin in rules.pins.test.ts).
|
|
20
|
+
if (code !== undefined && ROLES_ERROR_COPY[code])
|
|
21
|
+
return ROLES_ERROR_COPY[code];
|
|
22
|
+
return errorMessageOr({ error, fallback });
|
|
21
23
|
}
|
|
22
24
|
export function destructiveCopy({ target }, _optional = {}) {
|
|
23
25
|
if (target?.kind === 'permission')
|
package/dist/react/errors.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
/** A non-empty `Error.message`, or `undefined` for anything else — the "raw server text" both
|
|
2
|
+
* branches of {@link describeIdentityError} prefer over a translated default. */
|
|
3
|
+
function errorMessage(error) {
|
|
4
|
+
return error instanceof Error && error.message ? error.message : undefined;
|
|
5
|
+
}
|
|
6
|
+
/** The string `code` an Error or plain object carries, or `undefined` when there is none. */
|
|
7
|
+
function errorCode(error) {
|
|
8
|
+
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
9
|
+
}
|
|
1
10
|
/** Error codes are data, so any host transport can supply them without a shared error class. */
|
|
2
11
|
export function describeIdentityError({ error, fallback, translate }, { messages = {} } = {}) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
return error instanceof Error && error.message ? error.message : fallback;
|
|
12
|
+
const code = errorCode(error);
|
|
13
|
+
if (code === "VALIDATION_ERROR")
|
|
14
|
+
return errorMessage(error) ?? translate("Please correct the highlighted fields.");
|
|
15
|
+
const message = code === undefined ? undefined : messages[code];
|
|
16
|
+
if (message)
|
|
17
|
+
return translate(message);
|
|
18
|
+
return errorMessage(error) ?? fallback;
|
|
12
19
|
}
|
|
13
20
|
//# sourceMappingURL=errors.js.map
|
package/dist/react/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { LoginProps } from './auth/Login.js';
|
|
|
5
5
|
export { useLogin } from './auth/hooks/use-login.hooks.js';
|
|
6
6
|
export type { LoginController, LoginHookProps, LoginOptions } from './auth/hooks/use-login.hooks.js';
|
|
7
7
|
export { Users, UserManagePanel } from './users/Users.js';
|
|
8
|
-
export type { UsersProps, GrantOption, GrantSelectController, UserManageController, UserManagePanelProps, UserRowActionsController, UserRowProps, UsersTableProps } from './users/Users.js';
|
|
8
|
+
export type { UsersProps, UsersBaseProps, UsersHook, UsersHookDependencies, GrantOption, GrantSelectController, UserManageController, UserManagePanelProps, UserRowActionsController, UserRowProps, UsersTableProps } from './users/Users.js';
|
|
9
9
|
export { useUsers } from './users/hooks/use-users.hooks.js';
|
|
10
10
|
export type { UsersController, UsersDependencies, UsersOptions } from './users/hooks/use-users.hooks.js';
|
|
11
11
|
export { useResetPasswordFields } from './users/hooks/use-reset-password-fields.hooks.js';
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { describeApiError, emptyRowState } from "../rules.js";
|
|
3
3
|
import { useCachedLoader, useFetchMutation, useFetchQuery, useInvalidate } from "@jini-ai/ui/fetch-query";
|
|
4
|
+
/**
|
|
5
|
+
* The spinner row for the detail panel: the expanded row while its keyed observer is still
|
|
6
|
+
* fetching with nothing cached, `null` otherwise. Pure so `useMembers` stays a flat wiring list.
|
|
7
|
+
*/
|
|
8
|
+
function detailLoadingIdFor({ expandedId, detail }) {
|
|
9
|
+
return expandedId !== null && !detail.data && detail.isFetching ? expandedId : null;
|
|
10
|
+
}
|
|
4
11
|
export function useMembers({ port, translate, queryScope = "members" }, { refresh } = {}) {
|
|
5
12
|
const scopeRef = useRef(queryScope);
|
|
6
13
|
scopeRef.current = queryScope;
|
|
7
|
-
const boundT = translate;
|
|
8
14
|
const describeError = (error, fallback) => describeApiError({ error, fallback, translate });
|
|
15
|
+
/** `null` when there is nothing to describe, so each derived error string stays one line. */
|
|
16
|
+
const describeIfError = (error, fallback) => (error ? describeError(error, fallback) : null);
|
|
9
17
|
const listKey = [queryScope, 'members', 'list'];
|
|
10
18
|
const detailRoot = [queryScope, 'members', 'detail'];
|
|
11
19
|
const invalidate = useInvalidate();
|
|
20
|
+
// One request description shared by the observer and the imperative loader, so both read the
|
|
21
|
+
// same key through the same fetch.
|
|
22
|
+
const listRequest = { key: listKey, fetch: () => port.listMembers({}) };
|
|
12
23
|
// Mount reads and assistant refreshes can overlap; the shared cache owns list settlement
|
|
13
24
|
// order so a slow earlier success or failure cannot replace a newer roster.
|
|
14
|
-
const list = useFetchQuery(
|
|
25
|
+
const list = useFetchQuery(listRequest);
|
|
15
26
|
const members = list.data?.members ?? null;
|
|
16
|
-
const error =
|
|
17
|
-
const listLoader = useCachedLoader(
|
|
27
|
+
const error = describeIfError(list.error, translate("failed to load members"));
|
|
28
|
+
const listLoader = useCachedLoader(listRequest, { staleTime: 0 });
|
|
18
29
|
const disable = useFetchMutation({ run: ({ input: id }) => port.disableMember({ id }) }, { invalidates: [listKey, detailRoot] });
|
|
19
30
|
// Sending a link changes no list/detail fields; do not refetch the roster on success.
|
|
20
31
|
const resend = useFetchMutation({ run: ({ input: email }) => port.requestMemberMagicLink({ email }) });
|
|
@@ -33,11 +44,9 @@ export function useMembers({ port, translate, queryScope = "members" }, { refres
|
|
|
33
44
|
const [detailGeneration, setDetailGeneration] = useState(0);
|
|
34
45
|
// Entity keys own settlement: a late A response cannot paint B's error/spinner. Detail
|
|
35
46
|
// data remains cached on collapse, while successful disables invalidate both resources.
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}, { enabled: expandedId !== null, staleTime: Infinity });
|
|
40
|
-
const detailLoader = useCachedLoader({ key: [...detailRoot, expandedId ?? '', detailGeneration], fetch: () => port.getMember({ id: expandedId }) }, { staleTime: Infinity });
|
|
47
|
+
const detailRequest = { key: [...detailRoot, expandedId ?? '', detailGeneration], fetch: () => port.getMember({ id: expandedId }) };
|
|
48
|
+
const detail = useFetchQuery(detailRequest, { enabled: expandedId !== null, staleTime: Infinity });
|
|
49
|
+
const detailLoader = useCachedLoader(detailRequest, { staleTime: Infinity });
|
|
41
50
|
useEffect(() => {
|
|
42
51
|
expandedIdRef.current = null;
|
|
43
52
|
detailGenerations.current = {};
|
|
@@ -49,10 +58,10 @@ export function useMembers({ port, translate, queryScope = "members" }, { refres
|
|
|
49
58
|
}, [queryScope]);
|
|
50
59
|
// Only paint the failure onto the panel that is still expanded: the observer's entity key
|
|
51
60
|
// selects the current row, so a late rejection for a collapsed row cannot surface here.
|
|
52
|
-
const detailError =
|
|
61
|
+
const detailError = describeIfError(detail.error, translate("Failed to load member detail."));
|
|
53
62
|
// A superseded load leaves the spinner to its successor. Derive it from the current keyed
|
|
54
63
|
// observer instead of letting an old request's finally clear the new row's loading state.
|
|
55
|
-
const detailLoadingId = expandedId
|
|
64
|
+
const detailLoadingId = detailLoadingIdFor({ expandedId, detail });
|
|
56
65
|
useEffect(() => {
|
|
57
66
|
if (expandedId && detail.data)
|
|
58
67
|
setDetailById(current => ({ ...current, [expandedId]: detail.data.member }));
|
|
@@ -141,7 +150,7 @@ export function useMembers({ port, translate, queryScope = "members" }, { refres
|
|
|
141
150
|
confirmingDisable,
|
|
142
151
|
setConfirmingDisable,
|
|
143
152
|
confirmDisable,
|
|
144
|
-
t:
|
|
153
|
+
t: translate,
|
|
145
154
|
translate,
|
|
146
155
|
};
|
|
147
156
|
}
|
package/dist/react/testing.js
CHANGED
|
@@ -9,9 +9,9 @@ export function createFakeLoginPort(_required, options = {}) {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
const FAKE_WORKSPACE_ID = "fake-ws";
|
|
12
|
-
|
|
12
|
+
// Every caller mints a fresh id, so the id is required rather than defaulted.
|
|
13
|
+
function fakeUser(overrides) {
|
|
13
14
|
return {
|
|
14
|
-
principalId: overrides.principalId ?? "fake-user-1",
|
|
15
15
|
workspaceId: FAKE_WORKSPACE_ID,
|
|
16
16
|
username: "fake",
|
|
17
17
|
status: "active",
|
|
@@ -108,18 +108,16 @@ export function createFakeUsersPort(_required, options = {}) {
|
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
-
function fakeRole(overrides
|
|
111
|
+
function fakeRole(overrides) {
|
|
112
112
|
return {
|
|
113
|
-
id: overrides.id ?? "fake-role-1",
|
|
114
113
|
workspaceId: FAKE_WORKSPACE_ID,
|
|
115
114
|
name: "Fake role",
|
|
116
115
|
isBuiltin: false,
|
|
117
116
|
...overrides,
|
|
118
117
|
};
|
|
119
118
|
}
|
|
120
|
-
function fakePolicy(overrides
|
|
119
|
+
function fakePolicy(overrides) {
|
|
121
120
|
return {
|
|
122
|
-
id: overrides.id ?? "fake-policy-1",
|
|
123
121
|
workspaceId: FAKE_WORKSPACE_ID,
|
|
124
122
|
name: "Fake policy",
|
|
125
123
|
isBuiltin: false,
|