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
- - input
57
- - period-input
58
- - time-picker-input
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
- - column-table
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 = Object.keys(componentsConfig)
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() ? `${header.getSize()}px !important` : "auto",
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 className={cn("flex items-center gap-1 w-fit", classNames?.header?.content, getCellHeadClassNameByCondition({cell: header, table}))}>
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={{...rowBodyStyle, backgroundColor: getAlternateColor(index)}}
416
- className={cn(classNames?.body?.row, getRowBodyClassNameByCondition({row,table}))}
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(classNames?.body?.cell, getCellBodyClassNameByCondition({cell, table}))}
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kmod-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Stack components utilities fast setup in projects",
5
5
  "author": "kumo_d",
6
6
  "license": "MIT",