qdadm 0.35.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.
Files changed (43) hide show
  1. package/README.md +27 -174
  2. package/package.json +2 -1
  3. package/src/auth/SessionAuthAdapter.js +114 -3
  4. package/src/components/editors/PermissionEditor.vue +535 -0
  5. package/src/components/forms/FormField.vue +1 -11
  6. package/src/components/index.js +1 -0
  7. package/src/components/layout/AppLayout.vue +20 -8
  8. package/src/components/layout/defaults/DefaultToaster.vue +3 -3
  9. package/src/components/pages/LoginPage.vue +26 -5
  10. package/src/composables/useCurrentEntity.js +26 -17
  11. package/src/composables/useForm.js +7 -0
  12. package/src/composables/useFormPageBuilder.js +7 -0
  13. package/src/composables/useNavContext.js +30 -16
  14. package/src/core/index.js +0 -3
  15. package/src/debug/AuthCollector.js +199 -31
  16. package/src/debug/Collector.js +24 -2
  17. package/src/debug/EntitiesCollector.js +8 -0
  18. package/src/debug/SignalCollector.js +60 -2
  19. package/src/debug/components/panels/AuthPanel.vue +159 -27
  20. package/src/debug/components/panels/EntitiesPanel.vue +18 -2
  21. package/src/entity/EntityManager.js +205 -36
  22. package/src/entity/auth/EntityAuthAdapter.js +54 -46
  23. package/src/entity/auth/SecurityChecker.js +110 -42
  24. package/src/entity/auth/factory.js +11 -2
  25. package/src/entity/auth/factory.test.js +29 -0
  26. package/src/entity/storage/factory.test.js +6 -5
  27. package/src/index.js +3 -0
  28. package/src/kernel/Kernel.js +135 -25
  29. package/src/kernel/KernelContext.js +166 -0
  30. package/src/security/EntityRoleGranterAdapter.js +350 -0
  31. package/src/security/PermissionMatcher.js +148 -0
  32. package/src/security/PermissionRegistry.js +263 -0
  33. package/src/security/PersistableRoleGranterAdapter.js +618 -0
  34. package/src/security/RoleGranterAdapter.js +123 -0
  35. package/src/security/RoleGranterStorage.js +161 -0
  36. package/src/security/RolesManager.js +81 -0
  37. package/src/security/SecurityModule.js +73 -0
  38. package/src/security/StaticRoleGranterAdapter.js +114 -0
  39. package/src/security/UsersManager.js +122 -0
  40. package/src/security/index.js +45 -0
  41. package/src/security/pages/RoleForm.vue +212 -0
  42. package/src/security/pages/RoleList.vue +106 -0
  43. package/src/styles/main.css +62 -2
