mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
79700: Tracking idleness & idle modal
This commit is contained in:
@@ -167,7 +167,6 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
* @param value The value of the browse-entry to display items for
|
* @param value The value of the browse-entry to display items for
|
||||||
*/
|
*/
|
||||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||||
console.log('updatePAge', searchOptions);
|
|
||||||
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,6 +47,7 @@ import { ThemedHeaderComponent } from './header/themed-header.component';
|
|||||||
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||||
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||||
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -144,6 +145,7 @@ const DECLARATIONS = [
|
|||||||
ThemedBreadcrumbsComponent,
|
ThemedBreadcrumbsComponent,
|
||||||
ForbiddenComponent,
|
ForbiddenComponent,
|
||||||
ThemedForbiddenComponent,
|
ThemedForbiddenComponent,
|
||||||
|
IdleModalComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
|
@@ -34,7 +34,9 @@ export const AuthActionTypes = {
|
|||||||
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
||||||
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS')
|
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS'),
|
||||||
|
SET_USER_AS_IDLE: type('dspace/auth/SET_USER_AS_IDLE'),
|
||||||
|
UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -404,6 +406,24 @@ export class RetrieveAuthenticatedEpersonErrorAction implements Action {
|
|||||||
this.payload = payload ;
|
this.payload = payload ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current user as being idle.
|
||||||
|
* @class SetUserAsIdleAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class SetUserAsIdleAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.SET_USER_AS_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset the current user as being idle.
|
||||||
|
* @class UnsetUserAsIdleAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class UnsetUserAsIdleAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.UNSET_USER_AS_IDLE;
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -434,4 +454,7 @@ export type AuthActions
|
|||||||
| RetrieveAuthenticatedEpersonErrorAction
|
| RetrieveAuthenticatedEpersonErrorAction
|
||||||
| RetrieveAuthenticatedEpersonSuccessAction
|
| RetrieveAuthenticatedEpersonSuccessAction
|
||||||
| SetRedirectUrlAction
|
| SetRedirectUrlAction
|
||||||
| RedirectAfterLoginSuccessAction;
|
| RedirectAfterLoginSuccessAction
|
||||||
|
| SetUserAsIdleAction
|
||||||
|
| UnsetUserAsIdleAction;
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, timer } from 'rxjs';
|
||||||
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
@@ -37,9 +37,19 @@ import {
|
|||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
RetrieveAuthMethodsErrorAction,
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
RetrieveTokenAction
|
RetrieveTokenAction, SetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { RequestActionTypes } from '../data/request.actions';
|
||||||
|
import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions';
|
||||||
|
import { ObjectCacheActionTypes } from '../cache/object-cache.actions';
|
||||||
|
import { NO_OP_ACTION_TYPE } from '../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
|
// Action Types that do not break/prevent the user from an idle state
|
||||||
|
const IDLE_TIMER_IGNORE_TYPES: string[]
|
||||||
|
= [...Object.values(AuthActionTypes).filter((t: string) => t !== AuthActionTypes.UNSET_USER_AS_IDLE),
|
||||||
|
...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthEffects {
|
export class AuthEffects {
|
||||||
@@ -242,6 +252,25 @@ export class AuthEffects {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For any action that is not in {@link IDLE_TIMER_IGNORE_TYPES} that comes in => Start the idleness timer
|
||||||
|
* If the idleness timer runs out (so no un-ignored action come through for that amount of time)
|
||||||
|
* => Return the action to set the user as idle ({@link SetUserAsIdleAction})
|
||||||
|
* @method trackIdleness
|
||||||
|
*/
|
||||||
|
@Effect()
|
||||||
|
public trackIdleness$: Observable<Action> = this.actions$.pipe(
|
||||||
|
filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)),
|
||||||
|
// Using switchMap the timer will be interrupted and restarted if a new action comes in, so idleness timer restarts
|
||||||
|
switchMap(() => {
|
||||||
|
this.authService.isAuthenticated();
|
||||||
|
return timer(environment.auth.ui.timeUntilIdle);
|
||||||
|
}),
|
||||||
|
map(() => {
|
||||||
|
return new SetUserAsIdleAction();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {Actions} actions$
|
* @param {Actions} actions$
|
||||||
|
@@ -23,7 +23,7 @@ import {
|
|||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
RetrieveAuthMethodsErrorAction,
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction, SetUserAsIdleAction, UnsetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||||
@@ -44,6 +44,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticateAction('user', 'password');
|
const action = new AuthenticateAction('user', 'password');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -53,7 +54,8 @@ describe('authReducer', () => {
|
|||||||
blocking: true,
|
blocking: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -66,7 +68,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -81,7 +84,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticationErrorAction(mockError);
|
const action = new AuthenticationErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -92,7 +96,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: 'Test error message'
|
error: 'Test error message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -105,7 +110,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedAction(mockTokenInfo);
|
const action = new AuthenticatedAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -115,7 +121,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -127,7 +134,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -138,7 +146,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -150,7 +159,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedErrorAction(mockError);
|
const action = new AuthenticatedErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -161,7 +171,8 @@ describe('authReducer', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -172,6 +183,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenAction();
|
const action = new CheckAuthenticationTokenAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -180,6 +192,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -190,6 +203,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenCookieAction();
|
const action = new CheckAuthenticationTokenCookieAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -198,6 +212,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -211,7 +226,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutAction();
|
const action = new LogOutAction();
|
||||||
@@ -229,7 +245,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutSuccessAction();
|
const action = new LogOutSuccessAction();
|
||||||
@@ -243,7 +260,8 @@ describe('authReducer', () => {
|
|||||||
loading: true,
|
loading: true,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -257,7 +275,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutErrorAction(mockError);
|
const action = new LogOutErrorAction(mockError);
|
||||||
@@ -270,7 +289,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -283,7 +303,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -295,7 +316,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -307,7 +329,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -318,7 +341,8 @@ describe('authReducer', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -332,7 +356,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenAction(newTokenInfo);
|
const action = new RefreshTokenAction(newTokenInfo);
|
||||||
@@ -346,7 +371,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -361,7 +387,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
||||||
@@ -375,7 +402,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: false
|
refreshing: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -390,7 +418,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RefreshTokenErrorAction();
|
const action = new RefreshTokenErrorAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -403,7 +432,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -417,7 +447,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -428,7 +459,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: 'Message',
|
info: 'Message',
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -450,6 +482,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AddAuthenticationMessageAction('Message');
|
const action = new AddAuthenticationMessageAction('Message');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -458,7 +491,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: 'Message'
|
info: 'Message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -470,7 +504,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
info: 'Message'
|
info: 'Message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new ResetAuthenticationMessagesAction();
|
const action = new ResetAuthenticationMessagesAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -480,7 +515,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -490,7 +526,8 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new SetRedirectUrlAction('redirect.url');
|
const action = new SetRedirectUrlAction('redirect.url');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -499,7 +536,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
redirectUrl: 'redirect.url'
|
redirectUrl: 'redirect.url',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -510,7 +548,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthMethodsAction(new AuthStatus(), true);
|
const action = new RetrieveAuthMethodsAction(new AuthStatus(), true);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -519,7 +558,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -530,7 +570,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const authMethods = [
|
const authMethods = [
|
||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password),
|
||||||
@@ -543,7 +584,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: authMethods
|
authMethods: authMethods,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -554,7 +596,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const authMethods = [
|
const authMethods = [
|
||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password),
|
||||||
@@ -588,7 +631,50 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a SET_USER_AS_IDLE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new SetUserAsIdleAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: true
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a UNSET_USER_AS_IDLE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new UnsetUserAsIdleAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
@@ -59,6 +59,9 @@ export interface AuthState {
|
|||||||
// all authentication Methods enabled at the backend
|
// all authentication Methods enabled at the backend
|
||||||
authMethods?: AuthMethod[];
|
authMethods?: AuthMethod[];
|
||||||
|
|
||||||
|
// true when the current user is idle
|
||||||
|
idle: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +72,8 @@ const initialState: AuthState = {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,6 +238,22 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
blocking: true,
|
blocking: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.SET_USER_AS_IDLE:
|
||||||
|
if (state.authenticated) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case AuthActionTypes.UNSET_USER_AS_IDLE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: false,
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,10 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
|||||||
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
|
||||||
describe('AuthService test', () => {
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
@@ -107,6 +111,8 @@ describe('AuthService test', () => {
|
|||||||
{ provide: Store, useValue: mockStore },
|
{ provide: Store, useValue: mockStore },
|
||||||
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
|
{ provide: NotificationsService, useValue: NotificationsServiceStub },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
CookieService,
|
CookieService,
|
||||||
AuthService
|
AuthService
|
||||||
],
|
],
|
||||||
@@ -207,13 +213,13 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
store
|
store
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return true when user is logged in', () => {
|
it('should return true when user is logged in', () => {
|
||||||
@@ -277,7 +283,7 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
||||||
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
||||||
authenticatedState = {
|
authenticatedState = {
|
||||||
@@ -292,7 +298,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
storage = (authService as any).storage;
|
storage = (authService as any).storage;
|
||||||
routeServiceMock = TestBed.inject(RouteService);
|
routeServiceMock = TestBed.inject(RouteService);
|
||||||
routerStub = TestBed.inject(Router);
|
routerStub = TestBed.inject(Router);
|
||||||
@@ -493,13 +499,13 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
store
|
store
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = unAuthenticatedState;
|
(state as any).core.auth = unAuthenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return null for the shortlived token', () => {
|
it('should return null for the shortlived token', () => {
|
||||||
|
@@ -29,6 +29,7 @@ import {
|
|||||||
getRedirectUrl,
|
getRedirectUrl,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAuthenticatedLoaded,
|
isAuthenticatedLoaded,
|
||||||
|
isIdle,
|
||||||
isTokenRefreshing
|
isTokenRefreshing
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
@@ -36,7 +37,9 @@ import {
|
|||||||
CheckAuthenticationTokenAction, RefreshTokenAction,
|
CheckAuthenticationTokenAction, RefreshTokenAction,
|
||||||
ResetAuthenticationMessagesAction,
|
ResetAuthenticationMessagesAction,
|
||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction,
|
||||||
|
SetUserAsIdleAction,
|
||||||
|
UnsetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
@@ -197,7 +200,7 @@ export class AuthService {
|
|||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(getAuthenticatedUserId),
|
select(getAuthenticatedUserId),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((id: string) => this.epersonService.findById(id) ),
|
switchMap((id: string) => this.epersonService.findById(id)),
|
||||||
getAllSucceededRemoteDataPayload()
|
getAllSucceededRemoteDataPayload()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -279,7 +282,7 @@ export class AuthService {
|
|||||||
// Send a request that sign end the session
|
// Send a request that sign end the session
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
const options: HttpOptions = Object.create({ headers, responseType: 'text' });
|
const options: HttpOptions = Object.create({headers, responseType: 'text'});
|
||||||
return this.authRequestService.postToEndpoint('logout', options).pipe(
|
return this.authRequestService.postToEndpoint('logout', options).pipe(
|
||||||
map((rd: RemoteData<AuthStatus>) => {
|
map((rd: RemoteData<AuthStatus>) => {
|
||||||
const status = rd.payload;
|
const status = rd.payload;
|
||||||
@@ -324,20 +327,20 @@ export class AuthService {
|
|||||||
let currentlyRefreshingToken = false;
|
let currentlyRefreshingToken = false;
|
||||||
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
|
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
// If new token is undefined an it wasn't previously => Refresh failed
|
// If new token is undefined an it wasn't previously => Refresh failed
|
||||||
if (currentlyRefreshingToken && token != undefined && authTokenInfo == undefined) {
|
if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
|
||||||
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
|
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
|
||||||
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
|
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
|
||||||
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
|
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
|
||||||
currentlyRefreshingToken = false;
|
currentlyRefreshingToken = false;
|
||||||
}
|
}
|
||||||
// If new token.expires is different => Refresh succeeded
|
// If new token.expires is different => Refresh succeeded
|
||||||
if (currentlyRefreshingToken && authTokenInfo != undefined && token.expires != authTokenInfo.expires) {
|
if (currentlyRefreshingToken && authTokenInfo !== undefined && token.expires !== authTokenInfo.expires) {
|
||||||
currentlyRefreshingToken = false;
|
currentlyRefreshingToken = false;
|
||||||
}
|
}
|
||||||
// Check if/when token needs to be refreshed
|
// Check if/when token needs to be refreshed
|
||||||
if (!currentlyRefreshingToken) {
|
if (!currentlyRefreshingToken) {
|
||||||
token = authTokenInfo || null;
|
token = authTokenInfo || null;
|
||||||
if (token != undefined && token != null) {
|
if (token !== undefined && token !== null) {
|
||||||
let timeLeftBeforeRefresh = token.expires - new Date().getTime() - environment.auth.rest.timeLeftBeforeTokenRefresh;
|
let timeLeftBeforeRefresh = token.expires - new Date().getTime() - environment.auth.rest.timeLeftBeforeTokenRefresh;
|
||||||
if (timeLeftBeforeRefresh < 0) {
|
if (timeLeftBeforeRefresh < 0) {
|
||||||
timeLeftBeforeRefresh = 0;
|
timeLeftBeforeRefresh = 0;
|
||||||
@@ -394,7 +397,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Set the cookie expire date
|
// Set the cookie expire date
|
||||||
const expires = new Date(expireDate);
|
const expires = new Date(expireDate);
|
||||||
const options: CookieAttributes = { expires: expires };
|
const options: CookieAttributes = {expires: expires};
|
||||||
|
|
||||||
// Save cookie with the token
|
// Save cookie with the token
|
||||||
return this.storage.set(TOKENITEM, token, options);
|
return this.storage.set(TOKENITEM, token, options);
|
||||||
@@ -483,7 +486,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Set the cookie expire date
|
// Set the cookie expire date
|
||||||
const expires = new Date(expireDate);
|
const expires = new Date(expireDate);
|
||||||
const options: CookieAttributes = { expires: expires };
|
const options: CookieAttributes = {expires: expires};
|
||||||
this.storage.set(REDIRECT_COOKIE, url, options);
|
this.storage.set(REDIRECT_COOKIE, url, options);
|
||||||
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
||||||
}
|
}
|
||||||
@@ -576,4 +579,24 @@ export class AuthService {
|
|||||||
return new RetrieveAuthMethodsAction(authStatus, false);
|
return new RetrieveAuthMethodsAction(authStatus, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if current user is idle
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public isUserIdle(): Observable<boolean> {
|
||||||
|
return this.store.pipe(select(isIdle));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set idle of auth state
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public setIdle(idle: boolean): void {
|
||||||
|
if (idle) {
|
||||||
|
this.store.dispatch(new SetUserAsIdleAction());
|
||||||
|
} else {
|
||||||
|
this.store.dispatch(new UnsetUserAsIdleAction());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -115,6 +115,14 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
|||||||
|
|
||||||
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is idle.
|
||||||
|
* @function _isIdle
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isIdle = (state: AuthState) => state.idle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication methods enabled at the backend
|
* Returns the authentication methods enabled at the backend
|
||||||
* @function getAuthenticationMethods
|
* @function getAuthenticationMethods
|
||||||
@@ -231,3 +239,12 @@ export const getRegistrationError = createSelector(getAuthState, _getRegistratio
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is idle
|
||||||
|
* @function isIdle
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isIdle = createSelector(getAuthState, _isIdle);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { map } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
import { Component, Inject, OnInit, Optional, Input } from '@angular/core';
|
import { Component, Inject, OnInit, Optional, Input } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ import { environment } from '../../environments/environment';
|
|||||||
import { LocaleService } from '../core/locale/locale.service';
|
import { LocaleService } from '../core/locale/locale.service';
|
||||||
import { KlaroService } from '../shared/cookies/klaro.service';
|
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||||
import { slideSidebarPadding } from '../shared/animations/slide';
|
import { slideSidebarPadding } from '../shared/animations/slide';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { IdleModalComponent } from '../shared/idle-modal/idle-modal.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-root',
|
selector: 'ds-root',
|
||||||
@@ -47,6 +49,11 @@ export class RootComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() shouldShowRouteLoader: boolean;
|
@Input() shouldShowRouteLoader: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the idle modal is is currently open
|
||||||
|
*/
|
||||||
|
idleModalOpen: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
@@ -60,7 +67,8 @@ export class RootComponent implements OnInit {
|
|||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private localeService: LocaleService,
|
private localeService: LocaleService,
|
||||||
@Optional() private cookiesService: KlaroService
|
@Optional() private cookiesService: KlaroService,
|
||||||
|
private modalService: NgbModal
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,5 +83,19 @@ export class RootComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map(([collapsed, mobile]) => collapsed || mobile)
|
map(([collapsed, mobile]) => collapsed || mobile)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.authService.isUserIdle().subscribe((userIdle: boolean) => {
|
||||||
|
if (userIdle) {
|
||||||
|
if (!this.idleModalOpen) {
|
||||||
|
const modalRef = this.modalService.open(IdleModalComponent);
|
||||||
|
this.idleModalOpen = true;
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((closed: boolean) => {
|
||||||
|
if (closed) {
|
||||||
|
this.idleModalOpen = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
authState = {
|
authState = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -52,7 +53,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: new AuthTokenInfo('test_token'),
|
authToken: new AuthTokenInfo('test_token'),
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,7 +37,8 @@ describe('UserMenuComponent', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: new AuthTokenInfo('test_token'),
|
authToken: new AuthTokenInfo('test_token'),
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
authStateLoading = {
|
authStateLoading = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -45,7 +46,8 @@ describe('UserMenuComponent', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
authToken: null,
|
authToken: null,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
src/app/shared/idle-modal/idle-modal.component.html
Normal file
18
src/app/shared/idle-modal/idle-modal.component.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{ "idle-modal.header" | translate }}
|
||||||
|
<button type="button" class="close" (click)="closePressed()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{ "idle-modal.info" | translate:{timeToExpire: timeToExpire} }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="cancel btn btn-danger" (click)="logOutPressed()" aria-label="Log out">
|
||||||
|
<i class="fas fa-times"></i> {{ "idle-modal.log-out" | translate }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="confirm btn btn-outline-secondary" (click)="extendSessionPressed()" aria-label="Extend session" ngbAutofocus>
|
||||||
|
{{ "idle-modal.extend-session" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
85
src/app/shared/idle-modal/idle-modal.component.ts
Normal file
85
src/app/shared/idle-modal/idle-modal.component.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Component, OnInit, Output } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-idle-modal',
|
||||||
|
templateUrl: 'idle-modal.component.html',
|
||||||
|
})
|
||||||
|
export class IdleModalComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time of idleness before session expires (in minutes)
|
||||||
|
* (environment.auth.ui.timeUntilIdle + environment.auth.ui.idleGracePeriod / 1000 / 60)
|
||||||
|
*/
|
||||||
|
timeToExpire: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer to track time grace period
|
||||||
|
*/
|
||||||
|
private graceTimer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the modal is closed
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
response: Subject<boolean> = new Subject();
|
||||||
|
|
||||||
|
constructor(private activeModal: NgbActiveModal,
|
||||||
|
private authService: AuthService) {
|
||||||
|
this.timeToExpire = (environment.auth.ui.timeUntilIdle + environment.auth.ui.idleGracePeriod) / 1000 / 60; // ms => min
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (hasValue(this.graceTimer)) {
|
||||||
|
clearTimeout(this.graceTimer);
|
||||||
|
}
|
||||||
|
this.graceTimer = setTimeout(() => {
|
||||||
|
this.logOutPressed();
|
||||||
|
}, environment.auth.ui.idleGracePeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When extend session is pressed
|
||||||
|
*/
|
||||||
|
extendSessionPressed() {
|
||||||
|
this.extendSessionAndCloseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal and logout
|
||||||
|
*/
|
||||||
|
logOutPressed() {
|
||||||
|
this.authService.logout();
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When close is pressed
|
||||||
|
*/
|
||||||
|
closePressed() {
|
||||||
|
this.extendSessionAndCloseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal and extend session
|
||||||
|
*/
|
||||||
|
extendSessionAndCloseModal() {
|
||||||
|
if (hasValue(this.graceTimer)) {
|
||||||
|
clearTimeout(this.graceTimer);
|
||||||
|
}
|
||||||
|
this.authService.setIdle(false);
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal and set the response to true so RootComponent knows the modal was closed
|
||||||
|
*/
|
||||||
|
closeModal() {
|
||||||
|
this.activeModal.close();
|
||||||
|
this.response.next(true);
|
||||||
|
}
|
||||||
|
}
|
@@ -19,4 +19,11 @@ export class AuthServiceMock {
|
|||||||
|
|
||||||
public setRedirectUrl(url: string) {
|
public setRedirectUrl(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public trackTokenExpiration(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
public isUserIdle(): Observable<boolean> {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3691,5 +3691,15 @@
|
|||||||
|
|
||||||
"workflow-item.send-back.button.cancel": "Cancel",
|
"workflow-item.send-back.button.cancel": "Cancel",
|
||||||
|
|
||||||
"workflow-item.send-back.button.confirm": "Send back"
|
"workflow-item.send-back.button.confirm": "Send back",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"idle-modal.header": "Session will expire soon",
|
||||||
|
|
||||||
|
"idle-modal.info": "For security reasons, user sessions expire after {{ timeToExpire }} minutes of inactivity. Your session will expire soon. Would you like to extend it or log out?”",
|
||||||
|
|
||||||
|
"idle-modal.log-out": "Log out",
|
||||||
|
|
||||||
|
"idle-modal.extend-session": "Extend session"
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,22 @@ export const environment: Partial<GlobalConfig> = {
|
|||||||
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
|
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Authority settings
|
||||||
|
auth: {
|
||||||
|
// Authority UI settings
|
||||||
|
ui: {
|
||||||
|
// the amount of time before the idle warning is shown
|
||||||
|
timeUntilIdle: 20000, // 20 sec
|
||||||
|
// the amount of time the user has to react after the idle warning is shown before they are logged out.
|
||||||
|
idleGracePeriod: 20000, // 20 sec
|
||||||
|
},
|
||||||
|
// Authority REST settings
|
||||||
|
rest: {
|
||||||
|
// If the rest token expires in less than this amount of time, it will be refreshed automatically.
|
||||||
|
// This is independent from the idle warning.
|
||||||
|
timeLeftBeforeTokenRefresh: 20000, // 20 sec
|
||||||
|
},
|
||||||
|
},
|
||||||
// Form settings
|
// Form settings
|
||||||
form: {
|
form: {
|
||||||
// NOTE: Map server-side validators to comparative Angular form validators
|
// NOTE: Map server-side validators to comparative Angular form validators
|
||||||
|
Reference in New Issue
Block a user