@coolms/core-angular 2.0.0-alpha.3 → 2.0.0-alpha.4
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/CHANGELOG.md +88 -0
- package/README.md +16 -15
- package/fesm2022/coolms-core-angular.mjs +1971 -2008
- package/fesm2022/coolms-core-angular.mjs.map +1 -1
- package/package.json +4 -1
- package/types/coolms-core-angular.d.ts +1593 -995
|
@@ -1,1542 +1,1483 @@
|
|
|
1
|
-
import * as i0 from
|
|
2
|
-
import {
|
|
3
|
-
import { HttpClient,
|
|
4
|
-
import { Action, Selector, State, Store } from
|
|
5
|
-
import { __decorate } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { takeUntilDestroyed } from
|
|
9
|
-
import { Router } from
|
|
10
|
-
import { Location, NgIf } from
|
|
11
|
-
import * as i1 from
|
|
12
|
-
import { FormsModule } from
|
|
13
|
-
|
|
14
|
-
// Typed contract — must mirror ApiManifest PHP VOs exactly.
|
|
15
|
-
// No `any` anywhere.
|
|
16
|
-
/**
|
|
17
|
-
* Replaces {param} tokens in a URL pattern with encoded values.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* resolvePattern(manifest.sections!.item, { id: '123' })
|
|
21
|
-
* // -> '/api/v1/sections/123'
|
|
22
|
-
*/
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, Component, DestroyRef, Injectable, InjectionToken, computed, effect, inject, input, makeEnvironmentProviders, provideAppInitializer, signal, untracked } from "@angular/core";
|
|
3
|
+
import { HttpClient, HttpContext, HttpContextToken, HttpErrorResponse, HttpParams } from "@angular/common/http";
|
|
4
|
+
import { Action, Selector, State, Store, provideStore } from "@ngxs/store";
|
|
5
|
+
import { __decorate } from "tslib";
|
|
6
|
+
import { ReplaySubject, Subject, catchError, debounceTime, defer, finalize, firstValueFrom, from, map, of, shareReplay, switchMap, take, tap, throwError } from "rxjs";
|
|
7
|
+
import { catchError as catchError$1, map as map$1, shareReplay as shareReplay$1, switchMap as switchMap$1, tap as tap$1 } from "rxjs/operators";
|
|
8
|
+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
9
|
+
import { Router } from "@angular/router";
|
|
10
|
+
import { Location, NgIf } from "@angular/common";
|
|
11
|
+
import * as i1 from "@angular/forms";
|
|
12
|
+
import { FormsModule } from "@angular/forms";
|
|
23
13
|
function resolvePattern(pattern, params) {
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Typed contract — mirrors FormRenderDefinition PHP VOs exactly.
|
|
28
|
-
// No `any` anywhere.
|
|
29
|
-
|
|
30
|
-
// --- Action ------------------------------------------------------------------
|
|
31
|
-
class SetAppConfig {
|
|
32
|
-
config;
|
|
33
|
-
static type = '[AppConfig] Set';
|
|
34
|
-
constructor(config) {
|
|
35
|
-
this.config = config;
|
|
36
|
-
}
|
|
14
|
+
return Object.entries(params).reduce((url, [key, value]) => url.replace(`{${key}}`, encodeURIComponent(value)), pattern);
|
|
37
15
|
}
|
|
16
|
+
var SetAppConfig = class {
|
|
17
|
+
config;
|
|
18
|
+
static type = "[AppConfig] Set";
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
38
23
|
let AppConfigState = class AppConfigState {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
24
|
+
set(ctx, { config }) {
|
|
25
|
+
ctx.setState({
|
|
26
|
+
config,
|
|
27
|
+
loaded: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
static manifest(state) {
|
|
31
|
+
return state.config?.manifest ?? null;
|
|
32
|
+
}
|
|
33
|
+
static viewers(state) {
|
|
34
|
+
return state.config?.manifest.viewers ?? null;
|
|
35
|
+
}
|
|
36
|
+
static loaded(state) {
|
|
37
|
+
return state.loaded;
|
|
38
|
+
}
|
|
39
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
40
|
+
minVersion: "12.0.0",
|
|
41
|
+
version: "22.2.0",
|
|
42
|
+
ngImport: i0,
|
|
43
|
+
type: AppConfigState,
|
|
44
|
+
deps: [],
|
|
45
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
46
|
+
});
|
|
47
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
48
|
+
minVersion: "12.0.0",
|
|
49
|
+
version: "22.2.0",
|
|
50
|
+
ngImport: i0,
|
|
51
|
+
type: AppConfigState
|
|
52
|
+
});
|
|
58
53
|
};
|
|
59
|
-
__decorate([
|
|
60
|
-
|
|
61
|
-
], AppConfigState
|
|
62
|
-
__decorate([
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
* Persist terminal panel height.
|
|
432
|
-
*/
|
|
433
|
-
setTerminalHeight(height) {
|
|
434
|
-
const all = this.load();
|
|
435
|
-
const terminal = (all['terminal'] ?? {});
|
|
436
|
-
terminal['height'] = Math.round(height);
|
|
437
|
-
all['terminal'] = terminal;
|
|
438
|
-
this.save(all);
|
|
439
|
-
}
|
|
440
|
-
// -- Panel ----------------------------------------------------------------
|
|
441
|
-
/**
|
|
442
|
-
* Get persisted state for a resizable panel identified by key.
|
|
443
|
-
* Used by ExplorerLayoutComponent to persist panel widths across sessions.
|
|
444
|
-
*/
|
|
445
|
-
getPanelState(key) {
|
|
446
|
-
try {
|
|
447
|
-
const all = this.load();
|
|
448
|
-
const panel = all['panel'];
|
|
449
|
-
if (!panel)
|
|
450
|
-
return null;
|
|
451
|
-
const p = panel[key];
|
|
452
|
-
if (!p || typeof p !== 'object')
|
|
453
|
-
return null;
|
|
454
|
-
const { width, collapsed } = p;
|
|
455
|
-
return typeof width === 'number' && typeof collapsed === 'boolean'
|
|
456
|
-
? { width, collapsed }
|
|
457
|
-
: null;
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Persist generic panel state by key.
|
|
465
|
-
* Applies to localStorage instantly; server sync is debounced.
|
|
466
|
-
*/
|
|
467
|
-
setPanelState(key, width, collapsed) {
|
|
468
|
-
const all = this.load();
|
|
469
|
-
const panel = (all['panel'] ?? {});
|
|
470
|
-
panel[key] = { width: Math.round(width), collapsed };
|
|
471
|
-
all['panel'] = panel;
|
|
472
|
-
this.save(all);
|
|
473
|
-
}
|
|
474
|
-
// -- Page state -----------------------------------------------------------
|
|
475
|
-
/**
|
|
476
|
-
* Get all persisted state for a page namespace (e.g. 'vfs', 'media', 'nav').
|
|
477
|
-
*/
|
|
478
|
-
getPageState(pageKey) {
|
|
479
|
-
try {
|
|
480
|
-
const all = this.load();
|
|
481
|
-
const page = all['page'];
|
|
482
|
-
if (!page)
|
|
483
|
-
return null;
|
|
484
|
-
return page[pageKey] ?? null;
|
|
485
|
-
}
|
|
486
|
-
catch {
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Merge partial state into a page namespace.
|
|
492
|
-
* Existing keys in the namespace are preserved; only provided keys are updated.
|
|
493
|
-
* Applies to localStorage instantly; server sync is debounced.
|
|
494
|
-
*/
|
|
495
|
-
setPageState(pageKey, state) {
|
|
496
|
-
const all = this.load();
|
|
497
|
-
const page = (all['page'] ?? {});
|
|
498
|
-
page[pageKey] = { ...(page[pageKey] ?? {}), ...state };
|
|
499
|
-
all['page'] = page;
|
|
500
|
-
this.save(all);
|
|
501
|
-
}
|
|
502
|
-
// -- Media (legacy pass-through — kept for compatibility) -----------------
|
|
503
|
-
/** @deprecated Use getPageState('media') instead */
|
|
504
|
-
getMediaViewMode() {
|
|
505
|
-
return this.getPageState('media')?.viewMode ?? null;
|
|
506
|
-
}
|
|
507
|
-
/** @deprecated Use setPageState('media', ...) instead */
|
|
508
|
-
setMediaViewMode(mode) {
|
|
509
|
-
this.setPageState('media', { viewMode: mode });
|
|
510
|
-
}
|
|
511
|
-
/** @deprecated Use getPanelState('media_panel_left') instead */
|
|
512
|
-
getMediaCollectionsPanel() {
|
|
513
|
-
return this.getPanelState('media_panel_left');
|
|
514
|
-
}
|
|
515
|
-
/** @deprecated Use setPanelState('media_panel_left', ...) instead */
|
|
516
|
-
setMediaCollectionsPanel(width, collapsed) {
|
|
517
|
-
this.setPanelState('media_panel_left', width, collapsed);
|
|
518
|
-
}
|
|
519
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UserPreferencesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
520
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UserPreferencesService, providedIn: 'root' });
|
|
521
|
-
}
|
|
522
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: UserPreferencesService, decorators: [{
|
|
523
|
-
type: Injectable,
|
|
524
|
-
args: [{ providedIn: 'root' }]
|
|
525
|
-
}], ctorParameters: () => [] });
|
|
526
|
-
|
|
527
|
-
const STORAGE_KEY$1 = 'coolms_token';
|
|
54
|
+
__decorate([Action(SetAppConfig)], AppConfigState.prototype, "set", null);
|
|
55
|
+
__decorate([Selector()], AppConfigState, "manifest", null);
|
|
56
|
+
__decorate([Selector()], AppConfigState, "viewers", null);
|
|
57
|
+
__decorate([Selector()], AppConfigState, "loaded", null);
|
|
58
|
+
AppConfigState = __decorate([State({
|
|
59
|
+
name: "appConfig",
|
|
60
|
+
defaults: {
|
|
61
|
+
config: null,
|
|
62
|
+
loaded: false
|
|
63
|
+
}
|
|
64
|
+
})], AppConfigState);
|
|
65
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
66
|
+
minVersion: "12.0.0",
|
|
67
|
+
version: "22.2.0",
|
|
68
|
+
ngImport: i0,
|
|
69
|
+
type: AppConfigState,
|
|
70
|
+
decorators: [{ type: Injectable }],
|
|
71
|
+
propDecorators: { set: [] }
|
|
72
|
+
});
|
|
73
|
+
var IdentityApiClient = class IdentityApiClient {
|
|
74
|
+
http = inject(HttpClient);
|
|
75
|
+
store = inject(Store);
|
|
76
|
+
get manifest() {
|
|
77
|
+
const m = this.store.selectSnapshot(AppConfigState.manifest);
|
|
78
|
+
if (!m) throw new Error("ApiManifest not loaded — call AppInitService.load() first");
|
|
79
|
+
return m;
|
|
80
|
+
}
|
|
81
|
+
login(identifier, password) {
|
|
82
|
+
return this.http.post(this.manifest.auth.login, {
|
|
83
|
+
identifier,
|
|
84
|
+
password
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
refresh(refreshToken) {
|
|
88
|
+
return this.http.post(this.manifest.auth.refresh, { refreshToken });
|
|
89
|
+
}
|
|
90
|
+
logout() {
|
|
91
|
+
return this.http.post(this.manifest.auth.logout, {});
|
|
92
|
+
}
|
|
93
|
+
me() {
|
|
94
|
+
return this.http.get(this.manifest.auth.me);
|
|
95
|
+
}
|
|
96
|
+
getSettings() {
|
|
97
|
+
return this.http.get(this.manifest.identity.settingsUrl, { headers: { Accept: "application/json" } });
|
|
98
|
+
}
|
|
99
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
100
|
+
minVersion: "12.0.0",
|
|
101
|
+
version: "22.2.0",
|
|
102
|
+
ngImport: i0,
|
|
103
|
+
type: IdentityApiClient,
|
|
104
|
+
deps: [],
|
|
105
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
106
|
+
});
|
|
107
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
108
|
+
minVersion: "12.0.0",
|
|
109
|
+
version: "22.2.0",
|
|
110
|
+
ngImport: i0,
|
|
111
|
+
type: IdentityApiClient,
|
|
112
|
+
providedIn: "root"
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
116
|
+
minVersion: "12.0.0",
|
|
117
|
+
version: "22.2.0",
|
|
118
|
+
ngImport: i0,
|
|
119
|
+
type: IdentityApiClient,
|
|
120
|
+
decorators: [{
|
|
121
|
+
type: Injectable,
|
|
122
|
+
args: [{ providedIn: "root" }]
|
|
123
|
+
}]
|
|
124
|
+
});
|
|
125
|
+
var RealtimeTokenClient = class RealtimeTokenClient {
|
|
126
|
+
http = inject(HttpClient);
|
|
127
|
+
store = inject(Store);
|
|
128
|
+
get manifest() {
|
|
129
|
+
const m = this.store.selectSnapshot(AppConfigState.manifest);
|
|
130
|
+
if (!m) throw new Error("ApiManifest not loaded — call AppInitService.load() first");
|
|
131
|
+
return m;
|
|
132
|
+
}
|
|
133
|
+
connectionToken() {
|
|
134
|
+
return this.http.post(this.manifest.apiBase + "/centrifugo/connection-token", {});
|
|
135
|
+
}
|
|
136
|
+
subscriptionToken(channel) {
|
|
137
|
+
return this.http.post(this.manifest.apiBase + "/centrifugo/subscription-token", { channel });
|
|
138
|
+
}
|
|
139
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
140
|
+
minVersion: "12.0.0",
|
|
141
|
+
version: "22.2.0",
|
|
142
|
+
ngImport: i0,
|
|
143
|
+
type: RealtimeTokenClient,
|
|
144
|
+
deps: [],
|
|
145
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
146
|
+
});
|
|
147
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
148
|
+
minVersion: "12.0.0",
|
|
149
|
+
version: "22.2.0",
|
|
150
|
+
ngImport: i0,
|
|
151
|
+
type: RealtimeTokenClient,
|
|
152
|
+
providedIn: "root"
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
156
|
+
minVersion: "12.0.0",
|
|
157
|
+
version: "22.2.0",
|
|
158
|
+
ngImport: i0,
|
|
159
|
+
type: RealtimeTokenClient,
|
|
160
|
+
decorators: [{
|
|
161
|
+
type: Injectable,
|
|
162
|
+
args: [{ providedIn: "root" }]
|
|
163
|
+
}]
|
|
164
|
+
});
|
|
165
|
+
var Login = class {
|
|
166
|
+
identifier;
|
|
167
|
+
password;
|
|
168
|
+
static type = "[Auth] Login";
|
|
169
|
+
constructor(identifier, password) {
|
|
170
|
+
this.identifier = identifier;
|
|
171
|
+
this.password = password;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var Logout = class {
|
|
175
|
+
static type = "[Auth] Logout";
|
|
176
|
+
};
|
|
177
|
+
var RestoreSession = class {
|
|
178
|
+
static type = "[Auth] Restore Session";
|
|
179
|
+
};
|
|
180
|
+
var SetTokens = class {
|
|
181
|
+
response;
|
|
182
|
+
static type = "[Auth] Set Tokens";
|
|
183
|
+
constructor(response) {
|
|
184
|
+
this.response = response;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var PatchCurrentUser = class {
|
|
188
|
+
patch;
|
|
189
|
+
static type = "[Auth] Patch Current User";
|
|
190
|
+
constructor(patch) {
|
|
191
|
+
this.patch = patch;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var UserPreferencesService = class UserPreferencesService {
|
|
195
|
+
http = inject(HttpClient);
|
|
196
|
+
store = inject(Store);
|
|
197
|
+
destroyRef = inject(DestroyRef);
|
|
198
|
+
STORAGE_KEY = "coolms_ui_prefs";
|
|
199
|
+
SYNC_DELAY = 2e3;
|
|
200
|
+
syncSubject = new Subject();
|
|
201
|
+
constructor() {
|
|
202
|
+
this.syncSubject.pipe(debounceTime(this.SYNC_DELAY), takeUntilDestroyed(this.destroyRef)).subscribe((prefs) => this.syncToServer(prefs));
|
|
203
|
+
}
|
|
204
|
+
save(all) {
|
|
205
|
+
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(all));
|
|
206
|
+
this.syncSubject.next(all);
|
|
207
|
+
}
|
|
208
|
+
load() {
|
|
209
|
+
try {
|
|
210
|
+
const raw = localStorage.getItem(this.STORAGE_KEY);
|
|
211
|
+
const all = raw ? JSON.parse(raw) : {};
|
|
212
|
+
return this.migrate(all);
|
|
213
|
+
} catch {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
migrate(all) {
|
|
218
|
+
let dirty = false;
|
|
219
|
+
if (typeof all["terminal_height"] === "number") {
|
|
220
|
+
const terminal = all["terminal"] ?? {};
|
|
221
|
+
terminal["height"] = all["terminal_height"];
|
|
222
|
+
all["terminal"] = terminal;
|
|
223
|
+
delete all["terminal_height"];
|
|
224
|
+
dirty = true;
|
|
225
|
+
}
|
|
226
|
+
if (typeof all["media_view_mode"] === "string") {
|
|
227
|
+
const page = all["page"] ?? {};
|
|
228
|
+
const media = page["media"] ?? {};
|
|
229
|
+
media["viewMode"] = all["media_view_mode"];
|
|
230
|
+
page["media"] = media;
|
|
231
|
+
all["page"] = page;
|
|
232
|
+
delete all["media_view_mode"];
|
|
233
|
+
dirty = true;
|
|
234
|
+
}
|
|
235
|
+
if (all["media_collections_panel"] && typeof all["media_collections_panel"] === "object") {
|
|
236
|
+
const panel = all["panel"] ?? {};
|
|
237
|
+
panel["media_panel_left"] = all["media_collections_panel"];
|
|
238
|
+
all["panel"] = panel;
|
|
239
|
+
delete all["media_collections_panel"];
|
|
240
|
+
dirty = true;
|
|
241
|
+
}
|
|
242
|
+
const NAMESPACES = /* @__PURE__ */ new Set([
|
|
243
|
+
"grids",
|
|
244
|
+
"panel",
|
|
245
|
+
"page",
|
|
246
|
+
"terminal",
|
|
247
|
+
"nav"
|
|
248
|
+
]);
|
|
249
|
+
for (const ns of NAMESPACES) if (Array.isArray(all[ns])) {
|
|
250
|
+
all[ns] = {};
|
|
251
|
+
dirty = true;
|
|
252
|
+
}
|
|
253
|
+
for (const key of Object.keys(all)) {
|
|
254
|
+
if (NAMESPACES.has(key)) continue;
|
|
255
|
+
const val = all[key];
|
|
256
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
257
|
+
const obj = val;
|
|
258
|
+
if ("columns" in obj || "columnWidths" in obj) {
|
|
259
|
+
const grids = all["grids"] ?? {};
|
|
260
|
+
grids[key] = obj;
|
|
261
|
+
all["grids"] = grids;
|
|
262
|
+
delete all[key];
|
|
263
|
+
dirty = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (dirty) localStorage.setItem(this.STORAGE_KEY, JSON.stringify(all));
|
|
268
|
+
return all;
|
|
269
|
+
}
|
|
270
|
+
loadFromServer(uiPrefs) {
|
|
271
|
+
if (Object.keys(uiPrefs).length === 0) return;
|
|
272
|
+
try {
|
|
273
|
+
const raw = localStorage.getItem(this.STORAGE_KEY);
|
|
274
|
+
if (!raw) localStorage.setItem(this.STORAGE_KEY, JSON.stringify(uiPrefs));
|
|
275
|
+
else {
|
|
276
|
+
const local = JSON.parse(raw);
|
|
277
|
+
const merged = {
|
|
278
|
+
...uiPrefs,
|
|
279
|
+
...local
|
|
280
|
+
};
|
|
281
|
+
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(merged));
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(uiPrefs));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
syncToServer(prefs) {
|
|
288
|
+
const url = this.store.selectSnapshot(AppConfigState.manifest)?.auth?.preferencesUrl;
|
|
289
|
+
if (!url) return;
|
|
290
|
+
this.http.patch(url, { uiPrefs: prefs }, { headers: { "Content-Type": "application/merge-patch+json" } }).subscribe({ error: (err) => console.warn("Preferences sync failed:", err) });
|
|
291
|
+
}
|
|
292
|
+
getGridPref(gridId) {
|
|
293
|
+
try {
|
|
294
|
+
const grids = this.load()["grids"];
|
|
295
|
+
if (!grids) return null;
|
|
296
|
+
return grids[gridId] ?? null;
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
setColumns(gridId, columns) {
|
|
302
|
+
this.updateGrid(gridId, (existing) => ({
|
|
303
|
+
...existing,
|
|
304
|
+
columns
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
setColumnWidth(gridId, field, width) {
|
|
308
|
+
this.updateGrid(gridId, (existing) => ({
|
|
309
|
+
...existing,
|
|
310
|
+
columnWidths: {
|
|
311
|
+
...existing.columnWidths ?? {},
|
|
312
|
+
[field]: Math.max(60, width)
|
|
313
|
+
}
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
resetGrid(gridId) {
|
|
317
|
+
const all = this.load();
|
|
318
|
+
const grids = all["grids"] ?? {};
|
|
319
|
+
delete grids[gridId];
|
|
320
|
+
all["grids"] = grids;
|
|
321
|
+
this.save(all);
|
|
322
|
+
this.syncToServer(all);
|
|
323
|
+
}
|
|
324
|
+
updateGrid(gridId, updater) {
|
|
325
|
+
const all = this.load();
|
|
326
|
+
const grids = all["grids"] ?? {};
|
|
327
|
+
grids[gridId] = updater(grids[gridId] ?? {});
|
|
328
|
+
all["grids"] = grids;
|
|
329
|
+
this.save(all);
|
|
330
|
+
}
|
|
331
|
+
getTerminalHeight() {
|
|
332
|
+
const terminal = this.load()["terminal"];
|
|
333
|
+
return typeof terminal?.["height"] === "number" ? terminal["height"] : null;
|
|
334
|
+
}
|
|
335
|
+
setTerminalHeight(height) {
|
|
336
|
+
const all = this.load();
|
|
337
|
+
const terminal = all["terminal"] ?? {};
|
|
338
|
+
terminal["height"] = Math.round(height);
|
|
339
|
+
all["terminal"] = terminal;
|
|
340
|
+
this.save(all);
|
|
341
|
+
}
|
|
342
|
+
getPanelState(key) {
|
|
343
|
+
try {
|
|
344
|
+
const panel = this.load()["panel"];
|
|
345
|
+
if (!panel) return null;
|
|
346
|
+
const p = panel[key];
|
|
347
|
+
if (!p || typeof p !== "object") return null;
|
|
348
|
+
const { width, collapsed } = p;
|
|
349
|
+
return typeof width === "number" && typeof collapsed === "boolean" ? {
|
|
350
|
+
width,
|
|
351
|
+
collapsed
|
|
352
|
+
} : null;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
setPanelState(key, width, collapsed) {
|
|
358
|
+
const all = this.load();
|
|
359
|
+
const panel = all["panel"] ?? {};
|
|
360
|
+
panel[key] = {
|
|
361
|
+
width: Math.round(width),
|
|
362
|
+
collapsed
|
|
363
|
+
};
|
|
364
|
+
all["panel"] = panel;
|
|
365
|
+
this.save(all);
|
|
366
|
+
}
|
|
367
|
+
getPageState(pageKey) {
|
|
368
|
+
try {
|
|
369
|
+
const page = this.load()["page"];
|
|
370
|
+
if (!page) return null;
|
|
371
|
+
return page[pageKey] ?? null;
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
setPageState(pageKey, state) {
|
|
377
|
+
const all = this.load();
|
|
378
|
+
const page = all["page"] ?? {};
|
|
379
|
+
page[pageKey] = {
|
|
380
|
+
...page[pageKey] ?? {},
|
|
381
|
+
...state
|
|
382
|
+
};
|
|
383
|
+
all["page"] = page;
|
|
384
|
+
this.save(all);
|
|
385
|
+
}
|
|
386
|
+
getMediaViewMode() {
|
|
387
|
+
return this.getPageState("media")?.viewMode ?? null;
|
|
388
|
+
}
|
|
389
|
+
setMediaViewMode(mode) {
|
|
390
|
+
this.setPageState("media", { viewMode: mode });
|
|
391
|
+
}
|
|
392
|
+
getMediaCollectionsPanel() {
|
|
393
|
+
return this.getPanelState("media_panel_left");
|
|
394
|
+
}
|
|
395
|
+
setMediaCollectionsPanel(width, collapsed) {
|
|
396
|
+
this.setPanelState("media_panel_left", width, collapsed);
|
|
397
|
+
}
|
|
398
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
399
|
+
minVersion: "12.0.0",
|
|
400
|
+
version: "22.2.0",
|
|
401
|
+
ngImport: i0,
|
|
402
|
+
type: UserPreferencesService,
|
|
403
|
+
deps: [],
|
|
404
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
405
|
+
});
|
|
406
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
407
|
+
minVersion: "12.0.0",
|
|
408
|
+
version: "22.2.0",
|
|
409
|
+
ngImport: i0,
|
|
410
|
+
type: UserPreferencesService,
|
|
411
|
+
providedIn: "root"
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
415
|
+
minVersion: "12.0.0",
|
|
416
|
+
version: "22.2.0",
|
|
417
|
+
ngImport: i0,
|
|
418
|
+
type: UserPreferencesService,
|
|
419
|
+
decorators: [{
|
|
420
|
+
type: Injectable,
|
|
421
|
+
args: [{ providedIn: "root" }]
|
|
422
|
+
}],
|
|
423
|
+
ctorParameters: () => []
|
|
424
|
+
});
|
|
425
|
+
const STORAGE_KEY$2 = "coolms_token";
|
|
528
426
|
let AuthState = class AuthState {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
static currentUser(state) {
|
|
628
|
-
return state.user;
|
|
629
|
-
}
|
|
630
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AuthState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
631
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AuthState });
|
|
427
|
+
api = inject(IdentityApiClient);
|
|
428
|
+
prefs = inject(UserPreferencesService);
|
|
429
|
+
login(ctx, { identifier, password }) {
|
|
430
|
+
return this.api.login(identifier, password).pipe(tap$1((res) => {
|
|
431
|
+
const state = {
|
|
432
|
+
accessToken: res.accessToken,
|
|
433
|
+
refreshToken: res.refreshToken,
|
|
434
|
+
expiresAt: res.expiresAt,
|
|
435
|
+
user: res.user ?? null
|
|
436
|
+
};
|
|
437
|
+
ctx.setState(state);
|
|
438
|
+
localStorage.setItem(STORAGE_KEY$2, JSON.stringify(state));
|
|
439
|
+
}), switchMap$1(() => this.api.me().pipe(catchError(() => of(null)))), tap$1((me) => {
|
|
440
|
+
if (!me) return;
|
|
441
|
+
const updated = {
|
|
442
|
+
...ctx.getState(),
|
|
443
|
+
user: me
|
|
444
|
+
};
|
|
445
|
+
ctx.setState(updated);
|
|
446
|
+
localStorage.setItem(STORAGE_KEY$2, JSON.stringify(updated));
|
|
447
|
+
if (me.uiPrefs) this.prefs.loadFromServer(me.uiPrefs);
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
logout(ctx) {
|
|
451
|
+
ctx.setState({
|
|
452
|
+
accessToken: null,
|
|
453
|
+
refreshToken: null,
|
|
454
|
+
expiresAt: null,
|
|
455
|
+
user: null
|
|
456
|
+
});
|
|
457
|
+
localStorage.removeItem(STORAGE_KEY$2);
|
|
458
|
+
}
|
|
459
|
+
restore(ctx) {
|
|
460
|
+
const raw = localStorage.getItem(STORAGE_KEY$2);
|
|
461
|
+
if (!raw) return;
|
|
462
|
+
try {
|
|
463
|
+
const state = JSON.parse(raw);
|
|
464
|
+
if (state.accessToken || state.refreshToken) {
|
|
465
|
+
ctx.setState(state);
|
|
466
|
+
if (state.user?.uiPrefs) this.prefs.loadFromServer(state.user.uiPrefs);
|
|
467
|
+
} else localStorage.removeItem(STORAGE_KEY$2);
|
|
468
|
+
} catch {
|
|
469
|
+
localStorage.removeItem(STORAGE_KEY$2);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
setTokens(ctx, { response }) {
|
|
473
|
+
const state = {
|
|
474
|
+
accessToken: response.accessToken,
|
|
475
|
+
refreshToken: response.refreshToken,
|
|
476
|
+
expiresAt: response.expiresAt,
|
|
477
|
+
user: response.user ?? ctx.getState().user
|
|
478
|
+
};
|
|
479
|
+
ctx.setState(state);
|
|
480
|
+
localStorage.setItem(STORAGE_KEY$2, JSON.stringify(state));
|
|
481
|
+
}
|
|
482
|
+
patchCurrentUser(ctx, { patch }) {
|
|
483
|
+
const current = ctx.getState();
|
|
484
|
+
if (!current.user) return;
|
|
485
|
+
const updated = {
|
|
486
|
+
...current,
|
|
487
|
+
user: {
|
|
488
|
+
...current.user,
|
|
489
|
+
...patch
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
ctx.setState(updated);
|
|
493
|
+
localStorage.setItem(STORAGE_KEY$2, JSON.stringify(updated));
|
|
494
|
+
}
|
|
495
|
+
static isAuthenticated(state) {
|
|
496
|
+
return null !== state.accessToken;
|
|
497
|
+
}
|
|
498
|
+
static accessToken(state) {
|
|
499
|
+
return state.accessToken;
|
|
500
|
+
}
|
|
501
|
+
static refreshToken(state) {
|
|
502
|
+
return state.refreshToken;
|
|
503
|
+
}
|
|
504
|
+
static expiresAt(state) {
|
|
505
|
+
return state.expiresAt;
|
|
506
|
+
}
|
|
507
|
+
static currentUser(state) {
|
|
508
|
+
return state.user;
|
|
509
|
+
}
|
|
510
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
511
|
+
minVersion: "12.0.0",
|
|
512
|
+
version: "22.2.0",
|
|
513
|
+
ngImport: i0,
|
|
514
|
+
type: AuthState,
|
|
515
|
+
deps: [],
|
|
516
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
517
|
+
});
|
|
518
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
519
|
+
minVersion: "12.0.0",
|
|
520
|
+
version: "22.2.0",
|
|
521
|
+
ngImport: i0,
|
|
522
|
+
type: AuthState
|
|
523
|
+
});
|
|
632
524
|
};
|
|
633
|
-
__decorate([
|
|
634
|
-
|
|
635
|
-
], AuthState.prototype, "
|
|
636
|
-
__decorate([
|
|
637
|
-
|
|
638
|
-
], AuthState
|
|
639
|
-
__decorate([
|
|
640
|
-
|
|
641
|
-
], AuthState
|
|
642
|
-
__decorate([
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
],
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
],
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
* signalling that some other tab refreshed for us. Null
|
|
768
|
-
* otherwise.
|
|
769
|
-
*/
|
|
770
|
-
tryConsumeFreshState() {
|
|
771
|
-
const accessToken = this.store.selectSnapshot(AuthState.accessToken);
|
|
772
|
-
const expiresAtStr = this.store.selectSnapshot(AuthState.expiresAt);
|
|
773
|
-
if (accessToken === null || expiresAtStr === null) {
|
|
774
|
-
return null;
|
|
775
|
-
}
|
|
776
|
-
const expiresAtMs = new Date(expiresAtStr).getTime();
|
|
777
|
-
if (Number.isNaN(expiresAtMs)) {
|
|
778
|
-
return null;
|
|
779
|
-
}
|
|
780
|
-
if (Date.now() < expiresAtMs - FRESH_TOKEN_GRACE_MS) {
|
|
781
|
-
return accessToken;
|
|
782
|
-
}
|
|
783
|
-
return null;
|
|
784
|
-
}
|
|
785
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AuthRefreshCoordinator, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
786
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AuthRefreshCoordinator, providedIn: 'root' });
|
|
787
|
-
}
|
|
788
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AuthRefreshCoordinator, decorators: [{
|
|
789
|
-
type: Injectable,
|
|
790
|
-
args: [{ providedIn: 'root' }]
|
|
791
|
-
}] });
|
|
792
|
-
|
|
793
|
-
// -- Opt-out context token -----------------------------------------------------
|
|
794
|
-
//
|
|
795
|
-
// Set BYPASS_AUTH to true on any HttpRequest to skip adding the Authorization
|
|
796
|
-
// header entirely. Used by AppInitService for the bootstrap config request so
|
|
797
|
-
// that a stale localStorage token does not cause a 401 on a public endpoint.
|
|
525
|
+
__decorate([Action(Login)], AuthState.prototype, "login", null);
|
|
526
|
+
__decorate([Action(Logout)], AuthState.prototype, "logout", null);
|
|
527
|
+
__decorate([Action(RestoreSession)], AuthState.prototype, "restore", null);
|
|
528
|
+
__decorate([Action(SetTokens)], AuthState.prototype, "setTokens", null);
|
|
529
|
+
__decorate([Action(PatchCurrentUser)], AuthState.prototype, "patchCurrentUser", null);
|
|
530
|
+
__decorate([Selector()], AuthState, "isAuthenticated", null);
|
|
531
|
+
__decorate([Selector()], AuthState, "accessToken", null);
|
|
532
|
+
__decorate([Selector()], AuthState, "refreshToken", null);
|
|
533
|
+
__decorate([Selector()], AuthState, "expiresAt", null);
|
|
534
|
+
__decorate([Selector()], AuthState, "currentUser", null);
|
|
535
|
+
AuthState = __decorate([State({
|
|
536
|
+
name: "auth",
|
|
537
|
+
defaults: {
|
|
538
|
+
accessToken: null,
|
|
539
|
+
refreshToken: null,
|
|
540
|
+
expiresAt: null,
|
|
541
|
+
user: null
|
|
542
|
+
}
|
|
543
|
+
})], AuthState);
|
|
544
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
545
|
+
minVersion: "12.0.0",
|
|
546
|
+
version: "22.2.0",
|
|
547
|
+
ngImport: i0,
|
|
548
|
+
type: AuthState,
|
|
549
|
+
decorators: [{ type: Injectable }],
|
|
550
|
+
propDecorators: {
|
|
551
|
+
login: [],
|
|
552
|
+
logout: [],
|
|
553
|
+
restore: [],
|
|
554
|
+
setTokens: [],
|
|
555
|
+
patchCurrentUser: []
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
const LOCK_NAME = "coolms_auth_refresh";
|
|
559
|
+
const FRESH_TOKEN_GRACE_MS = 3e4;
|
|
560
|
+
const STORAGE_KEY$1 = "coolms_token";
|
|
561
|
+
var AuthRefreshCoordinator = class AuthRefreshCoordinator {
|
|
562
|
+
api = inject(IdentityApiClient);
|
|
563
|
+
store = inject(Store);
|
|
564
|
+
router = inject(Router);
|
|
565
|
+
inFlight = null;
|
|
566
|
+
refresh(refreshToken, refusedAccessToken = null) {
|
|
567
|
+
if (this.inFlight) return this.inFlight;
|
|
568
|
+
this.inFlight = defer(() => this.refreshWithCrossTabLock(refreshToken, refusedAccessToken)).pipe(catchError((refreshErr) => {
|
|
569
|
+
if (refreshErr instanceof HttpErrorResponse && refreshErr.status === 401) {
|
|
570
|
+
this.store.dispatch(new Logout());
|
|
571
|
+
if (this.router.navigated) this.router.navigate(["/login"]);
|
|
572
|
+
}
|
|
573
|
+
return throwError(() => refreshErr);
|
|
574
|
+
}), shareReplay(1), finalize(() => {
|
|
575
|
+
this.inFlight = null;
|
|
576
|
+
}));
|
|
577
|
+
return this.inFlight;
|
|
578
|
+
}
|
|
579
|
+
refreshWithCrossTabLock(refreshToken, refusedAccessToken) {
|
|
580
|
+
const lockBody = () => this.executeRefresh(refreshToken, refusedAccessToken);
|
|
581
|
+
if (typeof navigator === "undefined" || !("locks" in navigator)) return from(lockBody());
|
|
582
|
+
return from(navigator.locks.request(LOCK_NAME, lockBody).then((value) => {
|
|
583
|
+
if (value === void 0) throw new Error("Refresh lock aborted before completing.");
|
|
584
|
+
return value;
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
async executeRefresh(originalRefreshToken, refusedAccessToken) {
|
|
588
|
+
const shared = this.readShared();
|
|
589
|
+
if (refusedAccessToken !== null) {
|
|
590
|
+
if (shared !== null && shared.accessToken !== refusedAccessToken) {
|
|
591
|
+
if (this.store.selectSnapshot(AuthState.accessToken) !== shared.accessToken) this.store.dispatch(new SetTokens(shared));
|
|
592
|
+
return shared.accessToken;
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
const cachedAccess = this.tryConsumeFreshState(shared);
|
|
596
|
+
if (cachedAccess !== null) return cachedAccess;
|
|
597
|
+
}
|
|
598
|
+
const currentRefreshToken = shared?.refreshToken ?? this.store.selectSnapshot(AuthState.refreshToken) ?? originalRefreshToken;
|
|
599
|
+
const response = await firstValueFrom(this.api.refresh(currentRefreshToken));
|
|
600
|
+
this.store.dispatch(new SetTokens(response));
|
|
601
|
+
return response.accessToken;
|
|
602
|
+
}
|
|
603
|
+
readShared() {
|
|
604
|
+
let raw;
|
|
605
|
+
try {
|
|
606
|
+
raw = localStorage.getItem(STORAGE_KEY$1);
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
if (raw === null) return null;
|
|
611
|
+
try {
|
|
612
|
+
const parsed = JSON.parse(raw);
|
|
613
|
+
if (typeof parsed?.accessToken !== "string" || typeof parsed.refreshToken !== "string" || typeof parsed.expiresAt !== "string") return null;
|
|
614
|
+
return {
|
|
615
|
+
accessToken: parsed.accessToken,
|
|
616
|
+
refreshToken: parsed.refreshToken,
|
|
617
|
+
expiresAt: parsed.expiresAt
|
|
618
|
+
};
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
tryConsumeFreshState(shared) {
|
|
624
|
+
const accessToken = shared?.accessToken ?? this.store.selectSnapshot(AuthState.accessToken);
|
|
625
|
+
const expiresAtStr = shared?.expiresAt ?? this.store.selectSnapshot(AuthState.expiresAt);
|
|
626
|
+
if (accessToken === null || expiresAtStr === null) return null;
|
|
627
|
+
const expiresAtMs = new Date(expiresAtStr).getTime();
|
|
628
|
+
if (Number.isNaN(expiresAtMs)) return null;
|
|
629
|
+
if (Date.now() >= expiresAtMs - FRESH_TOKEN_GRACE_MS) return null;
|
|
630
|
+
if (shared !== null && this.store.selectSnapshot(AuthState.accessToken) !== shared.accessToken) this.store.dispatch(new SetTokens(shared));
|
|
631
|
+
return accessToken;
|
|
632
|
+
}
|
|
633
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
634
|
+
minVersion: "12.0.0",
|
|
635
|
+
version: "22.2.0",
|
|
636
|
+
ngImport: i0,
|
|
637
|
+
type: AuthRefreshCoordinator,
|
|
638
|
+
deps: [],
|
|
639
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
640
|
+
});
|
|
641
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
642
|
+
minVersion: "12.0.0",
|
|
643
|
+
version: "22.2.0",
|
|
644
|
+
ngImport: i0,
|
|
645
|
+
type: AuthRefreshCoordinator,
|
|
646
|
+
providedIn: "root"
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
650
|
+
minVersion: "12.0.0",
|
|
651
|
+
version: "22.2.0",
|
|
652
|
+
ngImport: i0,
|
|
653
|
+
type: AuthRefreshCoordinator,
|
|
654
|
+
decorators: [{
|
|
655
|
+
type: Injectable,
|
|
656
|
+
args: [{ providedIn: "root" }]
|
|
657
|
+
}]
|
|
658
|
+
});
|
|
798
659
|
const BYPASS_AUTH = new HttpContextToken(() => false);
|
|
799
|
-
// -----------------------------------------------------------------------------
|
|
800
660
|
const authInterceptor = (req, next) => {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// token snapshot is null and every request gets a 401 -> refresh cycle even
|
|
822
|
-
// for a perfectly valid session.
|
|
823
|
-
//
|
|
824
|
-
// ready$ is a ReplaySubject(1) that completes after load() finishes, so:
|
|
825
|
-
// - requests fired during init queue here and flush the moment ready$ emits;
|
|
826
|
-
// - requests fired after init pass through take(1) immediately (replayed value).
|
|
827
|
-
return initService.ready$.pipe(take(1), switchMap$1(() => {
|
|
828
|
-
const token = store.selectSnapshot(AuthState.accessToken);
|
|
829
|
-
const authed = token
|
|
830
|
-
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
|
|
831
|
-
: req;
|
|
832
|
-
return next(authed).pipe(catchError((err) => {
|
|
833
|
-
if (!(err instanceof HttpErrorResponse) || err.status !== 401) {
|
|
834
|
-
return throwError(() => err);
|
|
835
|
-
}
|
|
836
|
-
// Avoid infinite loop — skip refresh for auth endpoints themselves.
|
|
837
|
-
if (req.url.includes('/auth/refresh') || req.url.includes('/auth/login')) {
|
|
838
|
-
return throwError(() => err);
|
|
839
|
-
}
|
|
840
|
-
const refreshToken = store.selectSnapshot(AuthState.refreshToken);
|
|
841
|
-
if (!refreshToken) {
|
|
842
|
-
store.dispatch(new Logout());
|
|
843
|
-
// Only navigate programmatically once the router has already
|
|
844
|
-
// completed its initial navigation. During APP_INITIALIZER the
|
|
845
|
-
// router is still blocked; authGuard handles the redirect there.
|
|
846
|
-
if (router.navigated)
|
|
847
|
-
void router.navigate(['/login']);
|
|
848
|
-
return throwError(() => err);
|
|
849
|
-
}
|
|
850
|
-
// Delegate refresh to the shared coordinator so the
|
|
851
|
-
// Centrifuge getToken path and the interceptor share ONE
|
|
852
|
-
// in-flight refresh. Without this, two parallel
|
|
853
|
-
// `api.refresh(refreshToken)` calls (one from each mutex)
|
|
854
|
-
// would send the same refresh-token twice and trigger the
|
|
855
|
-
// backend's `isUsed` replay branch on the second arrival,
|
|
856
|
-
// wiping the whole session.
|
|
857
|
-
return coordinator.refresh(refreshToken).pipe(switchMap$1(newToken => next(req.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } }))));
|
|
858
|
-
}));
|
|
859
|
-
}));
|
|
661
|
+
const store = inject(Store);
|
|
662
|
+
const router = inject(Router);
|
|
663
|
+
const coordinator = inject(AuthRefreshCoordinator);
|
|
664
|
+
const initService = inject(AppInitService);
|
|
665
|
+
if (req.context.get(BYPASS_AUTH)) return next(req);
|
|
666
|
+
if (req.url.includes("/auth/login") || req.url.includes("/auth/refresh")) return next(req);
|
|
667
|
+
return initService.ready$.pipe(take(1), switchMap(() => {
|
|
668
|
+
const token = store.selectSnapshot(AuthState.accessToken);
|
|
669
|
+
return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req).pipe(catchError((err) => {
|
|
670
|
+
if (!(err instanceof HttpErrorResponse) || err.status !== 401) return throwError(() => err);
|
|
671
|
+
if (req.url.includes("/auth/refresh") || req.url.includes("/auth/login")) return throwError(() => err);
|
|
672
|
+
const refreshToken = store.selectSnapshot(AuthState.refreshToken);
|
|
673
|
+
if (!refreshToken) {
|
|
674
|
+
store.dispatch(new Logout());
|
|
675
|
+
if (router.navigated) router.navigate(["/login"]);
|
|
676
|
+
return throwError(() => err);
|
|
677
|
+
}
|
|
678
|
+
return coordinator.refresh(refreshToken, token).pipe(switchMap((newToken) => next(req.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } }))));
|
|
679
|
+
}));
|
|
680
|
+
}));
|
|
860
681
|
};
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}).pipe(tap$1(cfg => this.store.dispatch(new SetAppConfig(cfg))), catchError(() => {
|
|
995
|
-
console.warn('CoolMS: theme/config unreachable, using safe defaults');
|
|
996
|
-
return of(null);
|
|
997
|
-
})));
|
|
998
|
-
// -- Step 3: validate the restored session -----------------------------
|
|
999
|
-
//
|
|
1000
|
-
// RestoreSession trusts whatever tokens are in localStorage, but they may
|
|
1001
|
-
// have been revoked out-of-band — most notably, signing out on the public
|
|
1002
|
-
// SSR site revokes the user's WHOLE token set. We MUST settle the real
|
|
1003
|
-
// auth state here, BEFORE step 4 signals ready$: both the auth interceptor
|
|
1004
|
-
// AND authGuard gate on ready$, so if we signalled first, the guard would
|
|
1005
|
-
// race ahead and mount the shell against the stale isAuthenticated=true
|
|
1006
|
-
// before this probe finished (-> half-loaded shell + "Session expired"
|
|
1007
|
-
// toasts, the exact bug this fixes). Because the probe itself cannot wait
|
|
1008
|
-
// on ready$ (that would deadlock), validateSession() bypasses the
|
|
1009
|
-
// interceptor and attaches the token / drives the refresh by hand.
|
|
1010
|
-
const meUrl = config?.manifest.identity?.meUrl;
|
|
1011
|
-
if (meUrl && this.store.selectSnapshot(AuthState.isAuthenticated)) {
|
|
1012
|
-
await this.validateSession(meUrl);
|
|
1013
|
-
}
|
|
1014
|
-
// -- Step 4: signal authGuard + unblock the interceptor ----------------
|
|
1015
|
-
//
|
|
1016
|
-
// Auth state is now settled (session confirmed, refreshed, or Logout
|
|
1017
|
-
// dispatched). ReplaySubject(1) replays to guards/requests that subscribe
|
|
1018
|
-
// afterwards, so they evaluate against the final isAuthenticated value.
|
|
1019
|
-
this.initComplete.next();
|
|
1020
|
-
this.initComplete.complete();
|
|
1021
|
-
// -- Step 5: navigate to last-visited route ----------------------------
|
|
1022
|
-
//
|
|
1023
|
-
// Only when the session survived validation. If it did not, isAuthenticated
|
|
1024
|
-
// is now false, we skip the saved-route navigation, and authGuard sends the
|
|
1025
|
-
// initial navigation to /login.
|
|
1026
|
-
//
|
|
1027
|
-
// Only restore when the user opened the bare /admin root. An explicit
|
|
1028
|
-
// deep link (pasted /admin/sync-fleet, a bookmark, a shared URL) must
|
|
1029
|
-
// win over the saved route. The router cannot tell us which one this is:
|
|
1030
|
-
// withEnabledBlockingInitialNavigation holds the initial navigation until
|
|
1031
|
-
// this initializer resolves, so router.url is still '/'. The browser
|
|
1032
|
-
// location is the only record of what was requested — Location.path()
|
|
1033
|
-
// reads it with the /admin base href stripped, so the bare root (with or
|
|
1034
|
-
// without trailing slash) yields ''. Anything else, including a root URL
|
|
1035
|
-
// carrying query params, is treated as an explicit destination.
|
|
1036
|
-
if (this.store.selectSnapshot(AuthState.isAuthenticated)) {
|
|
1037
|
-
const requested = this.location.path();
|
|
1038
|
-
if (requested === '' || requested === '/') {
|
|
1039
|
-
const saved = this.prefs.getPageState('nav');
|
|
1040
|
-
if (saved?.lastRoute && !saved.lastRoute.startsWith('/login')) {
|
|
1041
|
-
void this.router.navigateByUrl(saved.lastRoute);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Confirm the restored session is still alive, settling AuthState before the
|
|
1048
|
-
* app unblocks. Runs OUTSIDE the auth interceptor (BYPASS_AUTH) because the
|
|
1049
|
-
* interceptor queues behind ready$, which has not fired yet at this point.
|
|
1050
|
-
*
|
|
1051
|
-
* - access token accepted -> done, session alive;
|
|
1052
|
-
* - access token rejected -> attempt ONE refresh (access tokens are short-lived,
|
|
1053
|
-
* so an expired one with a valid refresh token must
|
|
1054
|
-
* stay logged in). The coordinator dispatches Logout
|
|
1055
|
-
* on a refresh 401 and SetTokens on success; a
|
|
1056
|
-
* transient failure (5xx/network) is swallowed so a
|
|
1057
|
-
* momentary blip never kills a valid session.
|
|
1058
|
-
*/
|
|
1059
|
-
async validateSession(meUrl) {
|
|
1060
|
-
const token = this.store.selectSnapshot(AuthState.accessToken);
|
|
1061
|
-
if (await this.probe(meUrl, token)) {
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
const refreshToken = this.store.selectSnapshot(AuthState.refreshToken);
|
|
1065
|
-
if (!refreshToken) {
|
|
1066
|
-
this.store.dispatch(new Logout());
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
let refreshedToken = null;
|
|
1070
|
-
try {
|
|
1071
|
-
refreshedToken = await firstValueFrom(this.refreshCoordinator.refresh(refreshToken));
|
|
1072
|
-
}
|
|
1073
|
-
catch {
|
|
1074
|
-
// 401 -> coordinator already dispatched Logout; transient -> session kept.
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
// Refresh resolved — but the coordinator's fresh-state optimisation can hand
|
|
1078
|
-
// back the SAME access token (without a network round-trip) when its stored
|
|
1079
|
-
// expiry still looks fresh. For a token that was revoked but not yet expired
|
|
1080
|
-
// (signing out on the SSR site while the access token is still in its TTL),
|
|
1081
|
-
// that token is dead despite looking fresh. Re-probe with whatever we now
|
|
1082
|
-
// hold; a second rejection means the session is genuinely gone.
|
|
1083
|
-
if (!(await this.probe(meUrl, refreshedToken))) {
|
|
1084
|
-
this.store.dispatch(new Logout());
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
/** GET an authed endpoint with an explicit Bearer header, bypassing the interceptor. */
|
|
1088
|
-
probe(url, token) {
|
|
1089
|
-
return firstValueFrom(this.http.get(url, {
|
|
1090
|
-
context: new HttpContext().set(BYPASS_AUTH, true),
|
|
1091
|
-
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
1092
|
-
}).pipe(map(() => true), catchError(() => of(false))));
|
|
1093
|
-
}
|
|
1094
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AppInitService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1095
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AppInitService, providedIn: 'root' });
|
|
1096
|
-
}
|
|
1097
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AppInitService, decorators: [{
|
|
1098
|
-
type: Injectable,
|
|
1099
|
-
args: [{ providedIn: 'root' }]
|
|
1100
|
-
}] });
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
* Protects routes that require authentication.
|
|
1104
|
-
*
|
|
1105
|
-
* The guard is intentionally ASYNC: it waits for AppInitService.ready$ before
|
|
1106
|
-
* evaluating auth state. Without this, Angular's router can evaluate
|
|
1107
|
-
* isAuthenticated while APP_INITIALIZER is still in-flight — RestoreSession
|
|
1108
|
-
* will have placed a non-null (but expired) token in state, causing the guard
|
|
1109
|
-
* to pass even though the subsequent config/refresh requests will fail and
|
|
1110
|
-
* dispatch Logout. Waiting for ready$ guarantees the auth state is settled.
|
|
1111
|
-
*/
|
|
682
|
+
const STORAGE_KEY = "coolms_token";
|
|
683
|
+
var CrossTabAuthSyncService = class CrossTabAuthSyncService {
|
|
684
|
+
store = inject(Store);
|
|
685
|
+
listenerAttached = false;
|
|
686
|
+
start() {
|
|
687
|
+
if (this.listenerAttached) return;
|
|
688
|
+
this.listenerAttached = true;
|
|
689
|
+
window.addEventListener("storage", this.handleStorageEvent);
|
|
690
|
+
}
|
|
691
|
+
handleStorageEvent = (event) => {
|
|
692
|
+
if (event.key !== STORAGE_KEY) return;
|
|
693
|
+
if (event.newValue === null) {
|
|
694
|
+
this.store.dispatch(new Logout());
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
let parsed;
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(event.newValue);
|
|
700
|
+
} catch {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string" || typeof parsed.expiresAt !== "string") return;
|
|
704
|
+
const tokenResponse = {
|
|
705
|
+
accessToken: parsed.accessToken,
|
|
706
|
+
refreshToken: parsed.refreshToken,
|
|
707
|
+
expiresAt: parsed.expiresAt,
|
|
708
|
+
user: parsed.user ?? void 0
|
|
709
|
+
};
|
|
710
|
+
this.store.dispatch(new SetTokens(tokenResponse));
|
|
711
|
+
};
|
|
712
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
713
|
+
minVersion: "12.0.0",
|
|
714
|
+
version: "22.2.0",
|
|
715
|
+
ngImport: i0,
|
|
716
|
+
type: CrossTabAuthSyncService,
|
|
717
|
+
deps: [],
|
|
718
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
719
|
+
});
|
|
720
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
721
|
+
minVersion: "12.0.0",
|
|
722
|
+
version: "22.2.0",
|
|
723
|
+
ngImport: i0,
|
|
724
|
+
type: CrossTabAuthSyncService,
|
|
725
|
+
providedIn: "root"
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
729
|
+
minVersion: "12.0.0",
|
|
730
|
+
version: "22.2.0",
|
|
731
|
+
ngImport: i0,
|
|
732
|
+
type: CrossTabAuthSyncService,
|
|
733
|
+
decorators: [{
|
|
734
|
+
type: Injectable,
|
|
735
|
+
args: [{ providedIn: "root" }]
|
|
736
|
+
}]
|
|
737
|
+
});
|
|
738
|
+
var AppInitService = class AppInitService {
|
|
739
|
+
http = inject(HttpClient);
|
|
740
|
+
store = inject(Store);
|
|
741
|
+
router = inject(Router);
|
|
742
|
+
location = inject(Location);
|
|
743
|
+
prefs = inject(UserPreferencesService);
|
|
744
|
+
crossTabSync = inject(CrossTabAuthSyncService);
|
|
745
|
+
refreshCoordinator = inject(AuthRefreshCoordinator);
|
|
746
|
+
configUrl = "/api/v1/theme/config";
|
|
747
|
+
initComplete = new ReplaySubject(1);
|
|
748
|
+
ready$ = this.initComplete.asObservable();
|
|
749
|
+
async load() {
|
|
750
|
+
this.crossTabSync.start();
|
|
751
|
+
await firstValueFrom(this.store.dispatch(new RestoreSession()));
|
|
752
|
+
const meUrl = (await firstValueFrom(this.http.get(this.configUrl, { context: new HttpContext().set(BYPASS_AUTH, true) }).pipe(tap((cfg) => this.store.dispatch(new SetAppConfig(cfg))), catchError(() => {
|
|
753
|
+
console.warn("CoolMS: theme/config unreachable, using safe defaults");
|
|
754
|
+
return of(null);
|
|
755
|
+
}))))?.manifest.identity?.meUrl;
|
|
756
|
+
if (meUrl && this.store.selectSnapshot(AuthState.isAuthenticated)) await this.validateSession(meUrl);
|
|
757
|
+
this.initComplete.next();
|
|
758
|
+
this.initComplete.complete();
|
|
759
|
+
if (this.store.selectSnapshot(AuthState.isAuthenticated)) {
|
|
760
|
+
const requested = this.location.path();
|
|
761
|
+
if (requested === "" || requested === "/") {
|
|
762
|
+
const saved = this.prefs.getPageState("nav");
|
|
763
|
+
if (saved?.lastRoute && !saved.lastRoute.startsWith("/login")) this.router.navigateByUrl(saved.lastRoute);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async validateSession(meUrl) {
|
|
768
|
+
const token = this.store.selectSnapshot(AuthState.accessToken);
|
|
769
|
+
if (await this.probe(meUrl, token)) return;
|
|
770
|
+
const refreshToken = this.store.selectSnapshot(AuthState.refreshToken);
|
|
771
|
+
if (!refreshToken) {
|
|
772
|
+
this.store.dispatch(new Logout());
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
let refreshedToken = null;
|
|
776
|
+
try {
|
|
777
|
+
refreshedToken = await firstValueFrom(this.refreshCoordinator.refresh(refreshToken, token));
|
|
778
|
+
} catch {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (!await this.probe(meUrl, refreshedToken)) this.store.dispatch(new Logout());
|
|
782
|
+
}
|
|
783
|
+
probe(url, token) {
|
|
784
|
+
return firstValueFrom(this.http.get(url, {
|
|
785
|
+
context: new HttpContext().set(BYPASS_AUTH, true),
|
|
786
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
787
|
+
}).pipe(map(() => true), catchError(() => of(false))));
|
|
788
|
+
}
|
|
789
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
790
|
+
minVersion: "12.0.0",
|
|
791
|
+
version: "22.2.0",
|
|
792
|
+
ngImport: i0,
|
|
793
|
+
type: AppInitService,
|
|
794
|
+
deps: [],
|
|
795
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
796
|
+
});
|
|
797
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
798
|
+
minVersion: "12.0.0",
|
|
799
|
+
version: "22.2.0",
|
|
800
|
+
ngImport: i0,
|
|
801
|
+
type: AppInitService,
|
|
802
|
+
providedIn: "root"
|
|
803
|
+
});
|
|
804
|
+
};
|
|
805
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
806
|
+
minVersion: "12.0.0",
|
|
807
|
+
version: "22.2.0",
|
|
808
|
+
ngImport: i0,
|
|
809
|
+
type: AppInitService,
|
|
810
|
+
decorators: [{
|
|
811
|
+
type: Injectable,
|
|
812
|
+
args: [{ providedIn: "root" }]
|
|
813
|
+
}]
|
|
814
|
+
});
|
|
1112
815
|
const authGuard = () => {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
return router.createUrlTree(['/login']);
|
|
1121
|
-
}));
|
|
816
|
+
const init = inject(AppInitService);
|
|
817
|
+
const store = inject(Store);
|
|
818
|
+
const router = inject(Router);
|
|
819
|
+
return init.ready$.pipe(take(1), map(() => {
|
|
820
|
+
if (store.selectSnapshot(AuthState.isAuthenticated)) return true;
|
|
821
|
+
return router.createUrlTree(["/login"]);
|
|
822
|
+
}));
|
|
1122
823
|
};
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* Guards the `/login` route -- redirects already-authenticated
|
|
1126
|
-
* callers to admin home so a stale tab parked on `/login` cannot
|
|
1127
|
-
* remain mounted while another tab is logged in. When Tab A logs
|
|
1128
|
-
* in fresh, Tab B's `RestoreSession` (driven by APP_INITIALIZER)
|
|
1129
|
-
* reads the new token from shared `localStorage` and populates
|
|
1130
|
-
* `AuthState`; this guard then sees `isAuthenticated === true`
|
|
1131
|
-
* and routes Tab B to `/`.
|
|
1132
|
-
*
|
|
1133
|
-
* Same `ready$` wait as `authGuard` -- ensures `RestoreSession`
|
|
1134
|
-
* has run before the snapshot is read; otherwise a fresh F5 on
|
|
1135
|
-
* `/login` with a valid stored session would briefly evaluate
|
|
1136
|
-
* as anonymous, allow `/login`, and only then have the token
|
|
1137
|
-
* populated.
|
|
1138
|
-
*/
|
|
1139
824
|
const loginPageGuard = () => {
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return true;
|
|
1148
|
-
}));
|
|
825
|
+
const init = inject(AppInitService);
|
|
826
|
+
const store = inject(Store);
|
|
827
|
+
const router = inject(Router);
|
|
828
|
+
return init.ready$.pipe(take(1), map(() => {
|
|
829
|
+
if (store.selectSnapshot(AuthState.isAuthenticated)) return router.createUrlTree(["/"]);
|
|
830
|
+
return true;
|
|
831
|
+
}));
|
|
1149
832
|
};
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
833
|
+
const ELEVATION_WARNING_PASSWORD_NOT_SET = "elevation.password_not_set";
|
|
834
|
+
const ELEVATION_WARNING_FACTOR_MISSING = "elevation.factor_missing";
|
|
835
|
+
const ELEVATION_WARNING_MFA_OFF = "elevation.mfa_off";
|
|
836
|
+
const ELEVATION_PROMPT = new InjectionToken("coolms.elevation-prompt");
|
|
837
|
+
const BYPASS_ELEVATION = new HttpContextToken(() => false);
|
|
838
|
+
const CROSS_TAB_KEY = "coolms.elevation.changed";
|
|
839
|
+
const RECONCILE_MIN_INTERVAL_MS = 2e3;
|
|
840
|
+
const ELEVATION_REQUIRED_HEADER = "X-Elevation-Required";
|
|
841
|
+
var ElevationService = class ElevationService {
|
|
842
|
+
http = inject(HttpClient);
|
|
843
|
+
store = inject(Store);
|
|
844
|
+
prompt = inject(ELEVATION_PROMPT, { optional: true });
|
|
845
|
+
destroyRef = inject(DestroyRef);
|
|
846
|
+
served = signal(null, ...ngDevMode ? [{ debugName: "served" }] : /* istanbul ignore next */ []);
|
|
847
|
+
state = () => {
|
|
848
|
+
const served = this.served();
|
|
849
|
+
if (served === null || !isPast(served)) return served;
|
|
850
|
+
const expired = expiredView(served);
|
|
851
|
+
untracked(() => this.served.set(expired));
|
|
852
|
+
this.announceExpiryLater();
|
|
853
|
+
return expired;
|
|
854
|
+
};
|
|
855
|
+
elevated = () => this.state()?.elevated ?? false;
|
|
856
|
+
expiresAt = () => {
|
|
857
|
+
const iso = this.state()?.expiresAt;
|
|
858
|
+
return iso ? new Date(iso) : null;
|
|
859
|
+
};
|
|
860
|
+
changes$ = new Subject();
|
|
861
|
+
expiryTimer = null;
|
|
862
|
+
pendingPrompt = null;
|
|
863
|
+
expiryAnnouncePending = false;
|
|
864
|
+
lastReconcileAt = 0;
|
|
865
|
+
constructor() {
|
|
866
|
+
if (typeof window !== "undefined") {
|
|
867
|
+
window.addEventListener("storage", this.onStorage);
|
|
868
|
+
window.addEventListener("focus", this.onFocus);
|
|
869
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
870
|
+
this.destroyRef.onDestroy(() => {
|
|
871
|
+
window.removeEventListener("storage", this.onStorage);
|
|
872
|
+
window.removeEventListener("focus", this.onFocus);
|
|
873
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
874
|
+
this.clearTimer();
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
get available() {
|
|
879
|
+
return this.url !== null;
|
|
880
|
+
}
|
|
881
|
+
get url() {
|
|
882
|
+
return this.store.selectSnapshot(AppConfigState.manifest)?.identity?.elevationUrl ?? null;
|
|
883
|
+
}
|
|
884
|
+
isThisApi(url) {
|
|
885
|
+
const apiBase = this.store.selectSnapshot(AppConfigState.manifest)?.apiBase ?? "/api/v1";
|
|
886
|
+
let path = url.split("?")[0];
|
|
887
|
+
if (/^https?:\/\//.test(path)) try {
|
|
888
|
+
const parsed = new URL(path);
|
|
889
|
+
if (typeof window !== "undefined" && parsed.host !== window.location.host) return false;
|
|
890
|
+
path = parsed.pathname;
|
|
891
|
+
} catch {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
return path === apiBase || path.startsWith(`${apiBase}/`);
|
|
895
|
+
}
|
|
896
|
+
refresh() {
|
|
897
|
+
const url = this.url;
|
|
898
|
+
if (!url) return of(this.inert());
|
|
899
|
+
return this.http.get(url, this.options()).pipe(tap((s) => this.apply(s)));
|
|
900
|
+
}
|
|
901
|
+
elevate(password, code) {
|
|
902
|
+
const url = this.url;
|
|
903
|
+
if (!url) return of(this.inert());
|
|
904
|
+
const body = { password };
|
|
905
|
+
if (code) body.code = code;
|
|
906
|
+
return this.http.post(url, body, this.options()).pipe(tap((s) => this.apply(s, "granted")));
|
|
907
|
+
}
|
|
908
|
+
drop(reason = "dropped_by_user") {
|
|
909
|
+
const url = this.url;
|
|
910
|
+
if (!url) return of(this.inert());
|
|
911
|
+
return this.http.delete(`${url}?reason=${reason}`, this.options()).pipe(switchMap(() => this.refresh()));
|
|
912
|
+
}
|
|
913
|
+
offerFor(refusal) {
|
|
914
|
+
if (!this.available || !this.prompt) return of(false);
|
|
915
|
+
return this.refresh().pipe(switchMap((state) => state.elevated ? of(false) : this.requestElevation({
|
|
916
|
+
state,
|
|
917
|
+
refusal
|
|
918
|
+
})));
|
|
919
|
+
}
|
|
920
|
+
requestElevation(request) {
|
|
921
|
+
if (!this.prompt) return of(false);
|
|
922
|
+
if (this.pendingPrompt) return this.pendingPrompt;
|
|
923
|
+
const prompt = this.prompt;
|
|
924
|
+
this.pendingPrompt = defer(() => prompt.open(request)).pipe(finalize(() => {
|
|
925
|
+
this.pendingPrompt = null;
|
|
926
|
+
}), shareReplay(1));
|
|
927
|
+
return this.pendingPrompt;
|
|
928
|
+
}
|
|
929
|
+
options() {
|
|
930
|
+
return { context: new HttpContext().set(BYPASS_ELEVATION, true) };
|
|
931
|
+
}
|
|
932
|
+
inert() {
|
|
933
|
+
return {
|
|
934
|
+
elevated: false,
|
|
935
|
+
ended: {
|
|
936
|
+
reason: "never",
|
|
937
|
+
at: null
|
|
938
|
+
},
|
|
939
|
+
lifetimeSeconds: 0,
|
|
940
|
+
lifetimeCeilingSeconds: 0,
|
|
941
|
+
mfaRequired: false,
|
|
942
|
+
warnings: []
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
apply(next, explicit) {
|
|
946
|
+
const prev = untracked(() => this.served());
|
|
947
|
+
this.served.set(next);
|
|
948
|
+
this.schedule(next);
|
|
949
|
+
const kind = explicit ?? this.kindOf(prev, next);
|
|
950
|
+
if (kind === null) return;
|
|
951
|
+
this.expiryAnnouncePending = false;
|
|
952
|
+
this.changes$.next({
|
|
953
|
+
kind,
|
|
954
|
+
state: next
|
|
955
|
+
});
|
|
956
|
+
this.announceToOtherTabs();
|
|
957
|
+
}
|
|
958
|
+
kindOf(prev, next) {
|
|
959
|
+
const was = prev?.elevated ?? false;
|
|
960
|
+
if (was && !next.elevated) return next.ended.reason === "expired" ? "expired" : "dropped";
|
|
961
|
+
if (!was && next.elevated) return "granted";
|
|
962
|
+
if (was && next.elevated && (prev?.expiresAt ?? null) !== (next.expiresAt ?? null)) return "observed";
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
announceExpiryLater() {
|
|
966
|
+
if (this.expiryAnnouncePending) return;
|
|
967
|
+
this.expiryAnnouncePending = true;
|
|
968
|
+
queueMicrotask(() => {
|
|
969
|
+
if (!this.expiryAnnouncePending) return;
|
|
970
|
+
this.expiryAnnouncePending = false;
|
|
971
|
+
const state = untracked(() => this.served());
|
|
972
|
+
if (state !== null && !state.elevated) this.changes$.next({
|
|
973
|
+
kind: "expired",
|
|
974
|
+
state
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
schedule(state) {
|
|
979
|
+
this.clearTimer();
|
|
980
|
+
if (!state.elevated || !state.expiresAt) return;
|
|
981
|
+
const wanted = Date.parse(state.expiresAt) - Date.now() + 250;
|
|
982
|
+
const ms = Math.min(Math.max(1e3, wanted), 2147483647);
|
|
983
|
+
this.expiryTimer = setTimeout(() => {
|
|
984
|
+
this.expiryTimer = null;
|
|
985
|
+
this.refresh().subscribe({
|
|
986
|
+
next: (s) => {
|
|
987
|
+
if (s.elevated && s.expiresAt && Date.parse(s.expiresAt) <= Date.now()) this.scheduleRetry();
|
|
988
|
+
},
|
|
989
|
+
error: () => this.scheduleRetry()
|
|
990
|
+
});
|
|
991
|
+
}, ms);
|
|
992
|
+
}
|
|
993
|
+
scheduleRetry() {
|
|
994
|
+
this.clearTimer();
|
|
995
|
+
this.expiryTimer = setTimeout(() => {
|
|
996
|
+
this.expiryTimer = null;
|
|
997
|
+
this.refresh().subscribe({ error: () => void 0 });
|
|
998
|
+
}, 5e3);
|
|
999
|
+
}
|
|
1000
|
+
clearTimer() {
|
|
1001
|
+
if (this.expiryTimer !== null) {
|
|
1002
|
+
clearTimeout(this.expiryTimer);
|
|
1003
|
+
this.expiryTimer = null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
announceToOtherTabs() {
|
|
1007
|
+
try {
|
|
1008
|
+
localStorage.setItem(CROSS_TAB_KEY, String(Date.now()));
|
|
1009
|
+
} catch {}
|
|
1010
|
+
}
|
|
1011
|
+
onStorage = (event) => {
|
|
1012
|
+
if (event.key !== CROSS_TAB_KEY || !this.available) return;
|
|
1013
|
+
this.refresh().subscribe({ error: () => void 0 });
|
|
1014
|
+
};
|
|
1015
|
+
onFocus = () => this.reconcile();
|
|
1016
|
+
onVisibilityChange = () => {
|
|
1017
|
+
if (document.visibilityState === "visible") this.reconcile();
|
|
1018
|
+
};
|
|
1019
|
+
reconcile() {
|
|
1020
|
+
this.state();
|
|
1021
|
+
if (!this.available) return;
|
|
1022
|
+
const now = Date.now();
|
|
1023
|
+
if (now - this.lastReconcileAt < RECONCILE_MIN_INTERVAL_MS) return;
|
|
1024
|
+
this.lastReconcileAt = now;
|
|
1025
|
+
this.refresh().subscribe({ error: () => void 0 });
|
|
1026
|
+
}
|
|
1027
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1028
|
+
minVersion: "12.0.0",
|
|
1029
|
+
version: "22.2.0",
|
|
1030
|
+
ngImport: i0,
|
|
1031
|
+
type: ElevationService,
|
|
1032
|
+
deps: [],
|
|
1033
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1034
|
+
});
|
|
1035
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1036
|
+
minVersion: "12.0.0",
|
|
1037
|
+
version: "22.2.0",
|
|
1038
|
+
ngImport: i0,
|
|
1039
|
+
type: ElevationService,
|
|
1040
|
+
providedIn: "root"
|
|
1041
|
+
});
|
|
1042
|
+
};
|
|
1043
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1044
|
+
minVersion: "12.0.0",
|
|
1045
|
+
version: "22.2.0",
|
|
1046
|
+
ngImport: i0,
|
|
1047
|
+
type: ElevationService,
|
|
1048
|
+
decorators: [{
|
|
1049
|
+
type: Injectable,
|
|
1050
|
+
args: [{ providedIn: "root" }]
|
|
1051
|
+
}],
|
|
1052
|
+
ctorParameters: () => []
|
|
1053
|
+
});
|
|
1054
|
+
function isPast(state) {
|
|
1055
|
+
if (!state.elevated || !state.expiresAt) return false;
|
|
1056
|
+
const at = Date.parse(state.expiresAt);
|
|
1057
|
+
return Number.isFinite(at) && at <= Date.now();
|
|
1182
1058
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
* back to '/api/v1/config' when the manifest is not yet loaded.
|
|
1196
|
-
*/
|
|
1197
|
-
class ConfigService {
|
|
1198
|
-
http = inject(HttpClient);
|
|
1199
|
-
store = inject(Store);
|
|
1200
|
-
cache = new Map();
|
|
1201
|
-
layout(id) { return this.fetch('layout', id); }
|
|
1202
|
-
dialog(id) { return this.fetch('dialog', id); }
|
|
1203
|
-
navigraph(slug) { return this.fetch('navigraph', slug); }
|
|
1204
|
-
form(id) { return this.fetch('form', id); }
|
|
1205
|
-
datagrid(id) { return this.fetch('datagrid', id); }
|
|
1206
|
-
fetch(type, id) {
|
|
1207
|
-
const key = `${type}/${id}`;
|
|
1208
|
-
if (!this.cache.has(key)) {
|
|
1209
|
-
const base = this.store.selectSnapshot(AppConfigState.manifest)?.configBase
|
|
1210
|
-
?? '/api/v1/config';
|
|
1211
|
-
// The config endpoint wraps the YAML payload in a `data` field:
|
|
1212
|
-
// { type, id, data: { ...yaml content... } }
|
|
1213
|
-
// Unwrap so callers always receive the config payload directly.
|
|
1214
|
-
const obs = this.http.get(`${base}/${type}/${id}`).pipe(map(r => (r.data ?? r)), shareReplay(1));
|
|
1215
|
-
this.cache.set(key, obs);
|
|
1216
|
-
}
|
|
1217
|
-
return this.cache.get(key);
|
|
1218
|
-
}
|
|
1219
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1220
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ConfigService, providedIn: 'root' });
|
|
1059
|
+
function expiredView(state) {
|
|
1060
|
+
return {
|
|
1061
|
+
...state,
|
|
1062
|
+
elevated: false,
|
|
1063
|
+
expiresAt: null,
|
|
1064
|
+
grantedAt: null,
|
|
1065
|
+
factorConfirmed: null,
|
|
1066
|
+
ended: {
|
|
1067
|
+
reason: "expired",
|
|
1068
|
+
at: state.expiresAt ?? null
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1221
1071
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1072
|
+
var ErrorHandlerService = class ErrorHandlerService {
|
|
1073
|
+
humanize(err) {
|
|
1074
|
+
if (!(err instanceof HttpErrorResponse)) return err instanceof Error ? err.message : "An unexpected error occurred.";
|
|
1075
|
+
const body = err.error;
|
|
1076
|
+
if (body && typeof body === "object") {
|
|
1077
|
+
const detail = body["detail"] ?? body["hydra:description"] ?? body["message"];
|
|
1078
|
+
if (typeof detail === "string" && detail.trim()) return detail.trim();
|
|
1079
|
+
if (Array.isArray(body["violations"])) {
|
|
1080
|
+
const msgs = body["violations"].map((v) => v.message).filter(Boolean);
|
|
1081
|
+
if (msgs.length) return msgs.join(" ");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return this.statusMessage(err.status);
|
|
1085
|
+
}
|
|
1086
|
+
statusMessage(status) {
|
|
1087
|
+
switch (status) {
|
|
1088
|
+
case 0: return "Network error. Check your connection.";
|
|
1089
|
+
case 400: return "Invalid request.";
|
|
1090
|
+
case 401: return "Session expired. Please sign in again.";
|
|
1091
|
+
case 403: return "Access denied.";
|
|
1092
|
+
case 404: return "Resource not found.";
|
|
1093
|
+
case 409: return "Conflict — resource already exists.";
|
|
1094
|
+
case 422: return "Validation failed.";
|
|
1095
|
+
case 429: return "Too many requests. Please wait a moment.";
|
|
1096
|
+
case 500: return "Server error. Please try again later.";
|
|
1097
|
+
case 503: return "Service unavailable. Please try again later.";
|
|
1098
|
+
default: return `Unexpected error (${status}).`;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1102
|
+
minVersion: "12.0.0",
|
|
1103
|
+
version: "22.2.0",
|
|
1104
|
+
ngImport: i0,
|
|
1105
|
+
type: ErrorHandlerService,
|
|
1106
|
+
deps: [],
|
|
1107
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1108
|
+
});
|
|
1109
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1110
|
+
minVersion: "12.0.0",
|
|
1111
|
+
version: "22.2.0",
|
|
1112
|
+
ngImport: i0,
|
|
1113
|
+
type: ErrorHandlerService,
|
|
1114
|
+
providedIn: "root"
|
|
1115
|
+
});
|
|
1116
|
+
};
|
|
1117
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1118
|
+
minVersion: "12.0.0",
|
|
1119
|
+
version: "22.2.0",
|
|
1120
|
+
ngImport: i0,
|
|
1121
|
+
type: ErrorHandlerService,
|
|
1122
|
+
decorators: [{
|
|
1123
|
+
type: Injectable,
|
|
1124
|
+
args: [{ providedIn: "root" }]
|
|
1125
|
+
}]
|
|
1126
|
+
});
|
|
1127
|
+
const elevationInterceptor = (req, next) => {
|
|
1128
|
+
const elevation = inject(ElevationService);
|
|
1129
|
+
const errors = inject(ErrorHandlerService);
|
|
1130
|
+
if (req.context.get(BYPASS_ELEVATION) || !elevation.isThisApi(req.url)) return next(req);
|
|
1131
|
+
return next(req).pipe(catchError((err) => {
|
|
1132
|
+
if (!(err instanceof HttpErrorResponse) || err.status !== 403 || !err.headers.get("X-Elevation-Required")) return throwError(() => err);
|
|
1133
|
+
return elevation.offerFor(errors.humanize(err)).pipe(switchMap((granted) => granted ? next(req) : throwError(() => err)));
|
|
1134
|
+
}));
|
|
1135
|
+
};
|
|
1136
|
+
var LoginComponent = class LoginComponent {
|
|
1137
|
+
identifier = "";
|
|
1138
|
+
password = "";
|
|
1139
|
+
loading = false;
|
|
1140
|
+
error = null;
|
|
1141
|
+
store = inject(Store);
|
|
1142
|
+
router = inject(Router);
|
|
1143
|
+
submit() {
|
|
1144
|
+
if (!this.identifier || !this.password) return;
|
|
1145
|
+
this.loading = true;
|
|
1146
|
+
this.error = null;
|
|
1147
|
+
this.store.dispatch(new Login(this.identifier, this.password)).subscribe({
|
|
1148
|
+
next: () => {
|
|
1149
|
+
this.router.navigate(["/"]).catch(() => {
|
|
1150
|
+
this.loading = false;
|
|
1151
|
+
this.error = "Signed in, but the admin could not be opened. Please reload.";
|
|
1152
|
+
});
|
|
1153
|
+
},
|
|
1154
|
+
error: (err) => {
|
|
1155
|
+
this.loading = false;
|
|
1156
|
+
this.error = err?.error?.detail ?? err?.error?.message ?? err?.message ?? "Invalid credentials. Please try again.";
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1161
|
+
minVersion: "12.0.0",
|
|
1162
|
+
version: "22.2.0",
|
|
1163
|
+
ngImport: i0,
|
|
1164
|
+
type: LoginComponent,
|
|
1165
|
+
deps: [],
|
|
1166
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1167
|
+
});
|
|
1168
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1169
|
+
minVersion: "14.0.0",
|
|
1170
|
+
version: "22.2.0",
|
|
1171
|
+
type: LoginComponent,
|
|
1172
|
+
isStandalone: true,
|
|
1173
|
+
selector: "coolms-admin-login",
|
|
1174
|
+
ngImport: i0,
|
|
1175
|
+
template: "<div class=\"d-flex align-items-center justify-content-center min-vh-100 bg-light\">\n <div class=\"card shadow-sm\" style=\"width: 360px\">\n <div class=\"card-body p-4\">\n <h1 class=\"h5 mb-1\">CoolMS Admin</h1>\n <p class=\"text-muted small mb-4\">Sign in to continue</p>\n\n <div *ngIf=\"error\" class=\"alert alert-danger py-2 small\">{{ error }}</div>\n\n <form (ngSubmit)=\"submit()\" #loginForm=\"ngForm\">\n <div class=\"mb-3\">\n <label class=\"form-label\">Email or username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"identifier\"\n name=\"identifier\"\n required\n autocomplete=\"username\"\n />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n [(ngModel)]=\"password\"\n name=\"password\"\n required\n autocomplete=\"current-password\"\n />\n </div>\n <button\n type=\"submit\"\n class=\"cms-btn cms-btn-primary w-100 justify-content-center\"\n [disabled]=\"loading || loginForm.invalid\"\n >\n {{ loading ? 'Signing in…' : 'Sign in' }}\n </button>\n </form>\n </div>\n </div>\n</div>\n",
|
|
1176
|
+
dependencies: [
|
|
1177
|
+
{
|
|
1178
|
+
kind: "ngmodule",
|
|
1179
|
+
type: FormsModule
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
kind: "directive",
|
|
1183
|
+
type: i1.ɵNgNoValidate,
|
|
1184
|
+
selector: "form:not([ngNoForm]):not([ngNativeValidate])"
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
kind: "directive",
|
|
1188
|
+
type: i1.DefaultValueAccessor,
|
|
1189
|
+
selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
kind: "directive",
|
|
1193
|
+
type: i1.NgControlStatus,
|
|
1194
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
kind: "directive",
|
|
1198
|
+
type: i1.NgControlStatusGroup,
|
|
1199
|
+
selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
kind: "directive",
|
|
1203
|
+
type: i1.RequiredValidator,
|
|
1204
|
+
selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",
|
|
1205
|
+
inputs: ["required"]
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
kind: "directive",
|
|
1209
|
+
type: i1.NgModel,
|
|
1210
|
+
selector: "[ngModel]:not([formControlName]):not([formControl])",
|
|
1211
|
+
inputs: [
|
|
1212
|
+
"name",
|
|
1213
|
+
"disabled",
|
|
1214
|
+
"ngModel",
|
|
1215
|
+
"ngModelOptions"
|
|
1216
|
+
],
|
|
1217
|
+
outputs: ["ngModelChange"],
|
|
1218
|
+
exportAs: ["ngModel"]
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
kind: "directive",
|
|
1222
|
+
type: i1.NgForm,
|
|
1223
|
+
selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
|
|
1224
|
+
inputs: ["ngFormOptions"],
|
|
1225
|
+
outputs: ["ngSubmit"],
|
|
1226
|
+
exportAs: ["ngForm"]
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
kind: "directive",
|
|
1230
|
+
type: NgIf,
|
|
1231
|
+
selector: "[ngIf]",
|
|
1232
|
+
inputs: [
|
|
1233
|
+
"ngIf",
|
|
1234
|
+
"ngIfThen",
|
|
1235
|
+
"ngIfElse"
|
|
1236
|
+
]
|
|
1237
|
+
}
|
|
1238
|
+
]
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1242
|
+
minVersion: "12.0.0",
|
|
1243
|
+
version: "22.2.0",
|
|
1244
|
+
ngImport: i0,
|
|
1245
|
+
type: LoginComponent,
|
|
1246
|
+
decorators: [{
|
|
1247
|
+
type: Component,
|
|
1248
|
+
args: [{
|
|
1249
|
+
selector: "coolms-admin-login",
|
|
1250
|
+
standalone: true,
|
|
1251
|
+
imports: [FormsModule, NgIf],
|
|
1252
|
+
template: "<div class=\"d-flex align-items-center justify-content-center min-vh-100 bg-light\">\n <div class=\"card shadow-sm\" style=\"width: 360px\">\n <div class=\"card-body p-4\">\n <h1 class=\"h5 mb-1\">CoolMS Admin</h1>\n <p class=\"text-muted small mb-4\">Sign in to continue</p>\n\n <div *ngIf=\"error\" class=\"alert alert-danger py-2 small\">{{ error }}</div>\n\n <form (ngSubmit)=\"submit()\" #loginForm=\"ngForm\">\n <div class=\"mb-3\">\n <label class=\"form-label\">Email or username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"identifier\"\n name=\"identifier\"\n required\n autocomplete=\"username\"\n />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n [(ngModel)]=\"password\"\n name=\"password\"\n required\n autocomplete=\"current-password\"\n />\n </div>\n <button\n type=\"submit\"\n class=\"cms-btn cms-btn-primary w-100 justify-content-center\"\n [disabled]=\"loading || loginForm.invalid\"\n >\n {{ loading ? 'Signing in…' : 'Sign in' }}\n </button>\n </form>\n </div>\n </div>\n</div>\n"
|
|
1253
|
+
}]
|
|
1254
|
+
}]
|
|
1255
|
+
});
|
|
1256
|
+
var ConfigService = class ConfigService {
|
|
1257
|
+
http = inject(HttpClient);
|
|
1258
|
+
store = inject(Store);
|
|
1259
|
+
cache = /* @__PURE__ */ new Map();
|
|
1260
|
+
layout(id) {
|
|
1261
|
+
return this.fetch("layout", id);
|
|
1262
|
+
}
|
|
1263
|
+
dialog(id) {
|
|
1264
|
+
return this.fetch("dialog", id);
|
|
1265
|
+
}
|
|
1266
|
+
navigraph(slug) {
|
|
1267
|
+
return this.fetch("navigraph", slug);
|
|
1268
|
+
}
|
|
1269
|
+
form(id) {
|
|
1270
|
+
return this.fetch("form", id);
|
|
1271
|
+
}
|
|
1272
|
+
datagrid(id) {
|
|
1273
|
+
return this.fetch("datagrid", id);
|
|
1274
|
+
}
|
|
1275
|
+
fetch(type, id) {
|
|
1276
|
+
const key = `${type}/${id}`;
|
|
1277
|
+
if (!this.cache.has(key)) {
|
|
1278
|
+
const base = this.store.selectSnapshot(AppConfigState.manifest)?.configBase ?? "/api/v1/config";
|
|
1279
|
+
const obs = this.http.get(`${base}/${type}/${id}`).pipe(map((r) => r.data ?? r), shareReplay(1));
|
|
1280
|
+
this.cache.set(key, obs);
|
|
1281
|
+
}
|
|
1282
|
+
return this.cache.get(key);
|
|
1283
|
+
}
|
|
1284
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1285
|
+
minVersion: "12.0.0",
|
|
1286
|
+
version: "22.2.0",
|
|
1287
|
+
ngImport: i0,
|
|
1288
|
+
type: ConfigService,
|
|
1289
|
+
deps: [],
|
|
1290
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1291
|
+
});
|
|
1292
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1293
|
+
minVersion: "12.0.0",
|
|
1294
|
+
version: "22.2.0",
|
|
1295
|
+
ngImport: i0,
|
|
1296
|
+
type: ConfigService,
|
|
1297
|
+
providedIn: "root"
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1301
|
+
minVersion: "12.0.0",
|
|
1302
|
+
version: "22.2.0",
|
|
1303
|
+
ngImport: i0,
|
|
1304
|
+
type: ConfigService,
|
|
1305
|
+
decorators: [{
|
|
1306
|
+
type: Injectable,
|
|
1307
|
+
args: [{ providedIn: "root" }]
|
|
1308
|
+
}]
|
|
1309
|
+
});
|
|
1310
|
+
const CHOICES = [
|
|
1311
|
+
"light",
|
|
1312
|
+
"dark",
|
|
1313
|
+
"system"
|
|
1314
|
+
];
|
|
1315
|
+
const CACHE_KEY = "coolms_theme";
|
|
1316
|
+
const ACCENT_CACHE_KEY = "coolms_accent";
|
|
1300
1317
|
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
1301
1318
|
function isChoice(v) {
|
|
1302
|
-
|
|
1319
|
+
return typeof v === "string" && CHOICES.includes(v);
|
|
1303
1320
|
}
|
|
1304
1321
|
function isAccent(v) {
|
|
1305
|
-
|
|
1322
|
+
return typeof v === "string" && HEX.test(v);
|
|
1306
1323
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
// block has something to hang on.
|
|
1465
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
1466
|
-
}
|
|
1467
|
-
static readCache() {
|
|
1468
|
-
try {
|
|
1469
|
-
const raw = localStorage.getItem(CACHE_KEY);
|
|
1470
|
-
return isChoice(raw) ? raw : 'system';
|
|
1471
|
-
}
|
|
1472
|
-
catch {
|
|
1473
|
-
return 'system';
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
static readAccentCache() {
|
|
1477
|
-
try {
|
|
1478
|
-
const raw = localStorage.getItem(ACCENT_CACHE_KEY);
|
|
1479
|
-
return isAccent(raw) ? raw : null;
|
|
1480
|
-
}
|
|
1481
|
-
catch {
|
|
1482
|
-
return null;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1486
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
1487
|
-
}
|
|
1488
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ThemeService, decorators: [{
|
|
1489
|
-
type: Injectable,
|
|
1490
|
-
args: [{ providedIn: 'root' }]
|
|
1491
|
-
}], ctorParameters: () => [] });
|
|
1492
|
-
|
|
1493
|
-
/**
|
|
1494
|
-
* The platform's loading indicator: the exclamation mark from Cool!MS,
|
|
1495
|
-
* drawing itself.
|
|
1496
|
-
*
|
|
1497
|
-
* ## Why a mark and not a spinner
|
|
1498
|
-
*
|
|
1499
|
-
* A spinner says "something is happening" and nothing else; every product has
|
|
1500
|
-
* one and none of them are yours. The bang is already the one glyph in the
|
|
1501
|
-
* wordmark that carries the brand, so a loader built from it says whose
|
|
1502
|
-
* software is thinking — for free, in the place a user is already looking.
|
|
1503
|
-
*
|
|
1504
|
-
* ## Why one component rather than a snippet per surface
|
|
1505
|
-
*
|
|
1506
|
-
* It is meant for every wait long enough to notice: a dashboard assembling its
|
|
1507
|
-
* cards, an editor paginating a document, an explorer listing a folder. Those
|
|
1508
|
-
* are different teams' files, and a copied SVG in each is how three loaders
|
|
1509
|
-
* that no longer match each other happen.
|
|
1510
|
-
*
|
|
1511
|
-
* ## The animation
|
|
1512
|
-
*
|
|
1513
|
-
* The stem wipes upward and the dot lands after it, on a loop — the shape being
|
|
1514
|
-
* WRITTEN rather than spun. It is deliberately calm: a loader is background
|
|
1515
|
-
* furniture, and anything with a hard beat gets irritating on the third viewing.
|
|
1516
|
-
* Under `prefers-reduced-motion` it holds still and only breathes, because
|
|
1517
|
-
* motion sickness is not a stylistic preference.
|
|
1518
|
-
*/
|
|
1519
|
-
class CmsLoaderComponent {
|
|
1520
|
-
/** Optional caption. Absent renders the mark alone, which suits a small inline wait. */
|
|
1521
|
-
label = input('', /* @ts-ignore */
|
|
1522
|
-
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1523
|
-
/** Cover the positioned ancestor rather than sitting in the flow. */
|
|
1524
|
-
overlay = input(false, /* @ts-ignore */
|
|
1525
|
-
...(ngDevMode ? [{ debugName: "overlay" }] : /* istanbul ignore next */ []));
|
|
1526
|
-
/**
|
|
1527
|
-
* Row layout at text scale, for a wait inside a row, a status bar or a
|
|
1528
|
-
* button. Override `--cms-loader-size` to tune the mark.
|
|
1529
|
-
*/
|
|
1530
|
-
inline = input(false, /* @ts-ignore */
|
|
1531
|
-
...(ngDevMode ? [{ debugName: "inline" }] : /* istanbul ignore next */ []));
|
|
1532
|
-
/**
|
|
1533
|
-
* A clip-path needs a document-unique id, and this component is used more
|
|
1534
|
-
* than once per page — two loaders sharing an id makes the second one clip
|
|
1535
|
-
* against the first one's rectangle and stop animating.
|
|
1536
|
-
*/
|
|
1537
|
-
clipId = 'cms-loader-' + Math.random().toString(36).slice(2, 9);
|
|
1538
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CmsLoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1539
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: CmsLoaderComponent, isStandalone: true, selector: "cms-loader", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, overlay: { classPropertyName: "overlay", publicName: "overlay", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
1324
|
+
var ThemeService = class ThemeService {
|
|
1325
|
+
api = inject(IdentityApiClient);
|
|
1326
|
+
store = inject(Store);
|
|
1327
|
+
_choice = signal(ThemeService.readCache(), ...ngDevMode ? [{ debugName: "_choice" }] : /* istanbul ignore next */ []);
|
|
1328
|
+
_systemDark = signal(false, ...ngDevMode ? [{ debugName: "_systemDark" }] : /* istanbul ignore next */ []);
|
|
1329
|
+
_userAccent = signal(ThemeService.readAccentCache(), ...ngDevMode ? [{ debugName: "_userAccent" }] : /* istanbul ignore next */ []);
|
|
1330
|
+
_platformAccent = signal(null, ...ngDevMode ? [{ debugName: "_platformAccent" }] : /* istanbul ignore next */ []);
|
|
1331
|
+
loadOnce$;
|
|
1332
|
+
choice = this._choice.asReadonly();
|
|
1333
|
+
userAccent = this._userAccent.asReadonly();
|
|
1334
|
+
platformAccent = this._platformAccent.asReadonly();
|
|
1335
|
+
accent = computed(() => this._userAccent() ?? this._platformAccent(), ...ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []);
|
|
1336
|
+
resolved = computed(() => {
|
|
1337
|
+
const c = this._choice();
|
|
1338
|
+
return c === "system" ? this._systemDark() ? "dark" : "light" : c;
|
|
1339
|
+
}, ...ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []);
|
|
1340
|
+
constructor() {
|
|
1341
|
+
const mq = typeof window !== "undefined" && window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
1342
|
+
if (mq) {
|
|
1343
|
+
this._systemDark.set(mq.matches);
|
|
1344
|
+
mq.addEventListener("change", (e) => this._systemDark.set(e.matches));
|
|
1345
|
+
}
|
|
1346
|
+
effect(() => this.applyToDocument(this.resolved()));
|
|
1347
|
+
effect(() => this.applyAccent(this.accent()));
|
|
1348
|
+
}
|
|
1349
|
+
ensureLoaded() {
|
|
1350
|
+
if (this.loadOnce$) return this.loadOnce$;
|
|
1351
|
+
this.readPlatformAccent();
|
|
1352
|
+
this.loadOnce$ = this.api.getSettings().pipe(map$1((all) => {
|
|
1353
|
+
const prefs = all["preferences"];
|
|
1354
|
+
this.setAccent(isAccent(prefs?.["accentColor"]) ? prefs["accentColor"] : null);
|
|
1355
|
+
return isChoice(prefs?.["theme"]) ? prefs["theme"] : this._choice();
|
|
1356
|
+
}), tap$1((choice) => this.set(choice)), catchError$1(() => of(this._choice())), shareReplay$1({
|
|
1357
|
+
bufferSize: 1,
|
|
1358
|
+
refCount: false
|
|
1359
|
+
}));
|
|
1360
|
+
return this.loadOnce$;
|
|
1361
|
+
}
|
|
1362
|
+
update(value) {
|
|
1363
|
+
if (isChoice(value)) this.set(value);
|
|
1364
|
+
}
|
|
1365
|
+
updateAccent(value) {
|
|
1366
|
+
if (null === value || "" === value) this.setAccent(null);
|
|
1367
|
+
else if (isAccent(value)) this.setAccent(value);
|
|
1368
|
+
}
|
|
1369
|
+
set(choice) {
|
|
1370
|
+
this._choice.set(choice);
|
|
1371
|
+
try {
|
|
1372
|
+
localStorage.setItem(CACHE_KEY, choice);
|
|
1373
|
+
} catch {}
|
|
1374
|
+
}
|
|
1375
|
+
readPlatformAccent() {
|
|
1376
|
+
const value = this.store.selectSnapshot(AppConfigState.manifest)?.platformDefaults?.accentColor;
|
|
1377
|
+
this._platformAccent.set(isAccent(value) ? value : null);
|
|
1378
|
+
}
|
|
1379
|
+
setAccent(accent) {
|
|
1380
|
+
this._userAccent.set(accent);
|
|
1381
|
+
try {
|
|
1382
|
+
if (accent) localStorage.setItem(ACCENT_CACHE_KEY, accent);
|
|
1383
|
+
else localStorage.removeItem(ACCENT_CACHE_KEY);
|
|
1384
|
+
} catch {}
|
|
1385
|
+
}
|
|
1386
|
+
applyAccent(accent) {
|
|
1387
|
+
const root = document.documentElement;
|
|
1388
|
+
if (accent && HEX.test(accent)) root.style.setProperty("--cms-accent", accent);
|
|
1389
|
+
else root.style.removeProperty("--cms-accent");
|
|
1390
|
+
}
|
|
1391
|
+
applyToDocument(theme) {
|
|
1392
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
1393
|
+
}
|
|
1394
|
+
static readCache() {
|
|
1395
|
+
try {
|
|
1396
|
+
const raw = localStorage.getItem(CACHE_KEY);
|
|
1397
|
+
return isChoice(raw) ? raw : "system";
|
|
1398
|
+
} catch {
|
|
1399
|
+
return "system";
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
static readAccentCache() {
|
|
1403
|
+
try {
|
|
1404
|
+
const raw = localStorage.getItem(ACCENT_CACHE_KEY);
|
|
1405
|
+
return isAccent(raw) ? raw : null;
|
|
1406
|
+
} catch {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1411
|
+
minVersion: "12.0.0",
|
|
1412
|
+
version: "22.2.0",
|
|
1413
|
+
ngImport: i0,
|
|
1414
|
+
type: ThemeService,
|
|
1415
|
+
deps: [],
|
|
1416
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1417
|
+
});
|
|
1418
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1419
|
+
minVersion: "12.0.0",
|
|
1420
|
+
version: "22.2.0",
|
|
1421
|
+
ngImport: i0,
|
|
1422
|
+
type: ThemeService,
|
|
1423
|
+
providedIn: "root"
|
|
1424
|
+
});
|
|
1425
|
+
};
|
|
1426
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1427
|
+
minVersion: "12.0.0",
|
|
1428
|
+
version: "22.2.0",
|
|
1429
|
+
ngImport: i0,
|
|
1430
|
+
type: ThemeService,
|
|
1431
|
+
decorators: [{
|
|
1432
|
+
type: Injectable,
|
|
1433
|
+
args: [{ providedIn: "root" }]
|
|
1434
|
+
}],
|
|
1435
|
+
ctorParameters: () => []
|
|
1436
|
+
});
|
|
1437
|
+
var CmsLoaderComponent = class CmsLoaderComponent {
|
|
1438
|
+
label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
1439
|
+
overlay = input(false, ...ngDevMode ? [{ debugName: "overlay" }] : /* istanbul ignore next */ []);
|
|
1440
|
+
inline = input(false, ...ngDevMode ? [{ debugName: "inline" }] : /* istanbul ignore next */ []);
|
|
1441
|
+
clipId = "cms-loader-" + Math.random().toString(36).slice(2, 9);
|
|
1442
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1443
|
+
minVersion: "12.0.0",
|
|
1444
|
+
version: "22.2.0",
|
|
1445
|
+
ngImport: i0,
|
|
1446
|
+
type: CmsLoaderComponent,
|
|
1447
|
+
deps: [],
|
|
1448
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1449
|
+
});
|
|
1450
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1451
|
+
minVersion: "17.0.0",
|
|
1452
|
+
version: "22.2.0",
|
|
1453
|
+
type: CmsLoaderComponent,
|
|
1454
|
+
isStandalone: true,
|
|
1455
|
+
selector: "cms-loader",
|
|
1456
|
+
inputs: {
|
|
1457
|
+
label: {
|
|
1458
|
+
classPropertyName: "label",
|
|
1459
|
+
publicName: "label",
|
|
1460
|
+
isSignal: true,
|
|
1461
|
+
isRequired: false,
|
|
1462
|
+
transformFunction: null
|
|
1463
|
+
},
|
|
1464
|
+
overlay: {
|
|
1465
|
+
classPropertyName: "overlay",
|
|
1466
|
+
publicName: "overlay",
|
|
1467
|
+
isSignal: true,
|
|
1468
|
+
isRequired: false,
|
|
1469
|
+
transformFunction: null
|
|
1470
|
+
},
|
|
1471
|
+
inline: {
|
|
1472
|
+
classPropertyName: "inline",
|
|
1473
|
+
publicName: "inline",
|
|
1474
|
+
isSignal: true,
|
|
1475
|
+
isRequired: false,
|
|
1476
|
+
transformFunction: null
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
ngImport: i0,
|
|
1480
|
+
template: `
|
|
1540
1481
|
<div class="cms-loader"
|
|
1541
1482
|
[class.cms-loader--overlay]="overlay()"
|
|
1542
1483
|
[class.cms-loader--inline]="inline()"
|
|
@@ -1561,11 +1502,24 @@ class CmsLoaderComponent {
|
|
|
1561
1502
|
<span class="cms-loader__label">{{ label() }}</span>
|
|
1562
1503
|
}
|
|
1563
1504
|
</div>
|
|
1564
|
-
`,
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1505
|
+
`,
|
|
1506
|
+
isInline: true,
|
|
1507
|
+
styles: [".cms-loader{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:24px;color:var(--cms-text-muted)}.cms-loader--overlay{position:absolute;inset:0;z-index:5;background:var(--cms-surface)}.cms-loader--inline{flex-direction:row;gap:8px;padding:0}.cms-loader--inline .cms-loader__mark{width:var(--cms-loader-size, 14px)}.cms-loader__mark{width:var(--cms-loader-size, 40px);height:auto;overflow:visible}.cms-loader__ghost{fill:var(--cms-border)}.cms-loader__stem,.cms-loader__dot{fill:var(--cms-accent)}.cms-loader__wipe{animation:cms-loader-wipe 1.6s cubic-bezier(.65,0,.35,1) infinite;transform-origin:12px 32px}.cms-loader__dot{animation:cms-loader-dot 1.6s cubic-bezier(.65,0,.35,1) infinite;transform-origin:12px 42px}@keyframes cms-loader-wipe{0%{transform:scaleY(0)}45%,70%{transform:scaleY(1)}to{transform:scaleY(0)}}@keyframes cms-loader-dot{0%,40%{opacity:0;transform:scale(.4)}55%,70%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.4)}}.cms-loader__label{font-size:.8125rem}@media(prefers-reduced-motion:reduce){.cms-loader__wipe{animation:none;transform:scaleY(1)}.cms-loader__dot{animation:cms-loader-breathe 2s ease-in-out infinite;opacity:1}}@keyframes cms-loader-breathe{0%,to{opacity:.35}50%{opacity:1}}\n"],
|
|
1508
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
1509
|
+
});
|
|
1510
|
+
};
|
|
1511
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1512
|
+
minVersion: "12.0.0",
|
|
1513
|
+
version: "22.2.0",
|
|
1514
|
+
ngImport: i0,
|
|
1515
|
+
type: CmsLoaderComponent,
|
|
1516
|
+
decorators: [{
|
|
1517
|
+
type: Component,
|
|
1518
|
+
args: [{
|
|
1519
|
+
selector: "cms-loader",
|
|
1520
|
+
standalone: true,
|
|
1521
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1522
|
+
template: `
|
|
1569
1523
|
<div class="cms-loader"
|
|
1570
1524
|
[class.cms-loader--overlay]="overlay()"
|
|
1571
1525
|
[class.cms-loader--inline]="inline()"
|
|
@@ -1590,496 +1544,505 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
1590
1544
|
<span class="cms-loader__label">{{ label() }}</span>
|
|
1591
1545
|
}
|
|
1592
1546
|
</div>
|
|
1593
|
-
`,
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1547
|
+
`,
|
|
1548
|
+
styles: [".cms-loader{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:24px;color:var(--cms-text-muted)}.cms-loader--overlay{position:absolute;inset:0;z-index:5;background:var(--cms-surface)}.cms-loader--inline{flex-direction:row;gap:8px;padding:0}.cms-loader--inline .cms-loader__mark{width:var(--cms-loader-size, 14px)}.cms-loader__mark{width:var(--cms-loader-size, 40px);height:auto;overflow:visible}.cms-loader__ghost{fill:var(--cms-border)}.cms-loader__stem,.cms-loader__dot{fill:var(--cms-accent)}.cms-loader__wipe{animation:cms-loader-wipe 1.6s cubic-bezier(.65,0,.35,1) infinite;transform-origin:12px 32px}.cms-loader__dot{animation:cms-loader-dot 1.6s cubic-bezier(.65,0,.35,1) infinite;transform-origin:12px 42px}@keyframes cms-loader-wipe{0%{transform:scaleY(0)}45%,70%{transform:scaleY(1)}to{transform:scaleY(0)}}@keyframes cms-loader-dot{0%,40%{opacity:0;transform:scale(.4)}55%,70%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.4)}}.cms-loader__label{font-size:.8125rem}@media(prefers-reduced-motion:reduce){.cms-loader__wipe{animation:none;transform:scaleY(1)}.cms-loader__dot{animation:cms-loader-breathe 2s ease-in-out infinite;opacity:1}}@keyframes cms-loader-breathe{0%,to{opacity:.35}50%{opacity:1}}\n"]
|
|
1549
|
+
}]
|
|
1550
|
+
}],
|
|
1551
|
+
propDecorators: {
|
|
1552
|
+
label: [{
|
|
1553
|
+
type: i0.Input,
|
|
1554
|
+
args: [{
|
|
1555
|
+
isSignal: true,
|
|
1556
|
+
alias: "label",
|
|
1557
|
+
required: false
|
|
1558
|
+
}]
|
|
1559
|
+
}],
|
|
1560
|
+
overlay: [{
|
|
1561
|
+
type: i0.Input,
|
|
1562
|
+
args: [{
|
|
1563
|
+
isSignal: true,
|
|
1564
|
+
alias: "overlay",
|
|
1565
|
+
required: false
|
|
1566
|
+
}]
|
|
1567
|
+
}],
|
|
1568
|
+
inline: [{
|
|
1569
|
+
type: i0.Input,
|
|
1570
|
+
args: [{
|
|
1571
|
+
isSignal: true,
|
|
1572
|
+
alias: "inline",
|
|
1573
|
+
required: false
|
|
1574
|
+
}]
|
|
1575
|
+
}]
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
var SidebarStateService = class SidebarStateService {
|
|
1579
|
+
static STORAGE_KEY = "coolms.admin.sidebar.collapsed";
|
|
1580
|
+
static STORAGE_KEY_SECTIONS = "coolms.admin.sidebar.collapsedSections";
|
|
1581
|
+
static STORAGE_KEY_ITEMS = "coolms.admin.sidebar.collapsedItems";
|
|
1582
|
+
collapsed = signal(this.readPersisted(), ...ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []);
|
|
1583
|
+
collapsedSections = signal(this.readSectionsPersisted(), ...ngDevMode ? [{ debugName: "collapsedSections" }] : /* istanbul ignore next */ []);
|
|
1584
|
+
collapsedItems = signal(this.readItemsPersisted(), ...ngDevMode ? [{ debugName: "collapsedItems" }] : /* istanbul ignore next */ []);
|
|
1585
|
+
toggle() {
|
|
1586
|
+
const next = !this.collapsed();
|
|
1587
|
+
this.collapsed.set(next);
|
|
1588
|
+
this.persist(next);
|
|
1589
|
+
}
|
|
1590
|
+
isSectionCollapsed(id) {
|
|
1591
|
+
return this.collapsedSections().has(id);
|
|
1592
|
+
}
|
|
1593
|
+
toggleSection(id) {
|
|
1594
|
+
const next = new Set(this.collapsedSections());
|
|
1595
|
+
if (next.has(id)) next.delete(id);
|
|
1596
|
+
else next.add(id);
|
|
1597
|
+
this.collapsedSections.set(next);
|
|
1598
|
+
this.persistSections(next);
|
|
1599
|
+
}
|
|
1600
|
+
ensureExpanded(id) {
|
|
1601
|
+
if (!this.collapsedSections().has(id)) return;
|
|
1602
|
+
const next = new Set(this.collapsedSections());
|
|
1603
|
+
next.delete(id);
|
|
1604
|
+
this.collapsedSections.set(next);
|
|
1605
|
+
this.persistSections(next);
|
|
1606
|
+
}
|
|
1607
|
+
isItemCollapsed(id) {
|
|
1608
|
+
return this.collapsedItems().has(id);
|
|
1609
|
+
}
|
|
1610
|
+
toggleItem(id) {
|
|
1611
|
+
const next = new Set(this.collapsedItems());
|
|
1612
|
+
if (next.has(id)) next.delete(id);
|
|
1613
|
+
else next.add(id);
|
|
1614
|
+
this.collapsedItems.set(next);
|
|
1615
|
+
this.persistItems(next);
|
|
1616
|
+
}
|
|
1617
|
+
readPersisted() {
|
|
1618
|
+
try {
|
|
1619
|
+
return localStorage.getItem(SidebarStateService.STORAGE_KEY) === "true";
|
|
1620
|
+
} catch {
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
persist(value) {
|
|
1625
|
+
try {
|
|
1626
|
+
localStorage.setItem(SidebarStateService.STORAGE_KEY, String(value));
|
|
1627
|
+
} catch {}
|
|
1628
|
+
}
|
|
1629
|
+
readSectionsPersisted() {
|
|
1630
|
+
try {
|
|
1631
|
+
const raw = localStorage.getItem(SidebarStateService.STORAGE_KEY_SECTIONS);
|
|
1632
|
+
if (raw === null) return /* @__PURE__ */ new Set();
|
|
1633
|
+
const parsed = JSON.parse(raw);
|
|
1634
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
1635
|
+
return new Set(parsed.filter((v) => typeof v === "string"));
|
|
1636
|
+
} catch {
|
|
1637
|
+
return /* @__PURE__ */ new Set();
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
persistSections(value) {
|
|
1641
|
+
try {
|
|
1642
|
+
localStorage.setItem(SidebarStateService.STORAGE_KEY_SECTIONS, JSON.stringify(Array.from(value)));
|
|
1643
|
+
} catch {}
|
|
1644
|
+
}
|
|
1645
|
+
readItemsPersisted() {
|
|
1646
|
+
try {
|
|
1647
|
+
const raw = localStorage.getItem(SidebarStateService.STORAGE_KEY_ITEMS);
|
|
1648
|
+
if (raw === null) return /* @__PURE__ */ new Set();
|
|
1649
|
+
const parsed = JSON.parse(raw);
|
|
1650
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
1651
|
+
return new Set(parsed.filter((v) => typeof v === "string"));
|
|
1652
|
+
} catch {
|
|
1653
|
+
return /* @__PURE__ */ new Set();
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
persistItems(value) {
|
|
1657
|
+
try {
|
|
1658
|
+
localStorage.setItem(SidebarStateService.STORAGE_KEY_ITEMS, JSON.stringify(Array.from(value)));
|
|
1659
|
+
} catch {}
|
|
1660
|
+
}
|
|
1661
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1662
|
+
minVersion: "12.0.0",
|
|
1663
|
+
version: "22.2.0",
|
|
1664
|
+
ngImport: i0,
|
|
1665
|
+
type: SidebarStateService,
|
|
1666
|
+
deps: [],
|
|
1667
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1668
|
+
});
|
|
1669
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1670
|
+
minVersion: "12.0.0",
|
|
1671
|
+
version: "22.2.0",
|
|
1672
|
+
ngImport: i0,
|
|
1673
|
+
type: SidebarStateService,
|
|
1674
|
+
providedIn: "root"
|
|
1675
|
+
});
|
|
1676
|
+
};
|
|
1677
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1678
|
+
minVersion: "12.0.0",
|
|
1679
|
+
version: "22.2.0",
|
|
1680
|
+
ngImport: i0,
|
|
1681
|
+
type: SidebarStateService,
|
|
1682
|
+
decorators: [{
|
|
1683
|
+
type: Injectable,
|
|
1684
|
+
args: [{ providedIn: "root" }]
|
|
1685
|
+
}]
|
|
1686
|
+
});
|
|
1687
|
+
var EntitySearchService = class EntitySearchService {
|
|
1688
|
+
http = inject(HttpClient);
|
|
1689
|
+
search(entityType, query, limit = 20) {
|
|
1690
|
+
const params = new HttpParams().set("type", entityType).set("q", query).set("limit", String(limit));
|
|
1691
|
+
return this.http.get("/api/entity-search", { params });
|
|
1692
|
+
}
|
|
1693
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1694
|
+
minVersion: "12.0.0",
|
|
1695
|
+
version: "22.2.0",
|
|
1696
|
+
ngImport: i0,
|
|
1697
|
+
type: EntitySearchService,
|
|
1698
|
+
deps: [],
|
|
1699
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1700
|
+
});
|
|
1701
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1702
|
+
minVersion: "12.0.0",
|
|
1703
|
+
version: "22.2.0",
|
|
1704
|
+
ngImport: i0,
|
|
1705
|
+
type: EntitySearchService,
|
|
1706
|
+
providedIn: "root"
|
|
1707
|
+
});
|
|
1708
|
+
};
|
|
1709
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1710
|
+
minVersion: "12.0.0",
|
|
1711
|
+
version: "22.2.0",
|
|
1712
|
+
ngImport: i0,
|
|
1713
|
+
type: EntitySearchService,
|
|
1714
|
+
decorators: [{
|
|
1715
|
+
type: Injectable,
|
|
1716
|
+
args: [{ providedIn: "root" }]
|
|
1717
|
+
}]
|
|
1718
|
+
});
|
|
1719
|
+
var NaviGraphService = class NaviGraphService {
|
|
1720
|
+
http = inject(HttpClient);
|
|
1721
|
+
router = inject(Router);
|
|
1722
|
+
store = inject(Store);
|
|
1723
|
+
errors = inject(ErrorHandlerService);
|
|
1724
|
+
adminNav = signal([], ...ngDevMode ? [{ debugName: "adminNav" }] : /* istanbul ignore next */ []);
|
|
1725
|
+
topbarNav = signal([], ...ngDevMode ? [{ debugName: "topbarNav" }] : /* istanbul ignore next */ []);
|
|
1726
|
+
cache = /* @__PURE__ */ new Map();
|
|
1727
|
+
loadAdminNav(url) {
|
|
1728
|
+
return this.loadTree(url).pipe(map((tree) => {
|
|
1729
|
+
this.adminNav.set(tree);
|
|
1730
|
+
return tree;
|
|
1731
|
+
}));
|
|
1732
|
+
}
|
|
1733
|
+
loadTopbarNav(url) {
|
|
1734
|
+
return this.loadTree(url).pipe(map((tree) => {
|
|
1735
|
+
this.topbarNav.set(tree);
|
|
1736
|
+
return tree;
|
|
1737
|
+
}));
|
|
1738
|
+
}
|
|
1739
|
+
handleClick(node) {
|
|
1740
|
+
const target = node.meta.target;
|
|
1741
|
+
if (target === "action.logout") {
|
|
1742
|
+
this.store.dispatch(new Logout()).subscribe(() => {
|
|
1743
|
+
this.router.navigate(["/login"]);
|
|
1744
|
+
});
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (target === "_blank") {
|
|
1748
|
+
window.open(node.meta.routerLink ?? node.path, "_blank", "noopener");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const link = node.meta.routerLink ?? node.path;
|
|
1752
|
+
this.router.navigate([link]);
|
|
1753
|
+
}
|
|
1754
|
+
isVisible(node, record) {
|
|
1755
|
+
const showWhen = node.meta["showWhen"];
|
|
1756
|
+
if (!showWhen) return true;
|
|
1757
|
+
return this.evalCondition(showWhen, record);
|
|
1758
|
+
}
|
|
1759
|
+
matchesShowWhen(showWhen, record) {
|
|
1760
|
+
if (!showWhen) return true;
|
|
1761
|
+
return this.evalCondition(showWhen, record);
|
|
1762
|
+
}
|
|
1763
|
+
evalCondition(cond, record) {
|
|
1764
|
+
if ("and" in cond) return cond["and"].every((c) => this.evalCondition(c, record));
|
|
1765
|
+
if ("or" in cond) return cond["or"].some((c) => this.evalCondition(c, record));
|
|
1766
|
+
const field = cond["field"];
|
|
1767
|
+
if (!(field in record)) return false;
|
|
1768
|
+
const val = record[field];
|
|
1769
|
+
switch (cond["op"]) {
|
|
1770
|
+
case "eq": return val === cond["value"];
|
|
1771
|
+
case "ne": return val !== cond["value"];
|
|
1772
|
+
case "in": return Array.isArray(cond["value"]) && cond["value"].includes(val);
|
|
1773
|
+
case "nin": return Array.isArray(cond["value"]) && !cond["value"].includes(val);
|
|
1774
|
+
case "startsWith": return typeof val === "string" && typeof cond["value"] === "string" && val.startsWith(cond["value"]);
|
|
1775
|
+
default: return false;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
loadTree(url) {
|
|
1779
|
+
if (this.cache.has(url)) return of(this.cache.get(url));
|
|
1780
|
+
return this.http.get(url, { headers: { Accept: "application/ld+json" } }).pipe(map((r) => {
|
|
1781
|
+
const raw = r["member"] ?? r["hydra:member"] ?? [];
|
|
1782
|
+
const flat = Array.isArray(raw) ? raw : [];
|
|
1783
|
+
const tree = this.buildTree(flat);
|
|
1784
|
+
this.cache.set(url, tree);
|
|
1785
|
+
return tree;
|
|
1786
|
+
}), catchError((err) => {
|
|
1787
|
+
console.error("[NaviGraph] loadTree failed for", url, this.errors.humanize(err));
|
|
1788
|
+
return of([]);
|
|
1789
|
+
}));
|
|
1790
|
+
}
|
|
1791
|
+
buildTree(flat) {
|
|
1792
|
+
if (!flat.length) return [];
|
|
1793
|
+
const map = new Map(flat.map((n) => [n.id, {
|
|
1794
|
+
...n,
|
|
1795
|
+
children: []
|
|
1796
|
+
}]));
|
|
1797
|
+
const roots = [];
|
|
1798
|
+
for (const node of map.values()) if (node.parentId && map.has(node.parentId)) map.get(node.parentId).children.push(node);
|
|
1799
|
+
else roots.push(node);
|
|
1800
|
+
const sort = (nodes) => {
|
|
1801
|
+
nodes.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1802
|
+
nodes.forEach((n) => n.children?.length && sort(n.children));
|
|
1803
|
+
};
|
|
1804
|
+
sort(roots);
|
|
1805
|
+
return roots;
|
|
1806
|
+
}
|
|
1807
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1808
|
+
minVersion: "12.0.0",
|
|
1809
|
+
version: "22.2.0",
|
|
1810
|
+
ngImport: i0,
|
|
1811
|
+
type: NaviGraphService,
|
|
1812
|
+
deps: [],
|
|
1813
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1814
|
+
});
|
|
1815
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1816
|
+
minVersion: "12.0.0",
|
|
1817
|
+
version: "22.2.0",
|
|
1818
|
+
ngImport: i0,
|
|
1819
|
+
type: NaviGraphService,
|
|
1820
|
+
providedIn: "root"
|
|
1821
|
+
});
|
|
1822
|
+
};
|
|
1823
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1824
|
+
minVersion: "12.0.0",
|
|
1825
|
+
version: "22.2.0",
|
|
1826
|
+
ngImport: i0,
|
|
1827
|
+
type: NaviGraphService,
|
|
1828
|
+
decorators: [{
|
|
1829
|
+
type: Injectable,
|
|
1830
|
+
args: [{ providedIn: "root" }]
|
|
1831
|
+
}]
|
|
1832
|
+
});
|
|
1833
|
+
var ComponentRegistry = class ComponentRegistry {
|
|
1834
|
+
static map = /* @__PURE__ */ new Map();
|
|
1835
|
+
static register(key, component) {
|
|
1836
|
+
ComponentRegistry.map.set(key, component);
|
|
1837
|
+
}
|
|
1838
|
+
static get(key) {
|
|
1839
|
+
return ComponentRegistry.map.get(key);
|
|
1840
|
+
}
|
|
1841
|
+
static has(key) {
|
|
1842
|
+
return ComponentRegistry.map.has(key);
|
|
1843
|
+
}
|
|
1844
|
+
static keys() {
|
|
1845
|
+
return Array.from(ComponentRegistry.map.keys());
|
|
1846
|
+
}
|
|
1847
|
+
register(key, component) {
|
|
1848
|
+
ComponentRegistry.register(key, component);
|
|
1849
|
+
}
|
|
1850
|
+
get(key) {
|
|
1851
|
+
return ComponentRegistry.get(key) ?? null;
|
|
1852
|
+
}
|
|
1853
|
+
has(key) {
|
|
1854
|
+
return ComponentRegistry.has(key);
|
|
1855
|
+
}
|
|
1856
|
+
keys() {
|
|
1857
|
+
return ComponentRegistry.keys();
|
|
1858
|
+
}
|
|
1859
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1860
|
+
minVersion: "12.0.0",
|
|
1861
|
+
version: "22.2.0",
|
|
1862
|
+
ngImport: i0,
|
|
1863
|
+
type: ComponentRegistry,
|
|
1864
|
+
deps: [],
|
|
1865
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1866
|
+
});
|
|
1867
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1868
|
+
minVersion: "12.0.0",
|
|
1869
|
+
version: "22.2.0",
|
|
1870
|
+
ngImport: i0,
|
|
1871
|
+
type: ComponentRegistry,
|
|
1872
|
+
providedIn: "root"
|
|
1873
|
+
});
|
|
1874
|
+
};
|
|
1875
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1876
|
+
minVersion: "12.0.0",
|
|
1877
|
+
version: "22.2.0",
|
|
1878
|
+
ngImport: i0,
|
|
1879
|
+
type: ComponentRegistry,
|
|
1880
|
+
decorators: [{
|
|
1881
|
+
type: Injectable,
|
|
1882
|
+
args: [{ providedIn: "root" }]
|
|
1883
|
+
}]
|
|
1884
|
+
});
|
|
1885
|
+
const CONSOLE_CONTRACT = {
|
|
1886
|
+
name: "console",
|
|
1887
|
+
version: "1.0"
|
|
1888
|
+
};
|
|
1889
|
+
function isPortProvision(p) {
|
|
1890
|
+
return typeof p === "object" && "port" in p && "useExisting" in p;
|
|
1735
1891
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
args: [{ providedIn: 'root' }]
|
|
1739
|
-
}] });
|
|
1740
|
-
|
|
1741
|
-
/**
|
|
1742
|
-
* HTTP client for the Phase 2a `/api/entity-search` endpoint.
|
|
1743
|
-
* Returns a stream of search results for the given entity FQCN.
|
|
1744
|
-
* Permission filtering happens at the backend resolver layer —
|
|
1745
|
-
* this service is a thin RPC.
|
|
1746
|
-
*/
|
|
1747
|
-
class EntitySearchService {
|
|
1748
|
-
http = inject(HttpClient);
|
|
1749
|
-
/**
|
|
1750
|
-
* @param entityType Fully-qualified PHP class name of the entity
|
|
1751
|
-
* being searched (e.g. the backend's User entity).
|
|
1752
|
-
* @param query Free-text search input. Empty string is
|
|
1753
|
-
* valid — the backend returns the default
|
|
1754
|
-
* listing for that entity type.
|
|
1755
|
-
* @param limit Max rows to return; clamped server-side to
|
|
1756
|
-
* [1, 100].
|
|
1757
|
-
*/
|
|
1758
|
-
search(entityType, query, limit = 20) {
|
|
1759
|
-
const params = new HttpParams()
|
|
1760
|
-
.set('type', entityType)
|
|
1761
|
-
.set('q', query)
|
|
1762
|
-
.set('limit', String(limit));
|
|
1763
|
-
return this.http.get('/api/entity-search', { params });
|
|
1764
|
-
}
|
|
1765
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EntitySearchService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1766
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EntitySearchService, providedIn: 'root' });
|
|
1892
|
+
function consoleEntry(entry) {
|
|
1893
|
+
return entry;
|
|
1767
1894
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
*
|
|
1776
|
-
* Usage:
|
|
1777
|
-
* naviGraphService.loadAdminNav(url).subscribe();
|
|
1778
|
-
* const items = naviGraphService.adminNav(); // signal<NaviGraphNode[]>
|
|
1779
|
-
* naviGraphService.handleClick(node);
|
|
1780
|
-
*/
|
|
1781
|
-
class NaviGraphService {
|
|
1782
|
-
http = inject(HttpClient);
|
|
1783
|
-
router = inject(Router);
|
|
1784
|
-
store = inject(Store);
|
|
1785
|
-
errors = inject(ErrorHandlerService);
|
|
1786
|
-
/** Resolved root nodes for navi.admin (sidebar). */
|
|
1787
|
-
adminNav = signal([], /* @ts-ignore */
|
|
1788
|
-
...(ngDevMode ? [{ debugName: "adminNav" }] : /* istanbul ignore next */ []));
|
|
1789
|
-
/** Resolved root nodes for navi.admin.topbar. */
|
|
1790
|
-
topbarNav = signal([], /* @ts-ignore */
|
|
1791
|
-
...(ngDevMode ? [{ debugName: "topbarNav" }] : /* istanbul ignore next */ []));
|
|
1792
|
-
cache = new Map();
|
|
1793
|
-
// -------------------------------------------------------------------------
|
|
1794
|
-
loadAdminNav(url) {
|
|
1795
|
-
return this.loadTree(url).pipe(map(tree => { this.adminNav.set(tree); return tree; }));
|
|
1796
|
-
}
|
|
1797
|
-
loadTopbarNav(url) {
|
|
1798
|
-
return this.loadTree(url).pipe(map(tree => { this.topbarNav.set(tree); return tree; }));
|
|
1799
|
-
}
|
|
1800
|
-
// -------------------------------------------------------------------------
|
|
1801
|
-
/**
|
|
1802
|
-
* Handle a nav-node click.
|
|
1803
|
-
*
|
|
1804
|
-
* Routing logic based on meta.target:
|
|
1805
|
-
* 'action.logout' — dispatch Logout
|
|
1806
|
-
* '_blank' — window.open (uses node.path as href)
|
|
1807
|
-
* default — router.navigate with meta.routerLink or node.path
|
|
1808
|
-
*/
|
|
1809
|
-
handleClick(node) {
|
|
1810
|
-
const target = node.meta.target;
|
|
1811
|
-
if (target === 'action.logout') {
|
|
1812
|
-
this.store.dispatch(new Logout()).subscribe(() => {
|
|
1813
|
-
void this.router.navigate(['/login']);
|
|
1814
|
-
});
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
if (target === '_blank') {
|
|
1818
|
-
window.open(node.meta.routerLink ?? node.path, '_blank', 'noopener');
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
const link = node.meta.routerLink ?? node.path;
|
|
1822
|
-
void this.router.navigate([link]);
|
|
1823
|
-
}
|
|
1824
|
-
// -------------------------------------------------------------------------
|
|
1825
|
-
/**
|
|
1826
|
-
* Evaluate `showWhen` meta condition against a record.
|
|
1827
|
-
* Nodes with no `showWhen` are always visible.
|
|
1828
|
-
* Supports nested AND/OR logic: { and: [...] } / { or: [...] }
|
|
1829
|
-
*/
|
|
1830
|
-
isVisible(node, record) {
|
|
1831
|
-
const showWhen = node.meta['showWhen'];
|
|
1832
|
-
if (!showWhen)
|
|
1833
|
-
return true;
|
|
1834
|
-
return this.evalCondition(showWhen, record);
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Standalone `showWhen` evaluator for callers that don't have a
|
|
1838
|
-
* full `NaviGraphNode` (e.g. a grid's row context menu — a
|
|
1839
|
-
* `rowAction.showWhen` predicate is evaluated against the row's
|
|
1840
|
-
* own projection). `null`/`undefined` predicates default to
|
|
1841
|
-
* visible, mirroring `isVisible`'s "no rule = always shown"
|
|
1842
|
-
* semantics.
|
|
1843
|
-
*/
|
|
1844
|
-
matchesShowWhen(showWhen, record) {
|
|
1845
|
-
if (!showWhen)
|
|
1846
|
-
return true;
|
|
1847
|
-
return this.evalCondition(showWhen, record);
|
|
1848
|
-
}
|
|
1849
|
-
evalCondition(cond, record) {
|
|
1850
|
-
if ('and' in cond) {
|
|
1851
|
-
return cond['and']
|
|
1852
|
-
.every(c => this.evalCondition(c, record));
|
|
1853
|
-
}
|
|
1854
|
-
if ('or' in cond) {
|
|
1855
|
-
return cond['or']
|
|
1856
|
-
.some(c => this.evalCondition(c, record));
|
|
1857
|
-
}
|
|
1858
|
-
// Strict missing-field semantics. A leaf predicate
|
|
1859
|
-
// referencing a field absent from the record evaluates to
|
|
1860
|
-
// false (fail-loud) instead of the prior op-dependent lax
|
|
1861
|
-
// behavior that returned true for ne/nin/unknown ops. Aligns
|
|
1862
|
-
// with CSS attribute selectors / JSONPath / React conditional
|
|
1863
|
-
// rendering conventions.
|
|
1864
|
-
const field = cond['field'];
|
|
1865
|
-
if (!(field in record))
|
|
1866
|
-
return false;
|
|
1867
|
-
const val = record[field];
|
|
1868
|
-
switch (cond['op']) {
|
|
1869
|
-
case 'eq': return val === cond['value'];
|
|
1870
|
-
case 'ne': return val !== cond['value'];
|
|
1871
|
-
case 'in': return Array.isArray(cond['value']) && cond['value'].includes(val);
|
|
1872
|
-
case 'nin': return Array.isArray(cond['value']) && !cond['value'].includes(val);
|
|
1873
|
-
// Prefix match for strings — primary use case is filtering
|
|
1874
|
-
// actions by MIME-type major (e.g., "image/" matches all
|
|
1875
|
-
// image MIMEs without enumerating each subtype).
|
|
1876
|
-
case 'startsWith':
|
|
1877
|
-
return typeof val === 'string'
|
|
1878
|
-
&& typeof cond['value'] === 'string'
|
|
1879
|
-
&& val.startsWith(cond['value']);
|
|
1880
|
-
// Strict: unknown / typo op (e.g. 'equals' instead of 'eq')
|
|
1881
|
-
// never matches.
|
|
1882
|
-
default: return false;
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
/**
|
|
1886
|
-
* Fetch a graph endpoint and return a nested tree.
|
|
1887
|
-
* Handles both compact JSON-LD IRIs (`member`) and
|
|
1888
|
-
* the legacy Hydra key (`hydra:member`).
|
|
1889
|
-
* Results are cached by URL for the lifetime of the service instance.
|
|
1890
|
-
*/
|
|
1891
|
-
loadTree(url) {
|
|
1892
|
-
if (this.cache.has(url)) {
|
|
1893
|
-
return of(this.cache.get(url));
|
|
1894
|
-
}
|
|
1895
|
-
return this.http.get(url, {
|
|
1896
|
-
headers: { Accept: 'application/ld+json' },
|
|
1897
|
-
}).pipe(map(r => {
|
|
1898
|
-
const raw = r['member'] ?? r['hydra:member'] ?? [];
|
|
1899
|
-
const flat = Array.isArray(raw) ? raw : [];
|
|
1900
|
-
const tree = this.buildTree(flat);
|
|
1901
|
-
this.cache.set(url, tree);
|
|
1902
|
-
return tree;
|
|
1903
|
-
}), catchError(err => {
|
|
1904
|
-
console.error('[NaviGraph] loadTree failed for', url, this.errors.humanize(err));
|
|
1905
|
-
return of([]);
|
|
1906
|
-
}));
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Build a nested tree from a flat array of nodes.
|
|
1910
|
-
* Nodes are sorted by sortOrder at each level.
|
|
1911
|
-
*/
|
|
1912
|
-
buildTree(flat) {
|
|
1913
|
-
if (!flat.length) {
|
|
1914
|
-
return [];
|
|
1915
|
-
}
|
|
1916
|
-
const map = new Map(flat.map(n => [n.id, { ...n, children: [] }]));
|
|
1917
|
-
const roots = [];
|
|
1918
|
-
for (const node of map.values()) {
|
|
1919
|
-
if (node.parentId && map.has(node.parentId)) {
|
|
1920
|
-
map.get(node.parentId).children.push(node);
|
|
1921
|
-
}
|
|
1922
|
-
else {
|
|
1923
|
-
roots.push(node);
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
const sort = (nodes) => {
|
|
1927
|
-
nodes.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1928
|
-
nodes.forEach(n => n.children?.length && sort(n.children));
|
|
1929
|
-
};
|
|
1930
|
-
sort(roots);
|
|
1931
|
-
return roots;
|
|
1932
|
-
}
|
|
1933
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: NaviGraphService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1934
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: NaviGraphService, providedIn: 'root' });
|
|
1895
|
+
const VERSION = /^(\d+)\.(\d+)$/;
|
|
1896
|
+
const RANGE = /^\^(\d+)\.(\d+)$/;
|
|
1897
|
+
function satisfiesRange(range, version) {
|
|
1898
|
+
const r = RANGE.exec(range);
|
|
1899
|
+
const v = VERSION.exec(version);
|
|
1900
|
+
if (!r || !v) return false;
|
|
1901
|
+
return Number(r[1]) === Number(v[1]) && Number(v[2]) >= Number(r[2]);
|
|
1935
1902
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
register(key, component) {
|
|
1974
|
-
ComponentRegistry.register(key, component);
|
|
1975
|
-
}
|
|
1976
|
-
get(key) {
|
|
1977
|
-
return ComponentRegistry.get(key) ?? null;
|
|
1978
|
-
}
|
|
1979
|
-
has(key) {
|
|
1980
|
-
return ComponentRegistry.has(key);
|
|
1981
|
-
}
|
|
1982
|
-
keys() {
|
|
1983
|
-
return ComponentRegistry.keys();
|
|
1984
|
-
}
|
|
1985
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ComponentRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1986
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ComponentRegistry, providedIn: 'root' });
|
|
1903
|
+
var ConsoleAssemblyError = class extends Error {
|
|
1904
|
+
problems;
|
|
1905
|
+
constructor(problems) {
|
|
1906
|
+
super(`console assembly refused:\n ${problems.join("\n ")}`);
|
|
1907
|
+
this.problems = problems;
|
|
1908
|
+
this.name = "ConsoleAssemblyError";
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
function assembleConsole(entries) {
|
|
1912
|
+
const problems = [];
|
|
1913
|
+
const seenModules = /* @__PURE__ */ new Map();
|
|
1914
|
+
const paths = /* @__PURE__ */ new Map();
|
|
1915
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
1916
|
+
const topbar = /* @__PURE__ */ new Map();
|
|
1917
|
+
const overlays = /* @__PURE__ */ new Map();
|
|
1918
|
+
const panels = /* @__PURE__ */ new Map();
|
|
1919
|
+
const states = /* @__PURE__ */ new Map();
|
|
1920
|
+
const ports = /* @__PURE__ */ new Map();
|
|
1921
|
+
const claim = (map, key, owner, what) => {
|
|
1922
|
+
const prev = map.get(key);
|
|
1923
|
+
if (prev !== void 0 && prev !== owner) problems.push(`${what} '${String(key)}' is claimed by modules '${prev}' and '${owner}'`);
|
|
1924
|
+
else map.set(key, owner);
|
|
1925
|
+
};
|
|
1926
|
+
for (const e of entries) {
|
|
1927
|
+
if (!satisfiesRange(e.range, CONSOLE_CONTRACT.version)) problems.push(`module '${e.module}' offers console ${e.range}; this host implements console ${CONSOLE_CONTRACT.version} -- refused`);
|
|
1928
|
+
seenModules.set(e.module, (seenModules.get(e.module) ?? 0) + 1);
|
|
1929
|
+
for (const r of e.routes ?? []) claim(paths, r.path, e.module, "route path");
|
|
1930
|
+
for (const b of e.bindings ?? []) claim(bindings, b.name, e.module, "binding");
|
|
1931
|
+
for (const t of e.topbar ?? []) claim(topbar, t.id, e.module, "top-bar item");
|
|
1932
|
+
for (const o of e.overlays ?? []) claim(overlays, o.id, e.module, "overlay");
|
|
1933
|
+
for (const p of e.panels ?? []) claim(panels, p.id, e.module, "panel");
|
|
1934
|
+
for (const s of e.states ?? []) claim(states, s, e.module, "state");
|
|
1935
|
+
for (const p of e.providers ?? []) if (isPortProvision(p)) claim(ports, p.port, e.module, "port");
|
|
1936
|
+
}
|
|
1937
|
+
for (const [module, n] of seenModules) if (n > 1) problems.push(`module '${module}' has ${n} entries; one entry per module`);
|
|
1938
|
+
if (problems.length) throw new ConsoleAssemblyError(problems);
|
|
1939
|
+
return entries;
|
|
1987
1940
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
*/
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
1941
|
+
const CONSOLE_ENTRIES = new InjectionToken("coolms.console.entries");
|
|
1942
|
+
var ConsolePanelHost = class {};
|
|
1943
|
+
function installedConsoleModules(manifest) {
|
|
1944
|
+
return new Set((manifest?.ui?.modules ?? []).filter((m) => m.contract === CONSOLE_CONTRACT.name).map((m) => m.module));
|
|
1945
|
+
}
|
|
1946
|
+
var ConsoleActivation = class ConsoleActivation {
|
|
1947
|
+
entries = inject(CONSOLE_ENTRIES, { optional: true }) ?? [];
|
|
1948
|
+
manifest = inject(Store).selectSignal(AppConfigState.manifest);
|
|
1949
|
+
installed = computed(() => installedConsoleModules(this.manifest()), ...ngDevMode ? [{ debugName: "installed" }] : /* istanbul ignore next */ []);
|
|
1950
|
+
active = computed(() => this.entries.filter((e) => this.installed().has(e.module)), ...ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []);
|
|
1951
|
+
topbar = computed(() => this.active().flatMap((e) => e.topbar ?? []).slice().sort((a, b) => a.order - b.order || a.id.localeCompare(b.id)), ...ngDevMode ? [{ debugName: "topbar" }] : /* istanbul ignore next */ []);
|
|
1952
|
+
overlays = computed(() => this.active().flatMap((e) => e.overlays ?? []), ...ngDevMode ? [{ debugName: "overlays" }] : /* istanbul ignore next */ []);
|
|
1953
|
+
panels = computed(() => this.active().flatMap((e) => e.panels ?? []), ...ngDevMode ? [{ debugName: "panels" }] : /* istanbul ignore next */ []);
|
|
1954
|
+
missing = computed(() => [...this.installed()].filter((m) => !this.entries.some((e) => e.module === m)).sort(), ...ngDevMode ? [{ debugName: "missing" }] : /* istanbul ignore next */ []);
|
|
1955
|
+
isInstalled(module) {
|
|
1956
|
+
return this.installed().has(module);
|
|
1957
|
+
}
|
|
1958
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1959
|
+
minVersion: "12.0.0",
|
|
1960
|
+
version: "22.2.0",
|
|
1961
|
+
ngImport: i0,
|
|
1962
|
+
type: ConsoleActivation,
|
|
1963
|
+
deps: [],
|
|
1964
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
1965
|
+
});
|
|
1966
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
1967
|
+
minVersion: "12.0.0",
|
|
1968
|
+
version: "22.2.0",
|
|
1969
|
+
ngImport: i0,
|
|
1970
|
+
type: ConsoleActivation,
|
|
1971
|
+
providedIn: "root"
|
|
1972
|
+
});
|
|
1973
|
+
};
|
|
1974
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1975
|
+
minVersion: "12.0.0",
|
|
1976
|
+
version: "22.2.0",
|
|
1977
|
+
ngImport: i0,
|
|
1978
|
+
type: ConsoleActivation,
|
|
1979
|
+
decorators: [{
|
|
1980
|
+
type: Injectable,
|
|
1981
|
+
args: [{ providedIn: "root" }]
|
|
1982
|
+
}]
|
|
1983
|
+
});
|
|
1984
|
+
function consoleModuleInstalled(module) {
|
|
1985
|
+
return () => {
|
|
1986
|
+
const activation = inject(ConsoleActivation);
|
|
1987
|
+
return inject(AppInitService).ready$.pipe(take(1), map(() => activation.isInstalled(module)));
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function consoleChildren(entries) {
|
|
1991
|
+
const routes = [];
|
|
1992
|
+
for (const e of assembleConsole(entries)) for (const m of e.routes ?? []) {
|
|
1993
|
+
if ("redirectTo" in m) {
|
|
1994
|
+
routes.push({
|
|
1995
|
+
path: m.path,
|
|
1996
|
+
redirectTo: m.redirectTo,
|
|
1997
|
+
pathMatch: "full"
|
|
1998
|
+
});
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
const route = {
|
|
2002
|
+
path: m.path,
|
|
2003
|
+
canMatch: [consoleModuleInstalled(e.module)],
|
|
2004
|
+
...m.nav ? { data: { ...m.nav } } : {},
|
|
2005
|
+
...m.providers ? { providers: [...m.providers] } : {}
|
|
2006
|
+
};
|
|
2007
|
+
if ("children" in m) route.loadChildren = m.children;
|
|
2008
|
+
else route.loadComponent = m.component;
|
|
2009
|
+
routes.push(route);
|
|
2010
|
+
}
|
|
2011
|
+
return routes;
|
|
2012
|
+
}
|
|
2013
|
+
function provideConsole(entries, options) {
|
|
2014
|
+
const checked = assembleConsole(entries);
|
|
2015
|
+
const providers = [
|
|
2016
|
+
{
|
|
2017
|
+
provide: CONSOLE_ENTRIES,
|
|
2018
|
+
useValue: checked
|
|
2019
|
+
},
|
|
2020
|
+
provideStore([...options.hostStates, ...checked.flatMap((e) => e.states ?? [])]),
|
|
2021
|
+
provideAppInitializer(() => {
|
|
2022
|
+
const registry = inject(ComponentRegistry);
|
|
2023
|
+
for (const e of checked) for (const b of e.bindings ?? []) registry.register(b.name, b.component);
|
|
2024
|
+
})
|
|
2025
|
+
];
|
|
2026
|
+
for (const e of checked) for (const p of e.providers ?? []) providers.push(isPortProvision(p) ? {
|
|
2027
|
+
provide: p.port,
|
|
2028
|
+
useExisting: p.useExisting
|
|
2029
|
+
} : p);
|
|
2030
|
+
return makeEnvironmentProviders(providers);
|
|
2031
|
+
}
|
|
2032
|
+
const CURRENT_SECTION = new InjectionToken("coolms.current-section");
|
|
2029
2033
|
const sectionInterceptor = (req, next) => {
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
}
|
|
2036
|
-
// Optional: no Sections module installed means no section to stamp.
|
|
2037
|
-
const slug = inject(CURRENT_SECTION, { optional: true })?.currentSlug() ?? null;
|
|
2038
|
-
if (!slug)
|
|
2039
|
-
return next(req);
|
|
2040
|
-
return next(req.clone({ setHeaders: { 'X-CoolMS-Section': slug } }));
|
|
2034
|
+
if (!shouldStamp(req.url)) return next(req);
|
|
2035
|
+
if (req.headers.has("X-CoolMS-Section")) return next(req);
|
|
2036
|
+
const slug = inject(CURRENT_SECTION, { optional: true })?.currentSlug() ?? null;
|
|
2037
|
+
if (!slug) return next(req);
|
|
2038
|
+
return next(req.clone({ setHeaders: { "X-CoolMS-Section": slug } }));
|
|
2041
2039
|
};
|
|
2042
2040
|
function shouldStamp(url) {
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
if (path.startsWith('/api/v1/auth/'))
|
|
2048
|
-
return false;
|
|
2049
|
-
return true;
|
|
2041
|
+
const path = url.replace(/^https?:\/\/[^/]+/, "");
|
|
2042
|
+
if (!path.startsWith("/api/v1/")) return false;
|
|
2043
|
+
if (path.startsWith("/api/v1/auth/")) return false;
|
|
2044
|
+
return true;
|
|
2050
2045
|
}
|
|
2046
|
+
export { AppConfigState, AppInitService, AuthRefreshCoordinator, AuthState, BYPASS_AUTH, BYPASS_ELEVATION, CONSOLE_CONTRACT, CONSOLE_ENTRIES, CURRENT_SECTION, CmsLoaderComponent, ComponentRegistry, ConfigService, ConsoleActivation, ConsoleAssemblyError, ConsolePanelHost, CrossTabAuthSyncService, ELEVATION_PROMPT, ELEVATION_REQUIRED_HEADER, ELEVATION_WARNING_FACTOR_MISSING, ELEVATION_WARNING_MFA_OFF, ELEVATION_WARNING_PASSWORD_NOT_SET, ElevationService, EntitySearchService, ErrorHandlerService, IdentityApiClient, Login, LoginComponent, Logout, NaviGraphService, PatchCurrentUser, RealtimeTokenClient, RestoreSession, SetAppConfig, SetTokens, SidebarStateService, ThemeService, UserPreferencesService, assembleConsole, authGuard, authInterceptor, consoleChildren, consoleEntry, consoleModuleInstalled, elevationInterceptor, installedConsoleModules, isPortProvision, loginPageGuard, provideConsole, resolvePattern, satisfiesRange, sectionInterceptor };
|
|
2051
2047
|
|
|
2052
|
-
|
|
2053
|
-
* `@coolms/core-angular` -- the CoolMS client runtime for Angular.
|
|
2054
|
-
*
|
|
2055
|
-
* Everything a CoolMS front-end needs before it has any UI: the session and its
|
|
2056
|
-
* refresh/cross-tab machinery, the boot manifest and app config, theme and user
|
|
2057
|
-
* preferences, error handling, the HTTP interceptors, and the wire types the
|
|
2058
|
-
* backend's form-render and navi-graph endpoints emit.
|
|
2059
|
-
*
|
|
2060
|
-
* It is deliberately NOT "the admin's core": nothing here is admin chrome. It is
|
|
2061
|
-
* also not a general Angular library -- every endpoint it speaks is a CoolMS
|
|
2062
|
-
* one, so it is worthless without a CoolMS backend. The layering is
|
|
2063
|
-
*
|
|
2064
|
-
* @coolms/core-angular -> @coolms/admin-ui-angular -> @coolms/x-ui-angular
|
|
2065
|
-
*
|
|
2066
|
-
* and the reason it must be a package rather than app code is federation: the
|
|
2067
|
-
* kit and every remote need this at runtime as ONE `singleton`, and a remote
|
|
2068
|
-
* cannot import from its host.
|
|
2069
|
-
*
|
|
2070
|
-
* This barrel is the whole public surface. Nothing outside `core/` may deep-link
|
|
2071
|
-
* past it -- eslint enforces that, so this file is what "core exports" MEANS. A
|
|
2072
|
-
* symbol not listed here is internal, and adding one is a deliberate act.
|
|
2073
|
-
*
|
|
2074
|
-
* The single component (`LoginComponent`) is here on purpose: core owns the auth
|
|
2075
|
-
* lifecycle, so it ships a working sign-in rather than leaving every consumer to
|
|
2076
|
-
* rebuild one against endpoints only core knows.
|
|
2077
|
-
*/
|
|
2078
|
-
// -- Wire types + the API client core owns ------------------------------------
|
|
2079
|
-
|
|
2080
|
-
/**
|
|
2081
|
-
* Generated bundle index. Do not edit.
|
|
2082
|
-
*/
|
|
2083
|
-
|
|
2084
|
-
export { AppConfigState, AppInitService, AuthRefreshCoordinator, AuthState, BYPASS_AUTH, CURRENT_SECTION, CmsLoaderComponent, ComponentRegistry, ConfigService, CrossTabAuthSyncService, EntitySearchService, ErrorHandlerService, IdentityApiClient, Login, LoginComponent, Logout, NaviGraphService, PatchCurrentUser, RealtimeTokenClient, RestoreSession, SetAppConfig, SetTokens, SidebarStateService, ThemeService, UserPreferencesService, authGuard, authInterceptor, loginPageGuard, resolvePattern, sectionInterceptor };
|
|
2085
|
-
//# sourceMappingURL=coolms-core-angular.mjs.map
|
|
2048
|
+
//# sourceMappingURL=coolms-core-angular.mjs.map
|