@@ -0,0 +1,123 @@
1
+ /**
2
+ * RoleGranterAdapter - Interface for role → permissions mapping
3
+ *
4
+ * The framework provides StaticRoleGranterAdapter (from config object).
5
+ * Apps can implement custom adapters (entity-based, API-backed, etc.)
6
+ *
7
+ * This interface abstracts HOW roles and permissions are stored/retrieved,
8
+ * allowing apps to use:
9
+ * - Static config (simple apps, demos)
10
+ * - Database/entity storage (apps with role management UI)
11
+ * - External API (microservices architecture)
12
+ *
13
+ * @example
14
+ * // Static adapter (auto-created from config)
15
+ * security: {
16
+ * role_permissions: { ROLE_USER: ['entity:*:read'] }
17
+ * }
18
+ *
19
+ * // Custom adapter
20
+ * security: {
21
+ * roleGranter: new EntityRoleGranterAdapter({ entityName: 'roles' })
22
+ * }
23
+ */
24
+ export class RoleGranterAdapter {
25
+ /**
26
+ * Get permissions granted to a role
27
+ *
28
+ * @param {string} role - Role name (e.g., 'ROLE_USER')
29
+ * @returns {string[]} Array of permission strings (may include wildcards)
30
+ *
31
+ * @example
32
+ * adapter.getPermissions('ROLE_ADMIN')
33
+ * // ['entity:**', 'admin:**']
34
+ */
35
+ getPermissions(role) {
36
+ throw new Error('RoleGranterAdapter.getPermissions() must be implemented')
37
+ }
38
+
39
+ /**
40
+ * Get all defined roles
41
+ *
42
+ * @returns {string[]} Array of role names
43
+ *
44
+ * @example
45
+ * adapter.getRoles()
46
+ * // ['ROLE_USER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN']
47
+ */
48
+ getRoles() {
49
+ throw new Error('RoleGranterAdapter.getRoles() must be implemented')
50
+ }
51
+
52
+ /**
53
+ * Get role hierarchy map
54
+ *
55
+ * @returns {Object<string, string[]>} Role → inherited roles
56
+ *
57
+ * @example
58
+ * adapter.getHierarchy()
59
+ * // { ROLE_ADMIN: ['ROLE_USER'], ROLE_SUPER_ADMIN: ['ROLE_ADMIN'] }
60
+ */
61
+ getHierarchy() {
62
+ throw new Error('RoleGranterAdapter.getHierarchy() must be implemented')
63
+ }
64
+
65
+ /**
66
+ * Get the role for unauthenticated users
67
+ *
68
+ * Always returns 'ROLE_ANONYMOUS' (convention)
69
+ *
70
+ * @returns {string} Anonymous role name
71
+ */
72
+ getAnonymousRole() {
73
+ return 'ROLE_ANONYMOUS'
74
+ }
75
+
76
+ /**
77
+ * Get role metadata (label, description)
78
+ * Optional - for display purposes
79
+ *
80
+ * @param {string} role - Role name
81
+ * @returns {RoleMeta|null}
82
+ */
83
+ getRoleMeta(role) {
84
+ return null
85
+ }
86
+
87
+ /**
88
+ * Install adapter (called by Kernel when context is ready)
89
+ * Override for adapters that need initialization
90
+ *
91
+ * @param {Object} ctx - Kernel context
92
+ */
93
+ install(ctx) {
94
+ // Override if needed
95
+ }
96
+
97
+ /**
98
+ * Uninstall adapter (cleanup)
99
+ */
100
+ uninstall() {
101
+ // Override if needed
102
+ }
103
+
104
+ /**
105
+ * Check if adapter supports persistence (editing + saving)
106
+ *
107
+ * When true, the adapter supports mutations (setRolePermissions, etc.)
108
+ * AND can persist changes. UI should show edit controls.
109
+ *
110
+ * When false, the adapter is read-only. UI should hide edit controls.
111
+ *
112
+ * @returns {boolean}
113
+ */
114
+ get canPersist() {
115
+ return false
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @typedef {Object} RoleMeta
121
+ * @property {string} [label] - Human-readable label
122
+ * @property {string} [description] - Description
123
+ */
@@ -0,0 +1,161 @@
1
+ /**
2
+ * RoleGranterStorage - Storage bridge for roleGranter
3
+ *
4
+ * Implements Storage interface and delegates to roleGranter.
5
+ * Allows RolesManager to use standard EntityManager patterns.
6
+ */
7
+
8
+ export class RoleGranterStorage {
9
+ /**
10
+ * Static capabilities (standard storage pattern)
11
+ * @type {import('../entity/storage/index.js').StorageCapabilities}
12
+ */
13
+ static capabilities = {
14
+ supportsTotal: true,
15
+ supportsFilters: false,
16
+ supportsPagination: false,
17
+ supportsCaching: false // In-memory via roleGranter, no need for EntityManager cache
18
+ }
19
+
20
+ /**
21
+ * @param {import('./RoleGranterAdapter.js').RoleGranterAdapter} roleGranter
22
+ */
23
+ constructor(roleGranter) {
24
+ this._roleGranter = roleGranter
25
+ }
26
+
27
+ /**
28
+ * Instance capabilities (merge static + dynamic)
29
+ *
30
+ * Dynamic properties:
31
+ * - requiresAuth: false (roles are system data)
32
+ * - readOnly: based on roleGranter.canPersist
33
+ */
34
+ get capabilities() {
35
+ return {
36
+ ...RoleGranterStorage.capabilities,
37
+ requiresAuth: false,
38
+ readOnly: !this._roleGranter?.canPersist
39
+ }
40
+ }
41
+
42
+ /**
43
+ * List all roles
44
+ */
45
+ async list(params = {}) {
46
+ if (!this._roleGranter) {
47
+ return { data: [], total: 0 }
48
+ }
49
+
50
+ // Ensure loaded for adapters that need it
51
+ if (this._roleGranter.ensureReady) {
52
+ await this._roleGranter.ensureReady()
53
+ }
54
+
55
+ const roleNames = this._roleGranter.getRoles()
56
+ const roles = roleNames.map(name => this._getRole(name))
57
+
58
+ // Simple search filter
59
+ let filtered = roles
60
+ if (params.search) {
61
+ const search = params.search.toLowerCase()
62
+ filtered = roles.filter(r =>
63
+ r.name.toLowerCase().includes(search) ||
64
+ r.label?.toLowerCase().includes(search)
65
+ )
66
+ }
67
+
68
+ return {
69
+ items: filtered,
70
+ total: filtered.length
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get a single role by name
76
+ */
77
+ async get(name) {
78
+ if (!this._roleGranter) return null
79
+
80
+ if (this._roleGranter.ensureReady) {
81
+ await this._roleGranter.ensureReady()
82
+ }
83
+
84
+ return this._getRole(name)
85
+ }
86
+
87
+ /**
88
+ * Get many roles by names
89
+ */
90
+ async getMany(names) {
91
+ if (!this._roleGranter) return []
92
+
93
+ if (this._roleGranter.ensureReady) {
94
+ await this._roleGranter.ensureReady()
95
+ }
96
+
97
+ return names.map(name => this._getRole(name)).filter(Boolean)
98
+ }
99
+
100
+ /**
101
+ * Create a new role
102
+ */
103
+ async create(data) {
104
+ if (!this._roleGranter?.createRole) {
105
+ throw new Error('Role creation not supported by this adapter')
106
+ }
107
+ return this._roleGranter.createRole(data.name, {
108
+ label: data.label,
109
+ permissions: data.permissions || [],
110
+ inherits: data.inherits || []
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Update an existing role
116
+ */
117
+ async update(name, data) {
118
+ if (!this._roleGranter?.updateRole) {
119
+ throw new Error('Role update not supported by this adapter')
120
+ }
121
+ return this._roleGranter.updateRole(name, {
122
+ label: data.label,
123
+ permissions: data.permissions,
124
+ inherits: data.inherits
125
+ })
126
+ }
127
+
128
+ /**
129
+ * Patch a role (partial update)
130
+ */
131
+ async patch(name, data) {
132
+ return this.update(name, data)
133
+ }
134
+
135
+ /**
136
+ * Delete a role
137
+ */
138
+ async delete(name) {
139
+ if (!this._roleGranter?.deleteRole) {
140
+ throw new Error('Role deletion not supported by this adapter')
141
+ }
142
+ return this._roleGranter.deleteRole(name)
143
+ }
144
+
145
+ /**
146
+ * Get role object from roleGranter
147
+ * @private
148
+ */
149
+ _getRole(name) {
150
+ if (this._roleGranter.getRole) {
151
+ return this._roleGranter.getRole(name)
152
+ }
153
+ // Fallback for adapters without getRole
154
+ return {
155
+ name,
156
+ label: this._roleGranter.getLabels?.()[name] || name,
157
+ permissions: this._roleGranter.getPermissions(name),
158
+ inherits: this._roleGranter.getHierarchy()[name] || []
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * RolesManager - System entity manager for roles
3
+ *
4
+ * Uses RoleGranterStorage as bridge to roleGranter.
5
+ * Allows SecurityModule to use standard ctx.crud() pattern.
6
+ */
7
+
8
+ import { EntityManager } from '../entity/EntityManager.js'
9
+ import { RoleGranterStorage } from './RoleGranterStorage.js'
10
+
11
+ export class RolesManager extends EntityManager {
12
+ /**
13
+ * @param {Object} options
14
+ * @param {import('./RoleGranterAdapter.js').RoleGranterAdapter} options.roleGranter
15
+ * @param {import('./PermissionRegistry.js').PermissionRegistry} options.permissionRegistry
16
+ */
17
+ constructor(options = {}) {
18
+ const storage = new RoleGranterStorage(options.roleGranter)
19
+
20
+ super({
21
+ name: 'roles',
22
+ labelField: 'label',
23
+ idField: 'name',
24
+ system: true, // System-provided entity
25
+ fields: {
26
+ name: { type: 'text', label: 'Role Name', required: true },
27
+ label: { type: 'text', label: 'Display Label' },
28
+ permissions: { type: 'array', label: 'Permissions', default: [] },
29
+ inherits: { type: 'array', label: 'Inherits From', default: [] }
30
+ },
31
+ storage
32
+ })
33
+
34
+ this._roleGranter = options.roleGranter
35
+ this._permissionRegistry = options.permissionRegistry
36
+ }
37
+
38
+ /**
39
+ * Get roleGranter (for RoleForm permission picker)
40
+ */
41
+ get roleGranter() {
42
+ return this._roleGranter
43
+ }
44
+
45
+ /**
46
+ * Get permission registry (for RoleForm permission picker)
47
+ */
48
+ get permissionRegistry() {
49
+ return this._permissionRegistry
50
+ }
51
+
52
+ /**
53
+ * Check if roles can be edited
54
+ */
55
+ get canPersist() {
56
+ return this._roleGranter?.canPersist ?? false
57
+ }
58
+
59
+ /**
60
+ * Protected system roles that cannot be deleted
61
+ */
62
+ static PROTECTED_ROLES = ['ROLE_ANONYMOUS']
63
+
64
+ // Permission checks based on canPersist
65
+ canCreate() { return this.canPersist }
66
+ canUpdate() { return this.canPersist }
67
+
68
+ /**
69
+ * Check if can delete (general or row-specific)
70
+ * @param {object} [item] - Optional role to check
71
+ * @returns {boolean}
72
+ */
73
+ canDelete(item) {
74
+ if (!this.canPersist) return false
75
+ if (item) {
76
+ // Protected system roles cannot be deleted
77
+ return !RolesManager.PROTECTED_ROLES.includes(item.name)
78
+ }
79
+ return true
80
+ }
81
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * SecurityModule - Role management UI
3
+ *
4
+ * Provides a page to view/edit roles and permissions.
5
+ * Works with any RoleGranterAdapter:
6
+ * - StaticRoleGranterAdapter: read-only view
7
+ * - PersistableRoleGranterAdapter: full CRUD
8
+ * - EntityRoleGranterAdapter: full CRUD via entity
9
+ *
10
+ * Registers permissions:
11
+ * - security:roles:read - View roles list
12
+ * - security:roles:create - Create new roles
13
+ * - security:roles:update - Edit existing roles
14
+ * - security:roles:delete - Delete roles
15
+ *
16
+ * @example
17
+ * import { SecurityModule } from 'qdadm/security'
18
+ *
19
+ * const kernel = new Kernel({
20
+ * moduleDefs: [SecurityModule, ...],
21
+ * security: {
22
+ * roleGranter: createLocalStorageRoleGranter({ ... })
23
+ * }
24
+ * })
25
+ */
26
+
27
+ import { Module } from '../kernel/Module.js'
28
+ import { RolesManager } from './RolesManager.js'
29
+
30
+ export class SecurityModule extends Module {
31
+ static name = 'security'
32
+ static requires = []
33
+ static priority = 100 // Load late (after other modules register permissions)
34
+
35
+ async connect(ctx) {
36
+ // ════════════════════════════════════════════════════════════════════════
37
+ // PERMISSIONS
38
+ // ════════════════════════════════════════════════════════════════════════
39
+ ctx.permissions('security', {
40
+ 'roles:read': 'View roles and permissions',
41
+ 'roles:create': 'Create new roles',
42
+ 'roles:update': 'Edit role permissions',
43
+ 'roles:delete': 'Delete roles'
44
+ })
45
+
46
+ // ════════════════════════════════════════════════════════════════════════
47
+ // ENTITY (wraps roleGranter)
48
+ // ════════════════════════════════════════════════════════════════════════
49
+ const rolesManager = new RolesManager({
50
+ roleGranter: ctx.security?.roleGranter,
51
+ permissionRegistry: ctx.permissionRegistry
52
+ })
53
+
54
+ ctx.entity('roles', rolesManager)
55
+
56
+ // ════════════════════════════════════════════════════════════════════════
57
+ // ROUTES (using ctx.crud helper)
58
+ // ════════════════════════════════════════════════════════════════════════
59
+ ctx.crud('roles', {
60
+ list: () => import('./pages/RoleList.vue'),
61
+ form: () => import('./pages/RoleForm.vue')
62
+ }, {
63
+ basePath: 'security/roles',
64
+ nav: {
65
+ section: 'Security',
66
+ icon: 'pi pi-shield',
67
+ permission: 'security:roles:read'
68
+ }
69
+ })
70
+ }
71
+ }
72
+
73
+ export default SecurityModule
@@ -0,0 +1,114 @@
1
+ import { RoleGranterAdapter } from './RoleGranterAdapter.js'
2
+
3
+ /**
4
+ * StaticRoleGranterAdapter - Role granter from config object
5
+ *
6
+ * Default implementation for simple apps and demos.
7
+ * Auto-created when passing role_permissions object to Kernel.
8
+ *
9
+ * @example
10
+ * // Auto-created by Kernel
11
+ * const kernel = new Kernel({
12
+ * security: {
13
+ * role_hierarchy: { ROLE_ADMIN: ['ROLE_USER'] },
14
+ * role_permissions: {
15
+ * ROLE_USER: ['entity:*:read'],
16
+ * ROLE_ADMIN: ['**']
17
+ * }
18
+ * }
19
+ * })
20
+ *
21
+ * // Or explicit creation
22
+ * const granter = new StaticRoleGranterAdapter({
23
+ * role_hierarchy: { ROLE_ADMIN: ['ROLE_USER'] },
24
+ * role_permissions: {
25
+ * ROLE_USER: ['entity:*:read'],
26
+ * ROLE_ADMIN: ['**']
27
+ * },
28
+ * role_labels: {
29
+ * ROLE_USER: 'User',
30
+ * ROLE_ADMIN: 'Administrator'
31
+ * }
32
+ * })
33
+ */
34
+ export class StaticRoleGranterAdapter extends RoleGranterAdapter {
35
+ /**
36
+ * @param {Object} config
37
+ * @param {Object<string, string[]>} [config.role_hierarchy={}] - Role hierarchy
38
+ * @param {Object<string, string[]>} [config.role_permissions={}] - Role permissions
39
+ * @param {Object<string, string>} [config.role_labels={}] - Role display labels
40
+ */
41
+ constructor(config = {}) {
42
+ super()
43
+ this._hierarchy = config.role_hierarchy || {}
44
+ this._permissions = config.role_permissions || {}
45
+ this._labels = config.role_labels || {}
46
+ }
47
+
48
+ /**
49
+ * Get permissions for a role
50
+ * @param {string} role
51
+ * @returns {string[]}
52
+ */
53
+ getPermissions(role) {
54
+ return this._permissions[role] || []
55
+ }
56
+
57
+ /**
58
+ * Get all defined roles
59
+ * @returns {string[]}
60
+ */
61
+ getRoles() {
62
+ // Combine roles from permissions and hierarchy
63
+ const roles = new Set([
64
+ ...Object.keys(this._permissions),
65
+ ...Object.keys(this._hierarchy)
66
+ ])
67
+
68
+ // Also include inherited roles
69
+ for (const inherits of Object.values(this._hierarchy)) {
70
+ for (const r of inherits) {
71
+ roles.add(r)
72
+ }
73
+ }
74
+
75
+ return [...roles]
76
+ }
77
+
78
+ /**
79
+ * Get role hierarchy
80
+ * @returns {Object<string, string[]>}
81
+ */
82
+ getHierarchy() {
83
+ return this._hierarchy
84
+ }
85
+
86
+ /**
87
+ * Get role metadata
88
+ * @param {string} role
89
+ * @returns {RoleMeta|null}
90
+ */
91
+ getRoleMeta(role) {
92
+ const label = this._labels[role]
93
+ if (!label) return null
94
+ return { label }
95
+ }
96
+
97
+ /**
98
+ * Update role permissions at runtime (for testing or hot-reload)
99
+ * @param {string} role
100
+ * @param {string[]} permissions
101
+ */
102
+ setPermissions(role, permissions) {
103
+ this._permissions[role] = permissions
104
+ }
105
+
106
+ /**
107
+ * Update role hierarchy at runtime
108
+ * @param {string} role
109
+ * @param {string[]} inherits
110
+ */
111
+ setHierarchy(role, inherits) {
112
+ this._hierarchy[role] = inherits
113
+ }
114
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * UsersManager - System entity manager for users
3
+ *
4
+ * Provides standard user entity with:
5
+ * - username, password, role fields
6
+ * - role linked to roles entity via reference
7
+ * - Admin-only access by default (customizable)
8
+ *
9
+ * @example
10
+ * // In a module:
11
+ * ctx.userEntity({
12
+ * storage: new ApiStorage({ endpoint: '/api/users' }),
13
+ * extraFields: {
14
+ * email: { type: 'email', label: 'Email' }
15
+ * }
16
+ * })
17
+ */
18
+
19
+ import { EntityManager } from '../entity/EntityManager.js'
20
+
21
+ export class UsersManager extends EntityManager {
22
+ /**
23
+ * @param {Object} options
24
+ * @param {Object} options.storage - Storage adapter (required)
25
+ * @param {Object} [options.extraFields={}] - Additional fields beyond username/password/role
26
+ * @param {string} [options.adminRole='ROLE_ADMIN'] - Role required for user management
27
+ * @param {boolean} [options.adminOnly=true] - Restrict access to admin role only
28
+ * @param {Object} [options.fieldOverrides={}] - Override default field configs
29
+ */
30
+ constructor(options = {}) {
31
+ const {
32
+ storage,
33
+ extraFields = {},
34
+ adminRole = 'ROLE_ADMIN',
35
+ adminOnly = true,
36
+ fieldOverrides = {},
37
+ ...rest
38
+ } = options
39
+
40
+ if (!storage) {
41
+ throw new Error('[UsersManager] storage is required')
42
+ }
43
+
44
+ // Default fields for users
45
+ const defaultFields = {
46
+ username: {
47
+ type: 'text',
48
+ label: 'Username',
49
+ required: true,
50
+ default: '',
51
+ ...fieldOverrides.username
52
+ },
53
+ password: {
54
+ type: 'password',
55
+ label: 'Password',
56
+ required: true,
57
+ default: '',
58
+ listable: false, // Don't show in list view
59
+ ...fieldOverrides.password
60
+ },
61
+ role: {
62
+ type: 'select',
63
+ label: 'Role',
64
+ reference: { entity: 'roles' }, // Links to roles entity
65
+ default: 'ROLE_USER',
66
+ ...fieldOverrides.role
67
+ }
68
+ }
69
+
70
+ super({
71
+ name: 'users',
72
+ labelField: 'username',
73
+ system: true, // System-provided entity
74
+ fields: {
75
+ ...defaultFields,
76
+ ...extraFields
77
+ },
78
+ storage,
79
+ ...rest
80
+ })
81
+
82
+ this._adminRole = adminRole
83
+ this._adminOnly = adminOnly
84
+ }
85
+
86
+ /**
87
+ * Check if current user has admin role
88
+ * @returns {boolean}
89
+ * @private
90
+ */
91
+ _isAdmin() {
92
+ const user = this.authAdapter?.getCurrentUser?.()
93
+ if (!user) return false
94
+
95
+ // Check role directly or in roles array
96
+ const userRole = user.role || user.roles?.[0]
97
+ if (!userRole) return false
98
+
99
+ // Normalize to uppercase with ROLE_ prefix
100
+ const normalized = userRole.toUpperCase()
101
+ const roleToCheck = normalized.startsWith('ROLE_') ? normalized : `ROLE_${normalized}`
102
+
103
+ return roleToCheck === this._adminRole
104
+ }
105
+
106
+ // Permission checks - admin only by default
107
+ canRead() {
108
+ return this._adminOnly ? this._isAdmin() : true
109
+ }
110
+
111
+ canCreate() {
112
+ return this._adminOnly ? this._isAdmin() : true
113
+ }
114
+
115
+ canUpdate() {
116
+ return this._adminOnly ? this._isAdmin() : true
117
+ }
118
+
119
+ canDelete() {
120
+ return this._adminOnly ? this._isAdmin() : true
121
+ }
122
+ }