kmod-cli 1.6.0 → 1.7.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/README.md
CHANGED
|
@@ -41,64 +41,66 @@ npx kumod add button # add button component
|
|
|
41
41
|
## Components
|
|
42
42
|
|
|
43
43
|
- access-denied
|
|
44
|
+
- api-service
|
|
44
45
|
- breadcumb
|
|
45
|
-
- count-down
|
|
46
|
-
- count-input
|
|
47
46
|
- button
|
|
47
|
+
- calculate
|
|
48
48
|
- calendar
|
|
49
|
+
- color-by-text
|
|
50
|
+
- column-table
|
|
51
|
+
- config
|
|
52
|
+
- count-down
|
|
53
|
+
- count-input
|
|
54
|
+
- data-table
|
|
49
55
|
- date-input
|
|
50
56
|
- date-range-picker
|
|
51
|
-
- label
|
|
52
|
-
- popover
|
|
53
|
-
- select
|
|
54
|
-
- switch
|
|
55
57
|
- datetime-picker
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
- time-picker-utils
|
|
60
|
-
- time-picker
|
|
58
|
+
- fade-on-scroll
|
|
59
|
+
- feature-config
|
|
60
|
+
- feature-guard
|
|
61
61
|
- gradient-outline
|
|
62
62
|
- gradient-svg
|
|
63
63
|
- grid-layout
|
|
64
|
+
- hash-aes
|
|
64
65
|
- hydrate-guard
|
|
66
|
+
- idb
|
|
65
67
|
- image
|
|
68
|
+
- input
|
|
69
|
+
- keys
|
|
70
|
+
- kookies
|
|
71
|
+
- label
|
|
72
|
+
- lib
|
|
73
|
+
- list-map
|
|
66
74
|
- loader-slash-gradient
|
|
67
75
|
- masonry-gallery
|
|
68
76
|
- modal
|
|
69
77
|
- multi-select
|
|
70
78
|
- non-hydration
|
|
79
|
+
- period-input
|
|
80
|
+
- popover
|
|
71
81
|
- portal
|
|
82
|
+
- query
|
|
83
|
+
- rbac
|
|
84
|
+
- readme
|
|
85
|
+
- refine-provider
|
|
86
|
+
- safe-action
|
|
72
87
|
- segments-circle
|
|
88
|
+
- select
|
|
89
|
+
- simple-validate
|
|
73
90
|
- single-select
|
|
91
|
+
- spam-guard
|
|
92
|
+
- storage
|
|
93
|
+
- stripe-effect
|
|
74
94
|
- stroke-circle
|
|
75
|
-
-
|
|
76
|
-
- data-table
|
|
77
|
-
- readme
|
|
95
|
+
- switch
|
|
78
96
|
- table
|
|
79
97
|
- text-hover-effect
|
|
98
|
+
- time-picker
|
|
99
|
+
- time-picker-input
|
|
100
|
+
- time-picker-utils
|
|
80
101
|
- timout-loader
|
|
81
102
|
- toast
|
|
82
|
-
- config
|
|
83
|
-
- feature-config
|
|
84
|
-
- keys
|
|
85
|
-
- api-service
|
|
86
|
-
- calculate
|
|
87
|
-
- idb
|
|
88
|
-
- lib
|
|
89
|
-
- storage
|
|
90
|
-
- fade-on-scroll
|
|
91
|
-
- safe-action
|
|
92
|
-
- spam-guard
|
|
93
103
|
- utils
|
|
94
|
-
- feature-guard
|
|
95
|
-
- refine-provider
|
|
96
|
-
- query
|
|
97
|
-
- color-by-text
|
|
98
|
-
- stripe-effect
|
|
99
|
-
- kookies
|
|
100
|
-
- hash-aes
|
|
101
|
-
- simple-validate
|
|
102
104
|
## Contributing
|
|
103
105
|
|
|
104
106
|
Contributions are welcome! Please open issues or pull requests.
|
|
@@ -13,10 +13,13 @@ const componentsConfig = JSON.parse(
|
|
|
13
13
|
fs.readFileSync(componentsPath, 'utf8')
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
+
// sort a-z
|
|
17
|
+
const componentSorted = Object.keys(componentsConfig).sort(
|
|
18
|
+
(a, b) => a.localeCompare(b)
|
|
19
|
+
)
|
|
20
|
+
|
|
16
21
|
// build list
|
|
17
|
-
const componentList =
|
|
18
|
-
.map((k) => `- ${k}`)
|
|
19
|
-
.join('\n');
|
|
22
|
+
const componentList = componentSorted.map((k) => `- ${k}`).join('\n');
|
|
20
23
|
|
|
21
24
|
// đọc README
|
|
22
25
|
const readmeContent = fs.readFileSync(readmePath, 'utf8');
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
10
|
|
|
11
|
+
// Nếu bạn cần alias cho ITable type, dùng:
|
|
11
12
|
import type { Table as ITable } from '@tanstack/react-table';
|
|
12
13
|
import {
|
|
13
14
|
Cell,
|
|
@@ -90,9 +91,11 @@ export type TableCellProps<TData, TValue> =
|
|
|
90
91
|
e,
|
|
91
92
|
table,
|
|
92
93
|
cell,
|
|
94
|
+
row
|
|
93
95
|
}: {
|
|
94
96
|
e: React.MouseEvent<HTMLTableCellElement>;
|
|
95
97
|
cell: Cell<TData, TValue>;
|
|
98
|
+
row: Row<TData>;
|
|
96
99
|
table: ITable<TData>;
|
|
97
100
|
}) => void;
|
|
98
101
|
};
|
|
@@ -365,14 +368,22 @@ export function DataTable<TData, TValue>({
|
|
|
365
368
|
classNames?.header?.head
|
|
366
369
|
)}
|
|
367
370
|
style={{
|
|
368
|
-
width: header.getSize()
|
|
371
|
+
width: header.getSize()
|
|
372
|
+
? `${header.getSize()}px !important`
|
|
373
|
+
: "auto",
|
|
369
374
|
}}
|
|
370
375
|
onClick={(e) => {
|
|
371
376
|
cellHeadOnClick?.(e);
|
|
372
377
|
cellHeadHandleClick?.({ e, table, cell: header });
|
|
373
378
|
}}
|
|
374
379
|
>
|
|
375
|
-
<div
|
|
380
|
+
<div
|
|
381
|
+
className={cn(
|
|
382
|
+
"flex items-center gap-1 w-fit",
|
|
383
|
+
classNames?.header?.content,
|
|
384
|
+
getCellHeadClassNameByCondition({ cell: header, table })
|
|
385
|
+
)}
|
|
386
|
+
>
|
|
376
387
|
{flexRender(
|
|
377
388
|
header.column.columnDef.header,
|
|
378
389
|
header.getContext()
|
|
@@ -407,13 +418,18 @@ export function DataTable<TData, TValue>({
|
|
|
407
418
|
{!isLoading &&
|
|
408
419
|
table.getRowModel().rows.length > 0 &&
|
|
409
420
|
table.getRowModel().rows.map((row, index) => {
|
|
410
|
-
|
|
411
421
|
return (
|
|
412
422
|
<TableRow
|
|
413
423
|
{...rowBodyDomProps}
|
|
414
424
|
key={row.id}
|
|
415
|
-
style={{
|
|
416
|
-
|
|
425
|
+
style={{
|
|
426
|
+
...rowBodyStyle,
|
|
427
|
+
backgroundColor: getAlternateColor(index),
|
|
428
|
+
}}
|
|
429
|
+
className={cn(
|
|
430
|
+
classNames?.body?.row,
|
|
431
|
+
getRowBodyClassNameByCondition({ row, table })
|
|
432
|
+
)}
|
|
417
433
|
data-state={row.getIsSelected() && "selected"}
|
|
418
434
|
onClick={(e) => {
|
|
419
435
|
rowBodyOnClick?.(e);
|
|
@@ -424,10 +440,13 @@ export function DataTable<TData, TValue>({
|
|
|
424
440
|
<TableCell
|
|
425
441
|
{...cellBodyDomProps}
|
|
426
442
|
key={cell.id}
|
|
427
|
-
className={cn(
|
|
443
|
+
className={cn(
|
|
444
|
+
classNames?.body?.cell,
|
|
445
|
+
getCellBodyClassNameByCondition({ cell, table })
|
|
446
|
+
)}
|
|
428
447
|
onClick={(e) => {
|
|
429
448
|
cellBodyOnClick?.(e);
|
|
430
|
-
cellBodyHandleClick?.({ e, cell, table });
|
|
449
|
+
cellBodyHandleClick?.({ e, cell, row, table });
|
|
431
450
|
}}
|
|
432
451
|
>
|
|
433
452
|
{flexRender(
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
|
|
2
|
+
/* ============================================================
|
|
3
|
+
RBAC FINAL – BACKEND + FRONTEND – SINGLE FILE
|
|
4
|
+
============================================================ */
|
|
5
|
+
|
|
6
|
+
/* =======================
|
|
7
|
+
REACT HELPERS (OPTIONAL)
|
|
8
|
+
======================= */
|
|
9
|
+
import React, {
|
|
10
|
+
createContext,
|
|
11
|
+
useContext,
|
|
12
|
+
} from 'react';
|
|
13
|
+
|
|
14
|
+
/* =======================
|
|
15
|
+
SHARED CONCEPT
|
|
16
|
+
======================= */
|
|
17
|
+
export type ID = string;
|
|
18
|
+
/**
|
|
19
|
+
* @example "read"
|
|
20
|
+
*/
|
|
21
|
+
export type Action = string;
|
|
22
|
+
/**
|
|
23
|
+
* @description Resource is route name
|
|
24
|
+
* @example "user"
|
|
25
|
+
*/
|
|
26
|
+
export type Resource = string;
|
|
27
|
+
/**
|
|
28
|
+
* @example "user:read"
|
|
29
|
+
*/
|
|
30
|
+
export type PermissionKey = `${Resource}:${Action}`;
|
|
31
|
+
|
|
32
|
+
/* =======================
|
|
33
|
+
BACKEND RBAC (AUTHORITATIVE)
|
|
34
|
+
======================= */
|
|
35
|
+
|
|
36
|
+
export interface Permission {
|
|
37
|
+
id: ID;
|
|
38
|
+
action: Action;
|
|
39
|
+
resource: Resource;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Role {
|
|
43
|
+
id: ID;
|
|
44
|
+
name: string;
|
|
45
|
+
permissions: ID[];
|
|
46
|
+
inherits?: ID[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ConditionOperator = 'eq' | 'ne' | 'in';
|
|
50
|
+
|
|
51
|
+
export interface Condition {
|
|
52
|
+
field: string; // "user.id", "resource.ownerId", "user.tenantId"
|
|
53
|
+
op: ConditionOperator;
|
|
54
|
+
value: any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Policy {
|
|
58
|
+
id: ID;
|
|
59
|
+
permissionId: ID;
|
|
60
|
+
effect: 'allow' | 'deny';
|
|
61
|
+
conditions?: Condition[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UserIdentity {
|
|
65
|
+
id: ID;
|
|
66
|
+
roles: ID[];
|
|
67
|
+
tenantId?: ID;
|
|
68
|
+
[k: string]: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* =======================
|
|
72
|
+
RBAC STORE INTERFACE
|
|
73
|
+
======================= */
|
|
74
|
+
export interface RBACStore {
|
|
75
|
+
getRoles(ids: ID[]): Promise<Role[]>;
|
|
76
|
+
getPermissionId(action: Action, resource: Resource): Promise<ID | null>;
|
|
77
|
+
getPolicies(permissionId: ID): Promise<Policy[]>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* =======================
|
|
81
|
+
BACKEND RBAC ENGINE
|
|
82
|
+
======================= */
|
|
83
|
+
|
|
84
|
+
const getByPath = (obj: any, path: string) =>
|
|
85
|
+
path.split('.').reduce((o, k) => o?.[k], obj);
|
|
86
|
+
|
|
87
|
+
const evalCondition = (cond: Condition, ctx: any) => {
|
|
88
|
+
const left = getByPath(ctx, cond.field);
|
|
89
|
+
if (cond.op === 'eq') return left === cond.value;
|
|
90
|
+
if (cond.op === 'ne') return left !== cond.value;
|
|
91
|
+
if (cond.op === 'in') return Array.isArray(cond.value) && cond.value.includes(left);
|
|
92
|
+
return false;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const evalConditions = (conds: Condition[] = [], ctx: any) =>
|
|
96
|
+
conds.every(c => evalCondition(c, ctx));
|
|
97
|
+
|
|
98
|
+
export async function canAccess(
|
|
99
|
+
store: RBACStore,
|
|
100
|
+
user: UserIdentity,
|
|
101
|
+
action: Action,
|
|
102
|
+
resource: Resource,
|
|
103
|
+
context: {
|
|
104
|
+
resource?: any;
|
|
105
|
+
request?: any;
|
|
106
|
+
} = {}
|
|
107
|
+
): Promise<boolean> {
|
|
108
|
+
const permissionId = await store.getPermissionId(action, resource);
|
|
109
|
+
if (!permissionId) return false;
|
|
110
|
+
|
|
111
|
+
/* ---- Resolve role inheritance ---- */
|
|
112
|
+
const visited = new Set<ID>();
|
|
113
|
+
const collect = async (roleId: ID) => {
|
|
114
|
+
if (visited.has(roleId)) return;
|
|
115
|
+
visited.add(roleId);
|
|
116
|
+
const [role] = await store.getRoles([roleId]);
|
|
117
|
+
for (const p of role?.inherits ?? []) await collect(p);
|
|
118
|
+
};
|
|
119
|
+
for (const r of user.roles) await collect(r);
|
|
120
|
+
|
|
121
|
+
const roles = await store.getRoles([...visited]);
|
|
122
|
+
const hasRoleGrant = roles.some(r => r.permissions.includes(permissionId));
|
|
123
|
+
|
|
124
|
+
/* ---- Evaluate policies ---- */
|
|
125
|
+
const policies = await store.getPolicies(permissionId);
|
|
126
|
+
const ctx = {
|
|
127
|
+
user,
|
|
128
|
+
resource: context.resource,
|
|
129
|
+
request: context.request,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (const p of policies) {
|
|
133
|
+
if (!evalConditions(p.conditions, ctx)) continue;
|
|
134
|
+
if (p.effect === 'deny') return false;
|
|
135
|
+
if (p.effect === 'allow') return hasRoleGrant;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return hasRoleGrant;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* =======================
|
|
142
|
+
BACKEND MIDDLEWARE
|
|
143
|
+
======================= */
|
|
144
|
+
|
|
145
|
+
export const requirePermission =
|
|
146
|
+
(store: RBACStore, action: Action, resource: Resource) =>
|
|
147
|
+
async (req: any, res: any, next: any) => {
|
|
148
|
+
if (!req.user) return res.status(401).end();
|
|
149
|
+
|
|
150
|
+
const ok = await canAccess(store, req.user, action, resource, {
|
|
151
|
+
resource: req.body ?? req.params,
|
|
152
|
+
request: req,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!ok) return res.status(403).end();
|
|
156
|
+
next();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/* =======================
|
|
160
|
+
MEMORY STORE (DEV / DEMO)
|
|
161
|
+
======================= */
|
|
162
|
+
|
|
163
|
+
export function createMemoryRBACStore(data: {
|
|
164
|
+
roles: Role[];
|
|
165
|
+
permissions: Permission[];
|
|
166
|
+
policies: Policy[];
|
|
167
|
+
}): RBACStore {
|
|
168
|
+
const roleMap = new Map(data.roles.map(r => [r.id, r]));
|
|
169
|
+
const permMap = new Map<PermissionKey, ID>();
|
|
170
|
+
const policyMap = new Map<ID, Policy[]>();
|
|
171
|
+
|
|
172
|
+
data.permissions.forEach(p =>
|
|
173
|
+
permMap.set(`${p.resource}:${p.action}`, p.id)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
data.policies.forEach(p => {
|
|
177
|
+
const arr = policyMap.get(p.permissionId) ?? [];
|
|
178
|
+
arr.push(p);
|
|
179
|
+
policyMap.set(p.permissionId, arr);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
getRoles: async ids => ids.map(id => roleMap.get(id)!).filter(Boolean),
|
|
184
|
+
getPermissionId: async (a, r) => permMap.get(`${r}:${a}`) ?? null,
|
|
185
|
+
getPolicies: async pid => policyMap.get(pid) ?? [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* =======================
|
|
190
|
+
FRONTEND RBAC (UI ONLY)
|
|
191
|
+
======================= */
|
|
192
|
+
|
|
193
|
+
export class FrontendRBAC {
|
|
194
|
+
private permissions: Set<PermissionKey>;
|
|
195
|
+
|
|
196
|
+
constructor(perms: PermissionKey[]) {
|
|
197
|
+
this.permissions = new Set(perms);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
can(action: Action, resource: Resource): boolean {
|
|
201
|
+
return this.permissions.has(`${resource}:${action}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const RBACContext = createContext<FrontendRBAC | null>(null);
|
|
206
|
+
|
|
207
|
+
export const RBACProvider = ({
|
|
208
|
+
rbac,
|
|
209
|
+
children,
|
|
210
|
+
}: {
|
|
211
|
+
rbac: FrontendRBAC;
|
|
212
|
+
children: React.ReactNode;
|
|
213
|
+
}) => (
|
|
214
|
+
<RBACContext.Provider value={rbac}>
|
|
215
|
+
{children}
|
|
216
|
+
</RBACContext.Provider>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
export const useCan = (action: Action, resource: Resource) => {
|
|
220
|
+
const rbac = useContext(RBACContext);
|
|
221
|
+
return rbac?.can(action, resource) ?? false;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const Can = ({
|
|
225
|
+
action,
|
|
226
|
+
resource,
|
|
227
|
+
children,
|
|
228
|
+
fallback = null,
|
|
229
|
+
}: any) => {
|
|
230
|
+
const ok = useCan(action, resource);
|
|
231
|
+
return ok ? children : fallback;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// fully usage fe
|
|
236
|
+
|
|
237
|
+
// const rbac = new FrontendRBAC(['user:read', 'user:update']);
|
|
238
|
+
// const store = createMemoryRBACStore({
|
|
239
|
+
// roles: [],
|
|
240
|
+
// permissions: [],
|
|
241
|
+
// policies: [],
|
|
242
|
+
// });
|
|
243
|
+
|
|
244
|
+
// <RBACProvider rbac={rbac}>
|
|
245
|
+
// <Can action="user:read" resource="user">
|
|
246
|
+
// <div>Can read user</div>
|
|
247
|
+
// </Can>
|
|
248
|
+
// </RBACProvider>
|
|
249
|
+
|
|
250
|
+
// fully usage be
|
|
251
|
+
|
|
252
|
+
// import { requirePermission } from './rbac';
|
|
253
|
+
// router.use(requirePermission(store, 'read', 'user'));
|
|
254
|
+
|
|
255
|
+
// app.use(requirePermission(store, 'read', 'user'));
|
package/components.json
CHANGED
|
@@ -147,6 +147,11 @@
|
|
|
147
147
|
"dependencies": [],
|
|
148
148
|
"devDependencies": []
|
|
149
149
|
},
|
|
150
|
+
"list-map": {
|
|
151
|
+
"path": "component-templates/components/list-map.tsx",
|
|
152
|
+
"dependencies": [],
|
|
153
|
+
"devDependencies": []
|
|
154
|
+
},
|
|
150
155
|
"loader-slash-gradient": {
|
|
151
156
|
"path": "component-templates/components/loader-slash-gradient.tsx",
|
|
152
157
|
"dependencies": [],
|
|
@@ -328,6 +333,11 @@
|
|
|
328
333
|
"dependencies": [],
|
|
329
334
|
"devDependencies": []
|
|
330
335
|
},
|
|
336
|
+
"rbac": {
|
|
337
|
+
"path": "component-templates/providers/rbac.tsx",
|
|
338
|
+
"dependencies": [],
|
|
339
|
+
"devDependencies": []
|
|
340
|
+
},
|
|
331
341
|
"refine-provider": {
|
|
332
342
|
"path": "component-templates/providers/refine-provider.tsx",
|
|
333
343
|
"dependencies": [
|
|
@@ -358,9 +368,7 @@
|
|
|
358
368
|
"dependencies": [
|
|
359
369
|
"js-cookie"
|
|
360
370
|
],
|
|
361
|
-
"devDependencies": [
|
|
362
|
-
"@types/js-cookie"
|
|
363
|
-
]
|
|
371
|
+
"devDependencies": []
|
|
364
372
|
},
|
|
365
373
|
"hash-aes": {
|
|
366
374
|
"path": "component-templates/utils/hash/hash-aes.ts",
|