@masterteam/gateway-auth 0.0.21 → 0.0.23

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,11 +1,12 @@
1
- import { firstValueFrom, EMPTY, of, defer, from, isObservable, map, tap as tap$1, shareReplay, finalize, catchError as catchError$1, throwError, switchMap as switchMap$1 } from 'rxjs';
2
- import { HttpClient, HttpContextToken, HttpBackend } from '@angular/common/http';
1
+ import { firstValueFrom, EMPTY, of, defer, from, isObservable, map, tap as tap$1, catchError as catchError$1, throwError, switchMap as switchMap$1 } from 'rxjs';
2
+ import { HttpClient, HttpContextToken, HttpBackend, HttpResponse } from '@angular/common/http';
3
3
  import * as i0 from '@angular/core';
4
4
  import { InjectionToken, inject, Injectable, computed, input, ChangeDetectionStrategy, Component, signal, effect } from '@angular/core';
5
5
  import { Action, Selector, State, Store, select } from '@ngxs/store';
6
6
  import { Router, ActivatedRoute } from '@angular/router';
7
7
  import { ModalService } from '@masterteam/components/modal';
8
8
  import { switchMap, mergeMap, catchError, tap } from 'rxjs/operators';
9
+ import { ToastService } from '@masterteam/components/toast';
9
10
  import * as i2 from '@angular/common';
10
11
  import { NgTemplateOutlet, CommonModule } from '@angular/common';
11
12
  import * as i1 from '@angular/forms';
@@ -1465,6 +1466,9 @@ const runWithRefreshLock = (scopeKey, operation) => {
1465
1466
  };
1466
1467
  const GATEWAY_REFRESH_SCOPE = 'gateway';
1467
1468
  const inflightRefreshByScope = new Map();
1469
+ const refreshScopeKey = (scope, applicationCode) => scope === 'gateway'
1470
+ ? GATEWAY_REFRESH_SCOPE
1471
+ : `application:${applicationCode ?? ''}`;
1468
1472
  const getCurrentAuthTokens = (auth) => {
1469
1473
  const accessToken = auth.token();
1470
1474
  const refreshToken = auth.refreshToken();
@@ -1581,20 +1585,22 @@ const executeRefresh = (context, refreshToken) => {
1581
1585
  })));
1582
1586
  });
1583
1587
  };
1584
- const refreshTokens$ = (context, refreshToken) => {
1585
- const scopeKey = context.scope === 'gateway'
1586
- ? GATEWAY_REFRESH_SCOPE
1587
- : `application:${context.applicationCode ?? ''}`;
1588
+ const refreshAccessToken = (context, refreshToken) => {
1589
+ const scopeKey = refreshScopeKey(context.scope, context.applicationCode);
1588
1590
  const existing = inflightRefreshByScope.get(scopeKey);
1589
1591
  if (existing) {
1590
1592
  return existing;
1591
1593
  }
1592
- const observable = from(executeRefresh(context, refreshToken)).pipe(shareReplay({ bufferSize: 1, refCount: true }), finalize(() => {
1593
- inflightRefreshByScope.delete(scopeKey);
1594
- }));
1595
- inflightRefreshByScope.set(scopeKey, observable);
1596
- return observable;
1594
+ const promise = executeRefresh(context, refreshToken);
1595
+ inflightRefreshByScope.set(scopeKey, promise);
1596
+ promise.finally(() => {
1597
+ if (inflightRefreshByScope.get(scopeKey) === promise) {
1598
+ inflightRefreshByScope.delete(scopeKey);
1599
+ }
1600
+ });
1601
+ return promise;
1597
1602
  };
1603
+ const refreshTokens$ = (context, refreshToken) => from(refreshAccessToken(context, refreshToken));
1598
1604
  const prepareRequest = (req, token, markRetried, baseUrl) => {
1599
1605
  let modifiedReq = req;
1600
1606
  if (token) {
@@ -1717,10 +1723,20 @@ const gatewayAuthInterceptor = (req, next) => {
1717
1723
  return throwError(() => refreshError);
1718
1724
  }), switchMap$1((tokens) => next(prepareRequest(req, tokens.accessToken, true, baseUrl))));
1719
1725
  }
