@svadmin/core 0.17.1 → 0.18.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/package.json +1 -1
- package/src/adapters/casbin.ts +13 -6
- package/src/adapters/casl.ts +15 -6
- 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/package.json
CHANGED
package/src/adapters/casbin.ts
CHANGED
|
@@ -48,13 +48,20 @@ export function createCasbinAccessControl(
|
|
|
48
48
|
options: CasbinAdapterOptions,
|
|
49
49
|
): AccessControlProvider {
|
|
50
50
|
return {
|
|
51
|
-
can: async (
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
can: async (params: CanParams | CanParams[]): Promise<CanResult | CanResult[]> => {
|
|
52
|
+
const executeCheck = async ({ resource, action }: CanParams) => {
|
|
53
|
+
const user = options.getUser();
|
|
54
|
+
const allowed = await enforcer.enforce(user, resource, action);
|
|
55
|
+
return {
|
|
56
|
+
can: allowed,
|
|
57
|
+
reason: allowed ? undefined : `User "${user}" cannot "${action}" on "${resource}"`,
|
|
58
|
+
};
|
|
57
59
|
};
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(params)) {
|
|
62
|
+
return Promise.all(params.map(p => executeCheck(p)));
|
|
63
|
+
}
|
|
64
|
+
return executeCheck(params);
|
|
58
65
|
},
|
|
59
66
|
options: options.providerOptions,
|
|
60
67
|
};
|
package/src/adapters/casl.ts
CHANGED
|
@@ -38,13 +38,22 @@ export function createCaslAccessControl(
|
|
|
38
38
|
options?: AccessControlProvider['options'],
|
|
39
39
|
): AccessControlProvider {
|
|
40
40
|
return {
|
|
41
|
-
can: async (
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
can: async (params: CanParams | CanParams[]): Promise<CanResult | CanResult[]> => {
|
|
42
|
+
const executeCheck = async ({ resource, action, params: actionParams }: CanParams) => {
|
|
43
|
+
const field = actionParams?.field as string | undefined;
|
|
44
|
+
// CaslAbility's can only accepts string for subject based on its internal definition in this file
|
|
45
|
+
// To support object subjects when CanParams might pass them, cast it to any here to satisfy the local interface
|
|
46
|
+
const allowed = ability.can(action as string, resource as any, field);
|
|
47
|
+
return {
|
|
48
|
+
can: allowed,
|
|
49
|
+
reason: allowed ? undefined : `Cannot "${action}" on "${resource}"${field ? ` (field: ${field})` : ''}`,
|
|
50
|
+
};
|
|
47
51
|
};
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(params)) {
|
|
54
|
+
return Promise.all(params.map(p => executeCheck(p)));
|
|
55
|
+
}
|
|
56
|
+
return executeCheck(params);
|
|
48
57
|
},
|
|
49
58
|
options,
|
|
50
59
|
};
|
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
|
}
|