@svadmin/core 0.17.0 → 0.18.0
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/package.json +24 -24
- package/src/README.md +18 -0
- package/src/auth-hooks.svelte.ts +35 -62
- package/src/index.ts +1 -1
- package/src/permissions.test.ts +5 -5
- package/src/permissions.ts +17 -5
- package/src/toast.svelte.ts +3 -3
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"author": "zuohuadong",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/zuohuadong/svadmin.git",
|
|
8
|
+
"directory": "packages/core"
|
|
9
|
+
},
|
|
10
10
|
"main": "src/index.ts",
|
|
11
|
-
"
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "^1.3.11"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"svelte": "^5.0.0",
|
|
16
|
+
"@tanstack/svelte-query": "^6.0.0"
|
|
17
|
+
},
|
|
12
18
|
"exports": {
|
|
13
19
|
".": {
|
|
14
20
|
"types": "./src/index.ts",
|
|
@@ -43,25 +49,19 @@
|
|
|
43
49
|
"default": "./src/theme.svelte.ts"
|
|
44
50
|
}
|
|
45
51
|
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"svelte": "^5.0.0",
|
|
48
|
-
"@tanstack/svelte-query": "^6.0.0"
|
|
49
|
-
},
|
|
50
|
-
"license": "MIT",
|
|
51
|
-
"author": "zuohuadong",
|
|
52
|
-
"repository": {
|
|
53
|
-
"type": "git",
|
|
54
|
-
"url": "https://github.com/zuohuadong/svadmin.git",
|
|
55
|
-
"directory": "packages/core"
|
|
56
|
-
},
|
|
57
|
-
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
58
52
|
"bugs": {
|
|
59
53
|
"url": "https://github.com/zuohuadong/svadmin/issues"
|
|
60
54
|
},
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
"description": "Core SDK — hooks, types, context, i18n, permissions, router",
|
|
56
|
+
"files": [
|
|
57
|
+
"src/**/*"
|
|
58
|
+
],
|
|
59
|
+
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"sideEffects": false,
|
|
62
|
+
"type": "module",
|
|
63
|
+
"types": "src/index.ts",
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"svelte
|
|
65
|
+
"sonner-svelte": "^0.3.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/README.md
ADDED
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -266,17 +266,28 @@ export function useOnError() {
|
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
268
|
* Fetches permissions from authProvider.getPermissions().
|
|
269
|
-
* Returns a reactive object
|
|
269
|
+
* Returns a reactive integrated object.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```svelte
|
|
273
|
+
* <script>
|
|
274
|
+
* import { usePermissions } from '@svadmin/core';
|
|
275
|
+
* const perms = usePermissions();
|
|
276
|
+
* const canAdmin = $derived(perms.has('admin'));
|
|
277
|
+
* </script>
|
|
278
|
+
* ```
|
|
270
279
|
*/
|
|
271
280
|
export function usePermissions<T = unknown>() {
|
|
272
281
|
const provider = getAuthProvider();
|
|
273
|
-
let
|
|
282
|
+
let permissions = $state<T | null>(null);
|
|
274
283
|
let isLoading = $state(true);
|
|
275
284
|
let error = $state<Error | null>(null);
|
|
285
|
+
let version = $state(0); // Force reactive invalidation
|
|
276
286
|
|
|
277
287
|
if (provider?.getPermissions) {
|
|
278
|
-
provider.getPermissions().then(
|
|
279
|
-
|
|
288
|
+
provider.getPermissions().then(p => {
|
|
289
|
+
permissions = p as T;
|
|
290
|
+
version++;
|
|
280
291
|
isLoading = false;
|
|
281
292
|
}).catch(err => {
|
|
282
293
|
error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -288,66 +299,28 @@ export function usePermissions<T = unknown>() {
|
|
|
288
299
|
}
|
|
289
300
|
|
|
290
301
|
return {
|
|
291
|
-
get data() { return data; },
|
|
292
302
|
get isLoading() { return isLoading; },
|
|
293
303
|
get error() { return error; },
|
|
304
|
+
get version() { return version; },
|
|
305
|
+
|
|
306
|
+
/** Get the raw permissions data */
|
|
307
|
+
get raw() { return permissions; },
|
|
308
|
+
|
|
309
|
+
/** Convenience: check if a specific permission exists */
|
|
310
|
+
has(perm: string): boolean {
|
|
311
|
+
if (!permissions) return false;
|
|
312
|
+
if (Array.isArray(permissions)) return permissions.includes(perm);
|
|
313
|
+
if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
|
|
314
|
+
if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
|
|
315
|
+
return false;
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/** Convenience: check resource action (Casbin style representation if applicable) */
|
|
319
|
+
can(resource: string, action: string): boolean {
|
|
320
|
+
// Tries to map to a standard permission string format like "resource:action"
|
|
321
|
+
// Replace with your own logic if your permissions are structured differently.
|
|
322
|
+
return this.has(`${resource}:${action}`);
|
|
323
|
+
}
|
|
294
324
|
};
|
|
295
325
|
}
|
|
296
326
|
|
|
297
|
-
// ─── useHasPermission ────────────────────────────────────────
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Reactive permission checker — returns a closure that re-evaluates
|
|
301
|
-
* when the underlying permissions data changes.
|
|
302
|
-
*
|
|
303
|
-
* Works with Casbin-style permission arrays, Sets, or permission objects.
|
|
304
|
-
* The returned function is safe to use inside `$derived()` for reactive UI updates.
|
|
305
|
-
*
|
|
306
|
-
* @example
|
|
307
|
-
* ```svelte
|
|
308
|
-
* <script>
|
|
309
|
-
* import { useHasPermission } from '@svadmin/core';
|
|
310
|
-
*
|
|
311
|
-
* const hasPermission = useHasPermission();
|
|
312
|
-
* // ✅ Reactive — UI updates when permissions finish loading
|
|
313
|
-
* const canDelete = $derived(hasPermission('admin'));
|
|
314
|
-
* const canExport = $derived(hasPermission('data:export'));
|
|
315
|
-
* </script>
|
|
316
|
-
*
|
|
317
|
-
* {#if canDelete}
|
|
318
|
-
* <Button variant="destructive">Delete</Button>
|
|
319
|
-
* {/if}
|
|
320
|
-
* ```
|
|
321
|
-
*/
|
|
322
|
-
export function useHasPermission() {
|
|
323
|
-
const provider = getAuthProvider();
|
|
324
|
-
let permissions = $state<unknown>(null);
|
|
325
|
-
let loaded = $state(false);
|
|
326
|
-
|
|
327
|
-
if (provider?.getPermissions) {
|
|
328
|
-
provider.getPermissions().then(p => {
|
|
329
|
-
permissions = p;
|
|
330
|
-
loaded = true;
|
|
331
|
-
}).catch(err => {
|
|
332
|
-
console.warn('[svadmin] useHasPermission: getPermissions failed', err);
|
|
333
|
-
loaded = true;
|
|
334
|
-
});
|
|
335
|
-
} else {
|
|
336
|
-
loaded = true;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Check if the current user has a specific permission.
|
|
341
|
-
* Supports: `string[]`, `Set<string>`, `Record<string, boolean>`, or any object with `.includes` / `.has`.
|
|
342
|
-
*/
|
|
343
|
-
function hasPermission(perm: string): boolean {
|
|
344
|
-
if (!loaded || permissions == null) return false;
|
|
345
|
-
if (Array.isArray(permissions)) return permissions.includes(perm);
|
|
346
|
-
if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
|
|
347
|
-
if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return hasPermission;
|
|
352
|
-
}
|
|
353
|
-
|
package/src/index.ts
CHANGED
|
@@ -78,7 +78,7 @@ export {
|
|
|
78
78
|
useLogin, useLogout,
|
|
79
79
|
useRegister, useForgotPassword, useUpdatePassword,
|
|
80
80
|
useGetIdentity, useIsAuthenticated,
|
|
81
|
-
useOnError, usePermissions,
|
|
81
|
+
useOnError, usePermissions,
|
|
82
82
|
} from './auth-hooks.svelte';
|
|
83
83
|
export { useParsed } from './useParsed.svelte';
|
|
84
84
|
export * from './useStepsForm.svelte';
|
package/src/permissions.test.ts
CHANGED
|
@@ -27,7 +27,7 @@ describe('permissions', () => {
|
|
|
27
27
|
can: async () => ({ can: true }),
|
|
28
28
|
});
|
|
29
29
|
const result = await canAccessAsync('posts', 'edit');
|
|
30
|
-
expect(result.can).toBe(true);
|
|
30
|
+
expect((result as any).can).toBe(true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('canAccessAsync resolves with denied status', async () => {
|
|
@@ -35,8 +35,8 @@ describe('permissions', () => {
|
|
|
35
35
|
can: async () => ({ can: false, reason: 'No permission' }),
|
|
36
36
|
});
|
|
37
37
|
const result = await canAccessAsync('users', 'delete');
|
|
38
|
-
expect(result.can).toBe(false);
|
|
39
|
-
expect(result.reason).toBe('No permission');
|
|
38
|
+
expect((result as any).can).toBe(false);
|
|
39
|
+
expect((result as any).reason).toBe('No permission');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('resource-specific rules', async () => {
|
|
@@ -89,8 +89,8 @@ describe('AccessControlProvider', () => {
|
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
91
|
const result = await canAccessAsync('posts', 'edit', { id: 42 });
|
|
92
|
-
expect(result.can).toBe(false);
|
|
93
|
-
expect(result.reason).toBe('Record locked');
|
|
92
|
+
expect((result as any).can).toBe(false);
|
|
93
|
+
expect((result as any).reason).toBe('Record locked');
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test('extended action types work', async () => {
|
package/src/permissions.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface CanResult {
|
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
export interface AccessControlProvider {
|
|
41
|
-
can: (params: CanParams) => Promise<CanResult>;
|
|
41
|
+
can: (params: CanParams | CanParams[]) => Promise<CanResult | CanResult[]>;
|
|
42
42
|
options?: {
|
|
43
43
|
buttons?: {
|
|
44
44
|
/** Enable access control checks on CRUD buttons globally. Default: false */
|
|
@@ -73,9 +73,21 @@ export function getAccessControlOptions() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Async access check.
|
|
76
|
+
* Async access check.
|
|
77
|
+
* Supports both single capability requests or an array of batched checks.
|
|
77
78
|
*/
|
|
78
|
-
export async function canAccessAsync(
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
export async function canAccessAsync(params: CanParams[]): Promise<CanResult[]>;
|
|
80
|
+
export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>): Promise<CanResult>;
|
|
81
|
+
export async function canAccessAsync(resourceOrBatch: string | CanParams[], action?: Action, params?: Record<string, unknown>): Promise<CanResult | CanResult[]> {
|
|
82
|
+
if (!provider) {
|
|
83
|
+
return Array.isArray(resourceOrBatch)
|
|
84
|
+
? resourceOrBatch.map(() => ({ can: true }))
|
|
85
|
+
: { can: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(resourceOrBatch)) {
|
|
89
|
+
return provider.can(resourceOrBatch);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return provider.can({ resource: resourceOrBatch, action: action!, params }) as Promise<CanResult>;
|
|
81
93
|
}
|
package/src/toast.svelte.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Toast notification system — wraps svelte
|
|
1
|
+
// Toast notification system — wraps sonner-svelte for consistent API
|
|
2
2
|
// Maintains backward-compatible `toast.success()` / `toast.error()` etc.
|
|
3
3
|
|
|
4
|
-
import { toast as sonner } from 'svelte
|
|
4
|
+
import { toast as sonner } from 'sonner-svelte';
|
|
5
5
|
|
|
6
6
|
export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
|
|
7
7
|
|
|
@@ -23,7 +23,7 @@ export function getToasts(): Toast[] {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function addToast(type: ToastType, message: string, duration = 3000, options?: { onUndo?: () => void, onTimeout?: () => void }): void {
|
|
26
|
-
// Use svelte
|
|
26
|
+
// Use sonner-svelte for regular toasts
|
|
27
27
|
if (type !== 'undoable') {
|
|
28
28
|
switch (type) {
|
|
29
29
|
case 'success': sonner.success(message, { duration }); break;
|