1726
+ const initialAccessToken = accessToken;
1720
1727
  return next(prepareRequest(req, tokenToAttach, false, baseUrl)).pipe(catchError$1((error) => {
1721
1728
  if (error?.status !== 401 || isAuthRequest || alreadyRetried) {
1722
1729
  return throwError(() => error);
1723
1730
  }
1731
+ // If a concurrent flow already rotated the token while this request was
1732
+ // in flight, just retry with the freshest token — do NOT trigger another
1733
+ // refresh against the backend.
1734
+ const currentAccessToken = resolved.scope === 'application' && resolved.applicationCode
1735
+ ? (auth.getAppSession(resolved.applicationCode)?.accessToken ?? null)
1736
+ : auth.token();
1737
+ if (currentAccessToken && currentAccessToken !== initialAccessToken) {
1738
+ return next(prepareRequest(req, currentAccessToken, true, baseUrl));
1739
+ }
1724
1740
  const latestAppSession = resolved.scope === 'application' && resolved.applicationCode
1725
1741
  ? auth.getAppSession(resolved.applicationCode)
1726
1742
  : null;
@@ -1752,6 +1768,199 @@ const gatewayAuthInterceptor = (req, next) => {
1752
1768
  }));
1753
1769
  };
1754
1770
 
1771
+ const CORRELATION_ID_HEADER = 'X-Correlation-ID';
1772
+ function generateCorrelationId() {
1773
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
1774
+ return crypto.randomUUID();
1775
+ }
1776
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
1777
+ const random = (Math.random() * 16) | 0;
1778
+ const value = char === 'x' ? random : (random & 0x3) | 0x8;
1779
+ return value.toString(16);
1780
+ });
1781
+ }
1782
+ const correlationIdInterceptor = (req, next) => {
1783
+ if (req.headers.has(CORRELATION_ID_HEADER)) {
1784
+ return next(req);
1785
+ }
1786
+ return next(req.clone({
1787
+ setHeaders: {
1788
+ [CORRELATION_ID_HEADER]: generateCorrelationId(),
1789
+ },
1790
+ }));
1791
+ };
1792
+
1793
+ const DEFAULT_EXCLUDED_PATHS = [
1794
+ '/health',
1795
+ '/auth/login',
1796
+ '/auth/refresh',
1797
+ '/auth/logout',
1798
+ '/auth/register',
1799
+ '/auth/sso',
1800
+ '/swagger',
1801
+ ];
1802
+ const DEFAULT_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
1803
+ const DEFAULT_HEADER = 'X-Idempotency-Key';
1804
+ function generateUUID() {
1805
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
1806
+ return crypto.randomUUID();
1807
+ }
1808
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
1809
+ const r = (Math.random() * 16) | 0;
1810
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
1811
+ return v.toString(16);
1812
+ });
1813
+ }
1814
+ function createIdempotencyInterceptor(options) {
1815
+ const excludedPaths = options?.excludedPaths ?? DEFAULT_EXCLUDED_PATHS;
1816
+ const methods = (options?.methods ?? DEFAULT_METHODS).map((m) => m.toUpperCase());
1817
+ const header = options?.headerName ?? DEFAULT_HEADER;
1818
+ const requiresKey = (req) => {
1819
+ if (!methods.includes(req.method.toUpperCase()))
1820
+ return false;
1821
+ if (req.url.includes('/assets/'))
1822
+ return false;
1823
+ if (excludedPaths.some((path) => req.url.includes(path)))
1824
+ return false;
1825
+ return true;
1826
+ };
1827
+ return (req, next) => {
1828
+ if (!requiresKey(req) || req.headers.has(header)) {
1829
+ return next(req);
1830
+ }
1831
+ return next(req.clone({
1832
+ setHeaders: { [header]: generateUUID() },
1833
+ }));
1834
+ };
1835
+ }
1836
+ const idempotencyInterceptor = createIdempotencyInterceptor();
1837
+
1838
+ function createLanguageInterceptor(options) {
1839
+ const header = options.headerName ?? 'Accept-Language';
1840
+ return (req, next) => {
1841
+ const lang = options.getLanguage();
1842
+ if (!lang) {
1843
+ return next(req);
1844
+ }
1845
+ return next(req.clone({
1846
+ setHeaders: { [header]: lang },
1847
+ }));
1848
+ };
1849
+ }
1850
+
1851
+ const DEFAULT_MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
1852
+ const DEFAULT_SILENT_ENDPOINTS = [
1853
+ 'api/dashboards/chart',
1854
+ 'api/home/recent/visit',
1855
+ 'api/fetch/query',
1856
+ 'api/workcenter/runtime',
1857
+ 'api/search',
1858
+ 'api/process-form-load',
1859
+ '/workspace-record-action-states',
1860
+ '/schedule/read',
1861
+ '/metadata/modules/properties',
1862
+ '/auth/logout',
1863
+ '/auth/login',
1864
+ '/auth/2fa',
1865
+ '/auth/sso',
1866
+ '/schedule/query',
1867
+ '/context/resolve',
1868
+ 'read-state',
1869
+ 'process-submit/validate',
1870
+ 'api/formulas/validate',
1871
+ ];
1872
+ const DEFAULT_NO_MESSAGE_HEADER = 'noMessage';
1873
+ function createMessageInterceptor(options) {
1874
+ const silentEndpoints = options?.silentEndpoints ?? DEFAULT_SILENT_ENDPOINTS;
1875
+ const mutating = (options?.mutatingMethods ?? DEFAULT_MUTATING_METHODS).map((m) => m.toUpperCase());
1876
+ const noMessageHeader = options?.noMessageHeader ?? DEFAULT_NO_MESSAGE_HEADER;
1877
+ return (req, next) => {
1878
+ const auth = inject(GatewayAuthFacade);
1879
+ const toast = inject(ToastService);
1880
+ return next(req).pipe(tap$1({
1881
+ next: (res) => {
1882
+ const body = res
1883
+ ?.body;
1884
+ if (!body?.message)
1885
+ return;
1886
+ if (req.headers.get(noMessageHeader) === 'true')
1887
+ return;
1888
+ if (!mutating.includes(req.method.toUpperCase()))
1889
+ return;
1890
+ if (silentEndpoints.some((endpoint) => req.url.includes(endpoint)))
1891
+ return;
1892
+ toast.success(body.message);
1893
+ },
1894
+ error: (error) => {
1895
+ const errorMessage = error?.error?.message || error?.error?.Message || '';
1896
+ const errorCode = error?.status || error?.error?.status || error?.error?.code;
1897
+ const showError = (message) => {
1898
+ if (message)
1899
+ toast.error(message);
1900
+ };
1901
+ const canRefreshGateway = !!auth.refreshToken() && !isExpired(auth.refreshTokenExpiresAt());
1902
+ const activeCode = auth.activeApplicationCode();
1903
+ const appSession = activeCode ? auth.getAppSession(activeCode) : null;
1904
+ const canRefreshApp = !!appSession?.refreshToken &&
1905
+ !isExpired(appSession.refreshTokenExpiresAt);
1906
+ switch (errorCode) {
1907
+ case 401:
1908
+ if (!isGatewayAuthRequestUrl(req.url) &&
1909
+ (canRefreshGateway || canRefreshApp)) {
1910
+ break;
1911
+ }
1912
+ auth.logout();
1913
+ showError(errorMessage);
1914
+ break;
1915
+ case 404:
1916
+ if (req?.responseType !== 'blob') {
1917
+ showError(errorMessage);
1918
+ }
1919
+ break;
1920
+ case 400: {
1921
+ const validationErrors = error?.error?.errors;
1922
+ if (validationErrors && typeof validationErrors === 'object') {
1923
+ const messages = Object.values(validationErrors)
1924
+ .flat()
1925
+ .filter((m) => typeof m === 'string');
1926
+ messages.forEach(showError);
1927
+ }
1928
+ else {
1929
+ showError(errorMessage);
1930
+ }
1931
+ break;
1932
+ }
1933
+ case 402:
1934
+ case 403:
1935
+ case 422:
1936
+ case 500:
1937
+ showError(errorMessage);
1938
+ break;
1939
+ default:
1940
+ if (req.headers.get(noMessageHeader) !== 'true') {
1941
+ showError(errorMessage);
1942
+ }
1943
+ break;
1944
+ }
1945
+ },
1946
+ }));
1947
+ };
1948
+ }
1949
+ const messageInterceptor = createMessageInterceptor();
1950
+
1951
+ function createCacheSessionInterceptor(options) {
1952
+ const field = options.field ?? 'cacheSession';
1953
+ return (req, next) => next(req).pipe(tap$1((event) => {
1954
+ if (!(event instanceof HttpResponse) || !event.body)
1955
+ return;
1956
+ const body = event.body;
1957
+ const value = body[field];
1958
+ if (typeof value === 'string' && value) {
1959
+ options.onCacheSession(value);
1960
+ }
1961
+ }));
1962
+ }
1963
+
1755
1964
  class GatewaySsoButtons {
1756
1965
  authFacade = inject(GatewayAuthFacade);
1757
1966
  options = inject(GATEWAY_AUTH_OPTIONS);
@@ -2097,5 +2306,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
2097
2306
  * Generated bundle index. Do not edit.
2098
2307
  */
2099
2308
 
2100
- export { AUTH_STATE_DEFAULTS, ClearAllAppSessions, ClearAppSession, ClearError, ClearPendingMfa, ClearRateLimit, ExchangeSsoCode, GATEWAY_AUTH_ACCESS_TOKEN_REFRESH_SKEW_MS, GATEWAY_AUTH_DEVICE_TOKEN, GATEWAY_AUTH_DEVICE_TOKEN_STORAGE_KEY, GATEWAY_AUTH_ENDPOINTS, GATEWAY_AUTH_NGSW_BYPASS_PARAM, GATEWAY_AUTH_OPTIONS, GATEWAY_AUTH_RETRY_CONTEXT, GATEWAY_RATE_LIMIT_ERROR_CODE, GATEWAY_RATE_LIMIT_STATUS, GatewayAuthFacade, GatewayAuthState, GatewayLoginPage, GatewayMfa, GatewaySsoButtons, GatewaySsoCallback, GatewaySsoSession, LaunchApplication, LoadApplications, LoadSsoProviders, Login, LoginFailure, LoginSuccess, Logout, ResendMfa, SetAppSession, SetApplications, SetRateLimit, StartSso, UpdateAppTokens, UpdateTokens, UpdateUserData, VerifyMfa, buildApplicationContextUrl, buildGatewayUrl, buildSsoStartUrl, clearApplicationContextCache, createSecureClientState, extractGatewayRateLimitInfo, fetchApplicationContextCode, gatewayAuthInterceptor, getGatewayErrorMessage, hasGatewayTokens, isExpired, isGatewayAuthRequestUrl, mapGatewayTokens, mapGatewayUser, normalizeGatewayBase, readPersistedGatewayAuthTokens, resolveAccessTokenRefreshSkewMs, resolveApiDateValue, resolveApplicationCodeOption, resolveGatewayAuthPath, resolveGatewayDeviceToken, sanitizePersistedAuthState, withGatewayAuthNgswBypass };
2309
+ export { AUTH_STATE_DEFAULTS, ClearAllAppSessions, ClearAppSession, ClearError, ClearPendingMfa, ClearRateLimit, ExchangeSsoCode, GATEWAY_AUTH_ACCESS_TOKEN_REFRESH_SKEW_MS, GATEWAY_AUTH_DEVICE_TOKEN, GATEWAY_AUTH_DEVICE_TOKEN_STORAGE_KEY, GATEWAY_AUTH_ENDPOINTS, GATEWAY_AUTH_NGSW_BYPASS_PARAM, GATEWAY_AUTH_OPTIONS, GATEWAY_AUTH_RETRY_CONTEXT, GATEWAY_RATE_LIMIT_ERROR_CODE, GATEWAY_RATE_LIMIT_STATUS, GatewayAuthFacade, GatewayAuthState, GatewayLoginPage, GatewayMfa, GatewaySsoButtons, GatewaySsoCallback, GatewaySsoSession, LaunchApplication, LoadApplications, LoadSsoProviders, Login, LoginFailure, LoginSuccess, Logout, ResendMfa, SetAppSession, SetApplications, SetRateLimit, StartSso, UpdateAppTokens, UpdateTokens, UpdateUserData, VerifyMfa, buildApplicationContextUrl, buildGatewayUrl, buildSsoStartUrl, clearApplicationContextCache, correlationIdInterceptor, createCacheSessionInterceptor, createIdempotencyInterceptor, createLanguageInterceptor, createMessageInterceptor, createSecureClientState, extractGatewayRateLimitInfo, fetchApplicationContextCode, gatewayAuthInterceptor, getGatewayErrorMessage, hasGatewayTokens, idempotencyInterceptor, isExpired, isGatewayAuthRequestUrl, mapGatewayTokens, mapGatewayUser, messageInterceptor, normalizeGatewayBase, readPersistedGatewayAuthTokens, resolveAccessTokenRefreshSkewMs, resolveApiDateValue, resolveApplicationCodeOption, resolveGatewayAuthPath, resolveGatewayDeviceToken, sanitizePersistedAuthState, withGatewayAuthNgswBypass };
2101
2310
  //# sourceMappingURL=masterteam-gateway-auth.mjs.map