@acorex-platform/framework-client 0.0.59 → 0.0.61
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/fesm2022/acorex-platform-framework-client-auth.mjs +397 -93
- package/fesm2022/acorex-platform-framework-client-auth.mjs.map +1 -1
- package/fesm2022/acorex-platform-framework-client-common.mjs +33 -31
- package/fesm2022/acorex-platform-framework-client-common.mjs.map +1 -1
- package/fesm2022/acorex-platform-framework-client-layout-components.mjs +12 -4
- package/fesm2022/acorex-platform-framework-client-layout-components.mjs.map +1 -1
- package/fesm2022/{acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-BBuYugjl.mjs → acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-D0jDsA6M.mjs} +80 -13
- package/fesm2022/acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-D0jDsA6M.mjs.map +1 -0
- package/fesm2022/{acorex-platform-framework-client-layout-entity-attachments-page.component-DZDf_nVq.mjs → acorex-platform-framework-client-layout-entity-attachments-page.component-Bdvd1iYt.mjs} +2 -2
- package/fesm2022/{acorex-platform-framework-client-layout-entity-attachments-page.component-DZDf_nVq.mjs.map → acorex-platform-framework-client-layout-entity-attachments-page.component-Bdvd1iYt.mjs.map} +1 -1
- package/fesm2022/{acorex-platform-framework-client-layout-entity-bulk-entity.command-CFFv3jDs.mjs → acorex-platform-framework-client-layout-entity-bulk-entity.command-CsTP8YHv.mjs} +2 -2
- package/fesm2022/{acorex-platform-framework-client-layout-entity-bulk-entity.command-CFFv3jDs.mjs.map → acorex-platform-framework-client-layout-entity-bulk-entity.command-CsTP8YHv.mjs.map} +1 -1
- package/fesm2022/{acorex-platform-framework-client-layout-entity-gallery-page.component-BDNPJg_6.mjs → acorex-platform-framework-client-layout-entity-gallery-page.component-UvMk0GbU.mjs} +2 -2
- package/fesm2022/{acorex-platform-framework-client-layout-entity-gallery-page.component-BDNPJg_6.mjs.map → acorex-platform-framework-client-layout-entity-gallery-page.component-UvMk0GbU.mjs.map} +1 -1
- package/fesm2022/acorex-platform-framework-client-layout-entity.mjs +1 -1
- package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs +257 -30
- package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs.map +1 -1
- package/fesm2022/{acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-Rv0K9r_e.mjs → acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-KrDsAa3H.mjs} +555 -121
- package/fesm2022/acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-KrDsAa3H.mjs.map +1 -0
- package/fesm2022/{acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-Dzqn3rGa.mjs → acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-BDlo6nTx.mjs} +2 -2
- package/fesm2022/{acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-Dzqn3rGa.mjs.map → acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-BDlo6nTx.mjs.map} +1 -1
- package/fesm2022/{acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-C97rrrmu.mjs → acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-By3wntCB.mjs} +2 -2
- package/fesm2022/{acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-C97rrrmu.mjs.map → acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-By3wntCB.mjs.map} +1 -1
- package/fesm2022/acorex-platform-framework-client-layout-widgets.mjs +1 -1
- package/fesm2022/acorex-platform-framework-client-themes-default.mjs +4 -4
- package/fesm2022/acorex-platform-framework-client-themes-default.mjs.map +1 -1
- package/layout/builder/README.md +10 -0
- package/layout/widget-core/README.md +47 -3
- package/layout/widgets/README.md +8 -2
- package/package.json +2 -2
- package/types/acorex-platform-framework-client-auth.d.ts +60 -1
- package/types/acorex-platform-framework-client-common.d.ts +3 -2
- package/types/acorex-platform-framework-client-layout-components.d.ts +3 -1
- package/types/acorex-platform-framework-client-layout-entity.d.ts +8 -1
- package/types/acorex-platform-framework-client-layout-widget-core.d.ts +72 -1
- package/types/acorex-platform-framework-client-layout-widgets.d.ts +25 -5
- package/fesm2022/acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-BBuYugjl.mjs.map +0 -1
- package/fesm2022/acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-Rv0K9r_e.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, Injector, Injectable, signal, inject, Input, Directive, Optional, Inject, NgModule, input, output, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
-
import { map, BehaviorSubject, shareReplay, defaultIfEmpty, switchMap, filter, distinctUntilChanged, of, from,
|
|
3
|
+
import { map, BehaviorSubject, shareReplay, defaultIfEmpty, switchMap, filter, distinctUntilChanged, combineLatest, of, from, first } from 'rxjs';
|
|
4
4
|
import { AXPDebugService, AXPBroadcastEventService, AXP_SESSION_SERVICE, AXP_EXPRESSION_EVALUATOR_SCOPE_PROVIDER, provideAppStartUpTask, AXP_APP_STARTUP_PRIORITY } from '@acorex-platform/framework-client/core';
|
|
5
5
|
import { Router } from '@angular/router';
|
|
6
6
|
import { isEmpty } from 'lodash-es';
|
|
@@ -445,82 +445,136 @@ function withMemoryRefreshLock(run) {
|
|
|
445
445
|
}
|
|
446
446
|
//#endregion
|
|
447
447
|
|
|
448
|
-
//#region
|
|
449
|
-
/** Stable API / ForbiddenException code from AuthService. */
|
|
450
|
-
const MAX_ACTIVE_DEVICE_SESSIONS_CODE = 'MAX_ACTIVE_DEVICE_SESSIONS';
|
|
451
|
-
const PENDING_TENANT_STORAGE_KEY = 'axp.auth.sessionLimit.pendingTenant';
|
|
452
|
-
function textContainsMaxSessionsCode(value) {
|
|
453
|
-
if (typeof value === 'string') {
|
|
454
|
-
return value === MAX_ACTIVE_DEVICE_SESSIONS_CODE || value.includes(MAX_ACTIVE_DEVICE_SESSIONS_CODE);
|
|
455
|
-
}
|
|
456
|
-
if (Array.isArray(value)) {
|
|
457
|
-
return value.some((item) => textContainsMaxSessionsCode(item));
|
|
458
|
-
}
|
|
459
|
-
return false;
|
|
460
|
-
}
|
|
448
|
+
//#region ----- Auth refresh failure classification -----
|
|
461
449
|
/**
|
|
462
|
-
*
|
|
463
|
-
*
|
|
450
|
+
* Stable server codes for definitive refresh/session death (prefer over message text).
|
|
451
|
+
* Keep in sync with `AUTH_REFRESH_ERROR_CODES` / `SESSION_MEMBERSHIP_DENIED` on the server.
|
|
464
452
|
*/
|
|
465
|
-
|
|
453
|
+
const DEFINITIVE_AUTH_REFRESH_CODES = [
|
|
454
|
+
'REFRESH_MISSING',
|
|
455
|
+
'REFRESH_EXPIRED',
|
|
456
|
+
'REFRESH_REVOKED',
|
|
457
|
+
'REFRESH_REUSE',
|
|
458
|
+
'SESSION_REVOKED',
|
|
459
|
+
'USER_NOT_FOUND',
|
|
460
|
+
'SESSION_MEMBERSHIP_DENIED',
|
|
461
|
+
];
|
|
462
|
+
/**
|
|
463
|
+
* Backward-compatible message patterns when older servers omit `code`.
|
|
464
|
+
* Do NOT treat bare HTTP 401/403 / Redis unavailable / 5xx as definitive.
|
|
465
|
+
*/
|
|
466
|
+
const DEFINITIVE_AUTH_REFRESH_PATTERNS = [
|
|
467
|
+
/refresh token reuse detected/i,
|
|
468
|
+
/refresh token has been revoked/i,
|
|
469
|
+
/refreshToken is required/i,
|
|
470
|
+
/invalid or expired refresh token/i,
|
|
471
|
+
/invalid refresh token/i,
|
|
472
|
+
/session has been revoked/i,
|
|
473
|
+
/user not found/i,
|
|
474
|
+
/SESSION_MEMBERSHIP_DENIED/i,
|
|
475
|
+
/you do not have access to this organization/i,
|
|
476
|
+
/you do not have access to this application/i,
|
|
477
|
+
];
|
|
478
|
+
function collectErrorText(error) {
|
|
466
479
|
if (error == null) {
|
|
467
|
-
return
|
|
480
|
+
return '';
|
|
468
481
|
}
|
|
469
482
|
if (typeof error === 'string') {
|
|
470
|
-
return
|
|
483
|
+
return error;
|
|
471
484
|
}
|
|
472
|
-
if (
|
|
473
|
-
return
|
|
485
|
+
if (error instanceof Error) {
|
|
486
|
+
return error.message;
|
|
474
487
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return true;
|
|
488
|
+
if (typeof error !== 'object') {
|
|
489
|
+
return String(error);
|
|
478
490
|
}
|
|
479
491
|
const err = error;
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
492
|
+
const parts = [];
|
|
493
|
+
for (const key of ['message', 'error', 'code', 'statusText']) {
|
|
494
|
+
const value = err[key];
|
|
495
|
+
if (typeof value === 'string') {
|
|
496
|
+
parts.push(value);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const nested = err['error'];
|
|
500
|
+
if (nested && typeof nested === 'object') {
|
|
501
|
+
parts.push(collectErrorText(nested));
|
|
485
502
|
}
|
|
486
503
|
const apiError = err['apiError'];
|
|
487
504
|
if (apiError && typeof apiError === 'object') {
|
|
488
|
-
|
|
489
|
-
candidates.push(api['message'], api['error'], api['code']);
|
|
505
|
+
parts.push(collectErrorText(apiError));
|
|
490
506
|
}
|
|
491
|
-
return
|
|
507
|
+
return parts.filter(Boolean).join(' ');
|
|
492
508
|
}
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
|
|
509
|
+
function collectErrorCodes(error) {
|
|
510
|
+
if (error == null || typeof error !== 'object') {
|
|
511
|
+
return typeof error === 'string' && DEFINITIVE_AUTH_REFRESH_CODES.includes(error)
|
|
512
|
+
? [error]
|
|
513
|
+
: [];
|
|
496
514
|
}
|
|
497
|
-
|
|
498
|
-
|
|
515
|
+
const err = error;
|
|
516
|
+
const codes = [];
|
|
517
|
+
for (const key of ['code', 'error']) {
|
|
518
|
+
const value = err[key];
|
|
519
|
+
if (typeof value === 'string' && value.trim()) {
|
|
520
|
+
codes.push(value.trim());
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const apiError = err['apiError'];
|
|
524
|
+
if (apiError && typeof apiError === 'object') {
|
|
525
|
+
codes.push(...collectErrorCodes(apiError));
|
|
526
|
+
}
|
|
527
|
+
const nested = err['error'];
|
|
528
|
+
if (nested && typeof nested === 'object') {
|
|
529
|
+
codes.push(...collectErrorCodes(nested));
|
|
499
530
|
}
|
|
531
|
+
const details = err['details'];
|
|
532
|
+
if (details && typeof details === 'object') {
|
|
533
|
+
codes.push(...collectErrorCodes(details));
|
|
534
|
+
}
|
|
535
|
+
return codes;
|
|
500
536
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
return parsed;
|
|
537
|
+
/**
|
|
538
|
+
* True only when refresh failed because the refresh cookie / session family is dead.
|
|
539
|
+
* Network / 5xx / Redis unavailable / empty transient results → false.
|
|
540
|
+
*/
|
|
541
|
+
function isDefinitiveAuthRefreshFailure(error) {
|
|
542
|
+
const codes = collectErrorCodes(error);
|
|
543
|
+
if (codes.some((code) => DEFINITIVE_AUTH_REFRESH_CODES.includes(code))) {
|
|
544
|
+
return true;
|
|
512
545
|
}
|
|
513
|
-
|
|
514
|
-
|
|
546
|
+
const text = collectErrorText(error);
|
|
547
|
+
if (!text.trim()) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
// Do not treat generic "Authentication failed" / Redis unavailable as definitive.
|
|
551
|
+
if (/session store \(redis\) is unavailable/i.test(text)) {
|
|
552
|
+
return false;
|
|
515
553
|
}
|
|
554
|
+
return DEFINITIVE_AUTH_REFRESH_PATTERNS.some((pattern) => pattern.test(text));
|
|
516
555
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
556
|
+
//#endregion
|
|
557
|
+
|
|
558
|
+
//#region ----- Soft restore authorization status -----
|
|
559
|
+
/**
|
|
560
|
+
* After soft restore: keep identity without promoting to Authorized when
|
|
561
|
+
* access policy could not be loaded (transient API/refresh failure).
|
|
562
|
+
*/
|
|
563
|
+
function resolveStatusAfterSoftRestore(options) {
|
|
564
|
+
if (!options.hasUser) {
|
|
565
|
+
return AXPSessionStatus.Unauthenticated;
|
|
520
566
|
}
|
|
521
|
-
|
|
522
|
-
|
|
567
|
+
if (options.hasCompleteWorkspace && options.policyLoadSucceeded) {
|
|
568
|
+
return AXPSessionStatus.Authorized;
|
|
523
569
|
}
|
|
570
|
+
return AXPSessionStatus.Authenticated;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Soft-restore left the session Authenticated (policy unavailable).
|
|
574
|
+
* Retry access-policy load after connectivity / successful token refresh recovers.
|
|
575
|
+
*/
|
|
576
|
+
function shouldRetryAccessPolicyAuthorization(status) {
|
|
577
|
+
return status === AXPSessionStatus.Authenticated;
|
|
524
578
|
}
|
|
525
579
|
//#endregion
|
|
526
580
|
|
|
@@ -539,7 +593,13 @@ class AXPSessionService {
|
|
|
539
593
|
/** In-memory catalog caches — shared by guards and chooser UI (one List* per bind). */
|
|
540
594
|
this.tenantsListCache = null;
|
|
541
595
|
this.applicationsListCache = null;
|
|
596
|
+
/** Bumps when catalogs must re-fetch (e.g. after tenant-request approve). */
|
|
597
|
+
this.catalogEpoch$ = new BehaviorSubject(0);
|
|
542
598
|
this.signoutInFlight = null;
|
|
599
|
+
/** Coalesces soft-restore → Authorized promotion after connectivity recovers. */
|
|
600
|
+
this.accessPolicyRetryInFlight = null;
|
|
601
|
+
this.softRestoreOnlineRecoveryAttached = false;
|
|
602
|
+
this.softRestoreOnlineRecoveryHandler = null;
|
|
543
603
|
this.status = new BehaviorSubject(AXPSessionStatus.Unauthenticated);
|
|
544
604
|
this.status$ = this.status.asObservable().pipe(shareReplay(1));
|
|
545
605
|
// Add loading state to prevent premature redirects
|
|
@@ -568,7 +628,10 @@ class AXPSessionService {
|
|
|
568
628
|
this.isAuthorized$ = this.status$.pipe(map((status) => status === AXPSessionStatus.Authorized), shareReplay(1));
|
|
569
629
|
this.isReadyForAppScopedApi$ = this.isAuthorized$.pipe(map((authorized) => authorized && this.hasSessionCredentials()), distinctUntilChanged(), shareReplay(1));
|
|
570
630
|
/** Tenant list for the signed-in user; skips the API when there is no cookie session. */
|
|
571
|
-
this.tenants$ =
|
|
631
|
+
this.tenants$ = combineLatest([
|
|
632
|
+
this.isAuthenticated$,
|
|
633
|
+
this.catalogEpoch$,
|
|
634
|
+
]).pipe(switchMap(([authenticated]) => {
|
|
572
635
|
if (!authenticated || !this.hasSessionCredentials()) {
|
|
573
636
|
return of([]);
|
|
574
637
|
}
|
|
@@ -658,9 +721,12 @@ class AXPSessionService {
|
|
|
658
721
|
const strategyName = sessionData.strategy;
|
|
659
722
|
const strategy = strategyName ? this.authStrategyRegistry.get(strategyName) : null;
|
|
660
723
|
if (!strategy) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
724
|
+
// Keep AXP_SESSION — wiping here permanently logs the user out on a
|
|
725
|
+
// registry timing race. Soft-restore; interceptor can refresh later.
|
|
726
|
+
warnAuthLifecycle('Restore strategy missing — soft restore (kept session)', () => ({
|
|
727
|
+
strategyName,
|
|
728
|
+
}));
|
|
729
|
+
await this.softRestoreFromStoredSession(sessionData);
|
|
664
730
|
return;
|
|
665
731
|
}
|
|
666
732
|
// Keep subjects warm for refresh context, but do NOT mark Authenticated yet —
|
|
@@ -674,23 +740,48 @@ class AXPSessionService {
|
|
|
674
740
|
applicationId: sessionData.application?.id,
|
|
675
741
|
strategy: strategyName,
|
|
676
742
|
}));
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
743
|
+
try {
|
|
744
|
+
// Cross-tab exclusive refresh — avoids concurrent jti reuse / sid revoke.
|
|
745
|
+
const refreshResult = await this.refreshCookieSessionExclusive(strategyName, strategy);
|
|
746
|
+
if (refreshResult?.succeed && refreshResult.data?.user) {
|
|
747
|
+
await this.applySignInResult(strategyName, refreshResult);
|
|
748
|
+
traceAuthRestore('Restore complete', () => ({
|
|
749
|
+
ms: Date.now() - startedAt,
|
|
750
|
+
status: this.status.value,
|
|
751
|
+
tenantId: this.tenant?.id,
|
|
752
|
+
applicationId: this.application?.id,
|
|
753
|
+
}));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// succeed:false without throw — usually lock/metadata race; do not kick out.
|
|
757
|
+
warnAuthLifecycle('Restore refresh returned empty — soft restore (kept session)');
|
|
758
|
+
await this.softRestoreFromStoredSession(sessionData);
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
if (isDefinitiveAuthRefreshFailure(error)) {
|
|
762
|
+
this.clearSession();
|
|
763
|
+
this.status.next(AXPSessionStatus.Unauthorized);
|
|
764
|
+
traceAuthRestore('Restore refresh rejected — cleared', () => ({
|
|
765
|
+
error: error instanceof Error ? error.message : String(error),
|
|
766
|
+
}));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// Network / 5xx / aborted XHR after HMR reload — keep local session.
|
|
770
|
+
warnAuthLifecycle('Restore refresh transient failure — soft restore (kept session)', () => ({
|
|
771
|
+
error: error instanceof Error ? error.message : String(error),
|
|
772
|
+
}));
|
|
773
|
+
await this.softRestoreFromStoredSession(sessionData);
|
|
684
774
|
}
|
|
685
|
-
await this.applySignInResult(strategyName, refreshResult);
|
|
686
|
-
traceAuthRestore('Restore complete', () => ({
|
|
687
|
-
ms: Date.now() - startedAt,
|
|
688
|
-
status: this.status.value,
|
|
689
|
-
tenantId: this.tenant?.id,
|
|
690
|
-
applicationId: this.application?.id,
|
|
691
|
-
}));
|
|
692
775
|
}
|
|
693
776
|
catch (error) {
|
|
777
|
+
const sessionData = this.getSessionData();
|
|
778
|
+
if (sessionData?.user && sessionData.cookieSession && !isDefinitiveAuthRefreshFailure(error)) {
|
|
779
|
+
warnAuthLifecycle('Restore error — soft restore (kept session)', () => ({
|
|
780
|
+
error: error instanceof Error ? error.message : String(error),
|
|
781
|
+
}));
|
|
782
|
+
await this.softRestoreFromStoredSession(sessionData);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
694
785
|
this.clearSession();
|
|
695
786
|
this.status.next(AXPSessionStatus.Unauthorized);
|
|
696
787
|
traceAuthRestore('Restore error — cleared', () => ({
|
|
@@ -701,6 +792,122 @@ class AXPSessionService {
|
|
|
701
792
|
this.isLoading.next(false);
|
|
702
793
|
}
|
|
703
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Trust persisted AXP_SESSION after a non-definitive restore refresh failure.
|
|
797
|
+
* Keeps identity; promotes to Authorized only when access policy load succeeds.
|
|
798
|
+
*/
|
|
799
|
+
async softRestoreFromStoredSession(sessionData) {
|
|
800
|
+
this.currentUserSubject.next(sessionData.user ?? null);
|
|
801
|
+
this.currentTenantSubject.next(sessionData.tenant ?? null);
|
|
802
|
+
this.currentApplicationSubject.next(sessionData.application ?? null);
|
|
803
|
+
const policyLoadSucceeded = await this.tryLoadAccessPolicyForSoftRestore();
|
|
804
|
+
const nextStatus = resolveStatusAfterSoftRestore({
|
|
805
|
+
hasUser: !!sessionData.user,
|
|
806
|
+
hasCompleteWorkspace: this.hasCompleteSessionContext(),
|
|
807
|
+
policyLoadSucceeded,
|
|
808
|
+
});
|
|
809
|
+
this.status.next(nextStatus);
|
|
810
|
+
traceAuthRestore('Soft restore status', () => ({
|
|
811
|
+
status: nextStatus,
|
|
812
|
+
policyLoadSucceeded,
|
|
813
|
+
tenantId: sessionData.tenant?.id ?? null,
|
|
814
|
+
applicationId: sessionData.application?.id ?? null,
|
|
815
|
+
}));
|
|
816
|
+
if (shouldRetryAccessPolicyAuthorization(nextStatus)) {
|
|
817
|
+
this.armSoftRestorePolicyRecovery();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Soft-restore policy load: success = both loaders completed without throw
|
|
822
|
+
* (empty permission/feature lists still count as a successful load).
|
|
823
|
+
*/
|
|
824
|
+
async tryLoadAccessPolicyForSoftRestore() {
|
|
825
|
+
if (!this.hasCompleteSessionContext()) {
|
|
826
|
+
this.permissionsSubject.next([]);
|
|
827
|
+
this.featuresSubject.next([]);
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
let permissionsOk = false;
|
|
831
|
+
let featuresOk = false;
|
|
832
|
+
try {
|
|
833
|
+
const permissions = await this.permissionLoader.getList(this.getContext());
|
|
834
|
+
this.permissionsSubject.next(permissions ?? []);
|
|
835
|
+
permissionsOk = true;
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
warnAuthLifecycle('Soft restore permissions load failed — stay Authenticated', () => ({
|
|
839
|
+
error: error instanceof Error ? error.message : String(error),
|
|
840
|
+
}));
|
|
841
|
+
this.permissionsSubject.next([]);
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const features = await this.featureLoader.getList(this.getContext());
|
|
845
|
+
this.featuresSubject.next(features ?? []);
|
|
846
|
+
featuresOk = true;
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
warnAuthLifecycle('Soft restore features load failed — stay Authenticated', () => ({
|
|
850
|
+
error: error instanceof Error ? error.message : String(error),
|
|
851
|
+
}));
|
|
852
|
+
this.featuresSubject.next([]);
|
|
853
|
+
}
|
|
854
|
+
return permissionsOk && featuresOk;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Retries access-policy load after a soft restore that stayed Authenticated.
|
|
858
|
+
* On success with complete workspace, promotes to Authorized (no logout on failure).
|
|
859
|
+
* Coalesces concurrent callers (refresh / online / successful API).
|
|
860
|
+
*/
|
|
861
|
+
async retryAccessPolicyAuthorization() {
|
|
862
|
+
if (!this.user) {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
if (!shouldRetryAccessPolicyAuthorization(this.status.value)) {
|
|
866
|
+
return this.status.value === AXPSessionStatus.Authorized;
|
|
867
|
+
}
|
|
868
|
+
if (this.accessPolicyRetryInFlight) {
|
|
869
|
+
return this.accessPolicyRetryInFlight;
|
|
870
|
+
}
|
|
871
|
+
this.accessPolicyRetryInFlight = (async () => {
|
|
872
|
+
const policyLoadSucceeded = await this.tryLoadAccessPolicyForSoftRestore();
|
|
873
|
+
const nextStatus = resolveStatusAfterSoftRestore({
|
|
874
|
+
hasUser: true,
|
|
875
|
+
hasCompleteWorkspace: this.hasCompleteSessionContext(),
|
|
876
|
+
policyLoadSucceeded,
|
|
877
|
+
});
|
|
878
|
+
this.status.next(nextStatus);
|
|
879
|
+
if (nextStatus === AXPSessionStatus.Authorized) {
|
|
880
|
+
this.disarmSoftRestorePolicyRecovery();
|
|
881
|
+
}
|
|
882
|
+
return nextStatus === AXPSessionStatus.Authorized;
|
|
883
|
+
})().finally(() => {
|
|
884
|
+
this.accessPolicyRetryInFlight = null;
|
|
885
|
+
});
|
|
886
|
+
return this.accessPolicyRetryInFlight;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Bounded recovery after soft-restore Authenticated: browser `online` once per arm.
|
|
890
|
+
* Successful token refresh also retries (see {@link refreshToken}).
|
|
891
|
+
*/
|
|
892
|
+
armSoftRestorePolicyRecovery() {
|
|
893
|
+
if (this.softRestoreOnlineRecoveryAttached || typeof window === 'undefined') {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
this.softRestoreOnlineRecoveryAttached = true;
|
|
897
|
+
this.softRestoreOnlineRecoveryHandler = () => {
|
|
898
|
+
void this.retryAccessPolicyAuthorization();
|
|
899
|
+
};
|
|
900
|
+
window.addEventListener('online', this.softRestoreOnlineRecoveryHandler);
|
|
901
|
+
}
|
|
902
|
+
disarmSoftRestorePolicyRecovery() {
|
|
903
|
+
if (this.softRestoreOnlineRecoveryAttached &&
|
|
904
|
+
this.softRestoreOnlineRecoveryHandler &&
|
|
905
|
+
typeof window !== 'undefined') {
|
|
906
|
+
window.removeEventListener('online', this.softRestoreOnlineRecoveryHandler);
|
|
907
|
+
}
|
|
908
|
+
this.softRestoreOnlineRecoveryAttached = false;
|
|
909
|
+
this.softRestoreOnlineRecoveryHandler = null;
|
|
910
|
+
}
|
|
704
911
|
async signin(credentials) {
|
|
705
912
|
this.isLoading.next(true);
|
|
706
913
|
const startedAt = Date.now();
|
|
@@ -835,6 +1042,7 @@ class AXPSessionService {
|
|
|
835
1042
|
const result = await withAuthRefreshLock(() => strategy.updateToken(params));
|
|
836
1043
|
if (result?.succeed) {
|
|
837
1044
|
if (params?.['code']) {
|
|
1045
|
+
// OAuth code exchange replaces identity metadata; wipe stale local fields first.
|
|
838
1046
|
this.clearSession();
|
|
839
1047
|
clearOAuthPendingStrategy();
|
|
840
1048
|
}
|
|
@@ -863,30 +1071,28 @@ class AXPSessionService {
|
|
|
863
1071
|
}));
|
|
864
1072
|
}
|
|
865
1073
|
else {
|
|
866
|
-
|
|
867
|
-
throw new Error(`Invalid Username or Password`);
|
|
1074
|
+
throw new Error('UpdateSession failed');
|
|
868
1075
|
}
|
|
869
1076
|
}
|
|
870
1077
|
catch (error) {
|
|
871
1078
|
traceAuthLifecycleError('UpdateSession failed', () => ({
|
|
872
1079
|
error: error instanceof Error ? error.message : String(error),
|
|
873
1080
|
}));
|
|
874
|
-
warnAuthWorkspace('UpdateSession failed —
|
|
1081
|
+
warnAuthWorkspace('UpdateSession failed — identity session kept', () => ({
|
|
875
1082
|
error: error instanceof Error ? error.message : String(error),
|
|
876
1083
|
tenantId: params?.['tenant']?.id ?? params?.['tenantId'] ?? null,
|
|
877
1084
|
applicationId: params?.['application']?.id ?? params?.['applicationId'] ?? null,
|
|
878
1085
|
}));
|
|
879
|
-
// Failed workspace bind must not leave a half-authenticated cookie session
|
|
880
|
-
// (that causes splash restore loops after refresh).
|
|
881
|
-
// Exception: max device sessions — keep identity session so the user can
|
|
882
|
-
// terminate another device and retry tenant bind.
|
|
883
|
-
if (isMaxActiveDeviceSessionsError(error)) {
|
|
884
|
-
throw error;
|
|
885
|
-
}
|
|
886
1086
|
if (params?.['code']) {
|
|
887
1087
|
clearOAuthPendingStrategy();
|
|
888
1088
|
}
|
|
889
|
-
|
|
1089
|
+
// Workspace bind failure (tenant/app/network/membership) must not log the user out.
|
|
1090
|
+
// Restore subjects from AXP_SESSION after the optimistic application clear above.
|
|
1091
|
+
this.syncContextSubjectsFromStorage();
|
|
1092
|
+
const stored = this.getSessionData();
|
|
1093
|
+
if (stored?.user && stored.cookieSession) {
|
|
1094
|
+
await this.syncSessionAuthorization();
|
|
1095
|
+
}
|
|
890
1096
|
throw error;
|
|
891
1097
|
}
|
|
892
1098
|
finally {
|
|
@@ -954,21 +1160,31 @@ class AXPSessionService {
|
|
|
954
1160
|
try {
|
|
955
1161
|
const result = await this.refreshCookieSessionExclusive(strategyName, strategy);
|
|
956
1162
|
if (!result.succeed || !result.data?.user) {
|
|
957
|
-
|
|
1163
|
+
// Empty result is usually a lock/metadata race — not proof the cookie is dead.
|
|
1164
|
+
warnAuthLifecycle('Token refresh returned empty — session kept', () => ({
|
|
958
1165
|
strategy: strategyName,
|
|
959
1166
|
}));
|
|
960
|
-
this.clearSession();
|
|
961
|
-
this.status.next(AXPSessionStatus.Expired);
|
|
962
1167
|
throw new Error('Token refresh failed');
|
|
963
1168
|
}
|
|
1169
|
+
// Soft restore may have left Authenticated; promote once API/refresh works again.
|
|
1170
|
+
if (shouldRetryAccessPolicyAuthorization(this.status.value)) {
|
|
1171
|
+
await this.retryAccessPolicyAuthorization();
|
|
1172
|
+
}
|
|
964
1173
|
return true;
|
|
965
1174
|
}
|
|
966
1175
|
catch (error) {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1176
|
+
if (isDefinitiveAuthRefreshFailure(error)) {
|
|
1177
|
+
warnAuthLifecycle('Token refresh definitive failure — clearing session', () => ({
|
|
1178
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1179
|
+
}));
|
|
1180
|
+
this.clearSession();
|
|
1181
|
+
this.status.next(AXPSessionStatus.Expired);
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
warnAuthLifecycle('Token refresh transient error — session kept', () => ({
|
|
1185
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
972
1188
|
throw error;
|
|
973
1189
|
}
|
|
974
1190
|
}
|
|
@@ -1128,6 +1344,14 @@ class AXPSessionService {
|
|
|
1128
1344
|
this.tenantsListCache = null;
|
|
1129
1345
|
this.applicationsListCache = null;
|
|
1130
1346
|
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Drops ListTenants / ListApplications caches and re-emits {@link tenants$}.
|
|
1349
|
+
* Call after membership/tenant availability changes (e.g. approve tenant request).
|
|
1350
|
+
*/
|
|
1351
|
+
invalidateCatalogCaches() {
|
|
1352
|
+
this.clearCatalogCaches();
|
|
1353
|
+
this.catalogEpoch$.next(this.catalogEpoch$.value + 1);
|
|
1354
|
+
}
|
|
1131
1355
|
async signInComplete() {
|
|
1132
1356
|
// Ensure we have the required data
|
|
1133
1357
|
if (!this.user) {
|
|
@@ -1222,6 +1446,7 @@ class AXPSessionService {
|
|
|
1222
1446
|
}
|
|
1223
1447
|
}
|
|
1224
1448
|
clearSession() {
|
|
1449
|
+
this.disarmSoftRestorePolicyRecovery();
|
|
1225
1450
|
this.invalidateAccessPolicyCache();
|
|
1226
1451
|
this.clearCatalogCaches();
|
|
1227
1452
|
this.status.next(AXPSessionStatus.Unauthenticated);
|
|
@@ -2246,6 +2471,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
2246
2471
|
args: [{ providedIn: 'root' }]
|
|
2247
2472
|
}] });
|
|
2248
2473
|
|
|
2474
|
+
//#region ---- Max active device sessions (client) ----
|
|
2475
|
+
/** Stable API / ForbiddenException code from AuthService. */
|
|
2476
|
+
const MAX_ACTIVE_DEVICE_SESSIONS_CODE = 'MAX_ACTIVE_DEVICE_SESSIONS';
|
|
2477
|
+
const PENDING_TENANT_STORAGE_KEY = 'axp.auth.sessionLimit.pendingTenant';
|
|
2478
|
+
function textContainsMaxSessionsCode(value) {
|
|
2479
|
+
if (typeof value === 'string') {
|
|
2480
|
+
return value === MAX_ACTIVE_DEVICE_SESSIONS_CODE || value.includes(MAX_ACTIVE_DEVICE_SESSIONS_CODE);
|
|
2481
|
+
}
|
|
2482
|
+
if (Array.isArray(value)) {
|
|
2483
|
+
return value.some((item) => textContainsMaxSessionsCode(item));
|
|
2484
|
+
}
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Detects tenant max concurrent device-session rejection from command / HTTP shapes.
|
|
2489
|
+
* Must recognize {@link Error} subclasses (e.g. AuthApiError) via `.message`.
|
|
2490
|
+
*/
|
|
2491
|
+
function isMaxActiveDeviceSessionsError(error) {
|
|
2492
|
+
if (error == null) {
|
|
2493
|
+
return false;
|
|
2494
|
+
}
|
|
2495
|
+
if (typeof error === 'string') {
|
|
2496
|
+
return textContainsMaxSessionsCode(error);
|
|
2497
|
+
}
|
|
2498
|
+
if (typeof error !== 'object') {
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
// Error.message is reliable for AuthApiError / Error thrown from UpdateSession.
|
|
2502
|
+
if (error instanceof Error && textContainsMaxSessionsCode(error.message)) {
|
|
2503
|
+
return true;
|
|
2504
|
+
}
|
|
2505
|
+
const err = error;
|
|
2506
|
+
const candidates = [err['message'], err['error'], err['code']];
|
|
2507
|
+
const nestedError = err['error'];
|
|
2508
|
+
if (nestedError && typeof nestedError === 'object') {
|
|
2509
|
+
const nested = nestedError;
|
|
2510
|
+
candidates.push(nested['message'], nested['error'], nested['code']);
|
|
2511
|
+
}
|
|
2512
|
+
const apiError = err['apiError'];
|
|
2513
|
+
if (apiError && typeof apiError === 'object') {
|
|
2514
|
+
const api = apiError;
|
|
2515
|
+
candidates.push(api['message'], api['error'], api['code']);
|
|
2516
|
+
}
|
|
2517
|
+
return candidates.some((value) => textContainsMaxSessionsCode(value));
|
|
2518
|
+
}
|
|
2519
|
+
function storeSessionLimitPendingTenant(tenant) {
|
|
2520
|
+
try {
|
|
2521
|
+
sessionStorage.setItem(PENDING_TENANT_STORAGE_KEY, JSON.stringify(tenant));
|
|
2522
|
+
}
|
|
2523
|
+
catch {
|
|
2524
|
+
// Ignore quota / private-mode failures; route state may still carry the tenant.
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function readSessionLimitPendingTenant() {
|
|
2528
|
+
try {
|
|
2529
|
+
const raw = sessionStorage.getItem(PENDING_TENANT_STORAGE_KEY);
|
|
2530
|
+
if (!raw) {
|
|
2531
|
+
return null;
|
|
2532
|
+
}
|
|
2533
|
+
const parsed = JSON.parse(raw);
|
|
2534
|
+
if (!parsed?.id || typeof parsed.id !== 'string') {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
return parsed;
|
|
2538
|
+
}
|
|
2539
|
+
catch {
|
|
2540
|
+
return null;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
function clearSessionLimitPendingTenant() {
|
|
2544
|
+
try {
|
|
2545
|
+
sessionStorage.removeItem(PENDING_TENANT_STORAGE_KEY);
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
// ignore
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
//#endregion
|
|
2552
|
+
|
|
2249
2553
|
const AXP_USER_AVATAR_CACHE_PORT = new InjectionToken('AXP_USER_AVATAR_CACHE_PORT');
|
|
2250
2554
|
|
|
2251
2555
|
//#region ---- User profile identity service ----
|
|
@@ -2495,5 +2799,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
2495
2799
|
* Generated bundle index. Do not edit.
|
|
2496
2800
|
*/
|
|
2497
2801
|
|
|
2498
|
-
export { AUTH_SESSION_TRACE_NS, AUTH_SESSION_TRACE_TITLE, AXPAuthGuard, AXPAuthModule, AXPAuthStrategy, AXPAuthStrategyRegistryService, AXPFeatureDirective, AXPFeatureEvaluatorScopeProvider, AXPFeatureGuard, AXPLoginChallengeComponentBase, AXPLoginChallengeProvider, AXPPermissionDefinitionBuilder, AXPPermissionDefinitionGroupBuilder, AXPPermissionDefinitionProviderContext, AXPPermissionDefinitionService, AXPPermissionDirective, AXPPermissionEvaluatorScopeProvider, AXPPermissionGuard, AXPPlatformAuthRuntimeService, AXPSessionContext, AXPSessionService, AXPSessionStatus, AXPSignInMethod, AXPSignInMethodRegistry, AXPUnauthenticatedError, AXPUnauthorizedError, AXPUserProfileIdentityService, AXP_ACCESS_POLICY_CACHE_PORT, AXP_APPLICATION_LOADER, AXP_DEFAULT_PLATFORM_OAUTH_CLIENT_ID, AXP_DEFAULT_PLATFORM_OAUTH_SCOPE, AXP_FEATURE_CHECKER, AXP_FEATURE_LOADER, AXP_LOGIN_CHALLENGE_PROVIDER, AXP_OAUTH_PKCE_VERIFIER_KEY, AXP_PERMISSION_CHECKER, AXP_PERMISSION_DEFINITION_PROVIDER, AXP_PERMISSION_LOADER, AXP_PERMISSION_MODULES_RESOLVER, AXP_PLATFORM_OAUTH_CSRF_COOKIE, AXP_TENANT_LOADER, AXP_USER_AVATAR_CACHE_PORT, JwtUtil, MAX_ACTIVE_DEVICE_SESSIONS_CODE, PkceUtil, TimeUtil, asAuthListProvidersResponse, clearOAuthPendingStrategy, clearSessionLimitPendingTenant, commandMessageTextForError, exchangePlatformOAuthAuthorizationCode, fetchPlatformOAuthDiscoveryDocument, findIdentityProvider, getOAuthPendingStrategy, hasLegacyOAuthPkceVerifier, hasOAuthPendingFlow, initPlatformOAuthAuthorization, isMaxActiveDeviceSessionsError, isOAuthRedirectInFlight, platformOAuthSessionUpdateParams, readPlatformOAuthCsrfToken, readSessionLimitPendingTenant, resolvePlatformOAuthAuthorizeEndpoint, resolvePlatformOAuthClientConfig, resolvePlatformOAuthIssuer, resolvePlatformOAuthRedirectUri, resolvePlatformOAuthTokenEndpoint, setOAuthPendingStrategy, shouldPlatformOAuthBindSession, storeSessionLimitPendingTenant, toPlatformOAuthSignInResult, traceAuthAccessPolicy, traceAuthCatalog, traceAuthGuard, traceAuthLifecycle, traceAuthLifecycleError, traceAuthRestore, traceAuthWorkspace, warnAuthAccessPolicy, warnAuthLifecycle, warnAuthWorkspace };
|
|
2802
|
+
export { AUTH_SESSION_TRACE_NS, AUTH_SESSION_TRACE_TITLE, AXPAuthGuard, AXPAuthModule, AXPAuthStrategy, AXPAuthStrategyRegistryService, AXPFeatureDirective, AXPFeatureEvaluatorScopeProvider, AXPFeatureGuard, AXPLoginChallengeComponentBase, AXPLoginChallengeProvider, AXPPermissionDefinitionBuilder, AXPPermissionDefinitionGroupBuilder, AXPPermissionDefinitionProviderContext, AXPPermissionDefinitionService, AXPPermissionDirective, AXPPermissionEvaluatorScopeProvider, AXPPermissionGuard, AXPPlatformAuthRuntimeService, AXPSessionContext, AXPSessionService, AXPSessionStatus, AXPSignInMethod, AXPSignInMethodRegistry, AXPUnauthenticatedError, AXPUnauthorizedError, AXPUserProfileIdentityService, AXP_ACCESS_POLICY_CACHE_PORT, AXP_APPLICATION_LOADER, AXP_DEFAULT_PLATFORM_OAUTH_CLIENT_ID, AXP_DEFAULT_PLATFORM_OAUTH_SCOPE, AXP_FEATURE_CHECKER, AXP_FEATURE_LOADER, AXP_LOGIN_CHALLENGE_PROVIDER, AXP_OAUTH_PKCE_VERIFIER_KEY, AXP_PERMISSION_CHECKER, AXP_PERMISSION_DEFINITION_PROVIDER, AXP_PERMISSION_LOADER, AXP_PERMISSION_MODULES_RESOLVER, AXP_PLATFORM_OAUTH_CSRF_COOKIE, AXP_TENANT_LOADER, AXP_USER_AVATAR_CACHE_PORT, DEFINITIVE_AUTH_REFRESH_CODES, JwtUtil, MAX_ACTIVE_DEVICE_SESSIONS_CODE, PkceUtil, TimeUtil, asAuthListProvidersResponse, clearOAuthPendingStrategy, clearSessionLimitPendingTenant, commandMessageTextForError, exchangePlatformOAuthAuthorizationCode, fetchPlatformOAuthDiscoveryDocument, findIdentityProvider, getOAuthPendingStrategy, hasLegacyOAuthPkceVerifier, hasOAuthPendingFlow, initPlatformOAuthAuthorization, isDefinitiveAuthRefreshFailure, isMaxActiveDeviceSessionsError, isOAuthRedirectInFlight, platformOAuthSessionUpdateParams, readPlatformOAuthCsrfToken, readSessionLimitPendingTenant, resolvePlatformOAuthAuthorizeEndpoint, resolvePlatformOAuthClientConfig, resolvePlatformOAuthIssuer, resolvePlatformOAuthRedirectUri, resolvePlatformOAuthTokenEndpoint, resolveStatusAfterSoftRestore, setOAuthPendingStrategy, shouldPlatformOAuthBindSession, shouldRetryAccessPolicyAuthorization, storeSessionLimitPendingTenant, toPlatformOAuthSignInResult, traceAuthAccessPolicy, traceAuthCatalog, traceAuthGuard, traceAuthLifecycle, traceAuthLifecycleError, traceAuthRestore, traceAuthWorkspace, warnAuthAccessPolicy, warnAuthLifecycle, warnAuthWorkspace };
|
|
2499
2803
|
//# sourceMappingURL=acorex-platform-framework-client-auth.mjs.map
|