mesauth-angular 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { signal, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgModule, afterNextRender, input, booleanAttribute, computed, HostBinding, Component, output, Injector, ChangeDetectionStrategy, effect, HostListener, ElementRef, Pipe, viewChild, ViewChild, DestroyRef, Directive } from '@angular/core';
|
|
3
3
|
import { toObservable, toSignal, takeUntilDestroyed, rxResource } from '@angular/core/rxjs-interop';
|
|
4
|
-
import { catchError, of, Subject, EMPTY,
|
|
4
|
+
import { catchError, of, Subject, EMPTY, BehaviorSubject, throwError, timer, firstValueFrom, distinctUntilChanged, switchMap as switchMap$1, forkJoin } from 'rxjs';
|
|
5
5
|
import { HttpClient, HttpResponse } from '@angular/common/http';
|
|
6
6
|
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
|
|
7
|
-
import { map, tap, catchError as catchError$1, switchMap } from 'rxjs/operators';
|
|
7
|
+
import { map, tap, catchError as catchError$1, switchMap, filter, take } from 'rxjs/operators';
|
|
8
8
|
import { Router } from '@angular/router';
|
|
9
9
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
10
10
|
import { DatePipe, JsonPipe } from '@angular/common';
|
|
11
11
|
|
|
12
12
|
/** Current installed package version — keep in sync with package.json. */
|
|
13
|
-
const PACKAGE_VERSION = '1.
|
|
13
|
+
const PACKAGE_VERSION = '1.24.0';
|
|
14
14
|
/**
|
|
15
15
|
* Provides server-driven UI configuration loaded from the hosted manifest.
|
|
16
16
|
* Components read `labels()` and `features()` signals instead of hardcoded strings.
|
|
@@ -446,20 +446,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
446
446
|
type: Injectable
|
|
447
447
|
}], ctorParameters: () => [] });
|
|
448
448
|
|
|
449
|
-
//
|
|
449
|
+
// Module-scope state — single instance shared across all requests because
|
|
450
|
+
// HttpInterceptorFn is provided once per injector.
|
|
451
|
+
// True while a 401-triggered refresh is in flight; concurrent 401 retries wait for it.
|
|
452
|
+
const refreshing$ = new BehaviorSubject(false);
|
|
453
|
+
// Last access token written to localStorage from an X-Refreshed-Token response header.
|
|
454
|
+
// Used to collapse N duplicate writes (one per parallel response) into one notifySignedIn call.
|
|
455
|
+
let lastWrittenAccessToken = null;
|
|
456
|
+
// Track if we're currently redirecting to login to prevent loopback storms.
|
|
450
457
|
let isRedirecting = false;
|
|
451
458
|
/**
|
|
452
|
-
* Functional HTTP interceptor
|
|
453
|
-
*
|
|
454
|
-
*
|
|
459
|
+
* Functional HTTP interceptor.
|
|
460
|
+
*
|
|
461
|
+
* Responsibilities:
|
|
462
|
+
* 1. Attach `Authorization: Bearer` + `X-Refresh-Token` from localStorage to requests
|
|
463
|
+
* targeting the auth server / trusted hosts (so the server-side Authorizer middleware
|
|
464
|
+
* can perform silent refresh and emit X-Refreshed-* response headers).
|
|
465
|
+
* 2. On every successful response, if `X-Refreshed-Token` is present, write it to
|
|
466
|
+
* localStorage exactly once (dedup across parallel responses).
|
|
467
|
+
* 3. On 401, queue concurrent failures behind a single 1.5s wait so only ONE retry
|
|
468
|
+
* fires per refresh window. Retries strip the stale auth headers so the interceptor
|
|
469
|
+
* re-reads the freshly-stored tokens from localStorage.
|
|
470
|
+
* 4. On 403, redirect to /403.
|
|
455
471
|
*/
|
|
456
472
|
const mesAuthInterceptor = (req, next) => {
|
|
457
473
|
const authService = inject(MesAuthService);
|
|
458
474
|
const router = inject(Router);
|
|
459
|
-
// Attach access + refresh tokens from localStorage to requests going to the auth server
|
|
460
|
-
// or any backend host running MesAuth.Authorizer (trustedHosts). This lets the authorizer
|
|
461
|
-
// middleware silently refresh tokens and emit X-Refreshed-Token response headers.
|
|
462
|
-
// Relative URLs are always considered trusted (same origin).
|
|
463
475
|
const config = authService.getConfig();
|
|
464
476
|
const apiBase = config?.apiBaseUrl ?? '';
|
|
465
477
|
const trustedHosts = config?.trustedHosts ?? [];
|
|
@@ -473,9 +485,11 @@ const mesAuthInterceptor = (req, next) => {
|
|
|
473
485
|
return true; // relative URL — same origin
|
|
474
486
|
}
|
|
475
487
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
488
|
+
// ---- Build the outgoing request, attaching localStorage tokens for trusted hosts.
|
|
489
|
+
function attachAuthHeaders(r) {
|
|
490
|
+
if (typeof localStorage === 'undefined' || !isTrusted(r.url))
|
|
491
|
+
return r;
|
|
492
|
+
let headers = r.headers;
|
|
479
493
|
if (!headers.has('Authorization')) {
|
|
480
494
|
const accessToken = localStorage.getItem('mes_auth_token');
|
|
481
495
|
if (accessToken)
|
|
@@ -486,39 +500,50 @@ const mesAuthInterceptor = (req, next) => {
|
|
|
486
500
|
if (refreshToken)
|
|
487
501
|
headers = headers.set('X-Refresh-Token', refreshToken);
|
|
488
502
|
}
|
|
489
|
-
|
|
490
|
-
|
|
503
|
+
return headers !== r.headers ? r.clone({ headers }) : r;
|
|
504
|
+
}
|
|
505
|
+
// Strip the headers we set so a retry forces re-read from localStorage (which by then
|
|
506
|
+
// contains the refreshed tokens written by the X-Refreshed-Token response handler).
|
|
507
|
+
function stripAuthHeaders(r) {
|
|
508
|
+
return r.clone({
|
|
509
|
+
headers: r.headers.delete('Authorization').delete('X-Refresh-Token')
|
|
510
|
+
});
|
|
491
511
|
}
|
|
512
|
+
const authReq = attachAuthHeaders(req);
|
|
492
513
|
return next(authReq).pipe(tap(event => {
|
|
493
514
|
if (event instanceof HttpResponse) {
|
|
494
515
|
const newAccessToken = event.headers.get('X-Refreshed-Token');
|
|
495
|
-
if (newAccessToken) {
|
|
516
|
+
if (newAccessToken && newAccessToken !== lastWrittenAccessToken) {
|
|
517
|
+
lastWrittenAccessToken = newAccessToken;
|
|
496
518
|
const newRefreshToken = event.headers.get('X-Refreshed-Refresh-Token') ?? undefined;
|
|
497
519
|
authService.notifySignedIn(newAccessToken, newRefreshToken);
|
|
498
520
|
}
|
|
499
521
|
}
|
|
500
522
|
}), catchError$1((error) => {
|
|
501
523
|
const status = error.status;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
524
|
+
if (!(status === 401 || status === 403) || isRedirecting) {
|
|
525
|
+
return throwError(() => error);
|
|
526
|
+
}
|
|
527
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
528
|
+
const currentUrl = router.url + (window.location.hash || '');
|
|
529
|
+
const returnUrl = encodeURIComponent(currentUrl);
|
|
530
|
+
const isLoginPage = currentUrl.includes('/login');
|
|
531
|
+
const is403Page = currentUrl.includes('/403');
|
|
532
|
+
const isAuthPage = currentUrl.includes('/auth');
|
|
533
|
+
const isPublicPage = currentUrl.includes('/register')
|
|
534
|
+
|| currentUrl.includes('/forgot-password')
|
|
535
|
+
|| currentUrl.includes('/reset-password');
|
|
536
|
+
const isMeAuthPage = req.url.includes('/auth/me');
|
|
537
|
+
if (status === 401 && !isLoginPage && !isAuthPage && !isMeAuthPage && !isPublicPage) {
|
|
538
|
+
// Single-flight: the first 401 opens a 1.5s window during which all other 401s
|
|
539
|
+
// wait for the refresh signal to flip back to false, then retry once.
|
|
540
|
+
if (!refreshing$.value) {
|
|
541
|
+
refreshing$.next(true);
|
|
542
|
+
return timer(1500).pipe(switchMap(() => next(stripAuthHeaders(req))), tap({
|
|
543
|
+
next: () => refreshing$.next(false),
|
|
544
|
+
error: () => refreshing$.next(false),
|
|
545
|
+
complete: () => refreshing$.next(false)
|
|
546
|
+
}), catchError$1((retryError) => {
|
|
522
547
|
if (retryError.status === 401) {
|
|
523
548
|
isRedirecting = true;
|
|
524
549
|
setTimeout(() => { isRedirecting = false; }, 5000);
|
|
@@ -527,32 +552,29 @@ const mesAuthInterceptor = (req, next) => {
|
|
|
527
552
|
return throwError(() => retryError);
|
|
528
553
|
}));
|
|
529
554
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
555
|
+
// Refresh already in flight — wait for it to finish, then retry once with
|
|
556
|
+
// headers stripped so the interceptor re-reads the now-fresh tokens.
|
|
557
|
+
return refreshing$.pipe(filter(v => !v), take(1), switchMap(() => next(stripAuthHeaders(req))), catchError$1((retryError) => {
|
|
558
|
+
if (retryError.status === 401 && !isRedirecting) {
|
|
559
|
+
isRedirecting = true;
|
|
560
|
+
setTimeout(() => { isRedirecting = false; }, 5000);
|
|
561
|
+
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
536
562
|
}
|
|
537
|
-
|
|
563
|
+
return throwError(() => retryError);
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
if (status === 403 && !is403Page) {
|
|
567
|
+
isRedirecting = true;
|
|
568
|
+
setTimeout(() => { isRedirecting = false; }, 5000);
|
|
569
|
+
let redirectUrl = `${baseUrl}/403?returnUrl=${returnUrl}`;
|
|
570
|
+
if (error.error && error.error.required) {
|
|
571
|
+
redirectUrl += `&required=${encodeURIComponent(error.error.required)}`;
|
|
538
572
|
}
|
|
573
|
+
window.location.href = redirectUrl;
|
|
539
574
|
}
|
|
540
575
|
return throwError(() => error);
|
|
541
576
|
}));
|
|
542
577
|
};
|
|
543
|
-
function appendPermissions(body, allowedActions) {
|
|
544
|
-
if (body === null || body === undefined) {
|
|
545
|
-
return { 'x-ma-perms': allowedActions };
|
|
546
|
-
}
|
|
547
|
-
if (Array.isArray(body)) {
|
|
548
|
-
return { data: body, 'x-ma-perms': allowedActions };
|
|
549
|
-
}
|
|
550
|
-
if (typeof body === 'object') {
|
|
551
|
-
return { ...body, 'x-ma-perms': allowedActions };
|
|
552
|
-
}
|
|
553
|
-
// Primitive (string, number, boolean)
|
|
554
|
-
return { data: body, 'x-ma-perms': allowedActions };
|
|
555
|
-
}
|
|
556
578
|
|
|
557
579
|
class MesAuthModule {
|
|
558
580
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MesAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|