qdadm 0.36.0 → 0.38.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/README.md +27 -174
- package/package.json +2 -1
- package/src/auth/SessionAuthAdapter.js +114 -3
- package/src/components/editors/PermissionEditor.vue +535 -0
- package/src/components/forms/FormField.vue +1 -11
- package/src/components/index.js +1 -0
- package/src/components/layout/AppLayout.vue +20 -8
- package/src/components/layout/defaults/DefaultToaster.vue +3 -3
- package/src/components/pages/LoginPage.vue +26 -5
- package/src/composables/useCurrentEntity.js +26 -17
- package/src/composables/useForm.js +7 -0
- package/src/composables/useFormPageBuilder.js +7 -0
- package/src/composables/useNavContext.js +30 -16
- package/src/core/index.js +0 -3
- package/src/debug/AuthCollector.js +175 -33
- package/src/debug/Collector.js +24 -2
- package/src/debug/EntitiesCollector.js +8 -0
- package/src/debug/SignalCollector.js +60 -2
- package/src/debug/components/panels/AuthPanel.vue +157 -27
- package/src/debug/components/panels/EntitiesPanel.vue +17 -1
- package/src/entity/EntityManager.js +183 -34
- package/src/entity/auth/EntityAuthAdapter.js +54 -46
- package/src/entity/auth/SecurityChecker.js +110 -42
- package/src/entity/auth/factory.js +11 -2
- package/src/entity/auth/factory.test.js +29 -0
- package/src/entity/storage/factory.test.js +6 -5
- package/src/index.js +3 -0
- package/src/kernel/Kernel.js +132 -21
- package/src/kernel/KernelContext.js +158 -0
- package/src/security/EntityRoleGranterAdapter.js +350 -0
- package/src/security/PermissionMatcher.js +148 -0
- package/src/security/PermissionRegistry.js +263 -0
- package/src/security/PersistableRoleGranterAdapter.js +618 -0
- package/src/security/RoleGranterAdapter.js +123 -0
- package/src/security/RoleGranterStorage.js +161 -0
- package/src/security/RolesManager.js +81 -0
- package/src/security/SecurityModule.js +73 -0
- package/src/security/StaticRoleGranterAdapter.js +114 -0
- package/src/security/UsersManager.js +122 -0
- package/src/security/index.js +45 -0
- package/src/security/pages/RoleForm.vue +212 -0
- package/src/security/pages/RoleList.vue +106 -0
- package/src/styles/main.css +62 -2
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EntityAuthAdapter -
|
|
2
|
+
* EntityAuthAdapter - Thin layer for entity-level permission checks
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides entity-level permission checking by delegating to SecurityChecker.
|
|
5
|
+
* All methods have sensible defaults - subclass only if you need custom behavior.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Default behavior (when SecurityChecker is configured):
|
|
8
|
+
* - canPerform() → isGranted('entity:{entity}:{action}')
|
|
9
|
+
* - canAccessRecord() → isGranted('entity:{entity}:read', record)
|
|
10
|
+
* - getCurrentUser() → uses callback if provided, null otherwise
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* Example: Can user see invoice #123? (ownership, team membership, etc.)
|
|
12
|
-
*
|
|
13
|
-
* Note: For user session authentication (login/logout), see SessionAuthAdapter.
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
12
|
+
* Usage patterns:
|
|
16
13
|
* ```js
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* }
|
|
22
|
-
*
|
|
23
|
-
* canAccessRecord(entity, record) {
|
|
24
|
-
* const user = this.getCurrentUser()
|
|
25
|
-
* return record.owner_id === user?.id || record.team_id === user?.team_id
|
|
26
|
-
* }
|
|
14
|
+
* // 1. Callback-based (simplest - no subclass needed)
|
|
15
|
+
* const adapter = new EntityAuthAdapter({
|
|
16
|
+
* getCurrentUser: () => myAuthStore.user
|
|
17
|
+
* })
|
|
27
18
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
19
|
+
* // 2. Subclass-based (for custom permission logic)
|
|
20
|
+
* class MyAdapter extends EntityAuthAdapter {
|
|
21
|
+
* canPerform(entity, action) {
|
|
22
|
+
* if (['orders', 'invoices'].includes(entity) && !this.getCurrentUser()) {
|
|
23
|
+
* return false
|
|
24
|
+
* }
|
|
25
|
+
* return super.canPerform(entity, action)
|
|
30
26
|
* }
|
|
31
27
|
* }
|
|
32
28
|
* ```
|
|
33
|
-
*
|
|
34
|
-
* @interface
|
|
35
29
|
*/
|
|
36
30
|
export class EntityAuthAdapter {
|
|
37
31
|
/**
|
|
@@ -41,6 +35,23 @@ export class EntityAuthAdapter {
|
|
|
41
35
|
*/
|
|
42
36
|
_securityChecker = null
|
|
43
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Callback to get current user (alternative to subclassing)
|
|
40
|
+
* @type {Function|null}
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
_getCurrentUserCallback = null
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @param {Function} [options.getCurrentUser] - Callback that returns current user or null
|
|
48
|
+
*/
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
if (options.getCurrentUser) {
|
|
51
|
+
this._getCurrentUserCallback = options.getCurrentUser
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/**
|
|
45
56
|
* Set the SecurityChecker instance for isGranted() delegation
|
|
46
57
|
*
|
|
@@ -62,7 +73,7 @@ export class EntityAuthAdapter {
|
|
|
62
73
|
*
|
|
63
74
|
* @example
|
|
64
75
|
* adapter.isGranted('ROLE_ADMIN') // Check role
|
|
65
|
-
* adapter.isGranted('entity:delete')
|
|
76
|
+
* adapter.isGranted('entity:books:delete') // Check permission
|
|
66
77
|
* adapter.isGranted('books:delete', book) // Check with subject
|
|
67
78
|
*/
|
|
68
79
|
isGranted(attribute, subject = null) {
|
|
@@ -73,8 +84,6 @@ export class EntityAuthAdapter {
|
|
|
73
84
|
/**
|
|
74
85
|
* Check if current user can assign a specific role
|
|
75
86
|
*
|
|
76
|
-
* Uses SecurityChecker's canAssignRole if available.
|
|
77
|
-
*
|
|
78
87
|
* @param {string} targetRole - Role to assign
|
|
79
88
|
* @returns {boolean} True if user can assign this role
|
|
80
89
|
*/
|
|
@@ -96,50 +105,49 @@ export class EntityAuthAdapter {
|
|
|
96
105
|
/**
|
|
97
106
|
* Check if the current user can perform an action on an entity type (scope check)
|
|
98
107
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* record they want to act on.
|
|
108
|
+
* Default: delegates to isGranted('entity:{entity}:{action}')
|
|
109
|
+
* Override for custom authentication requirements or business rules.
|
|
102
110
|
*
|
|
103
111
|
* @param {string} entity - Entity name (e.g., 'users', 'invoices', 'products')
|
|
104
112
|
* @param {string} action - Action to check: 'read', 'create', 'update', 'delete', 'list'
|
|
105
113
|
* @returns {boolean} True if user can perform the action on this entity type
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* adapter.canPerform('invoices', 'create') // true/false
|
|
109
|
-
* adapter.canPerform('users', 'delete') // true/false
|
|
110
114
|
*/
|
|
111
115
|
canPerform(entity, action) {
|
|
112
|
-
|
|
116
|
+
return this.isGranted(`entity:${entity}:${action}`)
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
120
|
* Check if the current user can access a specific record (silo check)
|
|
117
121
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* ownership, team membership, or other business rules.
|
|
122
|
+
* Default: delegates to isGranted('entity:{entity}:read', record)
|
|
123
|
+
* Note: Ownership checks are typically handled via EntityManager's isOwn callback.
|
|
121
124
|
*
|
|
122
125
|
* @param {string} entity - Entity name (e.g., 'users', 'invoices')
|
|
123
126
|
* @param {object} record - The full entity record to check access for
|
|
124
127
|
* @returns {boolean} True if user can access this specific record
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* adapter.canAccessRecord('invoices', { id: 123, owner_id: 456, ... })
|
|
128
128
|
*/
|
|
129
129
|
canAccessRecord(entity, record) {
|
|
130
|
-
|
|
130
|
+
return this.isGranted(`entity:${entity}:read`, record)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Get the current authenticated user
|
|
135
135
|
*
|
|
136
|
-
* Returns
|
|
137
|
-
*
|
|
136
|
+
* Returns user from:
|
|
137
|
+
* 1. _getCurrentUserCallback if provided in constructor
|
|
138
|
+
* 2. Subclass override if using class extension pattern
|
|
139
|
+
* 3. null if neither is configured
|
|
140
|
+
*
|
|
141
|
+
* Note: SecurityChecker calls this method, so it cannot delegate back to SecurityChecker.
|
|
138
142
|
*
|
|
139
143
|
* @returns {object|null} Current user object or null if not authenticated
|
|
140
144
|
*/
|
|
141
145
|
getCurrentUser() {
|
|
142
|
-
|
|
146
|
+
if (this._getCurrentUserCallback) {
|
|
147
|
+
return this._getCurrentUserCallback()
|
|
148
|
+
}
|
|
149
|
+
// Subclasses can override this method
|
|
150
|
+
return null
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
|
|
@@ -1,47 +1,107 @@
|
|
|
1
1
|
import { RoleHierarchy } from './RoleHierarchy.js'
|
|
2
|
+
import { PermissionMatcher } from '../../security/PermissionMatcher.js'
|
|
3
|
+
import { StaticRoleGranterAdapter } from '../../security/StaticRoleGranterAdapter.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* SecurityChecker - Symfony-inspired permission checking
|
|
5
7
|
*
|
|
6
8
|
* Provides the `isGranted(attribute, subject?)` contract for checking permissions.
|
|
7
|
-
* Supports both role checks (ROLE_*) and permission checks
|
|
9
|
+
* Supports both role checks (ROLE_*) and permission checks with wildcards.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Permission format: namespace:target:action
|
|
12
|
+
* - entity:books:read - Entity CRUD
|
|
13
|
+
* - auth:impersonate - System feature
|
|
14
|
+
* - admin:config:edit - Admin feature
|
|
15
|
+
*
|
|
16
|
+
* Wildcard patterns (like signals):
|
|
17
|
+
* - `*` matches exactly one segment
|
|
18
|
+
* - `**` matches zero or more segments (greedy)
|
|
13
19
|
*
|
|
14
20
|
* @example
|
|
15
|
-
* ```js
|
|
16
21
|
* const checker = new SecurityChecker({
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* roleGranter: new StaticRoleGranterAdapter({
|
|
23
|
+
* role_hierarchy: { ROLE_ADMIN: ['ROLE_USER'] },
|
|
24
|
+
* role_permissions: {
|
|
25
|
+
* ROLE_USER: ['entity:*:read', 'entity:*:list'],
|
|
26
|
+
* ROLE_ADMIN: ['entity:**', 'admin:**']
|
|
27
|
+
* }
|
|
28
|
+
* }),
|
|
22
29
|
* getCurrentUser: () => authStore.user
|
|
23
30
|
* })
|
|
24
31
|
*
|
|
25
|
-
* checker.isGranted('ROLE_ADMIN')
|
|
26
|
-
* checker.isGranted('entity:
|
|
27
|
-
* checker.isGranted('books:delete'
|
|
28
|
-
* ```
|
|
32
|
+
* checker.isGranted('ROLE_ADMIN') // Check role
|
|
33
|
+
* checker.isGranted('entity:books:read') // Check permission
|
|
34
|
+
* checker.isGranted('entity:books:delete') // Matches 'entity:**' for ADMIN
|
|
29
35
|
*/
|
|
30
36
|
export class SecurityChecker {
|
|
31
37
|
/**
|
|
32
38
|
* @param {Object} options
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {
|
|
39
|
+
* @param {RoleGranterAdapter} [options.roleGranter] - Role granter adapter
|
|
40
|
+
* @param {RoleHierarchy} [options.roleHierarchy] - Role hierarchy (legacy, prefer roleGranter)
|
|
41
|
+
* @param {Object<string, string[]>} [options.rolePermissions] - Permissions per role (legacy)
|
|
35
42
|
* @param {Function} options.getCurrentUser - Function returning current user or null
|
|
36
43
|
*/
|
|
37
|
-
constructor({ roleHierarchy, rolePermissions
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
constructor({ roleGranter, roleHierarchy, rolePermissions, getCurrentUser }) {
|
|
45
|
+
// Support both new roleGranter and legacy rolePermissions
|
|
46
|
+
if (roleGranter) {
|
|
47
|
+
this._roleGranter = roleGranter
|
|
48
|
+
// Note: roleHierarchy is now a getter that reads dynamically from roleGranter
|
|
49
|
+
// This ensures hierarchy changes after async load() are reflected
|
|
50
|
+
this._legacyRoleHierarchy = null
|
|
51
|
+
} else {
|
|
52
|
+
// Legacy: create static granter from rolePermissions
|
|
53
|
+
this._roleGranter = new StaticRoleGranterAdapter({
|
|
54
|
+
role_hierarchy: roleHierarchy instanceof RoleHierarchy
|
|
55
|
+
? {} // Can't extract map from RoleHierarchy, use empty
|
|
56
|
+
: (roleHierarchy || {}),
|
|
57
|
+
role_permissions: rolePermissions || {}
|
|
58
|
+
})
|
|
59
|
+
this._legacyRoleHierarchy = roleHierarchy instanceof RoleHierarchy
|
|
60
|
+
? roleHierarchy
|
|
61
|
+
: new RoleHierarchy(roleHierarchy || {})
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
this.getCurrentUser = getCurrentUser
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Get role hierarchy (dynamically resolved from roleGranter)
|
|
69
|
+
*
|
|
70
|
+
* This is a getter instead of a cached property to ensure that
|
|
71
|
+
* hierarchy changes after async load() are reflected immediately.
|
|
72
|
+
*
|
|
73
|
+
* @returns {RoleHierarchy}
|
|
74
|
+
*/
|
|
75
|
+
get roleHierarchy() {
|
|
76
|
+
// Legacy mode: use cached hierarchy
|
|
77
|
+
if (this._legacyRoleHierarchy) {
|
|
78
|
+
return this._legacyRoleHierarchy
|
|
79
|
+
}
|
|
80
|
+
// Dynamic mode: create fresh RoleHierarchy from current granter state
|
|
81
|
+
return new RoleHierarchy(this._roleGranter.getHierarchy())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get role granter adapter
|
|
86
|
+
* @returns {RoleGranterAdapter}
|
|
87
|
+
*/
|
|
88
|
+
get roleGranter() {
|
|
89
|
+
return this._roleGranter
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get role permissions (for backward compatibility / debug panel)
|
|
94
|
+
* @returns {Object<string, string[]>}
|
|
95
|
+
*/
|
|
96
|
+
get rolePermissions() {
|
|
97
|
+
const roles = this._roleGranter.getRoles()
|
|
98
|
+
const perms = {}
|
|
99
|
+
for (const role of roles) {
|
|
100
|
+
perms[role] = this._roleGranter.getPermissions(role)
|
|
101
|
+
}
|
|
102
|
+
return perms
|
|
103
|
+
}
|
|
104
|
+
|
|
45
105
|
/**
|
|
46
106
|
* Check if current user is granted an attribute (role or permission)
|
|
47
107
|
*
|
|
@@ -49,16 +109,16 @@ export class SecurityChecker {
|
|
|
49
109
|
*
|
|
50
110
|
* Checking flow:
|
|
51
111
|
* 1. If attribute starts with 'ROLE_' → check role hierarchy
|
|
52
|
-
* 2. Check if user has the permission (
|
|
112
|
+
* 2. Check if user has the permission (with wildcard support)
|
|
53
113
|
*
|
|
54
|
-
* @param {string} attribute - Role (ROLE_*) or permission (
|
|
114
|
+
* @param {string} attribute - Role (ROLE_*) or permission (namespace:target:action)
|
|
55
115
|
* @param {object} [subject] - Optional subject for context-aware checks (reserved for future use)
|
|
56
116
|
* @returns {boolean} True if user is granted the attribute
|
|
57
117
|
*
|
|
58
118
|
* @example
|
|
59
|
-
* checker.isGranted('ROLE_ADMIN')
|
|
60
|
-
* checker.isGranted('entity:
|
|
61
|
-
* checker.isGranted('books:delete', book)
|
|
119
|
+
* checker.isGranted('ROLE_ADMIN') // true/false
|
|
120
|
+
* checker.isGranted('entity:books:read') // true/false
|
|
121
|
+
* checker.isGranted('entity:books:delete', book) // true/false (with subject)
|
|
62
122
|
*/
|
|
63
123
|
isGranted(attribute, subject = null) {
|
|
64
124
|
const user = this.getCurrentUser()
|
|
@@ -72,12 +132,9 @@ export class SecurityChecker {
|
|
|
72
132
|
)
|
|
73
133
|
}
|
|
74
134
|
|
|
75
|
-
// 2. Check if it's a permission
|
|
135
|
+
// 2. Check if it's a permission (with wildcard support)
|
|
76
136
|
const userPerms = this.getUserPermissions(user)
|
|
77
|
-
|
|
78
|
-
if (userPerms.includes(attribute)) return true
|
|
79
|
-
|
|
80
|
-
return false
|
|
137
|
+
return PermissionMatcher.any(userPerms, attribute)
|
|
81
138
|
}
|
|
82
139
|
|
|
83
140
|
/**
|
|
@@ -85,11 +142,11 @@ export class SecurityChecker {
|
|
|
85
142
|
*
|
|
86
143
|
* Resolves permissions by:
|
|
87
144
|
* 1. Getting all reachable roles from role hierarchy
|
|
88
|
-
* 2. Collecting permissions from each role
|
|
145
|
+
* 2. Collecting permissions from each role via roleGranter
|
|
89
146
|
* 3. Adding any user-specific permission overrides
|
|
90
147
|
*
|
|
91
148
|
* @param {object} user - User object with role/roles and optional permissions
|
|
92
|
-
* @returns {string[]} Array of all permissions
|
|
149
|
+
* @returns {string[]} Array of all permissions (may include wildcards)
|
|
93
150
|
*/
|
|
94
151
|
getUserPermissions(user) {
|
|
95
152
|
const roles = user.roles || [user.role]
|
|
@@ -99,7 +156,7 @@ export class SecurityChecker {
|
|
|
99
156
|
if (!role) continue
|
|
100
157
|
const reachable = this.roleHierarchy.getReachableRoles(role)
|
|
101
158
|
for (const r of reachable) {
|
|
102
|
-
const rolePerms = this.
|
|
159
|
+
const rolePerms = this._roleGranter.getPermissions(r)
|
|
103
160
|
rolePerms.forEach(p => perms.add(p))
|
|
104
161
|
}
|
|
105
162
|
}
|
|
@@ -115,14 +172,14 @@ export class SecurityChecker {
|
|
|
115
172
|
/**
|
|
116
173
|
* Check if user can assign a role to another user
|
|
117
174
|
*
|
|
118
|
-
* Rule: Can only assign roles if user has '
|
|
175
|
+
* Rule: Can only assign roles if user has 'security:roles:assign' permission
|
|
119
176
|
* AND has the target role (or higher) themselves.
|
|
120
177
|
*
|
|
121
178
|
* @param {string} targetRole - Role to assign
|
|
122
179
|
* @returns {boolean} True if user can assign this role
|
|
123
180
|
*/
|
|
124
181
|
canAssignRole(targetRole) {
|
|
125
|
-
return this.isGranted('
|
|
182
|
+
return this.isGranted('security:roles:assign') && this.isGranted(targetRole)
|
|
126
183
|
}
|
|
127
184
|
|
|
128
185
|
/**
|
|
@@ -131,7 +188,7 @@ export class SecurityChecker {
|
|
|
131
188
|
* @returns {string[]} Array of assignable role names
|
|
132
189
|
*/
|
|
133
190
|
getAssignableRoles() {
|
|
134
|
-
if (!this.isGranted('
|
|
191
|
+
if (!this.isGranted('security:roles:assign')) return []
|
|
135
192
|
|
|
136
193
|
const user = this.getCurrentUser()
|
|
137
194
|
if (!user) return []
|
|
@@ -153,15 +210,26 @@ export class SecurityChecker {
|
|
|
153
210
|
* Create a SecurityChecker instance from config
|
|
154
211
|
*
|
|
155
212
|
* @param {Object} config
|
|
156
|
-
* @param {
|
|
157
|
-
* @param {Object<string, string[]>} config.
|
|
213
|
+
* @param {RoleGranterAdapter} [config.roleGranter] - Role granter adapter
|
|
214
|
+
* @param {Object<string, string[]>} [config.role_hierarchy] - Role hierarchy config (legacy)
|
|
215
|
+
* @param {Object<string, string[]>} [config.role_permissions] - Permissions per role (legacy)
|
|
158
216
|
* @param {Function} config.getCurrentUser - User getter function
|
|
159
217
|
* @returns {SecurityChecker}
|
|
160
218
|
*/
|
|
161
219
|
export function createSecurityChecker(config) {
|
|
220
|
+
if (config.roleGranter) {
|
|
221
|
+
return new SecurityChecker({
|
|
222
|
+
roleGranter: config.roleGranter,
|
|
223
|
+
getCurrentUser: config.getCurrentUser
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Legacy config: auto-create static granter
|
|
162
228
|
return new SecurityChecker({
|
|
163
|
-
|
|
164
|
-
|
|
229
|
+
roleGranter: new StaticRoleGranterAdapter({
|
|
230
|
+
role_hierarchy: config.role_hierarchy || {},
|
|
231
|
+
role_permissions: config.role_permissions || {}
|
|
232
|
+
}),
|
|
165
233
|
getCurrentUser: config.getCurrentUser
|
|
166
234
|
})
|
|
167
235
|
}
|
|
@@ -105,16 +105,20 @@ function isCompositeConfig(config) {
|
|
|
105
105
|
*
|
|
106
106
|
* Handles:
|
|
107
107
|
* - AuthAdapter instance → return directly (backward compatible)
|
|
108
|
+
* - Function → wrap as EntityAuthAdapter with getCurrentUser callback
|
|
108
109
|
* - String pattern 'type' → parse and resolve
|
|
109
110
|
* - Config object with 'type' → resolve via registry
|
|
110
111
|
* - Config object with 'default' → create CompositeAuthAdapter
|
|
111
112
|
*
|
|
112
|
-
* @param {EntityAuthAdapter | string | object} config - Auth config
|
|
113
|
+
* @param {EntityAuthAdapter | Function | string | object} config - Auth config
|
|
113
114
|
* @param {object} [context={}] - Context with authTypes, authResolver
|
|
114
115
|
* @returns {EntityAuthAdapter} Adapter instance
|
|
115
116
|
*
|
|
116
117
|
* @example
|
|
117
|
-
* //
|
|
118
|
+
* // Function (simplest - for getCurrentUser callback)
|
|
119
|
+
* authFactory(() => authStore.user) // → EntityAuthAdapter
|
|
120
|
+
*
|
|
121
|
+
* // Instance passthrough (backward compatible)
|
|
118
122
|
* authFactory(myAdapter) // → myAdapter
|
|
119
123
|
*
|
|
120
124
|
* // String patterns
|
|
@@ -138,6 +142,11 @@ export function authFactory(config, context = {}) {
|
|
|
138
142
|
return new PermissiveAuthAdapter()
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
// Function → wrap in EntityAuthAdapter with getCurrentUser callback
|
|
146
|
+
if (typeof config === 'function') {
|
|
147
|
+
return new EntityAuthAdapter({ getCurrentUser: config })
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
// Already an EntityAuthAdapter instance → return directly (backward compatible)
|
|
142
151
|
if (config instanceof EntityAuthAdapter) {
|
|
143
152
|
return config
|
|
@@ -82,6 +82,35 @@ describe('authFactory', () => {
|
|
|
82
82
|
})
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
describe('function callback', () => {
|
|
86
|
+
it('wraps function in EntityAuthAdapter with getCurrentUser', () => {
|
|
87
|
+
const user = { id: 1, name: 'Test' }
|
|
88
|
+
const result = authFactory(() => user)
|
|
89
|
+
|
|
90
|
+
expect(result).toBeInstanceOf(EntityAuthAdapter)
|
|
91
|
+
expect(result.getCurrentUser()).toBe(user)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('function can return null', () => {
|
|
95
|
+
const result = authFactory(() => null)
|
|
96
|
+
|
|
97
|
+
expect(result).toBeInstanceOf(EntityAuthAdapter)
|
|
98
|
+
expect(result.getCurrentUser()).toBeNull()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('function is called each time getCurrentUser is called', () => {
|
|
102
|
+
let callCount = 0
|
|
103
|
+
const result = authFactory(() => {
|
|
104
|
+
callCount++
|
|
105
|
+
return { id: callCount }
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(result.getCurrentUser()).toEqual({ id: 1 })
|
|
109
|
+
expect(result.getCurrentUser()).toEqual({ id: 2 })
|
|
110
|
+
expect(callCount).toBe(2)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
85
114
|
describe('string patterns', () => {
|
|
86
115
|
it('resolves built-in types', () => {
|
|
87
116
|
const result = authFactory('permissive')
|
|
@@ -59,7 +59,7 @@ describe('defaultStorageResolver', () => {
|
|
|
59
59
|
it('creates LocalStorage for local type', () => {
|
|
60
60
|
const storage = defaultStorageResolver({ type: 'local', key: 'myKey' }, 'items')
|
|
61
61
|
expect(storage).toBeInstanceOf(LocalStorage)
|
|
62
|
-
expect(storage.
|
|
62
|
+
expect(storage.key).toBe('myKey')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
it('creates MemoryStorage for memory type', () => {
|
|
@@ -74,7 +74,7 @@ describe('defaultStorageResolver', () => {
|
|
|
74
74
|
|
|
75
75
|
it('throws for unknown type', () => {
|
|
76
76
|
expect(() => defaultStorageResolver({ type: 'unknown' }, 'items'))
|
|
77
|
-
.toThrow('Unknown storage type: unknown')
|
|
77
|
+
.toThrow('Unknown storage type: "unknown"')
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
80
|
|
|
@@ -111,9 +111,10 @@ describe('storageFactory', () => {
|
|
|
111
111
|
expect(result).toBeInstanceOf(MemoryStorage)
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
-
it('handles config object with
|
|
115
|
-
const result = storageFactory({
|
|
114
|
+
it('handles config object with endpoint (infers api type)', () => {
|
|
115
|
+
const result = storageFactory({ endpoint: '/api/items' }, 'items')
|
|
116
116
|
expect(result).toBeInstanceOf(ApiStorage)
|
|
117
|
+
expect(result.endpoint).toBe('/api/items')
|
|
117
118
|
})
|
|
118
119
|
|
|
119
120
|
it('uses custom resolver when provided', () => {
|
|
@@ -133,7 +134,7 @@ describe('storageFactory', () => {
|
|
|
133
134
|
|
|
134
135
|
it('throws for unparseable string', () => {
|
|
135
136
|
expect(() => storageFactory('invalid', 'test'))
|
|
136
|
-
.toThrow('
|
|
137
|
+
.toThrow('Invalid storage pattern')
|
|
137
138
|
})
|
|
138
139
|
})
|
|
139
140
|
|
package/src/index.js
CHANGED