@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,13 +48,20 @@ export function createCasbinAccessControl(
48
48
  options: CasbinAdapterOptions,
49
49
  ): AccessControlProvider {
50
50
  return {
51
- can: async ({ resource, action }: CanParams): Promise<CanResult> => {
52
- const user = options.getUser();
53
- const allowed = await enforcer.enforce(user, resource, action);
54
- return {
55
- can: allowed,
56
- reason: allowed ? undefined : `User "${user}" cannot "${action}" on "${resource}"`,
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
  };
@@ -38,13 +38,22 @@ export function createCaslAccessControl(
38
38
  options?: AccessControlProvider['options'],
39
39
  ): AccessControlProvider {
40
40
  return {
41
- can: async ({ resource, action, params }: CanParams): Promise<CanResult> => {
42
- const field = params?.field as string | undefined;
43
- const allowed = ability.can(action, resource, field);
44
- return {
45
- can: allowed,
46
- reason: allowed ? undefined : `Cannot "${action}" on "${resource}"${field ? ` (field: ${field})` : ''}`,
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
  };
@@ -266,17 +266,28 @@ export function useOnError() {
266
266
 
267
267
  /**
268
268
  * Fetches permissions from authProvider.getPermissions().
269
- * Returns a reactive object with data, isLoading, and error.
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 data = $state<T | null>(null);
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(permissions => {
279
- data = permissions as T;
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, useHasPermission,
81
+ useOnError, usePermissions,
82
82
  } from './auth-hooks.svelte';
83
83
  export { useParsed } from './useParsed.svelte';
84
84
  export * from './useStepsForm.svelte';
@@ -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 () => {
@@ -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. Returns `{ can: true }` if no provider is registered.
76
+ * Async access check.
77
+ * Supports both single capability requests or an array of batched checks.
77
78
  */
78
- export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>): Promise<CanResult> {
79
- if (!provider) return { can: true };
80
- return provider.can({ resource, action, params });
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
  }