asab_webui_shell 27.7.5-alpha.0 → 27.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/containers/Application.js +76 -24
- package/dist/modules/auth/components/LoginLoopCard.js +37 -0
- package/dist/modules/auth/components/LoginLoopCard.scss +6 -0
- package/dist/modules/auth/index.js +237 -102
- package/dist/modules/tenant/TenantDropdown.js +6 -18
- package/dist/utils/pageLifecycle.js +87 -0
- package/dist/utils/statusAlerts.js +5 -0
- package/dist/utils/statusAlerts.jsx +5 -0
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ var _axios = _interopRequireDefault(require("axios"));
|
|
|
12
12
|
var _asab_webui_components = require("asab_webui_components");
|
|
13
13
|
var _jsonParseWithBigInt2 = require("../utils/jsonParseWithBigInt");
|
|
14
14
|
var _statusAlerts = require("../utils/statusAlerts.js");
|
|
15
|
+
var _pageLifecycle = require("../utils/pageLifecycle");
|
|
15
16
|
var _Header = _interopRequireDefault(require("./Header"));
|
|
16
17
|
var _Sidebar = _interopRequireDefault(require("./Sidebar"));
|
|
17
18
|
var _ToastContainer = _interopRequireDefault(require("./Toast/ToastContainer.js"));
|
|
@@ -34,6 +35,7 @@ var _BrandingService = _interopRequireDefault(require("../services/BrandingServi
|
|
|
34
35
|
var _TitleService = _interopRequireDefault(require("../services/TitleService"));
|
|
35
36
|
var _HelpService = _interopRequireDefault(require("./Header/Help/HelpService"));
|
|
36
37
|
var _AccessDeniedCard = _interopRequireDefault(require("../modules/tenant/access/AccessDeniedCard"));
|
|
38
|
+
var _LoginLoopCard = require("../modules/auth/components/LoginLoopCard");
|
|
37
39
|
var _ApplicationRouter = _interopRequireDefault(require("./Router/ApplicationRouter"));
|
|
38
40
|
var _SuspenseScreen = _interopRequireDefault(require("../screens/SuspenseScreen"));
|
|
39
41
|
var _OfflineIndication = require("../modules/attentionrequired/components/OfflineIndication.js");
|
|
@@ -65,6 +67,7 @@ class Application extends _react.Component {
|
|
|
65
67
|
this.Navigation = new _Navigation.default(this);
|
|
66
68
|
this.SplashscreenRequestors = new Set(); // If not empty, the splash screen will be rendered
|
|
67
69
|
this.AxiosInterceptors = new Set();
|
|
70
|
+
this.AxiosResponseErrorInterceptors = new Set(); // Interceptors for handling error responses from the server (e.g 401)
|
|
68
71
|
this.WebSocketInterceptors = new Set();
|
|
69
72
|
this.HeaderService = new _HeaderService.default(this, "HeaderService");
|
|
70
73
|
this.ThemeService = new _ThemeService.default(this, "ThemeService");
|
|
@@ -106,6 +109,7 @@ class Application extends _react.Component {
|
|
|
106
109
|
// Subscribe and unsubscribe handlers for connectivity detection
|
|
107
110
|
this._initConnectivitySubscription = this._initConnectivitySubscription.bind(this);
|
|
108
111
|
this._unsubscribeConnectivity = null;
|
|
112
|
+
this._unsubscribePageHide = null;
|
|
109
113
|
this.ConfigService.addDefaults(props.configdefaults);
|
|
110
114
|
this.addSplashScreenRequestor(this);
|
|
111
115
|
this.state.splashscreenRequestors = this.SplashscreenRequestors.size;
|
|
@@ -360,23 +364,37 @@ class Application extends _react.Component {
|
|
|
360
364
|
}
|
|
361
365
|
// If the request was satisfied (application/json and presence of BigInt) return the modified object. If not, we return unchanged object
|
|
362
366
|
return response;
|
|
363
|
-
}, function (
|
|
364
|
-
var
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
that.popPrintReadyIndicator();
|
|
369
|
-
var contentType = error === null || error === void 0 || (_error$response = error.response) === null || _error$response === void 0 || (_error$response = _error$response.headers) === null || _error$response === void 0 ? void 0 : _error$response['content-type'];
|
|
370
|
-
// Check if the response content type is 'application/json' and data is a string
|
|
371
|
-
if (contentType !== null && contentType !== void 0 && contentType.startsWith('application/json') && typeof (error === null || error === void 0 || (_error$response2 = error.response) === null || _error$response2 === void 0 ? void 0 : _error$response2.data) === 'string') {
|
|
372
|
-
try {
|
|
373
|
-
error.response.data = JSON.parse(error.response.data);
|
|
374
|
-
} catch (e) {
|
|
375
|
-
console.error("Error parsing error of the error body:", e);
|
|
367
|
+
}, /*#__PURE__*/function () {
|
|
368
|
+
var _ref2 = (0, _asyncToGenerator2.default)(function* (error) {
|
|
369
|
+
var _error$config, _error$response, _error$response2;
|
|
370
|
+
if (!((_error$config = error.config) !== null && _error$config !== void 0 && _error$config._networkingIndicatorOff)) {
|
|
371
|
+
that.popNetworkingIndicator();
|
|
376
372
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
373
|
+
that.popPrintReadyIndicator();
|
|
374
|
+
var contentType = error === null || error === void 0 || (_error$response = error.response) === null || _error$response === void 0 || (_error$response = _error$response.headers) === null || _error$response === void 0 ? void 0 : _error$response['content-type'];
|
|
375
|
+
// Check if the response content type is 'application/json' and data is a string
|
|
376
|
+
if (contentType !== null && contentType !== void 0 && contentType.startsWith('application/json') && typeof (error === null || error === void 0 || (_error$response2 = error.response) === null || _error$response2 === void 0 ? void 0 : _error$response2.data) === 'string') {
|
|
377
|
+
try {
|
|
378
|
+
error.response.data = JSON.parse(error.response.data);
|
|
379
|
+
} catch (e) {
|
|
380
|
+
console.error("Error parsing error of the error body:", e);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Call registered response error interceptors (e.g. for 401 handling)
|
|
385
|
+
for (var _interceptor of that.AxiosResponseErrorInterceptors.keys()) {
|
|
386
|
+
try {
|
|
387
|
+
yield _interceptor(error);
|
|
388
|
+
} catch (interceptorError) {
|
|
389
|
+
console.error("Error in AxiosResponseErrorInterceptors", interceptorError);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return Promise.reject(error);
|
|
393
|
+
});
|
|
394
|
+
return function (_x) {
|
|
395
|
+
return _ref2.apply(this, arguments);
|
|
396
|
+
};
|
|
397
|
+
}());
|
|
380
398
|
return axios;
|
|
381
399
|
}
|
|
382
400
|
|
|
@@ -401,6 +419,12 @@ class Application extends _react.Component {
|
|
|
401
419
|
removeAxiosInterceptor(interceptor) {
|
|
402
420
|
this.AxiosInterceptors.delete(interceptor);
|
|
403
421
|
}
|
|
422
|
+
addAxiosResponseErrorInterceptor(interceptor) {
|
|
423
|
+
this.AxiosResponseErrorInterceptors.add(interceptor);
|
|
424
|
+
}
|
|
425
|
+
removeAxiosResponseErrorInterceptor(interceptor) {
|
|
426
|
+
this.AxiosResponseErrorInterceptors.delete(interceptor);
|
|
427
|
+
}
|
|
404
428
|
addWebSocketInterceptor(interceptor) {
|
|
405
429
|
this.WebSocketInterceptors.add(interceptor);
|
|
406
430
|
}
|
|
@@ -520,6 +544,8 @@ class Application extends _react.Component {
|
|
|
520
544
|
|
|
521
545
|
// Subscribe to Application.status! once PubSub is available
|
|
522
546
|
this._initConnectivitySubscription();
|
|
547
|
+
// Bridge window page lifecycle events to Application.lifecycle!
|
|
548
|
+
this._initPageLifecycleBridge();
|
|
523
549
|
// Add print-landscape class to body if not present
|
|
524
550
|
if (!document.body.classList.contains('print-landscape')) {
|
|
525
551
|
document.body.classList.add('print-landscape');
|
|
@@ -552,6 +578,12 @@ class Application extends _react.Component {
|
|
|
552
578
|
this._unsubscribeConnectivity();
|
|
553
579
|
this._unsubscribeConnectivity = null;
|
|
554
580
|
}
|
|
581
|
+
|
|
582
|
+
// Unsubscribe from Application.lifecycle! PubSub for pagehide event
|
|
583
|
+
if (this._unsubscribePageHide) {
|
|
584
|
+
this._unsubscribePageHide();
|
|
585
|
+
this._unsubscribePageHide = null;
|
|
586
|
+
}
|
|
555
587
|
this._clearOfflineIndicationTimeout();
|
|
556
588
|
this._clearPrintReadyTimeout();
|
|
557
589
|
document.body.removeAttribute('print-ready');
|
|
@@ -635,7 +667,7 @@ class Application extends _react.Component {
|
|
|
635
667
|
addAlertFromException will always use "danger" color as its background and the full exception will be printed into the console
|
|
636
668
|
*/
|
|
637
669
|
addAlertFromException(exception, message) {
|
|
638
|
-
var _exception$response, _exception$response2, _this$AppStore$dispat3, _this$
|
|
670
|
+
var _exception$response, _this$AppStore2, _exception$response2, _this$AppStore$dispat3, _this$AppStore4;
|
|
639
671
|
var expire = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 30;
|
|
640
672
|
var shouldBeTranslated = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
|
|
641
673
|
var component = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
|
|
@@ -646,11 +678,15 @@ class Application extends _react.Component {
|
|
|
646
678
|
if ((exceptionStatus === 502 || exceptionStatus === 503 || exceptionStatus === 504) && this._indicateGatewayTimeout()) {
|
|
647
679
|
return;
|
|
648
680
|
}
|
|
681
|
+
// Skip 401 alert when session has expired
|
|
682
|
+
if (exceptionStatus === 401 && (_this$AppStore2 = this.AppStore) !== null && _this$AppStore2 !== void 0 && (_this$AppStore2 = _this$AppStore2.getState()) !== null && _this$AppStore2 !== void 0 && (_this$AppStore2 = _this$AppStore2.auth) !== null && _this$AppStore2 !== void 0 && _this$AppStore2.sessionExpired) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
649
685
|
// Handle specific response statuses and set the appropriate level and message
|
|
650
686
|
var statusAlert = _statusAlerts.STATUS_ALERTS[exceptionStatus];
|
|
651
687
|
if (statusAlert) {
|
|
652
|
-
var _this$AppStore$dispat2, _this$
|
|
653
|
-
(_this$AppStore$dispat2 = (_this$
|
|
688
|
+
var _this$AppStore$dispat2, _this$AppStore3;
|
|
689
|
+
(_this$AppStore$dispat2 = (_this$AppStore3 = this.AppStore).dispatch) === null || _this$AppStore$dispat2 === void 0 || _this$AppStore$dispat2.call(_this$AppStore3, {
|
|
654
690
|
type: _actions.ADD_ALERT,
|
|
655
691
|
level: statusAlert.level,
|
|
656
692
|
message: statusAlert.message,
|
|
@@ -692,7 +728,7 @@ class Application extends _react.Component {
|
|
|
692
728
|
}, exception.message));
|
|
693
729
|
}
|
|
694
730
|
}
|
|
695
|
-
(_this$AppStore$dispat3 = (_this$
|
|
731
|
+
(_this$AppStore$dispat3 = (_this$AppStore4 = this.AppStore).dispatch) === null || _this$AppStore$dispat3 === void 0 || _this$AppStore$dispat3.call(_this$AppStore4, {
|
|
696
732
|
type: _actions.ADD_ALERT,
|
|
697
733
|
level: "danger",
|
|
698
734
|
message: exceptionMessage,
|
|
@@ -736,12 +772,12 @@ class Application extends _react.Component {
|
|
|
736
772
|
It takes a parameter called "status" to indicate whether to turn the full-screen mode on or off.
|
|
737
773
|
*/
|
|
738
774
|
setFullScreenMode(status) {
|
|
739
|
-
var _state$fullscreenmode, _this$AppStore$dispat4, _this$
|
|
775
|
+
var _state$fullscreenmode, _this$AppStore$dispat4, _this$AppStore5;
|
|
740
776
|
var state = this.AppStore.getState();
|
|
741
777
|
if (status === 'on' && (state === null || state === void 0 || (_state$fullscreenmode = state.fullscreenmode) === null || _state$fullscreenmode === void 0 ? void 0 : _state$fullscreenmode.status) === 'on') {
|
|
742
778
|
status = 'off';
|
|
743
779
|
}
|
|
744
|
-
(_this$AppStore$dispat4 = (_this$
|
|
780
|
+
(_this$AppStore$dispat4 = (_this$AppStore5 = this.AppStore).dispatch) === null || _this$AppStore$dispat4 === void 0 || _this$AppStore$dispat4.call(_this$AppStore5, {
|
|
745
781
|
type: _actions.SET_FULLSCREEN_MODE,
|
|
746
782
|
status: status
|
|
747
783
|
});
|
|
@@ -770,6 +806,8 @@ class Application extends _react.Component {
|
|
|
770
806
|
id: "app-main"
|
|
771
807
|
}, /*#__PURE__*/_react.default.createElement(_AccessDeniedCard.default, {
|
|
772
808
|
app: this
|
|
809
|
+
}), /*#__PURE__*/_react.default.createElement(_LoginLoopCard.LoginLoopCard, {
|
|
810
|
+
app: this
|
|
773
811
|
})))))
|
|
774
812
|
);
|
|
775
813
|
return /*#__PURE__*/_react.default.createElement(_asab_webui_components.AppStoreProvider, {
|
|
@@ -813,9 +851,9 @@ Application.prototype._initConnectivitySubscription = function () {
|
|
|
813
851
|
if (this.PubSub && typeof this.PubSub.subscribe === 'function') {
|
|
814
852
|
if (!this._unsubscribeConnectivity) {
|
|
815
853
|
this._unsubscribeConnectivity = this.PubSub.subscribe('Application.status!', value => {
|
|
816
|
-
var _this$AppStore$dispat5, _this$
|
|
854
|
+
var _this$AppStore$dispat5, _this$AppStore6;
|
|
817
855
|
// Prefer store so UI updates predictably
|
|
818
|
-
(_this$AppStore$dispat5 = (_this$
|
|
856
|
+
(_this$AppStore$dispat5 = (_this$AppStore6 = this.AppStore).dispatch) === null || _this$AppStore$dispat5 === void 0 || _this$AppStore$dispat5.call(_this$AppStore6, {
|
|
819
857
|
type: _actions.SET_CONNECTIVITY_STATUS,
|
|
820
858
|
status: value.status
|
|
821
859
|
});
|
|
@@ -829,4 +867,18 @@ Application.prototype._initConnectivitySubscription = function () {
|
|
|
829
867
|
*/
|
|
830
868
|
setTimeout(this._initConnectivitySubscription, 0);
|
|
831
869
|
};
|
|
870
|
+
|
|
871
|
+
/*
|
|
872
|
+
On Application lifecycle events, bridge the events to PubSub topic Application.lifecycle!
|
|
873
|
+
*/
|
|
874
|
+
Application.prototype._initPageLifecycleBridge = function () {
|
|
875
|
+
if (this._unsubscribePageHide) return;
|
|
876
|
+
this._unsubscribePageHide = (0, _pageLifecycle.subscribePageHide)(event => {
|
|
877
|
+
var _this$PubSub, _this$PubSub$publish;
|
|
878
|
+
(_this$PubSub = this.PubSub) === null || _this$PubSub === void 0 || (_this$PubSub$publish = _this$PubSub.publish) === null || _this$PubSub$publish === void 0 || _this$PubSub$publish.call(_this$PubSub, 'Application.lifecycle!', {
|
|
879
|
+
type: 'pagehide',
|
|
880
|
+
persisted: event.persisted
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
};
|
|
832
884
|
var _default = exports.default = Application;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.LoginLoopCard = LoginLoopCard;
|
|
8
|
+
var _react = _interopRequireDefault(require("react"));
|
|
9
|
+
var _reactI18next = require("react-i18next");
|
|
10
|
+
var _reactstrap = require("reactstrap");
|
|
11
|
+
require("./LoginLoopCard.scss");
|
|
12
|
+
function LoginLoopCard(props) {
|
|
13
|
+
var _props$app;
|
|
14
|
+
var _useTranslation = (0, _reactI18next.useTranslation)(),
|
|
15
|
+
t = _useTranslation.t;
|
|
16
|
+
|
|
17
|
+
// Validate if AuthModule is present
|
|
18
|
+
var hasAuthModule = (_props$app = props.app) === null || _props$app === void 0 || (_props$app = _props$app.Modules) === null || _props$app === void 0 ? void 0 : _props$app.some(m => m.Name === 'AuthModule');
|
|
19
|
+
if (!hasAuthModule) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Read the number of login attempts from sessionStorage and render the card if the number is greater than 10
|
|
24
|
+
var attempts = parseInt(sessionStorage.getItem('SeaCatLoginAttempts') || '0', 10);
|
|
25
|
+
if (!Number.isFinite(attempts) || attempts <= 10) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return /*#__PURE__*/_react.default.createElement("div", {
|
|
29
|
+
className: "auth-login-loop-wrapper"
|
|
30
|
+
}, /*#__PURE__*/_react.default.createElement(_reactstrap.Card, null, /*#__PURE__*/_react.default.createElement(_reactstrap.CardHeader, {
|
|
31
|
+
className: "card-header-flex"
|
|
32
|
+
}, /*#__PURE__*/_react.default.createElement("div", {
|
|
33
|
+
className: "flex-fill text-center"
|
|
34
|
+
}, /*#__PURE__*/_react.default.createElement("h2", {
|
|
35
|
+
className: "text-primary"
|
|
36
|
+
}, t('ASABAuthModule|Authentication error')))), /*#__PURE__*/_react.default.createElement(_reactstrap.CardBody, null, t('ASABAuthModule|Too many login redirects occurred. Please contact application administrator.'))));
|
|
37
|
+
}
|
|
@@ -46,6 +46,12 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
46
46
|
|
|
47
47
|
this.SessionExpiration = null; // Session expiration as defined in user info
|
|
48
48
|
this.sessionValidationInterval = null; // Initialize session validation interval
|
|
49
|
+
this._sessionExpired = false; // Guard which ensures _triggerSessionExpired() runs at most once
|
|
50
|
+
this._unsubscribeLifecycle = null; // Unsubscribe function for the Application.lifecycle! subscription (set by _markAuthPageActive)
|
|
51
|
+
|
|
52
|
+
// Login-loop protection counts consecutive login redirects and resets on auth success
|
|
53
|
+
var _n = parseInt(sessionStorage.getItem('SeaCatLoginAttempts') || '0', 10);
|
|
54
|
+
this._loginAttempts = Number.isFinite(_n) ? _n : 0;
|
|
49
55
|
|
|
50
56
|
// Access control screen
|
|
51
57
|
app.Router.addRoute({
|
|
@@ -86,6 +92,21 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
86
92
|
*/
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
Duplicated tabs clone sessionStorage and SeaCatAuthTabActive is set
|
|
98
|
+
while the tab is live and cleared on pagehide (refresh/close).
|
|
99
|
+
If the flag (SeaCatAuthTabActive) is still present at startup,
|
|
100
|
+
then its evaluated as a clone and it drops tokens and re-authenticate.
|
|
101
|
+
Skipping the evaluation during the OAuth `code` redirect
|
|
102
|
+
*/
|
|
103
|
+
if (authorization_code === null && _this.OAuthTokens != null && sessionStorage.getItem('SeaCatAuthTabActive')) {
|
|
104
|
+
_this.OAuthTokens = null;
|
|
105
|
+
sessionStorage.removeItem('SeaCatOAuth2Tokens');
|
|
106
|
+
sessionStorage.removeItem('SeaCatAuthTabActive');
|
|
107
|
+
yield _this._attemptLogin(_this.RedirectURL, false);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
89
110
|
if (authorization_code !== null) {
|
|
90
111
|
yield _this._exchangeCodeForTokens(authorization_code);
|
|
91
112
|
// Remove 'code' from a query string
|
|
@@ -125,13 +146,19 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
125
146
|
if (!result) {
|
|
126
147
|
// User info not found - go to login
|
|
127
148
|
sessionStorage.removeItem('SeaCatOAuth2Tokens');
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
sessionStorage.removeItem('SeaCatAuthTabActive');
|
|
150
|
+
var force_login_prompt = false;
|
|
151
|
+
yield _this._attemptLogin(_this.RedirectURL, force_login_prompt);
|
|
130
152
|
return;
|
|
131
153
|
}
|
|
132
154
|
|
|
155
|
+
// Mark current tab as holding an active auth session
|
|
156
|
+
_this._markAuthPageActive();
|
|
157
|
+
|
|
133
158
|
// Add interceptor with Bearer token in the Header into axios calls
|
|
134
159
|
_this.App.addAxiosInterceptor(_this.authInterceptor());
|
|
160
|
+
// Add response error interceptor to handle 401 Unauthorized globally
|
|
161
|
+
_this.App.addAxiosResponseErrorInterceptor(_this.unauthorizedInterceptor());
|
|
135
162
|
// Add webSocket interceptor with Bearer token into websocket calls
|
|
136
163
|
_this.App.addWebSocketInterceptor(_this.webSocketAuthInterceptor());
|
|
137
164
|
|
|
@@ -142,7 +169,7 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
142
169
|
if (!tenantAuthorized) {
|
|
143
170
|
// If tenant not authorized, redirect to Access denied card
|
|
144
171
|
var _force_login_prompt = false;
|
|
145
|
-
yield _this.
|
|
172
|
+
yield _this._attemptLogin(_this.RedirectURL, _force_login_prompt);
|
|
146
173
|
return;
|
|
147
174
|
}
|
|
148
175
|
}
|
|
@@ -160,7 +187,7 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
160
187
|
*/
|
|
161
188
|
if (resources == undefined) {
|
|
162
189
|
var _force_login_prompt2 = false;
|
|
163
|
-
yield _this.
|
|
190
|
+
yield _this._attemptLogin(_this.RedirectURL, _force_login_prompt2);
|
|
164
191
|
return;
|
|
165
192
|
}
|
|
166
193
|
(_this$App = _this.App) === null || _this$App === void 0 || (_this$App = _this$App.AppStore) === null || _this$App === void 0 || (_this$App$dispatch = _this$App.dispatch) === null || _this$App$dispatch === void 0 || _this$App$dispatch.call(_this$App, {
|
|
@@ -176,12 +203,15 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
176
203
|
}
|
|
177
204
|
}
|
|
178
205
|
if (_this.UserInfo == null && _this.MustAuthenticate) {
|
|
179
|
-
// TODO: force_login_prompt = true to break authentication failure loop
|
|
180
206
|
var _force_login_prompt3 = false;
|
|
181
|
-
yield _this.
|
|
207
|
+
yield _this._attemptLogin(_this.RedirectURL, _force_login_prompt3);
|
|
182
208
|
return;
|
|
183
209
|
}
|
|
184
210
|
}
|
|
211
|
+
|
|
212
|
+
// Authorization completed successfully so reset the login-loop counter
|
|
213
|
+
_this._loginAttempts = 0;
|
|
214
|
+
sessionStorage.removeItem('SeaCatLoginAttempts');
|
|
185
215
|
_this.App.removeSplashScreenRequestor(_this);
|
|
186
216
|
})();
|
|
187
217
|
}
|
|
@@ -192,12 +222,56 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
192
222
|
};
|
|
193
223
|
return interceptor;
|
|
194
224
|
}
|
|
225
|
+
|
|
226
|
+
// Handle 401 Unauthorized responses from the server
|
|
227
|
+
unauthorizedInterceptor() {
|
|
228
|
+
var _this2 = this;
|
|
229
|
+
var handlingPromise = null; // Shared promise so concurrent 401s await the same refresh/expire flow
|
|
230
|
+
return /*#__PURE__*/function () {
|
|
231
|
+
var _ref = (0, _asyncToGenerator2.default)(function* (error) {
|
|
232
|
+
var _error$response, _error$config, _error$config2;
|
|
233
|
+
if ((error === null || error === void 0 || (_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.status) !== 401) return;
|
|
234
|
+
|
|
235
|
+
// If session expired, do not continue
|
|
236
|
+
if (_this2._sessionExpired) return;
|
|
237
|
+
|
|
238
|
+
// Ignore 401s from auth endpoints themselves to avoid loops
|
|
239
|
+
var requestBaseURL = error === null || error === void 0 || (_error$config = error.config) === null || _error$config === void 0 ? void 0 : _error$config.baseURL;
|
|
240
|
+
var requestPath = error === null || error === void 0 || (_error$config2 = error.config) === null || _error$config2 === void 0 ? void 0 : _error$config2.url;
|
|
241
|
+
var oidcURL = _this2.App.getServiceURL('openidconnect');
|
|
242
|
+
var seacatAuthURL = _this2.App.getServiceURL('seacat-auth');
|
|
243
|
+
// Ignore 401 requests from the oidc service (token/userinfo endpoints) to avoid refresh loops
|
|
244
|
+
// For seacat-auth, only ignore the internal /openidconnect/* sub-path (used by the internal userinfo call)
|
|
245
|
+
if (requestBaseURL && (requestBaseURL === oidcURL || requestBaseURL === seacatAuthURL && (requestPath === '/openidconnect' || requestPath !== null && requestPath !== void 0 && requestPath.startsWith('/openidconnect/')))) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!handlingPromise) {
|
|
249
|
+
handlingPromise = (0, _asyncToGenerator2.default)(function* () {
|
|
250
|
+
try {
|
|
251
|
+
yield _this2._refreshTokens();
|
|
252
|
+
var isUserInfoUpdated = yield _this2.updateUserInfo();
|
|
253
|
+
if (!isUserInfoUpdated) {
|
|
254
|
+
_this2._triggerSessionExpired();
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
handlingPromise = null;
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
}
|
|
261
|
+
// Await so sessionExpired is set before callers reach addAlertFromException
|
|
262
|
+
yield handlingPromise;
|
|
263
|
+
});
|
|
264
|
+
return function (_x) {
|
|
265
|
+
return _ref.apply(this, arguments);
|
|
266
|
+
};
|
|
267
|
+
}();
|
|
268
|
+
}
|
|
195
269
|
webSocketAuthInterceptor() {
|
|
196
270
|
// Return a function to ensure that the token is always current and not static
|
|
197
271
|
return () => "access_token_".concat(this.OAuthTokens['access_token']);
|
|
198
272
|
}
|
|
199
273
|
simulateUserinfo(mock_userinfo) {
|
|
200
|
-
var
|
|
274
|
+
var _this3 = this;
|
|
201
275
|
return (0, _asyncToGenerator2.default)(function* () {
|
|
202
276
|
/*
|
|
203
277
|
This method takes parameters from wepack.dev.js settings
|
|
@@ -217,7 +291,7 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
217
291
|
...
|
|
218
292
|
],
|
|
219
293
|
*/
|
|
220
|
-
|
|
294
|
+
_this3.App.addAlert("warning", "ASABAuthModule|You are using MOCK_USERINFO!", 1, true);
|
|
221
295
|
var mockParams = mock_userinfo;
|
|
222
296
|
if (mockParams.resources) {
|
|
223
297
|
mockParams["resources"] = Object.values(mockParams.resources);
|
|
@@ -228,13 +302,13 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
228
302
|
if (mockParams.tenants) {
|
|
229
303
|
mockParams["tenants"] = Object.values(mockParams.tenants);
|
|
230
304
|
}
|
|
231
|
-
if (
|
|
232
|
-
var
|
|
233
|
-
(
|
|
305
|
+
if (_this3.App.AppStore) {
|
|
306
|
+
var _this3$App$AppStore$d, _this3$App$AppStore, _this3$App$AppStore$d2, _this3$App$AppStore2;
|
|
307
|
+
(_this3$App$AppStore$d = (_this3$App$AppStore = _this3.App.AppStore).dispatch) === null || _this3$App$AppStore$d === void 0 || _this3$App$AppStore$d.call(_this3$App$AppStore, {
|
|
234
308
|
type: _actions.types.AUTH_USERINFO,
|
|
235
309
|
payload: mockParams
|
|
236
310
|
});
|
|
237
|
-
(
|
|
311
|
+
(_this3$App$AppStore$d2 = (_this3$App$AppStore2 = _this3.App.AppStore).dispatch) === null || _this3$App$AppStore$d2 === void 0 || _this3$App$AppStore$d2.call(_this3$App$AppStore2, {
|
|
238
312
|
type: _actions.types.AUTH_RESOURCES,
|
|
239
313
|
resources: mockParams["resources"]
|
|
240
314
|
});
|
|
@@ -242,8 +316,8 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
242
316
|
|
|
243
317
|
/** Check for TenantService and pass tenants list obtained from userinfo */
|
|
244
318
|
var availableTenants = mockParams.tenants;
|
|
245
|
-
if (
|
|
246
|
-
yield
|
|
319
|
+
if (_this3.App.Services.TenantService) {
|
|
320
|
+
yield _this3.App.Services.TenantService.setTenants(availableTenants, _this3._getAuthorizedTenant(mockParams));
|
|
247
321
|
}
|
|
248
322
|
})();
|
|
249
323
|
}
|
|
@@ -251,7 +325,11 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
251
325
|
this.App.addSplashScreenRequestor(this);
|
|
252
326
|
this._stopSessionExpirationValidation(); // Stop session validation and clear the timeout
|
|
253
327
|
|
|
328
|
+
// Clear login-loop counter so a fresh login after logout starts from 0
|
|
329
|
+
this._loginAttempts = 0;
|
|
330
|
+
sessionStorage.removeItem('SeaCatLoginAttempts');
|
|
254
331
|
sessionStorage.removeItem('SeaCatOAuth2Tokens');
|
|
332
|
+
sessionStorage.removeItem('SeaCatAuthTabActive');
|
|
255
333
|
var promise = this.Api.logout(this.OAuthTokens['access_token']);
|
|
256
334
|
if (promise == null) {
|
|
257
335
|
window.location.reload();
|
|
@@ -262,9 +340,9 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
262
340
|
window.location.reload();
|
|
263
341
|
});
|
|
264
342
|
}
|
|
265
|
-
validateNavigation(
|
|
343
|
+
validateNavigation(_ref3) {
|
|
266
344
|
var _state$navigation, _this$App$AppStore$di, _this$App$AppStore;
|
|
267
|
-
var resources =
|
|
345
|
+
var resources = _ref3.resources;
|
|
268
346
|
var state = this.App.AppStore.getState();
|
|
269
347
|
var navItems = (_state$navigation = state.navigation) === null || _state$navigation === void 0 ? void 0 : _state$navigation.navItems;
|
|
270
348
|
var authorizedNavItems = [];
|
|
@@ -345,47 +423,47 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
345
423
|
return valid;
|
|
346
424
|
}
|
|
347
425
|
updateUserInfo() {
|
|
348
|
-
var
|
|
426
|
+
var _this4 = this;
|
|
349
427
|
return (0, _asyncToGenerator2.default)(function* () {
|
|
350
|
-
var
|
|
351
|
-
var internal =
|
|
428
|
+
var _this4$UserInfo;
|
|
429
|
+
var internal = _this4.OAuthTokens.internal || false;
|
|
352
430
|
if (!internal) {
|
|
353
431
|
var _response$data;
|
|
354
432
|
var response;
|
|
355
433
|
try {
|
|
356
|
-
response = yield
|
|
434
|
+
response = yield _this4.Api.userinfo(_this4.OAuthTokens.access_token);
|
|
357
435
|
} catch (err) {
|
|
358
436
|
console.error("Failed to update user info", err);
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
var
|
|
362
|
-
(
|
|
437
|
+
_this4.UserInfo = null;
|
|
438
|
+
if (_this4.App.AppStore) {
|
|
439
|
+
var _this4$App$AppStore$d, _this4$App$AppStore;
|
|
440
|
+
(_this4$App$AppStore$d = (_this4$App$AppStore = _this4.App.AppStore).dispatch) === null || _this4$App$AppStore$d === void 0 || _this4$App$AppStore$d.call(_this4$App$AppStore, {
|
|
363
441
|
type: _actions.types.AUTH_USERINFO,
|
|
364
|
-
payload:
|
|
442
|
+
payload: _this4.UserInfo
|
|
365
443
|
});
|
|
366
444
|
}
|
|
367
445
|
return false;
|
|
368
446
|
}
|
|
369
|
-
|
|
370
|
-
|
|
447
|
+
_this4.UserInfo = response.data;
|
|
448
|
+
_this4.SessionExpiration = (_response$data = response.data) === null || _response$data === void 0 ? void 0 : _response$data.exp;
|
|
371
449
|
} else {
|
|
372
450
|
var _response$data2;
|
|
373
451
|
var _response;
|
|
374
452
|
try {
|
|
375
|
-
_response = yield
|
|
453
|
+
_response = yield _this4.Api.userinfo(_this4.OAuthTokens.access_token, internal);
|
|
376
454
|
} catch (err) {
|
|
377
455
|
console.error("Failed to update user info", err);
|
|
378
|
-
|
|
379
|
-
if (
|
|
380
|
-
var
|
|
381
|
-
(
|
|
456
|
+
_this4.UserInfo = null;
|
|
457
|
+
if (_this4.App.AppStore) {
|
|
458
|
+
var _this4$App$AppStore$d2, _this4$App$AppStore2;
|
|
459
|
+
(_this4$App$AppStore$d2 = (_this4$App$AppStore2 = _this4.App.AppStore).dispatch) === null || _this4$App$AppStore$d2 === void 0 || _this4$App$AppStore$d2.call(_this4$App$AppStore2, {
|
|
382
460
|
type: _actions.types.AUTH_USERINFO,
|
|
383
|
-
payload:
|
|
461
|
+
payload: _this4.UserInfo
|
|
384
462
|
});
|
|
385
463
|
}
|
|
386
464
|
return false;
|
|
387
465
|
}
|
|
388
|
-
|
|
466
|
+
_this4.UserInfo = _response.data;
|
|
389
467
|
|
|
390
468
|
// If the resource is `{'*': [....]}` then it is a global resources token (i.e. for API keys)
|
|
391
469
|
var resources = _response.data.resources;
|
|
@@ -394,43 +472,43 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
394
472
|
var tenant = (0, _extractTenantFromUrl.extractTenantFromUrl)();
|
|
395
473
|
if (tenant) {
|
|
396
474
|
// Monkey patch the userinfo to add the tenant and resources
|
|
397
|
-
|
|
398
|
-
|
|
475
|
+
_this4.UserInfo['tenants'] = [tenant];
|
|
476
|
+
_this4.UserInfo['resources'][tenant] = resources['*'];
|
|
399
477
|
} else {
|
|
400
|
-
var
|
|
478
|
+
var _this4$App, _this4$App$dispatch;
|
|
401
479
|
console.error("Tenant not found in URL - if the global resources token is used, the tenant must be specified in the URL");
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
(
|
|
480
|
+
_this4.UserInfo = null;
|
|
481
|
+
_this4.SessionExpiration = null;
|
|
482
|
+
(_this4$App = _this4.App) === null || _this4$App === void 0 || (_this4$App = _this4$App.AppStore) === null || _this4$App === void 0 || (_this4$App$dispatch = _this4$App.dispatch) === null || _this4$App$dispatch === void 0 || _this4$App$dispatch.call(_this4$App, {
|
|
405
483
|
type: _actions.types.AUTH_USERINFO,
|
|
406
484
|
payload: null
|
|
407
485
|
});
|
|
408
486
|
return false;
|
|
409
487
|
}
|
|
410
488
|
}
|
|
411
|
-
|
|
489
|
+
_this4.SessionExpiration = (_response$data2 = _response.data) === null || _response$data2 === void 0 ? void 0 : _response$data2.exp;
|
|
412
490
|
}
|
|
413
|
-
if (
|
|
414
|
-
var
|
|
415
|
-
(
|
|
491
|
+
if (_this4.App.AppStore) {
|
|
492
|
+
var _this4$App$AppStore$d3, _this4$App$AppStore3;
|
|
493
|
+
(_this4$App$AppStore$d3 = (_this4$App$AppStore3 = _this4.App.AppStore).dispatch) === null || _this4$App$AppStore$d3 === void 0 || _this4$App$AppStore$d3.call(_this4$App$AppStore3, {
|
|
416
494
|
type: _actions.types.AUTH_USERINFO,
|
|
417
|
-
payload:
|
|
495
|
+
payload: _this4.UserInfo
|
|
418
496
|
});
|
|
419
497
|
}
|
|
420
498
|
|
|
421
499
|
// Check for TenantService and pass tenants list obtained from userinfo resources
|
|
422
|
-
var availableTenants =
|
|
500
|
+
var availableTenants = _this4.UserInfo.tenants;
|
|
423
501
|
if (availableTenants == null || availableTenants.length === 0) {
|
|
424
|
-
var
|
|
502
|
+
var _this4$UserInfo$resou;
|
|
425
503
|
// Fallback to tenant from resources if tenants list is not available
|
|
426
|
-
availableTenants = Object.keys((
|
|
504
|
+
availableTenants = Object.keys((_this4$UserInfo$resou = _this4.UserInfo.resources) !== null && _this4$UserInfo$resou !== void 0 ? _this4$UserInfo$resou : {}).filter(tenant => tenant !== '*');
|
|
427
505
|
}
|
|
428
|
-
if (((
|
|
506
|
+
if (((_this4$UserInfo = _this4.UserInfo) === null || _this4$UserInfo === void 0 ? void 0 : _this4$UserInfo.tenants) == null) {
|
|
429
507
|
// This is a monkey patch to add the tenants list to the userinfo if missing
|
|
430
|
-
|
|
508
|
+
_this4.UserInfo['tenants'] = availableTenants;
|
|
431
509
|
}
|
|
432
|
-
if (
|
|
433
|
-
yield
|
|
510
|
+
if (_this4.App.Services.TenantService) {
|
|
511
|
+
yield _this4.App.Services.TenantService.setTenants(availableTenants, _this4._getAuthorizedTenant(_this4.UserInfo) // Get the authorized tenant
|
|
434
512
|
);
|
|
435
513
|
}
|
|
436
514
|
return true;
|
|
@@ -439,12 +517,13 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
439
517
|
|
|
440
518
|
// Method for obtaining and storing the OAuth tokens based on authorization code
|
|
441
519
|
_exchangeCodeForTokens(authorization_code) {
|
|
442
|
-
var
|
|
520
|
+
var _this5 = this;
|
|
443
521
|
return (0, _asyncToGenerator2.default)(function* () {
|
|
444
522
|
try {
|
|
445
|
-
var response = yield
|
|
446
|
-
|
|
523
|
+
var response = yield _this5.Api.token_authorization_code(authorization_code, _this5.RedirectURL);
|
|
524
|
+
_this5.OAuthTokens = response.data;
|
|
447
525
|
sessionStorage.setItem('SeaCatOAuth2Tokens', JSON.stringify(response.data));
|
|
526
|
+
_this5._markAuthPageActive();
|
|
448
527
|
return true;
|
|
449
528
|
} catch (err) {
|
|
450
529
|
console.error("Failed to update token", err);
|
|
@@ -453,18 +532,61 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
453
532
|
})();
|
|
454
533
|
}
|
|
455
534
|
|
|
535
|
+
/*
|
|
536
|
+
Mark current tab as holding an active auth session
|
|
537
|
+
Cleared on pagehide (refresh/close) so the next load keeps tokens
|
|
538
|
+
A duplicated tab inherits the uncleared flag >> it is detected as a clone
|
|
539
|
+
*/
|
|
540
|
+
_markAuthPageActive() {
|
|
541
|
+
sessionStorage.setItem('SeaCatAuthTabActive', '1'); // 1 stands for true (active)
|
|
542
|
+
// Subscribe just once, it is not intentional to trigger pagehide twice
|
|
543
|
+
if (this._unsubscribeLifecycle) return;
|
|
544
|
+
// Subscribe to the pagehide event
|
|
545
|
+
this._unsubscribeLifecycle = this.App.PubSub.subscribe('Application.lifecycle!', _ref4 => {
|
|
546
|
+
var type = _ref4.type,
|
|
547
|
+
persisted = _ref4.persisted;
|
|
548
|
+
if (type === 'pagehide' && !persisted) {
|
|
549
|
+
sessionStorage.removeItem('SeaCatAuthTabActive');
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/*
|
|
555
|
+
Login-loop protection wrapper around Api.login()
|
|
556
|
+
Each call increments SeaCatLoginAttempts in sessionStorage. After more than
|
|
557
|
+
MAX_LOGIN_ATTEMPTS consecutive redirects without a successful auth, further
|
|
558
|
+
redirects are suppressed.
|
|
559
|
+
The counter is cleared when initialize() completes successfully.
|
|
560
|
+
*/
|
|
561
|
+
_attemptLogin(redirectURL) {
|
|
562
|
+
var _arguments = arguments,
|
|
563
|
+
_this6 = this;
|
|
564
|
+
return (0, _asyncToGenerator2.default)(function* () {
|
|
565
|
+
var force_login_prompt = _arguments.length > 1 && _arguments[1] !== undefined ? _arguments[1] : false;
|
|
566
|
+
var MAX_LOGIN_ATTEMPTS = 10;
|
|
567
|
+
_this6._loginAttempts += 1;
|
|
568
|
+
sessionStorage.setItem('SeaCatLoginAttempts', String(_this6._loginAttempts));
|
|
569
|
+
if (_this6._loginAttempts > MAX_LOGIN_ATTEMPTS) {
|
|
570
|
+
// The info card for a user who ends up in a login redirect loop is shown by LoginLoopCard via sessionStorage SeaCatLoginAttempts
|
|
571
|
+
console.error("AuthModule: Login redirect loop detected! ".concat(_this6._loginAttempts, " consecutive login redirects occurred! Please validate the authentication configuration."));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
yield _this6.Api.login(redirectURL, force_login_prompt);
|
|
575
|
+
})();
|
|
576
|
+
}
|
|
577
|
+
|
|
456
578
|
// Method for refreshing OAuth tokens
|
|
457
579
|
_refreshTokens() {
|
|
458
|
-
var
|
|
580
|
+
var _this7 = this;
|
|
459
581
|
return (0, _asyncToGenerator2.default)(function* () {
|
|
460
|
-
var
|
|
582
|
+
var _this7$OAuthTokens;
|
|
461
583
|
// If no refresh_token found, return false
|
|
462
|
-
if (!((
|
|
584
|
+
if (!((_this7$OAuthTokens = _this7.OAuthTokens) !== null && _this7$OAuthTokens !== void 0 && _this7$OAuthTokens.refresh_token)) {
|
|
463
585
|
return false;
|
|
464
586
|
}
|
|
465
587
|
try {
|
|
466
|
-
var response = yield
|
|
467
|
-
|
|
588
|
+
var response = yield _this7.Api.token_refresh(_this7.OAuthTokens.refresh_token);
|
|
589
|
+
_this7.OAuthTokens = response.data;
|
|
468
590
|
sessionStorage.setItem('SeaCatOAuth2Tokens', JSON.stringify(response.data));
|
|
469
591
|
return true;
|
|
470
592
|
} catch (err) {
|
|
@@ -489,11 +611,47 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
489
611
|
}
|
|
490
612
|
}
|
|
491
613
|
|
|
614
|
+
// Trigger session expiration UI: show alert, disable UI, stop validation loop
|
|
615
|
+
_triggerSessionExpired() {
|
|
616
|
+
if (this._sessionExpired) return;
|
|
617
|
+
this._sessionExpired = true;
|
|
618
|
+
clearTimeout(this.sessionValidationInterval);
|
|
619
|
+
this.sessionValidationInterval = null;
|
|
620
|
+
|
|
621
|
+
// Remove the SeaCatAuthTabActive flag and unsubscribe from the pagehide subscription
|
|
622
|
+
sessionStorage.removeItem('SeaCatAuthTabActive');
|
|
623
|
+
if (this._unsubscribeLifecycle) {
|
|
624
|
+
this._unsubscribeLifecycle();
|
|
625
|
+
this._unsubscribeLifecycle = null;
|
|
626
|
+
}
|
|
627
|
+
this.App.addAlert("info", "ASABAuthModule|Your session has expired.", 3600 * 1000, true, alert => /*#__PURE__*/_react.default.createElement(_SessionExpirationAlert.SessionExpirationAlert, {
|
|
628
|
+
alert: alert
|
|
629
|
+
}));
|
|
630
|
+
if (this.App.AppStore) {
|
|
631
|
+
var _this$App$AppStore$di2, _this$App$AppStore2;
|
|
632
|
+
(_this$App$AppStore$di2 = (_this$App$AppStore2 = this.App.AppStore).dispatch) === null || _this$App$AppStore$di2 === void 0 || _this$App$AppStore$di2.call(_this$App$AppStore2, {
|
|
633
|
+
type: _actions.types.AUTH_SESSION_EXPIRATION,
|
|
634
|
+
sessionExpired: true
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Disable UI elements
|
|
638
|
+
[...document.querySelectorAll('#app-sidebar .nav-link, [class^="btn"]:not(.alert-button), [class*=" btn"]:not(.alert-button), .btn-group a, .page-item, input, select')].forEach(i => {
|
|
639
|
+
i.classList.add("disabled");
|
|
640
|
+
i.setAttribute("disabled", "");
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Reload on navigation actions
|
|
644
|
+
window.addEventListener("popstate", () => {
|
|
645
|
+
window.location.reload();
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
492
650
|
// Loop validating session expiration
|
|
493
651
|
_startSessionExpirationValidation() {
|
|
494
|
-
var
|
|
652
|
+
var _this8 = this;
|
|
495
653
|
return (0, _asyncToGenerator2.default)(function* () {
|
|
496
|
-
if (!
|
|
654
|
+
if (!_this8.SessionExpiration) {
|
|
497
655
|
console.warn("Session expiration is not set.");
|
|
498
656
|
return;
|
|
499
657
|
}
|
|
@@ -502,7 +660,7 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
502
660
|
var MAX_SESSION_DURATION = Math.pow(2, 31) - 1;
|
|
503
661
|
|
|
504
662
|
// Proactivelly validate session expiration
|
|
505
|
-
var lastKnownExpiration =
|
|
663
|
+
var lastKnownExpiration = _this8.SessionExpiration; // Last known session expiration
|
|
506
664
|
var sessionStartTime = Date.now() / 1000; // Session start time
|
|
507
665
|
var sessionDuration = lastKnownExpiration - sessionStartTime; // Session duration
|
|
508
666
|
// Prevent glitches on very large time remaining values
|
|
@@ -515,9 +673,9 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
515
673
|
var warningDisplayed = false; // Tracks if the "about to expire" warning has been shown
|
|
516
674
|
|
|
517
675
|
var _validateSession = /*#__PURE__*/function () {
|
|
518
|
-
var
|
|
676
|
+
var _ref5 = (0, _asyncToGenerator2.default)(function* () {
|
|
519
677
|
var currentTime = Date.now() / 1000; // Convert milliseconds to seconds
|
|
520
|
-
var timeRemaining =
|
|
678
|
+
var timeRemaining = _this8.SessionExpiration - currentTime; // Time difference for triggering "about to expire" warning
|
|
521
679
|
// Prevent glitches on very large time remaining values
|
|
522
680
|
if (timeRemaining > MAX_SESSION_DURATION) {
|
|
523
681
|
// Set timeout to maximum allowed value
|
|
@@ -527,13 +685,13 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
527
685
|
// Validate session on half of the session expiration or if the remaining time is <= 5min
|
|
528
686
|
if (!refreshSessionDone && (timeRemaining <= 300 || currentTime >= sessionMidpoint)) {
|
|
529
687
|
refreshSessionDone = true;
|
|
530
|
-
var tokensRefreshed = yield
|
|
688
|
+
var tokensRefreshed = yield _this8._refreshTokens();
|
|
531
689
|
// Recalculate if session expiration was extended
|
|
532
690
|
if (tokensRefreshed) {
|
|
533
|
-
yield
|
|
691
|
+
yield _this8.updateUserInfo(); // Update userinfo
|
|
534
692
|
// If current session expiration differs from last known expiration, recalculate session variables
|
|
535
|
-
if (
|
|
536
|
-
lastKnownExpiration =
|
|
693
|
+
if (_this8.SessionExpiration !== lastKnownExpiration) {
|
|
694
|
+
lastKnownExpiration = _this8.SessionExpiration;
|
|
537
695
|
sessionStartTime = currentTime;
|
|
538
696
|
sessionDuration = lastKnownExpiration - sessionStartTime;
|
|
539
697
|
// Prevent glitches on very large time remaining values
|
|
@@ -549,55 +707,32 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
549
707
|
|
|
550
708
|
// If remaining time is between 1 and 60s, repetitivelly ask for userInfo and trigger "about to expire" warning
|
|
551
709
|
if (timeRemaining <= 60 && timeRemaining > 0) {
|
|
552
|
-
var _tokensRefreshed = yield
|
|
553
|
-
yield
|
|
710
|
+
var _tokensRefreshed = yield _this8._refreshTokens(); // Returns true/false if token is refreshed/not refreshed
|
|
711
|
+
yield _this8.updateUserInfo(); // Continue updating user info
|
|
554
712
|
if (!_tokensRefreshed && !warningDisplayed) {
|
|
555
|
-
|
|
713
|
+
_this8.App.addAlert("info", "General|Your session will expire soon", 30, true);
|
|
556
714
|
warningDisplayed = true; // Mark the warning as displayed
|
|
557
715
|
}
|
|
558
716
|
}
|
|
559
717
|
|
|
560
718
|
// Validation on expired session
|
|
561
|
-
if (
|
|
719
|
+
if (_this8.SessionExpiration <= currentTime) {
|
|
562
720
|
// Handle refresh token prior updating user info
|
|
563
|
-
yield
|
|
721
|
+
yield _this8._refreshTokens();
|
|
564
722
|
// Handle session expiration
|
|
565
|
-
var isUserInfoUpdated = yield
|
|
723
|
+
var isUserInfoUpdated = yield _this8.updateUserInfo();
|
|
566
724
|
if (!isUserInfoUpdated) {
|
|
567
725
|
// Stop further checks
|
|
568
|
-
|
|
569
|
-
_this6.sessionValidationInterval = null;
|
|
570
|
-
_this6.App.addAlert("info", "ASABAuthModule|Your session has expired.", 3600 * 1000, true, alert => /*#__PURE__*/_react.default.createElement(_SessionExpirationAlert.SessionExpirationAlert, {
|
|
571
|
-
alert: alert
|
|
572
|
-
}));
|
|
573
|
-
// Disable UI elements
|
|
574
|
-
if (_this6.App.AppStore) {
|
|
575
|
-
var _this6$App$AppStore$d, _this6$App$AppStore;
|
|
576
|
-
(_this6$App$AppStore$d = (_this6$App$AppStore = _this6.App.AppStore).dispatch) === null || _this6$App$AppStore$d === void 0 || _this6$App$AppStore$d.call(_this6$App$AppStore, {
|
|
577
|
-
type: _actions.types.AUTH_SESSION_EXPIRATION,
|
|
578
|
-
sessionExpired: true
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// Disable UI elements
|
|
582
|
-
[...document.querySelectorAll('#app-sidebar .nav-link, [class^="btn"]:not(.alert-button), [class*=" btn"]:not(.alert-button), .btn-group a, .page-item, input, select')].forEach(i => {
|
|
583
|
-
i.classList.add("disabled");
|
|
584
|
-
i.setAttribute("disabled", "");
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// Reload on navigation actions
|
|
588
|
-
window.addEventListener("popstate", () => {
|
|
589
|
-
window.location.reload();
|
|
590
|
-
});
|
|
591
|
-
}
|
|
726
|
+
_this8._triggerSessionExpired();
|
|
592
727
|
return;
|
|
593
728
|
}
|
|
594
729
|
}
|
|
595
730
|
|
|
596
731
|
// Re-trigger validation after 10 seconds
|
|
597
|
-
|
|
732
|
+
_this8.sessionValidationInterval = setTimeout(_validateSession, 10000);
|
|
598
733
|
});
|
|
599
734
|
return function validateSession() {
|
|
600
|
-
return
|
|
735
|
+
return _ref5.apply(this, arguments);
|
|
601
736
|
};
|
|
602
737
|
}();
|
|
603
738
|
|
|
@@ -609,7 +744,7 @@ class AuthModule extends _asab_webui_components.Module {
|
|
|
609
744
|
// Stop looping on session expiration validation
|
|
610
745
|
_stopSessionExpirationValidation() {
|
|
611
746
|
if (this.sessionValidationInterval) {
|
|
612
|
-
|
|
747
|
+
clearTimeout(this.sessionValidationInterval);
|
|
613
748
|
this.sessionValidationInterval = null;
|
|
614
749
|
}
|
|
615
750
|
}
|
|
@@ -7,15 +7,11 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
7
7
|
exports.default = TenantDropdown;
|
|
8
8
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
9
|
var _react = _interopRequireWildcard(require("react"));
|
|
10
|
-
var _reactRouter = require("react-router");
|
|
11
10
|
var _asab_webui_components = require("asab_webui_components");
|
|
12
11
|
var _reactI18next = require("react-i18next");
|
|
13
|
-
var _seacatAuth = require("asab_webui_components/seacat-auth");
|
|
14
12
|
var _reactstrap = require("reactstrap");
|
|
15
13
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
|
|
16
|
-
function TenantDropdown(
|
|
17
|
-
var _app$Modules;
|
|
18
|
-
var app = _ref.app;
|
|
14
|
+
function TenantDropdown() {
|
|
19
15
|
var _useTranslation = (0, _reactI18next.useTranslation)(),
|
|
20
16
|
t = _useTranslation.t;
|
|
21
17
|
var _useState = (0, _react.useState)(false),
|
|
@@ -34,9 +30,6 @@ function TenantDropdown(_ref) {
|
|
|
34
30
|
var _state$tenant2;
|
|
35
31
|
return state === null || state === void 0 || (_state$tenant2 = state.tenant) === null || _state$tenant2 === void 0 ? void 0 : _state$tenant2.tenants;
|
|
36
32
|
});
|
|
37
|
-
var canCreateTenant = (app === null || app === void 0 || (_app$Modules = app.Modules) === null || _app$Modules === void 0 ? void 0 : _app$Modules.some(obj => (obj === null || obj === void 0 ? void 0 : obj.Name) === "LmioTrexModule")) && (0, _seacatAuth.isAuthorized)(['lmio:tenant:create'], app);
|
|
38
|
-
var tenantsAvailable = tenants && tenants.length > 0;
|
|
39
|
-
var showDropdown = tenantsAvailable || canCreateTenant;
|
|
40
33
|
var hasSearch = (tenants === null || tenants === void 0 ? void 0 : tenants.length) >= 10; // Display search input if there is >= 10 tenants
|
|
41
34
|
var toggle = () => {
|
|
42
35
|
setIsOpen(prev => !prev);
|
|
@@ -59,12 +52,12 @@ function TenantDropdown(_ref) {
|
|
|
59
52
|
className: "bi bi-house-lock pe-2"
|
|
60
53
|
}), /*#__PURE__*/_react.default.createElement(TenantLabel, {
|
|
61
54
|
tenant: current
|
|
62
|
-
})), isOpen &&
|
|
55
|
+
})), isOpen && (tenants === null || tenants === void 0 ? void 0 : tenants.length) > 0 && /*#__PURE__*/_react.default.createElement(_reactstrap.DropdownMenu, {
|
|
63
56
|
className: "shadow overflow-y-auto".concat(hasSearch ? ' pt-0' : ''),
|
|
64
57
|
style: {
|
|
65
58
|
maxHeight: '20em'
|
|
66
59
|
}
|
|
67
|
-
},
|
|
60
|
+
}, /*#__PURE__*/_react.default.createElement(_reactstrap.DropdownItem, {
|
|
68
61
|
header: true
|
|
69
62
|
}, t('General|Tenants')), hasSearch && /*#__PURE__*/_react.default.createElement(_reactstrap.Input, {
|
|
70
63
|
value: searchInput,
|
|
@@ -78,14 +71,9 @@ function TenantDropdown(_ref) {
|
|
|
78
71
|
href: '?tenant=' + tenant + '#/'
|
|
79
72
|
}, /*#__PURE__*/_react.default.createElement(TenantLabel, {
|
|
80
73
|
tenant: tenant
|
|
81
|
-
}))))
|
|
82
|
-
divider: true
|
|
83
|
-
}), /*#__PURE__*/_react.default.createElement(_reactstrap.DropdownItem, {
|
|
84
|
-
tag: _reactRouter.Link,
|
|
85
|
-
to: "/config/tenant/!create"
|
|
86
|
-
}, t('TenantDropdown|Create tenant')))));
|
|
74
|
+
})))));
|
|
87
75
|
}
|
|
88
|
-
function TenantLabel(
|
|
89
|
-
var tenant =
|
|
76
|
+
function TenantLabel(_ref) {
|
|
77
|
+
var tenant = _ref.tenant;
|
|
90
78
|
return /*#__PURE__*/_react.default.createElement("span", null, tenant);
|
|
91
79
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.subscribePageHide = subscribePageHide;
|
|
7
|
+
/*
|
|
8
|
+
Page lifecycle helpers
|
|
9
|
+
|
|
10
|
+
Low-level fan-out wrappers around browser page lifecycle events.
|
|
11
|
+
These are used exclusively by Application._initPageLifecycleBridge to build
|
|
12
|
+
the app-level PubSub topic `Application.lifecycle!`
|
|
13
|
+
|
|
14
|
+
Exported event types bridged:
|
|
15
|
+
'pagehide' - subscribePageHide
|
|
16
|
+
TODO: implement 'pageshow'
|
|
17
|
+
|
|
18
|
+
subscribePageHide attaches a single window 'pagehide' listener and fans out
|
|
19
|
+
to registered callbacks. Callers receive the native PageTransitionEvent and
|
|
20
|
+
should check event.persisted when they care about bfcache vs real unload.
|
|
21
|
+
|
|
22
|
+
Returns an unsubscribe function. The window listener is removed when the
|
|
23
|
+
last subscriber unsubscribes.
|
|
24
|
+
|
|
25
|
+
An optional AbortSignal can be passed as the second argument to tie the
|
|
26
|
+
subscription lifetime to an AbortController - the callback is removed
|
|
27
|
+
automatically when the signal is aborted, without needing to call the
|
|
28
|
+
returned unsubscribe function explicitly.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
|
|
32
|
+
PREFERRED - subscribe via the app-level PubSub topic Application.lifecycle!
|
|
33
|
+
Current types: 'pagehide' (more will be added, e.g. 'pageshow')
|
|
34
|
+
|
|
35
|
+
import { usePubSub } from 'asab_webui_components';
|
|
36
|
+
|
|
37
|
+
function MyComponent() {
|
|
38
|
+
const { subscribe } = usePubSub();
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const unsubscribe = subscribe('Application.lifecycle!', ({ type, persisted }) => {
|
|
41
|
+
if (type === 'pagehide' && !persisted) {
|
|
42
|
+
// real unload
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return unsubscribe; // React cleans up on unmount
|
|
46
|
+
}, []);
|
|
47
|
+
}
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var pageHideCallbacks = new Set();
|
|
51
|
+
function onPageHide(event) {
|
|
52
|
+
pageHideCallbacks.forEach(callback => {
|
|
53
|
+
try {
|
|
54
|
+
callback(event);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('pageLifecycle: pagehide callback failed', err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Subscribe to window 'pagehide' event
|
|
62
|
+
function subscribePageHide(callback, signal) {
|
|
63
|
+
if (typeof callback !== 'function') {
|
|
64
|
+
return () => {};
|
|
65
|
+
}
|
|
66
|
+
if (typeof window === 'undefined') {
|
|
67
|
+
return () => {};
|
|
68
|
+
}
|
|
69
|
+
if (signal !== null && signal !== void 0 && signal.aborted) {
|
|
70
|
+
return () => {};
|
|
71
|
+
}
|
|
72
|
+
var wasEmpty = pageHideCallbacks.size === 0;
|
|
73
|
+
pageHideCallbacks.add(callback);
|
|
74
|
+
if (wasEmpty) {
|
|
75
|
+
window.addEventListener('pagehide', onPageHide);
|
|
76
|
+
}
|
|
77
|
+
var unsubscribe = () => {
|
|
78
|
+
pageHideCallbacks.delete(callback);
|
|
79
|
+
if (pageHideCallbacks.size === 0 && typeof window !== 'undefined') {
|
|
80
|
+
window.removeEventListener('pagehide', onPageHide);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
signal === null || signal === void 0 || signal.addEventListener('abort', unsubscribe, {
|
|
84
|
+
once: true
|
|
85
|
+
});
|
|
86
|
+
return unsubscribe;
|
|
87
|
+
}
|
|
@@ -9,8 +9,13 @@ exports.STATUS_ALERTS = void 0;
|
|
|
9
9
|
This is a extension to the addAlertFromException function in the Application.js file.
|
|
10
10
|
|
|
11
11
|
The 502, 503 and 504 errors are handled directly in the Application.js file, since it does not render an alert at all.
|
|
12
|
+
The 401 alert is suppressed in Application.js when the session has expired (AuthModule shows SessionExpirationAlert).
|
|
12
13
|
*/
|
|
13
14
|
var STATUS_ALERTS = exports.STATUS_ALERTS = {
|
|
15
|
+
401: {
|
|
16
|
+
level: 'warning',
|
|
17
|
+
message: 'General|Unauthorized request. Please contact the administrator.'
|
|
18
|
+
},
|
|
14
19
|
408: {
|
|
15
20
|
level: 'warning',
|
|
16
21
|
message: 'General|The request timed out. Please try again.'
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
This is a extension to the addAlertFromException function in the Application.js file.
|
|
4
4
|
|
|
5
5
|
The 502, 503 and 504 errors are handled directly in the Application.js file, since it does not render an alert at all.
|
|
6
|
+
The 401 alert is suppressed in Application.js when the session has expired (AuthModule shows SessionExpirationAlert).
|
|
6
7
|
*/
|
|
7
8
|
export const STATUS_ALERTS = {
|
|
9
|
+
401: {
|
|
10
|
+
level: 'warning',
|
|
11
|
+
message: 'General|Unauthorized request. Please contact the administrator.',
|
|
12
|
+
},
|
|
8
13
|
408: {
|
|
9
14
|
level: 'warning',
|
|
10
15
|
message: 'General|The request timed out. Please try again.',
|