qdadm 0.31.0 → 0.34.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 +2 -1
- package/src/auth/SessionAuthAdapter.js +254 -0
- package/src/auth/index.js +10 -0
- package/src/components/layout/AppLayout.vue +2 -0
- package/src/components/pages/LoginPage.vue +3 -2
- package/src/composables/useAuth.js +2 -1
- package/src/composables/useFormPageBuilder.js +7 -1
- package/src/composables/useListPageBuilder.js +7 -1
- package/src/debug/AuthCollector.js +104 -18
- package/src/debug/Collector.js +2 -1
- package/src/debug/EntitiesCollector.js +28 -1
- package/src/debug/components/panels/AuthPanel.vue +81 -10
- package/src/debug/components/panels/EntitiesPanel.vue +100 -4
- package/src/entity/EntityManager.js +1 -1
- package/src/entity/auth/CompositeAuthAdapter.js +212 -0
- package/src/entity/auth/{AuthAdapter.js → EntityAuthAdapter.js} +10 -37
- package/src/entity/auth/PermissiveAdapter.js +4 -6
- package/src/entity/auth/factory.js +207 -0
- package/src/entity/auth/factory.test.js +257 -0
- package/src/entity/auth/index.js +15 -1
- package/src/entity/storage/MockApiStorage.js +34 -1
- package/src/index.js +3 -0
- package/src/kernel/Kernel.js +50 -18
- package/src/kernel/KernelContext.js +142 -0
- package/src/orchestrator/Orchestrator.js +21 -1
- package/src/orchestrator/useOrchestrator.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdadm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./src/index.js",
|
|
23
|
+
"./auth": "./src/auth/index.js",
|
|
23
24
|
"./composables": "./src/composables/index.js",
|
|
24
25
|
"./components": "./src/components/index.js",
|
|
25
26
|
"./editors": "./src/editors/index.js",
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionAuthAdapter - Base class for user authentication
|
|
3
|
+
*
|
|
4
|
+
* Applications extend this class to implement their authentication logic.
|
|
5
|
+
* The adapter handles user sessions: login, logout, token management.
|
|
6
|
+
*
|
|
7
|
+
* This is different from EntityAuthAdapter which handles entity-level permissions.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```js
|
|
11
|
+
* class MyAuthAdapter extends SessionAuthAdapter {
|
|
12
|
+
* async login({ username, password }) {
|
|
13
|
+
* const response = await api.post('/auth/login', { username, password })
|
|
14
|
+
* this.setSession(response.token, response.user)
|
|
15
|
+
* return { token: response.token, user: response.user }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* logout() {
|
|
19
|
+
* this.clearSession()
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @abstract
|
|
25
|
+
*/
|
|
26
|
+
export class SessionAuthAdapter {
|
|
27
|
+
/**
|
|
28
|
+
* Internal session state
|
|
29
|
+
* @protected
|
|
30
|
+
*/
|
|
31
|
+
_token = null
|
|
32
|
+
_user = null
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Authenticate user with credentials
|
|
36
|
+
*
|
|
37
|
+
* @param {object} credentials - Login credentials
|
|
38
|
+
* @param {string} credentials.username - Username or email
|
|
39
|
+
* @param {string} credentials.password - Password
|
|
40
|
+
* @returns {Promise<{token: string, user: object}>} Session data
|
|
41
|
+
* @throws {Error} If authentication fails
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const { token, user } = await adapter.login({ username: 'admin', password: 'secret' })
|
|
45
|
+
*/
|
|
46
|
+
async login(credentials) {
|
|
47
|
+
throw new Error('[SessionAuthAdapter] login() must be implemented by subclass')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* End the current session
|
|
52
|
+
*
|
|
53
|
+
* Should clear all session data (tokens, user info).
|
|
54
|
+
* Called by AppLayout logout button and useAuth().logout()
|
|
55
|
+
*/
|
|
56
|
+
logout() {
|
|
57
|
+
throw new Error('[SessionAuthAdapter] logout() must be implemented by subclass')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if user is currently authenticated
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean} True if user has valid session
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* if (adapter.isAuthenticated()) {
|
|
67
|
+
* // Show dashboard
|
|
68
|
+
* } else {
|
|
69
|
+
* // Redirect to login
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
isAuthenticated() {
|
|
73
|
+
throw new Error('[SessionAuthAdapter] isAuthenticated() must be implemented by subclass')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current authentication token
|
|
78
|
+
*
|
|
79
|
+
* Used by API clients to include in request headers.
|
|
80
|
+
*
|
|
81
|
+
* @returns {string|null} JWT token or null if not authenticated
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const token = adapter.getToken()
|
|
85
|
+
* fetch('/api/data', {
|
|
86
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
87
|
+
* })
|
|
88
|
+
*/
|
|
89
|
+
getToken() {
|
|
90
|
+
throw new Error('[SessionAuthAdapter] getToken() must be implemented by subclass')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the current user object
|
|
95
|
+
*
|
|
96
|
+
* Returns user data from the session. The shape depends on your backend.
|
|
97
|
+
*
|
|
98
|
+
* @returns {object|null} User object or null if not authenticated
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const user = adapter.getUser()
|
|
102
|
+
* console.log(user.username, user.email, user.roles)
|
|
103
|
+
*/
|
|
104
|
+
getUser() {
|
|
105
|
+
throw new Error('[SessionAuthAdapter] getUser() must be implemented by subclass')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Synchronous user getter (optional)
|
|
110
|
+
*
|
|
111
|
+
* Some implementations prefer a property instead of method.
|
|
112
|
+
* useAuth() supports both patterns.
|
|
113
|
+
*
|
|
114
|
+
* @type {object|null}
|
|
115
|
+
*/
|
|
116
|
+
get user() {
|
|
117
|
+
return this.getUser()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────
|
|
121
|
+
// Helper methods for subclasses
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set session data (helper for subclasses)
|
|
126
|
+
*
|
|
127
|
+
* @param {string} token - Authentication token
|
|
128
|
+
* @param {object} user - User object
|
|
129
|
+
* @protected
|
|
130
|
+
*/
|
|
131
|
+
setSession(token, user) {
|
|
132
|
+
this._token = token
|
|
133
|
+
this._user = user
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clear session data (helper for subclasses)
|
|
138
|
+
*
|
|
139
|
+
* @protected
|
|
140
|
+
*/
|
|
141
|
+
clearSession() {
|
|
142
|
+
this._token = null
|
|
143
|
+
this._user = null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate that the adapter is properly configured
|
|
148
|
+
*
|
|
149
|
+
* Called during bootstrap to catch configuration errors early.
|
|
150
|
+
*
|
|
151
|
+
* @throws {Error} If required methods are not implemented
|
|
152
|
+
*/
|
|
153
|
+
static validate(adapter) {
|
|
154
|
+
const required = ['login', 'logout', 'isAuthenticated', 'getToken', 'getUser']
|
|
155
|
+
const missing = required.filter(method => typeof adapter[method] !== 'function')
|
|
156
|
+
|
|
157
|
+
if (missing.length > 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[SessionAuthAdapter] Missing required methods: ${missing.join(', ')}\n` +
|
|
160
|
+
'Ensure your authAdapter implements all required methods or extends SessionAuthAdapter.'
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* LocalStorage-based SessionAuthAdapter implementation
|
|
168
|
+
*
|
|
169
|
+
* Ready-to-use adapter that stores session in localStorage.
|
|
170
|
+
* Extend and override login() to add your API call.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```js
|
|
174
|
+
* class MyAuthAdapter extends LocalStorageSessionAuthAdapter {
|
|
175
|
+
* constructor() {
|
|
176
|
+
* super('my_app_auth') // localStorage key
|
|
177
|
+
* }
|
|
178
|
+
*
|
|
179
|
+
* async login({ username, password }) {
|
|
180
|
+
* const res = await fetch('/api/login', {
|
|
181
|
+
* method: 'POST',
|
|
182
|
+
* body: JSON.stringify({ username, password })
|
|
183
|
+
* })
|
|
184
|
+
* const data = await res.json()
|
|
185
|
+
* this.setSession(data.token, data.user)
|
|
186
|
+
* this.persist()
|
|
187
|
+
* return data
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} storageKey - localStorage key for session data
|
|
195
|
+
*/
|
|
196
|
+
constructor(storageKey = 'qdadm_auth') {
|
|
197
|
+
super()
|
|
198
|
+
this._storageKey = storageKey
|
|
199
|
+
this._restore()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Restore session from localStorage on init
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_restore() {
|
|
207
|
+
try {
|
|
208
|
+
const stored = localStorage.getItem(this._storageKey)
|
|
209
|
+
if (stored) {
|
|
210
|
+
const { token, user } = JSON.parse(stored)
|
|
211
|
+
this._token = token
|
|
212
|
+
this._user = user
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// Invalid stored data, ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Persist session to localStorage
|
|
221
|
+
* Call after login() to save session
|
|
222
|
+
* @protected
|
|
223
|
+
*/
|
|
224
|
+
persist() {
|
|
225
|
+
if (this._token && this._user) {
|
|
226
|
+
localStorage.setItem(this._storageKey, JSON.stringify({
|
|
227
|
+
token: this._token,
|
|
228
|
+
user: this._user
|
|
229
|
+
}))
|
|
230
|
+
} else {
|
|
231
|
+
localStorage.removeItem(this._storageKey)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Arrow functions to preserve `this` when used as callbacks
|
|
236
|
+
logout = () => {
|
|
237
|
+
this.clearSession()
|
|
238
|
+
localStorage.removeItem(this._storageKey)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
isAuthenticated = () => {
|
|
242
|
+
return !!this._token
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getToken = () => {
|
|
246
|
+
return this._token
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getUser = () => {
|
|
250
|
+
return this._user
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default SessionAuthAdapter
|
|
@@ -35,6 +35,7 @@ const route = useRoute()
|
|
|
35
35
|
const app = useApp()
|
|
36
36
|
const { navSections, isNavActive, sectionHasActiveItem, handleNavClick } = useNavigation()
|
|
37
37
|
const { isAuthenticated, user, logout, authEnabled } = useAuth()
|
|
38
|
+
const signals = inject('qdadmSignals', null)
|
|
38
39
|
|
|
39
40
|
// LocalStorage key for collapsed sections state (namespaced by app)
|
|
40
41
|
const STORAGE_KEY = computed(() => `${app.shortName.toLowerCase()}_nav_collapsed`)
|
|
@@ -150,6 +151,7 @@ const userSubtitle = computed(() => {
|
|
|
150
151
|
|
|
151
152
|
function handleLogout() {
|
|
152
153
|
logout()
|
|
154
|
+
signals?.emit('auth:logout', { reason: 'user' })
|
|
153
155
|
router.push({ name: 'login' })
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -90,11 +90,12 @@ const props = defineProps({
|
|
|
90
90
|
default: ''
|
|
91
91
|
},
|
|
92
92
|
/**
|
|
93
|
-
* Emit
|
|
93
|
+
* Emit auth:login signal on successful login
|
|
94
|
+
* Required for debug bar auth tracking and other signal listeners
|
|
94
95
|
*/
|
|
95
96
|
emitSignal: {
|
|
96
97
|
type: Boolean,
|
|
97
|
-
default:
|
|
98
|
+
default: true
|
|
98
99
|
}
|
|
99
100
|
})
|
|
100
101
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* For route guards or services, use authAdapter directly.
|
|
9
9
|
*
|
|
10
10
|
* Reactivity:
|
|
11
|
-
* - Listens to auth:login, auth:logout, auth:impersonate signals
|
|
11
|
+
* - Listens to auth:login, auth:logout, auth:impersonate, auth:impersonate:stop signals
|
|
12
12
|
* - User computed re-evaluates when these signals fire
|
|
13
13
|
* - No polling or manual refresh needed
|
|
14
14
|
*
|
|
@@ -54,6 +54,7 @@ export function useAuth() {
|
|
|
54
54
|
cleanups.push(signals.on('auth:login', () => { authTick.value++ }))
|
|
55
55
|
cleanups.push(signals.on('auth:logout', () => { authTick.value++ }))
|
|
56
56
|
cleanups.push(signals.on('auth:impersonate', () => { authTick.value++ }))
|
|
57
|
+
cleanups.push(signals.on('auth:impersonate:stop', () => { authTick.value++ }))
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// Cleanup signal subscriptions on unmount
|
|
@@ -120,7 +120,13 @@ export function useFormPageBuilder(config = {}) {
|
|
|
120
120
|
// Get EntityManager via orchestrator
|
|
121
121
|
const orchestrator = inject('qdadmOrchestrator')
|
|
122
122
|
if (!orchestrator) {
|
|
123
|
-
throw new Error(
|
|
123
|
+
throw new Error(
|
|
124
|
+
'[qdadm] Orchestrator not provided.\n' +
|
|
125
|
+
'Possible causes:\n' +
|
|
126
|
+
'1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
|
|
127
|
+
'2. Component used outside of qdadm app context\n' +
|
|
128
|
+
'3. Missing entityFactory in Kernel options'
|
|
129
|
+
)
|
|
124
130
|
}
|
|
125
131
|
const manager = orchestrator.get(entity)
|
|
126
132
|
|
|
@@ -143,7 +143,13 @@ export function useListPageBuilder(config = {}) {
|
|
|
143
143
|
// Get EntityManager via orchestrator
|
|
144
144
|
const orchestrator = inject('qdadmOrchestrator')
|
|
145
145
|
if (!orchestrator) {
|
|
146
|
-
throw new Error(
|
|
146
|
+
throw new Error(
|
|
147
|
+
'[qdadm] Orchestrator not provided.\n' +
|
|
148
|
+
'Possible causes:\n' +
|
|
149
|
+
'1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
|
|
150
|
+
'2. Component used outside of qdadm app context\n' +
|
|
151
|
+
'3. Missing entityFactory in Kernel options'
|
|
152
|
+
)
|
|
147
153
|
}
|
|
148
154
|
const manager = orchestrator.get(entity)
|
|
149
155
|
|
|
@@ -39,8 +39,11 @@ export class AuthCollector extends Collector {
|
|
|
39
39
|
this._ctx = null
|
|
40
40
|
this._signalCleanups = []
|
|
41
41
|
// Activity tracking for login/logout events
|
|
42
|
-
|
|
43
|
-
this.
|
|
42
|
+
// Keep recent events to show stacked (last N events)
|
|
43
|
+
this._recentEvents = [] // Array of { type: 'login'|'logout', timestamp: Date, seen: boolean }
|
|
44
|
+
this._maxEvents = 5
|
|
45
|
+
this._eventTtl = options.eventTtl ?? 60000 // Events expire after 60s by default
|
|
46
|
+
this._expiryTimer = null
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -70,19 +73,81 @@ export class AuthCollector extends Collector {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// Listen to auth events and track activity
|
|
73
|
-
const loginCleanup = signals.on('auth:login', () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this.
|
|
76
|
+
const loginCleanup = signals.on('auth:login', (event) => {
|
|
77
|
+
// QuarKernel wraps payload in KernelEvent - extract data from event.data
|
|
78
|
+
const data = event?.data || event
|
|
79
|
+
const user = data?.user || this._authAdapter?.getUser?.()
|
|
80
|
+
this._addEvent('login', { user })
|
|
77
81
|
})
|
|
78
82
|
this._signalCleanups.push(loginCleanup)
|
|
79
83
|
|
|
80
84
|
const logoutCleanup = signals.on('auth:logout', () => {
|
|
81
|
-
this.
|
|
82
|
-
this._lastEvent = 'logout'
|
|
83
|
-
this.notifyChange()
|
|
85
|
+
this._addEvent('logout')
|
|
84
86
|
})
|
|
85
87
|
this._signalCleanups.push(logoutCleanup)
|
|
88
|
+
|
|
89
|
+
const impersonateCleanup = signals.on('auth:impersonate', (payload) => {
|
|
90
|
+
this._addEvent('impersonate', payload)
|
|
91
|
+
})
|
|
92
|
+
this._signalCleanups.push(impersonateCleanup)
|
|
93
|
+
|
|
94
|
+
const impersonateStopCleanup = signals.on('auth:impersonate:stop', (payload) => {
|
|
95
|
+
this._addEvent('impersonate-stop', payload)
|
|
96
|
+
})
|
|
97
|
+
this._signalCleanups.push(impersonateStopCleanup)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Add an auth event to the recent events list
|
|
102
|
+
* @param {string} type - 'login' | 'logout' | 'impersonate' | 'impersonate-stop'
|
|
103
|
+
* @param {object} [data] - Optional event data (e.g., { user } for impersonate)
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
_addEvent(type, data = null) {
|
|
107
|
+
this._recentEvents.unshift({
|
|
108
|
+
type,
|
|
109
|
+
timestamp: new Date(),
|
|
110
|
+
id: Date.now(), // Unique ID for Vue key
|
|
111
|
+
seen: false,
|
|
112
|
+
data
|
|
113
|
+
})
|
|
114
|
+
// Keep only last N events
|
|
115
|
+
if (this._recentEvents.length > this._maxEvents) {
|
|
116
|
+
this._recentEvents.pop()
|
|
117
|
+
}
|
|
118
|
+
this._scheduleExpiry()
|
|
119
|
+
this.notifyChange()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Schedule event expiry check
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_scheduleExpiry() {
|
|
127
|
+
if (this._expiryTimer) return // Already scheduled
|
|
128
|
+
this._expiryTimer = setTimeout(() => {
|
|
129
|
+
this._expireOldEvents()
|
|
130
|
+
}, this._eventTtl)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove expired events
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_expireOldEvents() {
|
|
138
|
+
this._expiryTimer = null
|
|
139
|
+
const now = Date.now()
|
|
140
|
+
const before = this._recentEvents.length
|
|
141
|
+
this._recentEvents = this._recentEvents.filter(
|
|
142
|
+
e => (now - e.timestamp.getTime()) < this._eventTtl
|
|
143
|
+
)
|
|
144
|
+
if (this._recentEvents.length < before) {
|
|
145
|
+
this.notifyChange()
|
|
146
|
+
}
|
|
147
|
+
// Reschedule if still have events
|
|
148
|
+
if (this._recentEvents.length > 0) {
|
|
149
|
+
this._scheduleExpiry()
|
|
150
|
+
}
|
|
86
151
|
}
|
|
87
152
|
|
|
88
153
|
/**
|
|
@@ -90,6 +155,10 @@ export class AuthCollector extends Collector {
|
|
|
90
155
|
* @protected
|
|
91
156
|
*/
|
|
92
157
|
_doUninstall() {
|
|
158
|
+
if (this._expiryTimer) {
|
|
159
|
+
clearTimeout(this._expiryTimer)
|
|
160
|
+
this._expiryTimer = null
|
|
161
|
+
}
|
|
93
162
|
for (const cleanup of this._signalCleanups) {
|
|
94
163
|
if (typeof cleanup === 'function') cleanup()
|
|
95
164
|
}
|
|
@@ -99,11 +168,11 @@ export class AuthCollector extends Collector {
|
|
|
99
168
|
}
|
|
100
169
|
|
|
101
170
|
/**
|
|
102
|
-
* Get badge - show
|
|
103
|
-
* @returns {number}
|
|
171
|
+
* Get badge - show count of unseen auth events
|
|
172
|
+
* @returns {number} Number of unseen events
|
|
104
173
|
*/
|
|
105
174
|
getBadge() {
|
|
106
|
-
return this.
|
|
175
|
+
return this._recentEvents.filter(e => !e.seen).length
|
|
107
176
|
}
|
|
108
177
|
|
|
109
178
|
/**
|
|
@@ -111,24 +180,41 @@ export class AuthCollector extends Collector {
|
|
|
111
180
|
* @returns {boolean}
|
|
112
181
|
*/
|
|
113
182
|
hasActivity() {
|
|
114
|
-
return this.
|
|
183
|
+
return this._recentEvents.some(e => !e.seen)
|
|
115
184
|
}
|
|
116
185
|
|
|
117
186
|
/**
|
|
118
|
-
* Get
|
|
187
|
+
* Get all recent auth events (newest first)
|
|
188
|
+
* @returns {Array<{type: string, timestamp: Date, id: number, seen: boolean}>}
|
|
189
|
+
*/
|
|
190
|
+
getRecentEvents() {
|
|
191
|
+
return this._recentEvents
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the last auth event type (for backward compatibility)
|
|
119
196
|
* @returns {string|null} 'login' | 'logout' | null
|
|
120
197
|
*/
|
|
121
198
|
getLastEvent() {
|
|
122
|
-
return this.
|
|
199
|
+
return this._recentEvents[0]?.type || null
|
|
123
200
|
}
|
|
124
201
|
|
|
125
202
|
/**
|
|
126
|
-
* Mark
|
|
203
|
+
* Mark all events as seen (badge resets but events stay visible)
|
|
127
204
|
* Note: Does not call notifyChange() to avoid re-render loop
|
|
128
205
|
*/
|
|
129
206
|
markSeen() {
|
|
130
|
-
this.
|
|
131
|
-
|
|
207
|
+
for (const event of this._recentEvents) {
|
|
208
|
+
event.seen = true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clear all events (for explicit dismissal)
|
|
214
|
+
*/
|
|
215
|
+
clearEvents() {
|
|
216
|
+
this._recentEvents = []
|
|
217
|
+
this.notifyChange()
|
|
132
218
|
}
|
|
133
219
|
|
|
134
220
|
/**
|
package/src/debug/Collector.js
CHANGED
|
@@ -179,10 +179,11 @@ export class Collector {
|
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* Get all entries
|
|
182
|
+
* Returns a shallow copy to trigger Vue reactivity when used in computed
|
|
182
183
|
* @returns {Array<object>} All recorded entries
|
|
183
184
|
*/
|
|
184
185
|
getEntries() {
|
|
185
|
-
return this.entries
|
|
186
|
+
return [...this.entries]
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
/**
|
|
@@ -219,10 +219,11 @@ export class EntitiesCollector extends Collector {
|
|
|
219
219
|
idField: manager.idField,
|
|
220
220
|
|
|
221
221
|
// Storage info
|
|
222
|
+
// Prefer instance capabilities (may include requiresAuth) over static ones
|
|
222
223
|
storage: {
|
|
223
224
|
type: storage?.constructor?.name || 'None',
|
|
224
225
|
endpoint: storage?.endpoint || storage?._endpoint || null,
|
|
225
|
-
capabilities: storage?.constructor?.capabilities || {}
|
|
226
|
+
capabilities: storage?.capabilities || storage?.constructor?.capabilities || {}
|
|
226
227
|
},
|
|
227
228
|
|
|
228
229
|
// Cache info
|
|
@@ -373,4 +374,30 @@ export class EntitiesCollector extends Collector {
|
|
|
373
374
|
markEntitySeen(entityName) {
|
|
374
375
|
this._activeEntities.delete(entityName)
|
|
375
376
|
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Test fetch data from a specific entity
|
|
380
|
+
* Used to test auth protection - will throw 401 if not authenticated
|
|
381
|
+
* @param {string} entityName - Entity name
|
|
382
|
+
* @returns {Promise<{success: boolean, count?: number, error?: string, status?: number}>}
|
|
383
|
+
*/
|
|
384
|
+
async testFetch(entityName) {
|
|
385
|
+
if (!this._orchestrator) {
|
|
386
|
+
return { success: false, error: 'No orchestrator' }
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const manager = this._orchestrator.get(entityName)
|
|
390
|
+
const result = await manager.storage.list({ page: 1, page_size: 1 })
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
count: result.total ?? result.items?.length ?? 0
|
|
394
|
+
}
|
|
395
|
+
} catch (e) {
|
|
396
|
+
return {
|
|
397
|
+
success: false,
|
|
398
|
+
error: e.message,
|
|
399
|
+
status: e.status
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
376
403
|
}
|