@jini-ai/user-management 0.3.0 → 0.3.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/CHANGELOG.md +4 -0
- package/dist/core/principal-kind-policy.d.ts +1 -1
- package/dist/core/principal-kind-policy.js +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/members/Members.d.ts +28 -1
- package/dist/react/members/Members.js +14 -10
- package/dist/react/members/hooks/use-members.hooks.d.ts +43 -1
- package/dist/react/members/hooks/use-members.hooks.js +37 -9
- package/dist/react/members/rules.d.ts +31 -4
- package/dist/react/members/rules.js +17 -5
- package/dist/react/models.d.ts +3 -0
- package/dist/react/users/Users.d.ts +105 -6
- package/dist/react/users/Users.js +105 -43
- package/dist/react/users/hooks/use-reset-password-fields.hooks.d.ts +33 -1
- package/dist/react/users/hooks/use-reset-password-fields.hooks.js +16 -1
- package/dist/react/users/hooks/use-users.hooks.d.ts +120 -3
- package/dist/react/users/hooks/use-users.hooks.js +192 -23
- package/dist/react/users/rules.d.ts +115 -8
- package/dist/react/users/rules.js +109 -6
- package/package.json +15 -10
|
@@ -1,20 +1,101 @@
|
|
|
1
1
|
import { describeIdentityError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file Pure logic for the `users` feature — everything that computes a value rather than
|
|
4
|
+
* rendering one. Follows the `posts/rules.ts` convention: no React import, no hooks, directly
|
|
5
|
+
* testable.
|
|
6
|
+
*
|
|
7
|
+
* Moved here from `Users.tsx`: the screen's own `describeApiError` override (a branch per server
|
|
8
|
+
* error `code`), the row-menu item builder (a branch on `user.status` plus the confirm-vs-immediate
|
|
9
|
+
* split for Disable/Enable), and the role/policy grant label formatter (a branch on whether the
|
|
10
|
+
* user holds any grants of that kind). All three were previously closures or free functions inside
|
|
11
|
+
* the component, reachable only by rendering the full screen.
|
|
12
|
+
*
|
|
13
|
+
* `KEYS` (fetch-query migration, 2026-08-12): one identity for the combined users+roles+policies
|
|
14
|
+
* read — matches the pre-migration `reload()`, which always refetched all three together.
|
|
15
|
+
*/
|
|
2
16
|
export const KEYS = {
|
|
3
17
|
list: ["users-roles-policies"],
|
|
4
18
|
};
|
|
19
|
+
/** Server error `code` -> a plain-language prefix, for every code on this screen whose message is
|
|
20
|
+
* a fixed string. Keyed by the same closed set of `ApiError` codes the old if-chain checked, in a
|
|
21
|
+
* flat lookup rather than sequential branches — this is what actually lowered the function's
|
|
22
|
+
* cognitive score (11 -> under the ceiling): the codes are a closed set of *literal string* keys,
|
|
23
|
+
* not a discriminated union, so there is no TypeScript exhaustiveness to lose by using a table
|
|
24
|
+
* (contrast `translateRunAgentPayload`'s `switch`, which stays a `switch` for exactly that reason).
|
|
25
|
+
* Values are the English source strings — `t()`'s own keys — not yet localized; `describeApiError`
|
|
26
|
+
* below is what translates them. */
|
|
5
27
|
const STATIC_ERROR_MESSAGES = {
|
|
6
28
|
GRANT_EXCEEDS_ISSUER: "You cannot grant a permission you do not hold.",
|
|
7
29
|
FORBIDDEN: "You do not have permission to do that.",
|
|
8
30
|
RESOURCE_CONFLICT: "That username is already in use.",
|
|
9
31
|
OWNER_REQUIRED: "The workspace must keep at least one active owner.",
|
|
32
|
+
// Delete-user plan v2 (2026-09-24), Slice 3/decision 7 — the DELETE route's own 409s.
|
|
10
33
|
SELF_DELETE: "You cannot delete your own account.",
|
|
11
34
|
USER_IN_TRASH: "This user is in the Trash; restore them first.",
|
|
12
35
|
USERNAME_IN_TRASH: "A user with this username is in the Trash; restore or delete them permanently first.",
|
|
13
36
|
};
|
|
14
|
-
|
|
37
|
+
/** Server error `code` -> a plain-language prefix (SPEC-006 errors.spec.md §2), layered on the
|
|
38
|
+
* shared default (`lib/api.ts`'s `describeApiError`) — this screen's `RESOURCE_CONFLICT` means
|
|
39
|
+
* "username already in use", a different meaning than `Roles.tsx`'s "still referenced" or
|
|
40
|
+
* `Workspace.tsx`'s "slug already taken" for the same code (audit cross-cutting finding #2 —
|
|
41
|
+
* deliberately not unified into one table). `VALIDATION_ERROR` stays a dedicated branch rather than
|
|
42
|
+
* joining the table above: its message comes from the error itself (`e.message`), not a fixed
|
|
43
|
+
* string, so it isn't a value a plain lookup can hold.
|
|
44
|
+
*
|
|
45
|
+
* `locale` (C4 fix, 2026-09-20): every branch here used to return its English literal directly,
|
|
46
|
+
* leaking English into every non-`en` locale regardless of the caller's own translated fallback —
|
|
47
|
+
* the fallback was already threaded through `t()` at each call site, but these overrides were not.
|
|
48
|
+
* Each literal is now also a key into `users-i18n.ts`'s `USERS_DICT`. */
|
|
49
|
+
export function describeUsersError({ error, fallback, translate }, _optional = {}) {
|
|
15
50
|
return describeIdentityError({ error, fallback, translate }, { messages: STATIC_ERROR_MESSAGES });
|
|
16
51
|
}
|
|
17
|
-
export
|
|
52
|
+
export { describeUsersError as describeApiError };
|
|
53
|
+
/** Courtesy mirror of the server target rule. Unknown identity/roles fail closed. */
|
|
54
|
+
export function userAccountCapabilities(required, _optional = {}) {
|
|
55
|
+
const { user, callerPrincipalId, callerIsOwner, canManageUserTrash, roles } = required;
|
|
56
|
+
const known = callerPrincipalId !== null;
|
|
57
|
+
const protectedAccount = user.isProtectedAccount ?? (user.roleIds.some(id => {
|
|
58
|
+
const role = roles.get(id);
|
|
59
|
+
return !role || (role.isBuiltin && (role.name === "admin" || role.name === "owner"));
|
|
60
|
+
}));
|
|
61
|
+
const mayAct = known && (callerIsOwner || !protectedAccount);
|
|
62
|
+
const destructive = mayAct && callerPrincipalId !== user.principalId;
|
|
63
|
+
return { canDelete: destructive && canManageUserTrash, canDisable: destructive,
|
|
64
|
+
canEnable: mayAct, canResetPassword: mayAct };
|
|
65
|
+
}
|
|
66
|
+
export function isOwnerAccount(required, _optional = {}) {
|
|
67
|
+
return required.user.isOwner ?? required.user.roleIds.some(id => {
|
|
68
|
+
const role = required.roles.get(id);
|
|
69
|
+
return role?.isBuiltin === true && role.name === "owner";
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `RowMenu` items for one user row — matches `Posts.tsx`/`Pages.tsx`'s three-dot menu shape:
|
|
74
|
+
* Disable/Enable, Manage, Reset password, and — delete-user plan v2 (2026-09-24) — Delete, appended
|
|
75
|
+
* only when `canDelete` is true. The server gate (owner or the built-in `admin` role,
|
|
76
|
+
* `delete-user-service.ts`'s `callerMayManageUserTrash`) is the real boundary; `canDelete` is this
|
|
77
|
+
* row's own affordance-hiding mirror of it, computed once per render from `/auth/me` by
|
|
78
|
+
* `use-users.hooks.ts` — see that hook's `canManageUserTrash` state for where it comes from.
|
|
79
|
+
*
|
|
80
|
+
* Disable/Enable share a single "toggle" item (label follows status, same shape as
|
|
81
|
+
* `Redirects.tsx`'s own toggle item) — Disable confirms via the modal below; Enable fires
|
|
82
|
+
* immediately, matching this screen's existing behavior (Enable was never confirm-gated). The
|
|
83
|
+
* `toggleSaving` guard reproduces the original inline check (`if (toggleSavingId) return`) against
|
|
84
|
+
* whatever row action is currently in flight, read fresh at click time.
|
|
85
|
+
*
|
|
86
|
+
* "Manage" always reads "Manage", never "Close": the item still toggles the expanded panel
|
|
87
|
+
* (`onManage` — closing it again by selecting "Manage" a second time still works exactly as it did
|
|
88
|
+
* as a standalone button), but a `RowMenu` item disappears the instant it is selected, so a label
|
|
89
|
+
* that flips to "Close" is never actually visible mid-interaction — it would only ever describe a
|
|
90
|
+
* state the operator cannot see while the menu that shows it is open. A static label sidesteps that
|
|
91
|
+
* without losing any capability.
|
|
92
|
+
*
|
|
93
|
+
* "Delete" always opens the confirm dialog (`onRequestDelete`), matching Disable's shape rather than
|
|
94
|
+
* Enable's immediate-fire one — see `UserRowMenuHandlers.onRequestDelete`'s own doc comment.
|
|
95
|
+
*
|
|
96
|
+
* @complexity Time/space: O(1) — at most four entries, no iteration.
|
|
97
|
+
*/
|
|
98
|
+
export function userRowMenuItems({ user, toggleSaving, handlers, translate, canDelete, capabilities = canDelete }, _optional = {}) {
|
|
18
99
|
const items = [
|
|
19
100
|
{
|
|
20
101
|
key: "toggle",
|
|
@@ -43,7 +124,10 @@ export function userRowMenuItems({ user, toggleSaving, handlers, translate, canD
|
|
|
43
124
|
onSelect: () => handlers.onResetPassword(user),
|
|
44
125
|
},
|
|
45
126
|
];
|
|
46
|
-
|
|
127
|
+
const permissions = typeof capabilities === "boolean"
|
|
128
|
+
? { canDelete: capabilities, canDisable: true, canEnable: true, canResetPassword: true }
|
|
129
|
+
: capabilities;
|
|
130
|
+
if (permissions.canDelete) {
|
|
47
131
|
items.push({
|
|
48
132
|
key: "delete",
|
|
49
133
|
label: translate("Delete"),
|
|
@@ -51,11 +135,30 @@ export function userRowMenuItems({ user, toggleSaving, handlers, translate, canD
|
|
|
51
135
|
onSelect: () => handlers.onRequestDelete(user),
|
|
52
136
|
});
|
|
53
137
|
}
|
|
54
|
-
return items
|
|
138
|
+
return items.filter(item => item.key === "toggle"
|
|
139
|
+
? (user.status === "active" ? permissions.canDisable : permissions.canEnable)
|
|
140
|
+
: item.key !== "reset-password" || permissions.canResetPassword);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The role or policy names a user holds, for the "Roles"/"Policies" table cells — `null` when the
|
|
144
|
+
* user holds none, which the caller renders as the `muted-cell` "none" fallback (a plain ternary on
|
|
145
|
+
* this already-computed value, per this feature's extraction rule).
|
|
146
|
+
*
|
|
147
|
+
* A grant id with no matching entry in `byId` (a role/policy deleted out from under a still-held
|
|
148
|
+
* grant) falls back to rendering the raw id rather than dropping it silently — the operator sees
|
|
149
|
+
* something is wrong instead of an undercount with no explanation.
|
|
150
|
+
*
|
|
151
|
+
* @complexity Time: O(n) in the number of granted ids; space: O(n) for the joined string.
|
|
152
|
+
*/
|
|
153
|
+
export function grantOptionLabel(required, _optional = {}) {
|
|
154
|
+
return required.option.isBuiltin && required.option.name === "owner" ? required.translate("Owner") : required.option.name;
|
|
55
155
|
}
|
|
56
|
-
export function formatGrantLabel({ ids, byId }) {
|
|
156
|
+
export function formatGrantLabel({ ids, byId, translate = (key) => key }, _optional = {}) {
|
|
57
157
|
if (ids.length === 0)
|
|
58
158
|
return null;
|
|
59
|
-
return ids.map(
|
|
159
|
+
return ids.map(id => {
|
|
160
|
+
const role = byId.get(id);
|
|
161
|
+
return role?.isBuiltin && role.name === "owner" ? translate("Owner") : role?.name ?? id;
|
|
162
|
+
}).join(", ");
|
|
60
163
|
}
|
|
61
164
|
//# sourceMappingURL=rules.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/user-management",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Runtime-separated user management contracts and Node authentication, authorization, and administration services.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -86,13 +86,17 @@
|
|
|
86
86
|
"access": "public",
|
|
87
87
|
"registry": "https://registry.npmjs.org"
|
|
88
88
|
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@jini-ai/core": "^0.4.2"
|
|
91
|
+
},
|
|
89
92
|
"peerDependencies": {
|
|
90
93
|
"argon2": "^0.44.0",
|
|
91
94
|
"react": "^18.3.0 || ^19.0.0",
|
|
92
95
|
"react-dom": "^18.3.0 || ^19.0.0",
|
|
93
96
|
"@jini-ai/ui": "^0.4.0",
|
|
94
97
|
"@jini-ai/admin": "^0.4.0",
|
|
95
|
-
"@jini-ai/agentic": "^0.4.0"
|
|
98
|
+
"@jini-ai/agentic": "^0.4.0",
|
|
99
|
+
"@jini-ai/ui-kit": "^0.1.1"
|
|
96
100
|
},
|
|
97
101
|
"peerDependenciesMeta": {
|
|
98
102
|
"argon2": {
|
|
@@ -112,6 +116,9 @@
|
|
|
112
116
|
},
|
|
113
117
|
"@jini-ai/agentic": {
|
|
114
118
|
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"@jini-ai/ui-kit": {
|
|
121
|
+
"optional": true
|
|
115
122
|
}
|
|
116
123
|
},
|
|
117
124
|
"devDependencies": {
|
|
@@ -127,14 +134,12 @@
|
|
|
127
134
|
"@testing-library/user-event": "^14.5.0",
|
|
128
135
|
"@testing-library/jest-dom": "^6.6.3",
|
|
129
136
|
"jsdom": "^25.0.1",
|
|
130
|
-
"@jini-ai/ui": "0.4.
|
|
131
|
-
"@jini-ai/
|
|
132
|
-
"@jini-ai/
|
|
133
|
-
"@jini-ai/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"@jini-ai/ui-kit": "^0.1.1",
|
|
137
|
-
"@jini-ai/core": "^0.4.1"
|
|
137
|
+
"@jini-ai/ui": "0.4.5",
|
|
138
|
+
"@jini-ai/cms": "0.5.3",
|
|
139
|
+
"@jini-ai/core": "^0.4.2",
|
|
140
|
+
"@jini-ai/ui-kit": "^0.1.2",
|
|
141
|
+
"@jini-ai/agentic": "0.4.3",
|
|
142
|
+
"@jini-ai/admin": "0.4.3"
|
|
138
143
|
},
|
|
139
144
|
"scripts": {
|
|
140
145
|
"build": "tsc -p tsconfig.json",
|