From 4d13687a08531e2a680106dc8efc01faf15c6282 Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Mon, 23 Apr 2018 15:00:44 +0200 Subject: [PATCH] feat(all): add session pages Add includesState method in StarkRoutingService. Add MockStarkUserService ISSUES CLOSED: #407 #408 #409 #410 --- packages/stark-core/src/common.ts | 1 - packages/stark-core/src/common/routes.ts | 1 - .../stark-core/src/common/routes/routes.ts | 132 -------- .../common/translations/translations/en.ts | 46 ++- .../common/translations/translations/fr.ts | 46 ++- .../common/translations/translations/nl.ts | 46 ++- .../routing/services/routing.service.intf.ts | 7 + .../routing/services/routing.service.spec.ts | 25 ++ .../routing/services/routing.service.ts | 269 ++++++++-------- .../modules/routing/testing/routing.mock.ts | 1 + packages/stark-core/src/modules/session.ts | 1 + .../src/modules/session/entities.ts | 1 + .../entities/session-config.entity.intf.ts | 16 + .../stark-core/src/modules/session/routes.ts | 44 +++ .../session/services/session.service.spec.ts | 73 ++++- .../session/services/session.service.ts | 27 +- .../src/modules/session/session.module.ts | 15 +- .../stark-core/src/modules/user/testing.ts | 1 + .../src/modules/user/testing/user.mock.ts | 14 + packages/stark-core/testing/public_api.ts | 1 + packages/stark-ui/assets/styles/_base.scss | 74 ++++- packages/stark-ui/src/modules.ts | 1 + .../components/app-logout.component.spec.ts | 32 +- .../components/app-logout.component.ts | 21 +- packages/stark-ui/src/modules/session-ui.ts | 3 + .../src/modules/session-ui/components.ts | 1 + .../components/app-container.component.html | 6 + .../components/app-container.component.ts | 54 ++++ .../stark-ui/src/modules/session-ui/pages.ts | 4 + .../src/modules/session-ui/pages/login.ts | 1 + .../pages/login/_login-page.component.scss | 41 +++ .../pages/login/login-page.component.html | 21 ++ .../pages/login/login-page.component.spec.ts | 134 ++++++++ .../pages/login/login-page.component.ts | 91 ++++++ .../modules/session-ui/pages/preloading.ts | 1 + .../_preloading-page.component.scss | 15 + .../preloading/preloading-page.component.html | 21 ++ .../preloading-page.component.spec.ts | 56 ++++ .../preloading/preloading-page.component.ts | 80 +++++ .../session-ui/pages/session-expired.ts | 1 + .../_session-expired-page.component.scss | 19 ++ .../session-expired-page.component.html | 13 + .../session-expired-page.component.spec.ts | 59 ++++ .../session-expired-page.component.ts | 41 +++ .../session-ui/pages/session-logout.ts | 1 + .../_session-logout-page.component.scss | 15 + .../session-logout-page.component.html | 14 + .../session-logout-page.component.spec.ts | 58 ++++ .../session-logout-page.component.ts | 41 +++ .../stark-ui/src/modules/session-ui/routes.ts | 138 ++++++++ .../modules/session-ui/session-ui.module.ts | 90 ++++++ packages/stark-ui/tsconfig.spec.json | 1 + packages/tsconfig.json | 2 + showcase/config/json-server/data.json | 7 + showcase/src/app/app.component.html | 303 +++++++++--------- showcase/src/app/app.component.ts | 8 +- showcase/src/app/app.module.ts | 61 ++-- showcase/src/app/home/home.component.spec.ts | 2 - showcase/src/app/home/home.component.ts | 3 +- showcase/src/app/router.config.ts | 3 +- showcase/src/index.html | 24 +- showcase/src/styles/_stark-styles.scss | 6 + 62 files changed, 1779 insertions(+), 555 deletions(-) delete mode 100644 packages/stark-core/src/common/routes.ts delete mode 100644 packages/stark-core/src/common/routes/routes.ts create mode 100644 packages/stark-core/src/modules/session/entities/session-config.entity.intf.ts create mode 100644 packages/stark-core/src/modules/session/routes.ts create mode 100644 packages/stark-core/src/modules/user/testing.ts create mode 100644 packages/stark-core/src/modules/user/testing/user.mock.ts create mode 100644 packages/stark-ui/src/modules/session-ui.ts create mode 100644 packages/stark-ui/src/modules/session-ui/components.ts create mode 100644 packages/stark-ui/src/modules/session-ui/components/app-container.component.html create mode 100644 packages/stark-ui/src/modules/session-ui/components/app-container.component.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/login.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/login/_login-page.component.scss create mode 100644 packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.html create mode 100644 packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.spec.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/preloading.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/preloading/_preloading-page.component.scss create mode 100644 packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.html create mode 100644 packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.spec.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-expired.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-expired/_session-expired-page.component.scss create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.html create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.spec.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-logout.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-logout/_session-logout-page.component.scss create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.html create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.spec.ts create mode 100644 packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts create mode 100644 packages/stark-ui/src/modules/session-ui/routes.ts create mode 100644 packages/stark-ui/src/modules/session-ui/session-ui.module.ts create mode 100644 showcase/config/json-server/data.json diff --git a/packages/stark-core/src/common.ts b/packages/stark-core/src/common.ts index d649e6649f..07e4919cf8 100644 --- a/packages/stark-core/src/common.ts +++ b/packages/stark-core/src/common.ts @@ -1,6 +1,5 @@ export * from "./common/bootstrap"; export * from "./common/environment"; export * from "./common/error"; -export * from "./common/routes"; export * from "./common/store"; export * from "./common/translations"; diff --git a/packages/stark-core/src/common/routes.ts b/packages/stark-core/src/common/routes.ts deleted file mode 100644 index 644ff93efe..0000000000 --- a/packages/stark-core/src/common/routes.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./routes/routes"; diff --git a/packages/stark-core/src/common/routes/routes.ts b/packages/stark-core/src/common/routes/routes.ts deleted file mode 100644 index f342441ba5..0000000000 --- a/packages/stark-core/src/common/routes/routes.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Location } from "@angular/common"; -import { STARK_ROUTING_SERVICE, StarkRoutingService, StarkStateConfigWithParams } from "../../modules/routing/services"; -import { StatesModule } from "@uirouter/angular"; - -/** - * Name of the initialization states of the application - */ -export const starkAppInitStateName: string = "starkAppInit"; -/** - * Name of the exit states of the application - */ -export const starkAppExitStateName: string = "starkAppExit"; -/** - * Name of the login state of the application - */ -export const starkLoginStateName: string = "starkAppInit.starkLogin"; -/** - * URL of the login state of the application - */ -export const starkLoginStateUrl: string = "/starkLogin"; -/** - * Name of the preloading state of the application - */ -export const starkPreloadingStateName: string = "starkAppInit.starkPreloading"; -/** - * URL of the preloading state of the application - */ -export const starkPreloadingStateUrl: string = "/starkPreloading"; -/** - * Name of the SessionExpired state of the application - */ -export const starkSessionExpiredStateName: string = "starkAppExit.starkSessionExpired"; -/** - * URL of the SessionExpired state of the application - */ -export const starkSessionExpiredStateUrl: string = "/starkSessionExpired"; -/** - * Name of the SessionLogout state of the application - */ -export const starkSessionLogoutStateName: string = "starkAppExit.starkSessionLogout"; -/** - * URL of the SessionLogout state of the application - */ -export const starkSessionLogoutStateUrl: string = "/starkSessionLogout"; - -// FIXME Fix states declaration -/* tslint:disable:no-duplicate-string */ -/** - * Configuration of the route state of the application - */ -export const starkCoreRouteConfig: StatesModule = { - states: [ - { - name: starkAppInitStateName, // parent state for any initialization state (used to show/hide the main app component) - abstract: true, - resolve: { - targetRoute: [ - "$location", - STARK_ROUTING_SERVICE, - ($location: Location, routingService: StarkRoutingService) => { - // get the path of the current URL in the browser's navigation bar - const targetUrlPath: string = $location.path(); - const targetRoute: StarkStateConfigWithParams | undefined = routingService.getStateConfigByUrlPath(targetUrlPath); - - // skip any init/exit state - const initOrExitStateRegex: RegExp = new RegExp("(" + starkAppInitStateName + "|" + starkAppExitStateName + ")"); - - if (targetRoute) { - if ((targetRoute.state.$$state)().parent) { - if (!(targetRoute.state.$$state)().parent.name.match(initOrExitStateRegex)) { - return targetRoute; - } else { - return undefined; - } - } else { - return targetRoute; - } - } else { - return undefined; - } - } - ], - targetState: [ - "targetRoute", - (targetRoute: StarkStateConfigWithParams) => { - return typeof targetRoute !== "undefined" ? targetRoute.state.name : undefined; - } - ], - targetStateParams: [ - "targetRoute", - (targetRoute: StarkStateConfigWithParams) => { - return typeof targetRoute !== "undefined" ? targetRoute.paramValues : undefined; - } - ] - } - }, - { - name: starkAppExitStateName, // parent state for any exit state (used to show/hide the main app component) - abstract: true - }, - { - name: starkLoginStateName, // the parent is defined in the state's name (contains a dot) - url: starkLoginStateUrl, - views: { - // TODO: how to use a constant instead of a hard-coded string? (without loading the component file to avoid circular dependencies!) - "initOrExit@": "starkLoginPage" - } - }, - { - name: starkPreloadingStateName, // the parent is defined in the state's name (contains a dot) - url: starkPreloadingStateUrl, - views: { - "initOrExit@": "starkPreloadingPage" - } - }, - { - name: starkSessionExpiredStateName, // the parent is defined in the state's name (contains a dot) - url: starkSessionExpiredStateUrl, - views: { - "initOrExit@": "starkSessionExpiredPage" - } - }, - { - name: starkSessionLogoutStateName, // the parent is defined in the state's name (contains a dot) - url: starkSessionLogoutStateUrl, - views: { - "initOrExit@": "starkSessionLogoutPage" - } - } - ] -}; -/* tslint:enable */ diff --git a/packages/stark-core/src/common/translations/translations/en.ts b/packages/stark-core/src/common/translations/translations/en.ts index 3ef0917867..51b4847f70 100644 --- a/packages/stark-core/src/common/translations/translations/en.ts +++ b/packages/stark-core/src/common/translations/translations/en.ts @@ -3,22 +3,12 @@ */ export const translationsEn: object = { STARK: { - DATE_RANGE_PICKER: { - FROM: "From", - TO: "To" - }, APP_LOGOUT: { TITLE: "Log out" }, - LANGUAGES: { - EN: "English", - FR: "Français", - NL: "Nederlands", - DE: "Deutsch" - }, - SORTING: { - ASC: "Ascending", - DESC: "Descending" + DATE_RANGE_PICKER: { + FROM: "From", + TO: "To" }, ICONS: { ADD_ITEM: "Add", @@ -36,6 +26,16 @@ export const translationsEn: object = { NEW_ITEM: "New", SAVE_AND_NEXT: "Save And Next" }, + LANGUAGES: { + EN: "English", + FR: "Français", + NL: "Nederlands", + DE: "Deutsch" + }, + LOGIN: { + TITLE: "Login", + NO_PROFILE: "No profile available" + }, MULTI_COLUMN_SORTING: { TITLE: "Multi-Column Sorting", ADD_SORTING_LEVEL: "Add sorting level", @@ -45,6 +45,26 @@ export const translationsEn: object = { CANCEL: "Cancel", SAVE: "Save" }, + PRELOADING: { + FETCHING_USER_PROFILE: "Initializing...", + FETCHING_USER_PROFILE_FAILURE: "Initialization failed: could not fetch user profile.", + CONTACT_IT_SUPPORT: "Please contact your IT support department.", + CORRELATION_ID: "Correlation ID", + RELOAD: "Reload" + }, + SESSION_EXPIRED: { + TITLE: "Session expired", + MESSAGE: "", + RELOAD: "Reload" + }, + SESSION_LOGOUT: { + TITLE: "Logged out", + LOGIN: "Log in again" + }, + SORTING: { + ASC: "Ascending", + DESC: "Descending" + }, TABLE: { NB_SELECTED_ROWS: "Selected", TOGGLE_SELECTION: "Toggle selection", diff --git a/packages/stark-core/src/common/translations/translations/fr.ts b/packages/stark-core/src/common/translations/translations/fr.ts index b80dd60c2d..728c8cde3f 100644 --- a/packages/stark-core/src/common/translations/translations/fr.ts +++ b/packages/stark-core/src/common/translations/translations/fr.ts @@ -3,22 +3,12 @@ */ export const translationsFr: object = { STARK: { - DATE_RANGE_PICKER: { - FROM: "De", - TO: "A" - }, APP_LOGOUT: { TITLE: "Déconnecter" }, - LANGUAGES: { - EN: "English", - FR: "Français", - NL: "Nederlands", - DE: "Deutsch" - }, - SORTING: { - ASC: "Ascendant", - DESC: "Descendant" + DATE_RANGE_PICKER: { + FROM: "De", + TO: "A" }, ICONS: { ADD_ITEM: "Ajouter", @@ -36,6 +26,16 @@ export const translationsFr: object = { NEW_ITEM: "Nouveau", SAVE_AND_NEXT: "Sauver et Suivant" }, + LANGUAGES: { + EN: "English", + FR: "Français", + NL: "Nederlands", + DE: "Deutsch" + }, + LOGIN: { + TITLE: "Connexion", + NO_PROFILE: "Aucun profil utilisateur disponible" + }, MULTI_COLUMN_SORTING: { TITLE: "Tri multi-colonnes", ADD_SORTING_LEVEL: "Ajouter le niveau de tri", @@ -45,6 +45,26 @@ export const translationsFr: object = { CANCEL: "Annuler", SAVE: "Enregistrer" }, + PRELOADING: { + FETCHING_USER_PROFILE: "Initialisation...", + FETCHING_USER_PROFILE_FAILURE: "Échec de l'initialisation: impossible de récupérer le profil de l'utilisateur.", + CONTACT_IT_SUPPORT: "Veuillez contacter votre service d'assistance informatique.", + CORRELATION_ID: "ID de corrélation", + RELOAD: "Recharger" + }, + SESSION_EXPIRED: { + TITLE: "La session a expiré", + MESSAGE: "", + RELOAD: "Recharger" + }, + SESSION_LOGOUT: { + TITLE: "Déconnecté", + LOGIN: "Connexion" + }, + SORTING: { + ASC: "Ascendant", + DESC: "Descendant" + }, TABLE: { NB_SELECTED_ROWS: "Sélectionné", TOGGLE_SELECTION: "Inverser la sélection", diff --git a/packages/stark-core/src/common/translations/translations/nl.ts b/packages/stark-core/src/common/translations/translations/nl.ts index 9aa0cb919f..b439b723f9 100644 --- a/packages/stark-core/src/common/translations/translations/nl.ts +++ b/packages/stark-core/src/common/translations/translations/nl.ts @@ -3,22 +3,12 @@ */ export const translationsNl: object = { STARK: { - DATE_RANGE_PICKER: { - FROM: "Van", - TO: "Tot" - }, APP_LOGOUT: { TITLE: "Afmelden" }, - LANGUAGES: { - EN: "English", - FR: "Français", - NL: "Nederlands", - DE: "Deutsch" - }, - SORTING: { - ASC: "Oplopend", - DESC: "Aflopend" + DATE_RANGE_PICKER: { + FROM: "Van", + TO: "Tot" }, ICONS: { ADD_ITEM: "Toevoegen", @@ -36,6 +26,16 @@ export const translationsNl: object = { NEW_ITEM: "Nieuw", SAVE_AND_NEXT: "Bewaar en volgende" }, + LANGUAGES: { + EN: "English", + FR: "Français", + NL: "Nederlands", + DE: "Deutsch" + }, + LOGIN: { + TITLE: "Aanmelden", + NO_PROFILE: "Geen profiel beschikbaar" + }, MULTI_COLUMN_SORTING: { TITLE: "Sorteren van meerdere kolommen", ADD_SORTING_LEVEL: "Voeg sorteringsniveau toe", @@ -45,6 +45,26 @@ export const translationsNl: object = { CANCEL: "Annuleer", SAVE: "Opslaan" }, + PRELOADING: { + FETCHING_USER_PROFILE: "Initialiseren...", + FETCHING_USER_PROFILE_FAILURE: "Initialisatie mislukt: het gebruikersprofiel kon niet worden opgehaald.", + CONTACT_IT_SUPPORT: "Neem contact op met uw IT-support afdeling.", + CORRELATION_ID: "Correlatie ID", + RELOAD: "Herladen" + }, + SESSION_EXPIRED: { + TITLE: "Sessie verlopen", + MESSAGE: "", + RELOAD: "Herladen" + }, + SESSION_LOGOUT: { + TITLE: "Afgemeld", + LOGIN: "Opnieuw aanmelden" + }, + SORTING: { + ASC: "Oplopend", + DESC: "Aflopend" + }, TABLE: { NB_SELECTED_ROWS: "Geselecteerd", TOGGLE_SELECTION: "Selectie omkeren", diff --git a/packages/stark-core/src/modules/routing/services/routing.service.intf.ts b/packages/stark-core/src/modules/routing/services/routing.service.intf.ts index 6a6e9d1c2f..dfe2cc075c 100644 --- a/packages/stark-core/src/modules/routing/services/routing.service.intf.ts +++ b/packages/stark-core/src/modules/routing/services/routing.service.intf.ts @@ -127,6 +127,13 @@ export interface StarkRoutingService { */ isCurrentUiState(stateName: string, stateParams?: RawParams): boolean; + /** + * Check whether the stateName passed as parameter is included in the current state. + * @param stateName - Partial name, relative name, glob pattern, or state object to be searched for within the current state name. + * @param stateParams - Param object, e.g. {sectionId: section.id}, to test against the current active state. + */ + isCurrentUiStateIncludedIn(stateName: string, stateParams?: RawParams): boolean; + /** * Adds a navigation rejection cause to the rejections causes known by the routing service. These known rejection causes * will be treated differently than any other navigation error (a Rejection action will be dispatched instead of a Failure action). diff --git a/packages/stark-core/src/modules/routing/services/routing.service.spec.ts b/packages/stark-core/src/modules/routing/services/routing.service.spec.ts index 5b12db7cb1..9e6c552ff0 100644 --- a/packages/stark-core/src/modules/routing/services/routing.service.spec.ts +++ b/packages/stark-core/src/modules/routing/services/routing.service.spec.ts @@ -636,6 +636,31 @@ describe("Service: StarkRoutingService", () => { }); }); + describe("isCurrentUiStateIncludedIn", () => { + it("should return whether or not the state is included in the current state", (done: DoneFn) => { + spyOn($state, "go").and.callThrough(); + const statesConfig: StateDeclaration[] = $state.get(); + expect(statesConfig.length).toBe(numberOfMockStates); // UI-Router's root state + defined states + + routingService + .navigateTo("page-01") + .pipe( + tap((enteredState: StateObject) => { + expect(enteredState).toBeDefined(); + expect(enteredState.name).toBe("page-01"); + expect($state.go).toHaveBeenCalledTimes(1); + expect($state.go).toHaveBeenCalledWith("page-01", undefined, undefined); + expect(routingService.isCurrentUiStateIncludedIn("page-01")).toBe(true); + expect(routingService.isCurrentUiStateIncludedIn("otherState")).toBe(false); + }), + catchError((error: any) => { + return throwError("navigateTo " + error); + }) + ) + .subscribe(() => done(), (error: any) => fail(error)); + }); + }); + describe("navigateTo", () => { it("should navigate to the requested page", (done: DoneFn) => { spyOn($state, "go").and.callThrough(); diff --git a/packages/stark-core/src/modules/routing/services/routing.service.ts b/packages/stark-core/src/modules/routing/services/routing.service.ts index a99f5ee4ba..11b0be65d1 100644 --- a/packages/stark-core/src/modules/routing/services/routing.service.ts +++ b/packages/stark-core/src/modules/routing/services/routing.service.ts @@ -42,6 +42,7 @@ import { StarkRoutingTransitionHook } from "./routing-transition-hook.constants" import { StarkStateConfigWithParams } from "./state-config-with-params.intf"; import { StarkCoreApplicationState } from "../../../common/store"; import { StarkConfigurationUtil } from "../../../util/configuration.util"; +import { starkAppExitStateName, starkAppInitStateName } from "../../session/routes"; /** * @ignore @@ -108,14 +109,16 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { const previousState: StarkState = this._starkStateHistory[this._starkStateHistory.length - 2]; this._starkStateHistory = this._starkStateHistory.slice(0, this._starkStateHistory.length - 2); + const regexInitExitStateName: RegExp = new RegExp("(" + starkAppInitStateName + "|" + starkAppExitStateName + ")"); + if ( - (this._starkStateHistory.length === 1 && this._starkStateHistory[0].name.match(/(starkAppInit|starkAppExit)/)) || + (this._starkStateHistory.length === 1 && this._starkStateHistory[0].name.match(regexInitExitStateName)) || this._starkStateHistory.length === 0 ) { this.store.dispatch(new StarkNavigationHistoryLimitReached()); } - if (previousState && !previousState.name.match(/(starkAppInit|starkAppExit)/) && previousState.name !== "") { + if (previousState && !previousState.name.match(regexInitExitStateName) && previousState.name !== "") { return this.navigateTo(previousState.name, previousState.params); } } @@ -225,6 +228,12 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { return stateName === this.getCurrentStateName(); } + public isCurrentUiStateIncludedIn(stateName: string, stateParams?: RawParams): boolean { + // === true is necessary here because includes method returns TRUE, FALSE or undefined + // tslint:disable-next-line:no-boolean-literal-compare + return this.$state.includes(stateName, stateParams) === true; + } + public addKnownNavigationRejectionCause(rejectionCause: string): void { this.knownRejectionCauses.push(rejectionCause); this.knownRejectionCausesRegex = new RegExp(this.knownRejectionCauses.join("|")); @@ -236,7 +245,7 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { callback: HookFn, options?: HookRegOptions ): Function { - // FIXME: this tslint disable flag is due to a bug in 'no-useless-cast' rule (https://github.com/SonarSource/SonarTS/issues/650). Remove it once it is solved + // FIXME Check if we can remove the "useless" casts /* tslint:disable:no-useless-cast */ switch (lifecycleHook) { case StarkRoutingTransitionHook.ON_BEFORE: @@ -269,6 +278,127 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { /* tslint:enable:no-useless-cast */ } + // FIXME: re-enable this TSLINT rule and refactor this function to reduce its cognitive complexity + // tslint:disable-next-line:cognitive-complexity + public getStateTreeParams(): Map { + const stateTreeParams: Map = new Map(); + + if (typeof this.lastTransition !== "undefined") { + // we use the TO pathNodes because the resolved values can only be found in those and not in the FROM pathNodes + const pathNodes: PathNode[] = this.lastTransition.treeChanges().to; + + // the array is processed in reverse to start with the child state first (the pathNodesArray is [rootState, ..., childState]) + let index: number = pathNodes.length - 1; + + for (index; index >= 0; index--) { + const pathNode: PathNode = pathNodes[index]; + + // skipping abstract states and the root state + if (!pathNode.state.abstract && pathNode.state !== pathNode.state.root()) { + let stateParams: RawParams | undefined; + + for (let i: number = this._starkStateHistory.length - 1; i >= 0; i--) { + if (this._starkStateHistory[i].name === pathNode.state.name) { + stateParams = this._starkStateHistory[i].params; + break; + } + } + + stateTreeParams.set(pathNode.state.name, stateParams); + } + } + } else { + this.logger.debug(this.errorLastTransition); + } + + return stateTreeParams; + } + + // FIXME: re-enable this TSLINT rule and refactor this function to reduce its cognitive complexity + // tslint:disable-next-line:cognitive-complexity + public getStateTreeResolves(): Map { + const stateTreeResolves: Map = new Map(); + + if (typeof this.lastTransition !== "undefined") { + // we use the TO pathNodes because the resolved values can only be found in those and not in the FROM pathNodes + const pathNodes: PathNode[] = this.lastTransition.treeChanges().to; + + // the array is processed in reverse to start with the child state first (the pathNodesArray is [rootState, ..., childState]) + let index: number = pathNodes.length - 1; + + for (index; index >= 0; index--) { + const pathNode: PathNode = pathNodes[index]; + + // skipping abstract states and the root state + if (!pathNode.state.abstract && pathNode.state !== pathNode.state.root()) { + // taking only the current state and parent/ancestor states + if (pathNode.state === this.getCurrentState() || this.isParentState(pathNode.state)) { + const resolvablesData: { [key: string]: any } = this.extractResolvablesData(pathNode.resolvables); + const stateResolves: any = _isEmpty(resolvablesData) ? undefined : resolvablesData; + stateTreeResolves.set(pathNode.state.name, stateResolves); + } + } + } + } else { + this.logger.debug(this.errorLastTransition); + } + + return stateTreeResolves; + } + + // FIXME: re-enable this TSLINT rule and refactor this function to reduce its cognitive complexity + // tslint:disable-next-line:cognitive-complexity + public getStateTreeData(): Map { + const stateTreeData: Map = new Map(); + + if (typeof this.lastTransition !== "undefined") { + // we use the TO pathNodes to get also the current state (the FROM pathNodes include only the previous/parent states) + const pathNodes: PathNode[] = this.lastTransition.treeChanges().to; + + // the array is processed in reverse to start with the child state first (the pathNodesArray is [rootState, ..., childState]) + let index: number = pathNodes.length - 1; + + for (index; index >= 0; index--) { + const pathNode: PathNode = pathNodes[index]; + + // skipping abstract states and the root state + if (!pathNode.state.abstract && pathNode.state !== pathNode.state.root()) { + // taking only the current state and parent/ancestor states + if (pathNode.state === this.getCurrentState() || this.isParentState(pathNode.state)) { + const stateData: any = _isEmpty(pathNode.state.data) ? undefined : pathNode.state.data; + stateTreeData.set(pathNode.state.name, stateData); + } + } + } + } else { + this.logger.debug(this.errorLastTransition); + } + + return stateTreeData; + } + + public getTranslationKeyFromState(stateName: string): string { + const stateTreeResolves: Map = this.getStateTreeResolves(); + const stateTreeData: Map = this.getStateTreeData(); + + let stateTranslationKey: string | undefined; + // get the translationKey in case it is defined as a resolve in the state definition + if (stateTreeResolves.get(stateName)) { + stateTranslationKey = stateTreeResolves.get(stateName)["translationKey"]; + } + // if not found in the resolves then check the state's data object + if (!stateTranslationKey && stateTreeData.get(stateName)) { + stateTranslationKey = stateTreeData.get(stateName)["translationKey"]; + } + // if no translationKey so far, then the state name is used + if (!stateTranslationKey) { + this.logger.warn(starkRoutingServiceName + ": translation key not found for state " + stateName); + stateTranslationKey = stateName; + } + + return stateTranslationKey; + } + /** * Adds Angular UI-Router specific handlers for errors.. * It logs an error and dispatches a NAVIGATE_FAILURE action to the NGRX Store @@ -394,8 +524,15 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { // https://ui-router.github.io/ng1/docs/latest/classes/state.stateservice.html#defaulterrorhandler this.$state.defaultErrorHandler( (error: any): void => { - if (!this.knownRejectionCausesRegex.test(String(error))) { - this.logger.error(starkRoutingServiceName + ": defaultErrorHandler => ", error); + let stringError: string; + if (error instanceof Rejection) { + stringError = error.toString(); + } else { + stringError = String(error); + } + + if (!this.knownRejectionCausesRegex.test(stringError)) { + this.logger.error(starkRoutingServiceName + ": defaultErrorHandler => ", new Error(stringError)); } } ); @@ -435,105 +572,6 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { ); } - // FIXME: re-enable this TSLINT rule and refactor this function to reduce its cognitive complexity - // tslint:disable-next-line:cognitive-complexity - public getStateTreeParams(): Map { - const stateTreeParams: Map = new Map(); - - if (typeof this.lastTransition !== "undefined") { - // we use the TO pathNodes because the resolved values can only be found in those and not in the FROM pathNodes - const pathNodes: PathNode[] = this.lastTransition.treeChanges().to; - - // the array is processed in reverse to start with the child state first (the pathNodesArray is [rootState, ..., childState]) - let index: number = pathNodes.length - 1; - - for (index; index >= 0; index--) { - const pathNode: PathNode = pathNodes[index]; - - // skipping abstract states and the root state - if (!pathNode.state.abstract && pathNode.state !== pathNode.state.root()) { - let stateParams: RawParams | undefined; - - for (let i: number = this._starkStateHistory.length - 1; i >= 0; i--) { - if (this._starkStateHistory[i].name === pathNode.state.name) { - stateParams = this._starkStateHistory[i].params; - break; - } - } - - stateTreeParams.set(pathNode.state.name, stateParams); - } - } - } else { - this.logger.debug(this.errorLastTransition); - } - - return stateTreeParams; - } - - // FIXME: re-enable this TSLINT rule and refactor this function to reduce its cognitive complexity - // tslint:disable-next-line:cognitive-complexity - public getStateTreeResolves(): Map { - const stateTreeResolves: Map = new Map(); - - if (typeof this.lastTransition !== "undefined") { - // we use the TO pathNodes because the resolved values can only be found in those and not in the FROM pathNodes - const pathNodes: PathNode[] = this.lastTransition.treeChanges().to; - - // the array is processed in reverse to start with the child state first (the pathNodesArray is [rootState, ..., childState]) - let index: number = pathNodes.length - 1; - - for (index; index >= 0; index--) { - const pathNode: PathNode = pathNodes[index]; - - // skipping abstract states and the root state - if (!pathNode.state.abstract && pathNode.state !== pathNode.state.root()) { - // taking only the current state and parent/ancestor states - if (pathNode.state === this.getCurrentState() || this.isParentState(pathNode.state)) { - const resolvablesData: { [key: string]: any } = this.extractResolvablesData(pathNode.resolvables); - const stateResolves: any = _isEmpty(resolvablesData) ? undefined : resolvablesData; - stateTreeResolves.set(pathNode.state.name, stateResolves); - } - } - } - } else { - this.logger.debug(this.errorLastTransition); - } - - return stateTreeResolves; - } - - // FIXME: re-enable this TSLINT rule and refactor this function to reduce its cognitive complexity - // tslint:disable-next-line:cognitive-complexity - public getStateTreeData(): Map { - const stateTreeData: Map = new Map(); - - if (typeof this.lastTransition !== "undefined") { - // we use the TO pathNodes to get also the current state (the FROM pathNodes include only the previous/parent states) - const pathNodes: PathNode[] = this.lastTransition.treeChanges().to; - - // the array is processed in reverse to start with the child state first (the pathNodesArray is [rootState, ..., childState]) - let index: number = pathNodes.length - 1; - - for (index; index >= 0; index--) { - const pathNode: PathNode = pathNodes[index]; - - // skipping abstract states and the root state - if (!pathNode.state.abstract && pathNode.state !== pathNode.state.root()) { - // taking only the current state and parent/ancestor states - if (pathNode.state === this.getCurrentState() || this.isParentState(pathNode.state)) { - const stateData: any = _isEmpty(pathNode.state.data) ? undefined : pathNode.state.data; - stateTreeData.set(pathNode.state.name, stateData); - } - } - } - } else { - this.logger.debug(this.errorLastTransition); - } - - return stateTreeData; - } - /** * Check whether the given state is a parent/ancestor of the given currentState * @param state - The state that will be checked whether is a parent of the currentState @@ -564,27 +602,6 @@ export class StarkRoutingServiceImpl implements StarkRoutingService { return resolvablesData; } - - public getTranslationKeyFromState(stateName: string): string { - const stateTreeResolves: Map = this.getStateTreeResolves(); - const stateTreeData: Map = this.getStateTreeData(); - - let stateTranslationKey: string | undefined; - // get the translationKey in case it is defined as a resolve in the state definition - if (stateTreeResolves.get(stateName)) { - stateTranslationKey = stateTreeResolves.get(stateName)["translationKey"]; - } - // if not found in the resolves then check the state's data object - if (!stateTranslationKey && stateTreeData.get(stateName)) { - stateTranslationKey = stateTreeData.get(stateName)["translationKey"]; - } - // if no translationKey so far, then the state name is used - if (!stateTranslationKey) { - this.logger.warn(starkRoutingServiceName + ": translation key not found for state " + stateName); - stateTranslationKey = stateName; - } - - return stateTranslationKey; - } } + /* tslint:enable */ diff --git a/packages/stark-core/src/modules/routing/testing/routing.mock.ts b/packages/stark-core/src/modules/routing/testing/routing.mock.ts index cc9d8a0d21..2f0bb2e4b2 100644 --- a/packages/stark-core/src/modules/routing/testing/routing.mock.ts +++ b/packages/stark-core/src/modules/routing/testing/routing.mock.ts @@ -26,6 +26,7 @@ export class MockStarkRoutingService implements StarkRoutingService { public getStateTreeResolves: () => Map = jasmine.createSpy("getStateTreeResolves"); public getStateTreeData: () => Map = jasmine.createSpy("getStateTreeData"); public isCurrentUiState: (stateName: string, stateParams?: RawParams) => boolean = jasmine.createSpy("isCurrentUiState"); + public isCurrentUiStateIncludedIn: (stateName: string, stateParams?: RawParams) => boolean = jasmine.createSpy("includesState"); public addKnownNavigationRejectionCause: (rejectionCause: string) => void = jasmine.createSpy("addKnownNavigationRejectionCause"); public addTransitionHook: ( lifecycleHook: string, diff --git a/packages/stark-core/src/modules/session.ts b/packages/stark-core/src/modules/session.ts index 988887100d..8a32fa7105 100644 --- a/packages/stark-core/src/modules/session.ts +++ b/packages/stark-core/src/modules/session.ts @@ -3,3 +3,4 @@ export * from "./session/entities"; export * from "./session/reducers"; export * from "./session/services"; export * from "./session/session.module"; +export * from "./session/routes"; diff --git a/packages/stark-core/src/modules/session/entities.ts b/packages/stark-core/src/modules/session/entities.ts index 05ae749a0b..7c4edc223d 100644 --- a/packages/stark-core/src/modules/session/entities.ts +++ b/packages/stark-core/src/modules/session/entities.ts @@ -1,2 +1,3 @@ export * from "./entities/session.entity"; export * from "./entities/session.entity.intf"; +export * from "./entities/session-config.entity.intf"; diff --git a/packages/stark-core/src/modules/session/entities/session-config.entity.intf.ts b/packages/stark-core/src/modules/session/entities/session-config.entity.intf.ts new file mode 100644 index 0000000000..069bb6e204 --- /dev/null +++ b/packages/stark-core/src/modules/session/entities/session-config.entity.intf.ts @@ -0,0 +1,16 @@ +import {InjectionToken} from "@angular/core"; + +/** + * The InjectionToken version of the config name + */ +export const STARK_SESSION_CONFIG: InjectionToken = new InjectionToken< + StarkSessionConfig + >("StarkSessionConfig"); + +/** + * Definition of the configuration object for the Stark Session service + */ +export interface StarkSessionConfig { + sessionExpiredStateName?: string; + sessionLogoutStateName?: string; +} diff --git a/packages/stark-core/src/modules/session/routes.ts b/packages/stark-core/src/modules/session/routes.ts new file mode 100644 index 0000000000..761c646af2 --- /dev/null +++ b/packages/stark-core/src/modules/session/routes.ts @@ -0,0 +1,44 @@ +/** + * Name of the initialization states of the application + */ +export const starkAppInitStateName: string = "starkAppInit"; +/** + * Name of the exit states of the application + */ +export const starkAppExitStateName: string = "starkAppExit"; + +/** + * Name of the login state of the application + */ +export const starkLoginStateName: string = starkAppInitStateName + ".starkLogin"; +/** + * URL of the login state of the application + */ +export const starkLoginStateUrl: string = "/starkLogin"; + +/** + * Name of the Preloading state of the application + */ +export const starkPreloadingStateName: string = starkAppInitStateName + ".starkPreloading"; +/** + * URL of the Preloading state of the application + */ +export const starkPreloadingStateUrl: string = "/starkPreloading"; + +/** + * Name of the SessionExpired state of the application + */ +export const starkSessionExpiredStateName: string = starkAppExitStateName + ".starkSessionExpired"; +/** + * URL of the SessionExpired state of the application + */ +export const starkSessionExpiredStateUrl: string = "/starkSessionExpired"; + +/** + * Name of the SessionLogout state of the application + */ +export const starkSessionLogoutStateName: string = starkAppExitStateName + ".starkSessionLogout"; +/** + * URL of the SessionLogout state of the application + */ +export const starkSessionLogoutStateUrl: string = "/starkSessionLogout"; diff --git a/packages/stark-core/src/modules/session/services/session.service.spec.ts b/packages/stark-core/src/modules/session/services/session.service.spec.ts index 69b5a402ee..5033062aed 100644 --- a/packages/stark-core/src/modules/session/services/session.service.spec.ts +++ b/packages/stark-core/src/modules/session/services/session.service.spec.ts @@ -26,17 +26,17 @@ import { StarkUserActivityTrackingResume } from "../actions"; import { StarkSessionServiceImpl, starkUnauthenticatedUserError } from "./session.service"; -import { StarkSession } from "../entities"; +import { StarkSession, StarkSessionConfig } from "../entities"; import { StarkApplicationConfig, StarkApplicationConfigImpl } from "../../../configuration/entities/application"; import { StarkUser } from "../../user/entities"; import { StarkLoggingService } from "../../logging/services"; import { MockStarkLoggingService } from "../../logging/testing"; import { StarkRoutingService, StarkRoutingTransitionHook } from "../../routing/services"; import { MockStarkRoutingService } from "../../routing/testing"; -import { starkSessionExpiredStateName } from "../../../common/routes"; import { StarkCoreApplicationState } from "../../../common/store"; import Spy = jasmine.Spy; import SpyObj = jasmine.SpyObj; +import { starkSessionExpiredStateName } from "../routes"; // tslint:disable-next-line:no-big-function describe("Service: StarkSessionService", () => { @@ -63,6 +63,9 @@ describe("Service: StarkSessionService", () => { referenceNumber: "dummy ref number", roles: ["a role", "another role", "yet another role"] }; + const mockSessionConfig: StarkSessionConfig = { + sessionExpiredStateName: "mock-session-state-name" + }; // Inject module dependencies beforeEach(() => { @@ -105,7 +108,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ); mockIdleService.setIdle.calls.reset(); mockIdleService.setTimeout.calls.reset(); @@ -131,7 +135,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ) ).toThrowError(/sessionTimeout/); } @@ -153,7 +158,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ) ).toThrowError(/sessionTimeoutWarning/); } @@ -369,6 +375,8 @@ describe("Service: StarkSessionService", () => { }); }); + // FIXME rewrite those tests to reduce function + /* tslint:disable-next-line:no-big-function */ describe("configureIdleService", () => { it("should set the necessary options of the idle service", () => { const interruptsToBeSet: InterruptSource[] = DEFAULT_INTERRUPTSOURCES; @@ -488,7 +496,17 @@ describe("Service: StarkSessionService", () => { mockIdleService.onTimeout.complete(); }); - it("should dispatch the COUNTDOWN_FINISH action, trigger the logout and navigate to the SessionExpired state", () => { + it("should dispatch the COUNTDOWN_FINISH action, trigger the logout and navigate to the starkSessionExpired state", () => { + sessionService = new SessionServiceHelper( + mockStore, + mockLogger, + mockRoutingService, + appConfig, + mockIdleService, + mockInjectorService, + mockTranslateService + ); + spyOn(sessionService, "logout"); (>mockIdleService.onTimeout) = new EventEmitter(); @@ -506,6 +524,25 @@ describe("Service: StarkSessionService", () => { mockIdleService.onTimeout.complete(); }); + + it("should dispatch the COUNTDOWN_FINISH action, trigger the logout and navigate to the SessionExpired state set in the injected sessionConfig", () => { + spyOn(sessionService, "logout"); + + (>mockIdleService.onTimeout) = new EventEmitter(); + expect(mockIdleService.onTimeout.observers.length).toBe(0); + + sessionService.configureIdleService(); + + mockIdleService.onTimeout.next(321); + + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockStore.dispatch.calls.argsFor(0)[0]).toEqual(new StarkSessionTimeoutCountdownFinish()); + expect(sessionService.logout).toHaveBeenCalledTimes(1); + expect(mockRoutingService.navigateTo).toHaveBeenCalledTimes(1); + expect(mockRoutingService.navigateTo).toHaveBeenCalledWith(mockSessionConfig.sessionExpiredStateName); + + mockIdleService.onTimeout.complete(); + }); }); describe("onTimeoutWarning notifications", () => { @@ -570,7 +607,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ); expect(sessionServiceHelper.keepalive).toBeDefined(); @@ -609,7 +647,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ); expect(sessionServiceHelper.keepalive).toBeUndefined(); @@ -631,7 +670,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ); (>mockKeepaliveService.onPing) = new EventEmitter(); @@ -684,7 +724,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ); sessionServiceHelper.startKeepaliveService(); @@ -714,7 +755,8 @@ describe("Service: StarkSessionService", () => { appConfig, mockIdleService, mockInjectorService, - mockTranslateService + mockTranslateService, + mockSessionConfig ); sessionServiceHelper.stopKeepaliveService(); @@ -833,16 +875,19 @@ describe("Service: StarkSessionService", () => { }); class SessionServiceHelper extends StarkSessionServiceImpl { + // TODO Check if we can simplify this service + /* tslint:disable-next-line:parameters-max-number */ public constructor( store: Store, logger: StarkLoggingService, routingService: StarkRoutingService, appConfig: StarkApplicationConfig, - /*xsrfService: StarkXSRFService,*/ idle: Idle, + idle: Idle, injector: Injector, - translateService: TranslateService + translateService: TranslateService, + sessionConfig?: StarkSessionConfig ) { - super(store, logger, routingService, appConfig, /*xsrfService,*/ idle, injector, translateService); + super(store, logger, routingService, appConfig, idle, injector, translateService, sessionConfig); } public registerTransitionHook(): void { diff --git a/packages/stark-core/src/modules/session/services/session.service.ts b/packages/stark-core/src/modules/session/services/session.service.ts index e4a6afe67c..445823cdb0 100644 --- a/packages/stark-core/src/modules/session/services/session.service.ts +++ b/packages/stark-core/src/modules/session/services/session.service.ts @@ -8,13 +8,13 @@ import { select, Store } from "@ngrx/store"; import { StateObject } from "@uirouter/core"; import { validateSync } from "class-validator"; import { defer, Observable, Subject } from "rxjs"; -import { map, take, distinctUntilChanged } from "rxjs/operators"; +import { distinctUntilChanged, map, take } from "rxjs/operators"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "../../logging/services"; import { StarkSessionService, starkSessionServiceName } from "./session.service.intf"; import { STARK_ROUTING_SERVICE, StarkRoutingService, StarkRoutingTransitionHook } from "../../routing/services"; import { STARK_APP_CONFIG, StarkApplicationConfig } from "../../../configuration/entities/application"; -import { StarkSession } from "../entities"; +import { STARK_SESSION_CONFIG, StarkSession, StarkSessionConfig } from "../entities"; import { StarkUser } from "../../user/entities"; import { StarkChangeLanguage, @@ -33,11 +33,11 @@ import { } from "../actions"; import { StarkHttpStatusCodes } from "../../http/enumerators"; import { StarkHttpHeaders } from "../../http/constants"; -import { starkSessionExpiredStateName } from "../../../common/routes"; import { StarkCoreApplicationState } from "../../../common/store"; import { selectStarkSession } from "../reducers"; import { StarkConfigurationUtil } from "../../../util/configuration.util"; import { StarkValidationErrorsUtil } from "../../../util"; +import { starkAppExitStateName, starkAppInitStateName, starkSessionExpiredStateName } from "../routes"; /** * @ignore @@ -56,6 +56,8 @@ export class StarkSessionServiceImpl implements StarkSessionService { protected _devAuthenticationHeaders: Map; public countdownStarted: boolean; + // TODO Check if we can simplify this service + /* tslint:disable-next-line:parameters-max-number */ public constructor( public store: Store, @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, @@ -63,7 +65,8 @@ export class StarkSessionServiceImpl implements StarkSessionService { @Inject(STARK_APP_CONFIG) private appConfig: StarkApplicationConfig, public idle: Idle, injector: Injector, - public translateService: TranslateService + public translateService: TranslateService, + @Inject(STARK_SESSION_CONFIG) private sessionConfig?: StarkSessionConfig ) { // ensuring that the app config is valid before doing anything StarkConfigurationUtil.validateConfig(this.appConfig, ["session"], starkSessionServiceName); @@ -106,7 +109,8 @@ export class StarkSessionServiceImpl implements StarkSessionService { // match any state except the ones that are children of starkAppInit/starkAppExit or the Ui-Router's root state entering: (state?: StateObject) => { if (state && typeof state.name !== "undefined") { - return !state.name.match(/(starkAppInit|starkAppExit)/) && !(state.abstract && state.name === ""); + const regexInitExitStateName: RegExp = new RegExp("(" + starkAppInitStateName + "|" + starkAppExitStateName + ")"); + return !state.name.match(regexInitExitStateName) && !(state.abstract && state.name === ""); } else { return true; // always match } @@ -253,7 +257,18 @@ export class StarkSessionServiceImpl implements StarkSessionService { // dispatch action so an effect can run any logic if needed this.store.dispatch(new StarkSessionTimeoutCountdownFinish()); this.logout(); - this.routingService.navigateTo(starkSessionExpiredStateName); + + let sessionExpiredStateName: string; + if ( + typeof this.sessionConfig !== "undefined" && + typeof this.sessionConfig.sessionExpiredStateName !== "undefined" && + this.sessionConfig.sessionExpiredStateName !== "" + ) { + sessionExpiredStateName = this.sessionConfig.sessionExpiredStateName; + } else { + sessionExpiredStateName = starkSessionExpiredStateName; + } + this.routingService.navigateTo(sessionExpiredStateName); }); this.idle.onTimeoutWarning.subscribe((countdown: number) => { if (countdown === this.idle.getTimeout()) { diff --git a/packages/stark-core/src/modules/session/session.module.ts b/packages/stark-core/src/modules/session/session.module.ts index 7923433d76..6c879507d9 100644 --- a/packages/stark-core/src/modules/session/session.module.ts +++ b/packages/stark-core/src/modules/session/session.module.ts @@ -1,22 +1,29 @@ import { ModuleWithProviders, NgModule, Optional, SkipSelf } from "@angular/core"; -import { StoreModule } from "@ngrx/store"; import { starkSessionReducers } from "./reducers"; +import { StarkSessionConfig, STARK_SESSION_CONFIG } from "./entities"; import { STARK_SESSION_SERVICE, StarkSessionServiceImpl } from "./services"; +import { StoreModule } from "@ngrx/store"; + +import { StarkUserModule } from "../user/user.module"; @NgModule({ - imports: [StoreModule.forFeature("StarkSession", starkSessionReducers)] + imports: [StoreModule.forFeature("StarkSession", starkSessionReducers), StarkUserModule] }) export class StarkSessionModule { /** * Instantiates the services only once since they should be singletons * so the forRoot() should be called only by the AppModule * @link https://angular.io/guide/singleton-services#forroot + * @param sessionConfig - Object containing the configuration (if any) for the Session service * @returns a module with providers */ - public static forRoot(): ModuleWithProviders { + public static forRoot(sessionConfig?: StarkSessionConfig): ModuleWithProviders { return { ngModule: StarkSessionModule, - providers: [{ provide: STARK_SESSION_SERVICE, useClass: StarkSessionServiceImpl }] + providers: [ + { provide: STARK_SESSION_SERVICE, useClass: StarkSessionServiceImpl }, + { provide: STARK_SESSION_CONFIG, useValue: sessionConfig } + ] }; } diff --git a/packages/stark-core/src/modules/user/testing.ts b/packages/stark-core/src/modules/user/testing.ts new file mode 100644 index 0000000000..f7415e0a7f --- /dev/null +++ b/packages/stark-core/src/modules/user/testing.ts @@ -0,0 +1 @@ +export * from "./testing/user.mock"; diff --git a/packages/stark-core/src/modules/user/testing/user.mock.ts b/packages/stark-core/src/modules/user/testing/user.mock.ts new file mode 100644 index 0000000000..57df56c781 --- /dev/null +++ b/packages/stark-core/src/modules/user/testing/user.mock.ts @@ -0,0 +1,14 @@ +import { StarkUserService, StarkUser } from "@nationalbankbelgium/stark-core"; +import { Observable } from "rxjs"; + +/** + * @ignore + */ +export class MockStarkUserService implements StarkUserService { + public fetchUserProfile: () => Observable = jasmine.createSpy("fetchUserProfile"); + public getAllUsers: () => StarkUser[] = jasmine.createSpy("getAllUsers"); + + public constructor() { + // empty constructor + } +} diff --git a/packages/stark-core/testing/public_api.ts b/packages/stark-core/testing/public_api.ts index 7452e7f646..9c98543a29 100644 --- a/packages/stark-core/testing/public_api.ts +++ b/packages/stark-core/testing/public_api.ts @@ -7,5 +7,6 @@ export * from "../src/modules/http/testing"; export * from "../src/modules/logging/testing"; export * from "../src/modules/routing/testing"; export * from "../src/modules/session/testing"; +export * from "../src/modules/user/testing"; // This file only reexports content of the `src/modules/**/testing` folders. Keep it that way. diff --git a/packages/stark-ui/assets/styles/_base.scss b/packages/stark-ui/assets/styles/_base.scss index 2e1c157074..ba6d40d7c7 100644 --- a/packages/stark-ui/assets/styles/_base.scss +++ b/packages/stark-ui/assets/styles/_base.scss @@ -5,11 +5,6 @@ body { .stark-app { visibility: visible; - &.stark-init, - &.stark-exit { - visibility: hidden; - display: none; - } /* class added by translate-cloak directive while loading translations */ &.translate-cloak { visibility: hidden; @@ -20,14 +15,6 @@ body { margin-bottom: 64px; } -.stark-login-container { - display: none; - &.stark-init, - &.stark-exit { - display: block; - } -} - .stark-header-container { padding: 0 15px; } @@ -139,3 +126,64 @@ td { padding-right: 15px; } } + +/* stark session ui pages */ +.stark-session-ui-page { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-sizing: border-box; + max-width: 100%; + width: 500px; + overflow: hidden; + border: solid 1px $divider-color; + box-shadow: $elevation-2; + & header { + padding: 16px; + background: mat-color(map-get($base-theme, primary-palette), 900) url(/assets/images/app-header.png) top right no-repeat; + border-radius: 2px 2px 0 0; + & i { + margin: auto; + display: block; + width: 220px; + height: 77px; + background: url("/assets/images/logo/logo-nbb-en.gif") no-repeat; + } + } + & content { + display: block; + padding: 16px; + background: #fff; + } +} + +.stark-loading { + margin: 16px auto; + box-sizing: border-box; + border-radius: 8px; + text-align: center; +} + +.stark-loading-icon { + margin: 0 auto 8px auto; + display: block; + width: 64px; + height: 64px; + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDUwIDUwIj48cGF0aCBmaWxsPSIjN2ZiYWUzIiBkPSJNMjUuMjUxLDYuNDYxYy0xMC4zMTgsMC0xOC42ODMsOC4zNjUtMTguNjgzLDE4LjY4M2g0LjA2OGMwLTguMDcxLDYuNTQzLTE0LjYxNSwxNC42MTUtMTQuNjE1VjYuNDYxeiI+PC9wYXRoPjwvc3ZnPg=="); + /* Base64 created from SVG on this site: http://www.opinionatedgeek.com/DotNet/Tools/Base64Encode/ and from this source: + + */ + fill: currentColor; + transform-origin: 50% 50%; + animation: spin 0.8s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } + } +} diff --git a/packages/stark-ui/src/modules.ts b/packages/stark-ui/src/modules.ts index 80a64343c3..7eddcf3373 100644 --- a/packages/stark-ui/src/modules.ts +++ b/packages/stark-ui/src/modules.ts @@ -11,6 +11,7 @@ export * from "./modules/dropdown"; export * from "./modules/keyboard-directives"; export * from "./modules/language-selector"; export * from "./modules/pretty-print"; +export * from "./modules/session-ui"; export * from "./modules/slider"; export * from "./modules/svg-view-box"; export * from "./modules/table"; diff --git a/packages/stark-ui/src/modules/app-logout/components/app-logout.component.spec.ts b/packages/stark-ui/src/modules/app-logout/components/app-logout.component.spec.ts index d2a7d203d3..5e06932d85 100644 --- a/packages/stark-ui/src/modules/app-logout/components/app-logout.component.spec.ts +++ b/packages/stark-ui/src/modules/app-logout/components/app-logout.component.spec.ts @@ -4,9 +4,10 @@ import { async, ComponentFixture, TestBed } from "@angular/core/testing"; import { STARK_LOGGING_SERVICE, STARK_ROUTING_SERVICE, + STARK_SESSION_CONFIG, STARK_SESSION_SERVICE, - STARK_APP_CONFIG, - StarkApplicationConfig + StarkSessionConfig, + starkSessionLogoutStateName } from "@nationalbankbelgium/stark-core"; import { MockStarkLoggingService, MockStarkRoutingService, MockStarkSessionService } from "@nationalbankbelgium/stark-core/testing"; import { StarkAppLogoutComponent } from "./app-logout.component"; @@ -20,8 +21,8 @@ describe("AppLogoutComponent", () => { let component: StarkAppLogoutComponent; let fixture: ComponentFixture; - const mockStarkAppConfig: Partial = { - homeStateName: "home" + const mockStarkSessionConfig: Partial = { + sessionLogoutStateName: "logout-state" }; /** @@ -36,7 +37,7 @@ describe("AppLogoutComponent", () => { { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, { provide: STARK_SESSION_SERVICE, useValue: new MockStarkSessionService() }, { provide: STARK_ROUTING_SERVICE, useClass: MockStarkRoutingService }, - { provide: STARK_APP_CONFIG, useValue: mockStarkAppConfig } + { provide: STARK_SESSION_CONFIG, useValue: mockStarkSessionConfig } ], schemas: [NO_ERRORS_SCHEMA] // tells the Angular compiler to ignore unrecognized elements and attributes (svgIcon) }) @@ -68,11 +69,11 @@ describe("AppLogoutComponent", () => { expect(component.routingService).toBeDefined(); expect(component.sessionService).not.toBeNull(); expect(component.sessionService).toBeDefined(); - expect(component.appConfig).not.toBeNull(); - expect(component.appConfig).toBeDefined(); + expect(component.sessionConfig).not.toBeNull(); + expect(component.sessionConfig).toBeDefined(); }); - it("should have its imput property filled", () => { + it("should have its input property filled", () => { expect(component.icon).not.toBeNull(); expect(component.icon).toBeDefined(); expect(component.icon).toBe("power"); @@ -80,13 +81,22 @@ describe("AppLogoutComponent", () => { }); describe("logout()", () => { - it("should log out the user", () => { - // routingService.navigateTo is already a Spy + it("should log out the user and navigate to sessionLogoutStateName defined in sessionConfig", () => { (component.routingService.navigateTo).calls.reset(); component.logout(); expect(component.sessionService.logout).toHaveBeenCalledTimes(1); expect(component.routingService.navigateTo).toHaveBeenCalledTimes(1); - expect(component.routingService.navigateTo).toHaveBeenCalledWith(component.appConfig.homeStateName); + expect(component.routingService.navigateTo).toHaveBeenCalledWith(mockStarkSessionConfig.sessionLogoutStateName); + }); + + it("should log out the user and navigate to starkSessionLogoutStateName", () => { + component.sessionConfig.sessionLogoutStateName = undefined; + + (component.routingService.navigateTo).calls.reset(); + component.logout(); + expect(component.sessionService.logout).toHaveBeenCalledTimes(1); + expect(component.routingService.navigateTo).toHaveBeenCalledTimes(1); + expect(component.routingService.navigateTo).toHaveBeenCalledWith(starkSessionLogoutStateName); }); }); }); diff --git a/packages/stark-ui/src/modules/app-logout/components/app-logout.component.ts b/packages/stark-ui/src/modules/app-logout/components/app-logout.component.ts index dc66d51cd2..27978e03e0 100644 --- a/packages/stark-ui/src/modules/app-logout/components/app-logout.component.ts +++ b/packages/stark-ui/src/modules/app-logout/components/app-logout.component.ts @@ -3,11 +3,12 @@ import { Component, ElementRef, Inject, Input, OnInit, Renderer2, ViewEncapsulat import { STARK_LOGGING_SERVICE, STARK_ROUTING_SERVICE, - STARK_APP_CONFIG, + STARK_SESSION_CONFIG, + STARK_SESSION_SERVICE, StarkLoggingService, StarkRoutingService, - StarkApplicationConfig, - STARK_SESSION_SERVICE, + StarkSessionConfig, + starkSessionLogoutStateName, StarkSessionService } from "@nationalbankbelgium/stark-core"; import { AbstractStarkUiComponent } from "../../../common/classes/abstract-component"; @@ -41,7 +42,7 @@ export class StarkAppLogoutComponent extends AbstractStarkUiComponent implements * @param logger - The logger of the application * @param routingService - The routing service of the application * @param sessionService - The session service of the application - * @param appConfig - The configuration of the application + * @param sessionConfig - The configuration of the session module * @param renderer - Angular Renderer wrapper for DOM manipulations. * @param elementRef - Reference to the DOM element where this directive is applied to. */ @@ -49,7 +50,7 @@ export class StarkAppLogoutComponent extends AbstractStarkUiComponent implements @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, @Inject(STARK_ROUTING_SERVICE) public routingService: StarkRoutingService, @Inject(STARK_SESSION_SERVICE) public sessionService: StarkSessionService, - @Inject(STARK_APP_CONFIG) public appConfig: StarkApplicationConfig, + @Inject(STARK_SESSION_CONFIG) public sessionConfig: StarkSessionConfig, protected renderer: Renderer2, protected elementRef: ElementRef ) { @@ -68,6 +69,14 @@ export class StarkAppLogoutComponent extends AbstractStarkUiComponent implements */ public logout(): void { this.sessionService.logout(); - this.routingService.navigateTo(this.appConfig.homeStateName); // TODO change this to the correct logout url (when available) + if ( + typeof this.sessionConfig !== "undefined" && + typeof this.sessionConfig.sessionLogoutStateName !== "undefined" && + this.sessionConfig.sessionLogoutStateName !== "" + ) { + this.routingService.navigateTo(this.sessionConfig.sessionLogoutStateName); + } else { + this.routingService.navigateTo(starkSessionLogoutStateName); + } } } diff --git a/packages/stark-ui/src/modules/session-ui.ts b/packages/stark-ui/src/modules/session-ui.ts new file mode 100644 index 0000000000..dcd73085cd --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui.ts @@ -0,0 +1,3 @@ +export * from "./session-ui/session-ui.module"; +export * from "./session-ui/pages"; +export * from "./session-ui/components"; diff --git a/packages/stark-ui/src/modules/session-ui/components.ts b/packages/stark-ui/src/modules/session-ui/components.ts new file mode 100644 index 0000000000..5f7b849f2e --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/components.ts @@ -0,0 +1 @@ +export * from "./components/app-container.component"; diff --git a/packages/stark-ui/src/modules/session-ui/components/app-container.component.html b/packages/stark-ui/src/modules/session-ui/components/app-container.component.html new file mode 100644 index 0000000000..10dcf91661 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/components/app-container.component.html @@ -0,0 +1,6 @@ + + + + diff --git a/packages/stark-ui/src/modules/session-ui/components/app-container.component.ts b/packages/stark-ui/src/modules/session-ui/components/app-container.component.ts new file mode 100644 index 0000000000..133d2b8eee --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/components/app-container.component.ts @@ -0,0 +1,54 @@ +import {Component, HostBinding, Inject, OnInit, ViewEncapsulation} from "@angular/core"; +import { + STARK_LOGGING_SERVICE, + STARK_ROUTING_SERVICE, + starkAppExitStateName, + starkAppInitStateName, + StarkLoggingService, + StarkRoutingService +} from "@nationalbankbelgium/stark-core"; + +/** + * Name of the component + */ +const componentName: string = "stark-app-container"; + +@Component({ + selector: "stark-app-container", + templateUrl: "./app-container.component.html", + encapsulation: ViewEncapsulation.None +}) +export class StarkAppContainerComponent implements OnInit { + /** + * Adds class="stark-app-container" attribute on the host component + */ + @HostBinding("class") + public class: string = componentName; + + /** + * Class constructor + * @param logger - The logger of the application + * @param routingService - The routing service of the application + */ + public constructor( + @Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService, + @Inject(STARK_ROUTING_SERVICE) private routingService: StarkRoutingService + ) {} + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.logger.debug(componentName + ": component initialized."); + } + + /** + * Check if the current state is a "session-ui" state (with "starkAppInit" or "starkAppExit" as parent state name) + */ + public isSessionUIState(): boolean { + return ( + this.routingService.isCurrentUiStateIncludedIn(starkAppInitStateName + ".**") || + this.routingService.isCurrentUiStateIncludedIn(starkAppExitStateName + ".**") + ); + } +} diff --git a/packages/stark-ui/src/modules/session-ui/pages.ts b/packages/stark-ui/src/modules/session-ui/pages.ts new file mode 100644 index 0000000000..992be3fee6 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages.ts @@ -0,0 +1,4 @@ +export * from "./pages/login"; +export * from "./pages/preloading"; +export * from "./pages/session-expired"; +export * from "./pages/session-logout"; diff --git a/packages/stark-ui/src/modules/session-ui/pages/login.ts b/packages/stark-ui/src/modules/session-ui/pages/login.ts new file mode 100644 index 0000000000..af54f3cf3b --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/login.ts @@ -0,0 +1 @@ +export * from "./login/login-page.component"; diff --git a/packages/stark-ui/src/modules/session-ui/pages/login/_login-page.component.scss b/packages/stark-ui/src/modules/session-ui/pages/login/_login-page.component.scss new file mode 100644 index 0000000000..70de09dd89 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/login/_login-page.component.scss @@ -0,0 +1,41 @@ +/* ============================================================================== */ +/* S t a r k L o g i n P a g e */ +/* ============================================================================== */ +/* stark-ui: src/modules/session-ui/pages/login/_login-page.component.scss */ + +.stark-login-page { + max-height: 95%; + display: flex; + flex-direction: column; + & content { + display: block; + overflow-y: auto; + } + & .dialog-title { + font-size: 2em; + } + & ul { + margin: 0; + padding: 0; + list-style: none; + & a { + display: block; + padding: 8px; + color: inherit; + text-decoration: none; + cursor: pointer; + border-bottom: solid 1px $divider-color; + &:hover { + background-color: $offwhite; + } + } + & h3 { + font-weight: bold; + } + & p { + margin: 0; + } + } +} + +/* END stark-ui: src/modules/session-ui/pages/login/_login-page.component.scss */ diff --git a/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.html b/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.html new file mode 100644 index 0000000000..1ed256d876 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.html @@ -0,0 +1,21 @@ +
+
+ +

STARK.LOGIN.TITLE

+ +
STARK.LOGIN.NO_PROFILE
+
+
diff --git a/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.spec.ts b/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.spec.ts new file mode 100644 index 0000000000..655a114ae0 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.spec.ts @@ -0,0 +1,134 @@ +/* tslint:disable:completed-docs */ +/* angular imports */ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +/* stark-core imports */ +import { + STARK_LOGGING_SERVICE, + STARK_ROUTING_SERVICE, + STARK_SESSION_SERVICE, + STARK_USER_SERVICE, + StarkUser +} from "@nationalbankbelgium/stark-core"; + +import { TranslateModule } from "@ngx-translate/core"; +import { RawParams } from "@uirouter/core"; + +import { + MockStarkLoggingService, + MockStarkRoutingService, + MockStarkSessionService, + MockStarkUserService +} from "@nationalbankbelgium/stark-core/testing"; +/* stark-ui imports */ +import { StarkLoginPageComponent } from "./login-page.component"; + +describe("StarkLoginPageComponent", () => { + let component: StarkLoginPageComponent; + let fixture: ComponentFixture; + + const mockUser: StarkUser = { + firstName: "John", + lastName: "Doe", + username: "jdoe", + uuid: "mock-uuid", + roles: [] + }; + + const mockUserWithRoles: StarkUser = { + firstName: "John", + lastName: "Doe", + username: "jdoe", + uuid: "mock-uuid", + roles: ["admin", "developer"] + }; + + beforeEach(async(() => { + const mockLogger: MockStarkLoggingService = new MockStarkLoggingService(); + return TestBed.configureTestingModule({ + declarations: [StarkLoginPageComponent], + imports: [CommonModule, TranslateModule.forRoot()], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: mockLogger }, + { provide: STARK_ROUTING_SERVICE, useClass: MockStarkRoutingService }, + { provide: STARK_USER_SERVICE, useClass: MockStarkUserService }, + { provide: STARK_SESSION_SERVICE, useValue: new MockStarkSessionService() } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StarkLoginPageComponent); + component = fixture.componentInstance; + }); + + describe("on initialization", () => { + it("should set internal component properties", () => { + expect(fixture).toBeDefined(); + expect(component).toBeDefined(); + expect(component.logger).not.toBeNull(); + expect(component.logger).toBeDefined(); + expect(component.routingService).not.toBeNull(); + expect(component.routingService).toBeDefined(); + expect(component.sessionService).not.toBeNull(); + expect(component.sessionService).toBeDefined(); + expect(component.userService).not.toBeNull(); + expect(component.userService).toBeDefined(); + }); + }); + + describe("userProfilesAvailable", () => { + it("should return FALSE if users is undefined", () => { + component.users = undefined; + expect(component.userProfilesAvailable()).toBe(false); + }); + + it("should return FALSE if users array is empty", () => { + component.users = []; + expect(component.userProfilesAvailable()).toBe(false); + }); + + it("should return TRUE if users array is NOT empty", () => { + component.users = [mockUser]; + expect(component.userProfilesAvailable()).toBe(true); + }); + }); + + describe("getUserRoles", () => { + it("should return empty string if passed user has no defined roles", () => { + expect(component.getUserRoles(mockUser)).toBe(""); + }); + + it("should return string with defined roles of the passed user", () => { + expect(component.getUserRoles(mockUserWithRoles)).toBe("admin,developer"); + }); + }); + + describe("authenticateUser", () => { + it("should navigateTo to the provided targetState and login the user through the session service", () => { + const mockState: string = "mock-state"; + const mockStateParams: RawParams = { + param: "mock-state-param" + }; + component.targetState = mockState; + component.targetStateParams = mockStateParams; + fixture.detectChanges(); + component.authenticateUser(mockUser); + expect(component.routingService.navigateTo).toHaveBeenCalledTimes(1); + expect(component.routingService.navigateTo).toHaveBeenCalledWith(mockState, mockStateParams); + expect(component.routingService.navigateToHome).not.toHaveBeenCalled(); + expect(component.sessionService.login).toHaveBeenCalledTimes(1); + expect(component.sessionService.login).toHaveBeenCalledWith(mockUser); + expect(component.logger.error).not.toHaveBeenCalled(); + }); + + it("should navigateToHome and login the user through the session service", () => { + component.authenticateUser(mockUser); + expect(component.routingService.navigateTo).not.toHaveBeenCalled(); + expect(component.routingService.navigateToHome).toHaveBeenCalledTimes(1); + expect(component.sessionService.login).toHaveBeenCalledTimes(1); + expect(component.sessionService.login).toHaveBeenCalledWith(mockUser); + expect(component.logger.error).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.ts new file mode 100644 index 0000000000..c4c2b0d1f8 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/login/login-page.component.ts @@ -0,0 +1,91 @@ +import { Component, Inject, Input, OnInit, ViewEncapsulation } from "@angular/core"; +import { RawParams } from "@uirouter/core"; +import { + STARK_LOGGING_SERVICE, + STARK_ROUTING_SERVICE, + STARK_SESSION_SERVICE, + STARK_USER_SERVICE, + StarkLoggingService, + StarkRoutingService, + StarkSessionService, + StarkUser, + StarkUserService +} from "@nationalbankbelgium/stark-core"; + +/** + * Name of the component + */ +const componentName: string = "stark-login-page"; + +/** + * Login Page smart component + */ +@Component({ + selector: "stark-login-page", + templateUrl: "./login-page.component.html", + encapsulation: ViewEncapsulation.None, + host: { + class: componentName + } +}) +export class StarkLoginPageComponent implements OnInit { + @Input() + public targetState: string; + @Input() + public targetStateParams: RawParams; + + public users: StarkUser[]; + + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_USER_SERVICE) public userService: StarkUserService, + @Inject(STARK_SESSION_SERVICE) public sessionService: StarkSessionService, + @Inject(STARK_ROUTING_SERVICE) public routingService: StarkRoutingService + ) {} + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.users = this.userService.getAllUsers(); + this.logger.debug(componentName + ": component initialized"); + } + + /** + * Authenticate the passed user as the current user. + * @param user - The user to be authenticated + */ + public authenticateUser(user: StarkUser): void { + this.sessionService.login(user); + if (this.targetState) { + this.routingService.navigateTo(this.targetState, this.targetStateParams); + } else { + this.routingService.navigateToHome(); + } + } + + /** + * Check if users are defined in the component + */ + public userProfilesAvailable(): boolean { + return typeof this.users !== "undefined" && this.users.length > 0; + } + + /** + * Returns a string with the roles of the user. + * @param user - The user who has the roles + */ + public getUserRoles(user: StarkUser): string { + if (user.roles.length > 0) { + return user.roles.toString(); + } + return ""; + } + + /** + * @ignore + */ + public trackItemFn(_index: number, item: any): string { + return item; + } +} diff --git a/packages/stark-ui/src/modules/session-ui/pages/preloading.ts b/packages/stark-ui/src/modules/session-ui/pages/preloading.ts new file mode 100644 index 0000000000..73b1689214 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/preloading.ts @@ -0,0 +1 @@ +export * from "./preloading/preloading-page.component"; diff --git a/packages/stark-ui/src/modules/session-ui/pages/preloading/_preloading-page.component.scss b/packages/stark-ui/src/modules/session-ui/pages/preloading/_preloading-page.component.scss new file mode 100644 index 0000000000..edef5eecc2 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/preloading/_preloading-page.component.scss @@ -0,0 +1,15 @@ +/* ============================================================================== */ +/* S t a r k P r e l o a d i n g P a g e */ +/* ============================================================================== */ +/* stark-ui: src/modules/session-ui/pages/preloading/_preloading-page.component.scss */ + +.stark-preloading-page { + & .stark-session-ui-page { + margin: 16px auto; + box-sizing: border-box; + border-radius: 8px; + text-align: center; + } +} + +/* END stark-ui: src/modules/session-ui/pages/preloading/_preloading-page.component.scss */ diff --git a/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.html b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.html new file mode 100644 index 0000000000..e8d635927c --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.html @@ -0,0 +1,21 @@ +
+
+ +
+
+

STARK.PRELOADING.FETCHING_USER_PROFILE

+
+
+

STARK.PRELOADING.FETCHING_USER_PROFILE_FAILURE

+

STARK.PRELOADING.CONTACT_IT_SUPPORT

+

STARK.PRELOADING.CORRELATION_ID: {{ correlationId }}

+ +
+
+
diff --git a/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.spec.ts b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.spec.ts new file mode 100644 index 0000000000..6763a2e356 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.spec.ts @@ -0,0 +1,56 @@ +/* tslint:disable:completed-docs */ +/* angular imports */ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +/* stark-core imports */ +import { STARK_LOGGING_SERVICE, STARK_ROUTING_SERVICE, STARK_USER_SERVICE } from "@nationalbankbelgium/stark-core"; + +import { TranslateModule } from "@ngx-translate/core"; + +import { MockStarkLoggingService, MockStarkRoutingService, MockStarkUserService } from "@nationalbankbelgium/stark-core/testing"; +/* stark-ui imports */ +import { StarkPreloadingPageComponent } from "./preloading-page.component"; +import { MatButtonModule } from "@angular/material/button"; + +describe("StarkPreloadingPageComponent", () => { + let component: StarkPreloadingPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + const mockLogger: MockStarkLoggingService = new MockStarkLoggingService(); + return TestBed.configureTestingModule({ + declarations: [StarkPreloadingPageComponent], + imports: [CommonModule, MatButtonModule, TranslateModule.forRoot()], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: mockLogger }, + { provide: STARK_ROUTING_SERVICE, useClass: MockStarkRoutingService }, + { provide: STARK_USER_SERVICE, useClass: MockStarkUserService } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StarkPreloadingPageComponent); + component = fixture.componentInstance; + }); + + describe("on initialization", () => { + it("should set internal component properties", () => { + expect(fixture).toBeDefined(); + expect(component).toBeDefined(); + expect(component.logger).not.toBeNull(); + expect(component.logger).toBeDefined(); + expect(component.routingService).not.toBeNull(); + expect(component.routingService).toBeDefined(); + expect(component.userService).not.toBeNull(); + expect(component.userService).toBeDefined(); + }); + }); + + describe("reload", () => { + it("should call reload method of routingService", () => { + component.reload(); + expect(component.routingService.reload).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts new file mode 100644 index 0000000000..f29504a109 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component.ts @@ -0,0 +1,80 @@ +import { Component, Inject, Input, OnInit, ViewEncapsulation } from "@angular/core"; +import { RawParams } from "@uirouter/core"; +import { delay, take } from "rxjs/operators"; + +import { + STARK_LOGGING_SERVICE, + STARK_ROUTING_SERVICE, + STARK_USER_SERVICE, + StarkLoggingService, + StarkRoutingService, + StarkUserService +} from "@nationalbankbelgium/stark-core"; + +/** + * Name of the component + */ +const componentName: string = "stark-preloading-page"; + +/** + * Preloading Page smart component + */ +@Component({ + selector: "stark-preloading-page", + templateUrl: "./preloading-page.component.html", + encapsulation: ViewEncapsulation.None, + host: { + class: componentName + } +}) +export class StarkPreloadingPageComponent implements OnInit { + @Input() + public targetState: string; + @Input() + public targetStateParams: RawParams; + + public userFetchingFailed: boolean; + public correlationId: string; + + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_USER_SERVICE) public userService: StarkUserService, + @Inject(STARK_ROUTING_SERVICE) public routingService: StarkRoutingService + ) {} + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + // the result is delayed for some milliseconds, + // otherwise the page will show an ugly flickering (if the profile is fetched immediately) + this.userService + .fetchUserProfile() + .pipe( + take(1), // this ensures that the observable will be automatically unsubscribed after emitting the value + delay(200) + ) + .subscribe( + (/*user: StarkUser*/) => { + if (this.targetState) { + this.routingService.navigateTo(this.targetState, this.targetStateParams); + } else { + this.routingService.navigateToHome(); + } + }, + () => { + this.correlationId = this.logger.correlationId; + this.userFetchingFailed = true; + } + ); + + this.logger.debug(componentName + ": component initialized"); + } + + /** + * Reload the page through the routingService. + */ + public reload(): void { + this.routingService.reload(); + } +} diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-expired.ts b/packages/stark-ui/src/modules/session-ui/pages/session-expired.ts new file mode 100644 index 0000000000..9363401203 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-expired.ts @@ -0,0 +1 @@ +export * from "./session-expired/session-expired-page.component"; diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-expired/_session-expired-page.component.scss b/packages/stark-ui/src/modules/session-ui/pages/session-expired/_session-expired-page.component.scss new file mode 100644 index 0000000000..a37a0bc889 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-expired/_session-expired-page.component.scss @@ -0,0 +1,19 @@ +/* ============================================================================== */ +/* S t a r k S e s s i o n E x p i r e d P a g e */ +/* ============================================================================== */ +/* stark-ui: src/modules/session-ui/pages/session-expired/_session-expired-page.component.scss */ + +.stark-session-expired-page { + & .stark-session-ui-page { + margin: 16px auto; + box-sizing: border-box; + border-radius: 8px; + text-align: center; + } + & p { + text-align: left; + line-height: 1.5; + } +} + +/* END stark-ui: src/modules/session-ui/pages/session-expired/_session-expired-page.component.scss */ diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.html b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.html new file mode 100644 index 0000000000..a045cc20b9 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.html @@ -0,0 +1,13 @@ +
+
+ +

STARK.SESSION_EXPIRED.TITLE

+ +

STARK.SESSION_EXPIRED.MESSAGE

+ + +
+
diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.spec.ts b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.spec.ts new file mode 100644 index 0000000000..540011fa57 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.spec.ts @@ -0,0 +1,59 @@ +/* tslint:disable:completed-docs */ +/* angular imports */ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +/* stark-core imports */ +import { STARK_APP_CONFIG, STARK_LOGGING_SERVICE, StarkApplicationConfig } from "@nationalbankbelgium/stark-core"; + +import { TranslateModule } from "@ngx-translate/core"; + +import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing"; +/* stark-ui imports */ +import { StarkSessionExpiredPageComponent } from "./session-expired-page.component"; +import { MatButtonModule } from "@angular/material/button"; + +describe("StarkSessionExpiredPageComponent", () => { + let component: StarkSessionExpiredPageComponent; + let fixture: ComponentFixture; + + const mockStarkAppConfig: Partial = { + baseUrl: "base-url" + }; + + beforeEach(async(() => { + const mockLogger: MockStarkLoggingService = new MockStarkLoggingService(); + return TestBed.configureTestingModule({ + declarations: [StarkSessionExpiredPageComponent], + imports: [CommonModule, MatButtonModule, TranslateModule.forRoot()], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: mockLogger }, + { provide: STARK_APP_CONFIG, useValue: mockStarkAppConfig } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StarkSessionExpiredPageComponent); + component = fixture.componentInstance; + }); + + describe("on initialization", () => { + it("should set internal component properties", () => { + expect(fixture).toBeDefined(); + expect(component).toBeDefined(); + expect(component.appConfig).not.toBeNull(); + expect(component.appConfig).toBeDefined(); + expect(component.logger).not.toBeNull(); + expect(component.logger).toBeDefined(); + }); + }); + + describe("reload", () => { + it("should open url", () => { + spyOn(window, "open"); + component.reload(); + expect(window.open).toHaveBeenCalledTimes(1); + expect(window.open).toHaveBeenCalledWith("base-url", "_self"); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts new file mode 100644 index 0000000000..001bd8c54e --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component.ts @@ -0,0 +1,41 @@ +import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core"; + +import { STARK_APP_CONFIG, STARK_LOGGING_SERVICE, StarkApplicationConfig, StarkLoggingService } from "@nationalbankbelgium/stark-core"; + +/** + * Name of the component + */ +const componentName: string = "stark-session-expired-page"; + +/** + * Session expired page smart component + */ +@Component({ + selector: "stark-session-expired-page", + templateUrl: "./session-expired-page.component.html", + encapsulation: ViewEncapsulation.None, + host: { + class: componentName + } +}) +export class StarkSessionExpiredPageComponent implements OnInit { + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_APP_CONFIG) public appConfig: StarkApplicationConfig + ) {} + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.logger.debug(componentName + ": component initialized"); + } + + /** + * Open baseUrl page (defined in the appConfig) in the current window. + */ + public reload(): void { + // reload app base URL (stark will redirect to the Login/Preloading page) + window.open(this.appConfig.baseUrl, "_self"); + } +} diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-logout.ts b/packages/stark-ui/src/modules/session-ui/pages/session-logout.ts new file mode 100644 index 0000000000..4de8ef3e54 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-logout.ts @@ -0,0 +1 @@ +export * from "./session-logout/session-logout-page.component"; diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-logout/_session-logout-page.component.scss b/packages/stark-ui/src/modules/session-ui/pages/session-logout/_session-logout-page.component.scss new file mode 100644 index 0000000000..a5b5bf5514 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-logout/_session-logout-page.component.scss @@ -0,0 +1,15 @@ +/* ============================================================================== */ +/* S t a r k S e s s i o n L o g o u t P a g e */ +/* ============================================================================== */ +/* stark-ui: src/modules/session-ui/pages/session-logout/_session-logout-page.component.scss */ + +.stark-session-logout-page { + & .stark-session-ui-page { + margin: 16px auto; + box-sizing: border-box; + border-radius: 8px; + text-align: center; + } +} + +/* END stark-ui: src/modules/session-ui/pages/session-logout/_session-logout-page.component.scss */ diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.html b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.html new file mode 100644 index 0000000000..1b975e7307 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.html @@ -0,0 +1,14 @@ +
+
+ +

STARK.SESSION_LOGOUT.TITLE

+ + +
+
diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.spec.ts b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.spec.ts new file mode 100644 index 0000000000..a207cf2019 --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.spec.ts @@ -0,0 +1,58 @@ +/* tslint:disable:completed-docs */ +/* angular imports */ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { CommonModule } from "@angular/common"; +/* stark-core imports */ +import { STARK_APP_CONFIG, STARK_LOGGING_SERVICE, StarkApplicationConfig } from "@nationalbankbelgium/stark-core"; + +import { TranslateModule } from "@ngx-translate/core"; + +import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing"; +/* stark-ui imports */ +import { StarkSessionLogoutPageComponent } from "./session-logout-page.component"; + +describe("StarkSessionLogoutPageComponent", () => { + let component: StarkSessionLogoutPageComponent; + let fixture: ComponentFixture; + + const mockStarkAppConfig: Partial = { + baseUrl: "base-url" + }; + + beforeEach(async(() => { + const mockLogger: MockStarkLoggingService = new MockStarkLoggingService(); + return TestBed.configureTestingModule({ + declarations: [StarkSessionLogoutPageComponent], + imports: [CommonModule, TranslateModule.forRoot()], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: mockLogger }, + { provide: STARK_APP_CONFIG, useValue: mockStarkAppConfig } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StarkSessionLogoutPageComponent); + component = fixture.componentInstance; + }); + + describe("on initialization", () => { + it("should set internal component properties", () => { + expect(fixture).toBeDefined(); + expect(component).toBeDefined(); + expect(component.appConfig).not.toBeNull(); + expect(component.appConfig).toBeDefined(); + expect(component.logger).not.toBeNull(); + expect(component.logger).toBeDefined(); + }); + }); + + describe("logon", () => { + it("should open url", () => { + spyOn(window, "open"); + component.logon(); + expect(window.open).toHaveBeenCalledTimes(1); + expect(window.open).toHaveBeenCalledWith("base-url", "_self"); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts new file mode 100644 index 0000000000..ec7fb9a6ff --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component.ts @@ -0,0 +1,41 @@ +import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core"; + +import { STARK_APP_CONFIG, STARK_LOGGING_SERVICE, StarkApplicationConfig, StarkLoggingService } from "@nationalbankbelgium/stark-core"; + +/** + * Name of the component + */ +const componentName: string = "stark-session-logout-page"; + +/** + * Session logout page smart component + */ +@Component({ + selector: "stark-session-logout-page", + templateUrl: "./session-logout-page.component.html", + encapsulation: ViewEncapsulation.None, + host: { + class: componentName + } +}) +export class StarkSessionLogoutPageComponent implements OnInit { + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_APP_CONFIG) public appConfig: StarkApplicationConfig + ) {} + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.logger.debug(componentName + ": component initialized"); + } + + /** + * Open baseUrl page (defined in the appConfig) in the current window. + */ + public logon(): void { + // reload app base URL (stark will redirect to the Login/Preloading page) + window.open(this.appConfig.baseUrl, "_self"); + } +} diff --git a/packages/stark-ui/src/modules/session-ui/routes.ts b/packages/stark-ui/src/modules/session-ui/routes.ts new file mode 100644 index 0000000000..67d9a3030a --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/routes.ts @@ -0,0 +1,138 @@ +import { Ng2StateDeclaration, RawParams } from "@uirouter/angular"; +import { Location } from "@angular/common"; +import { + STARK_ROUTING_SERVICE, + starkAppExitStateName, + starkAppInitStateName, + starkLoginStateName, + starkLoginStateUrl, + starkPreloadingStateName, + starkPreloadingStateUrl, + StarkRoutingService, + starkSessionExpiredStateName, + starkSessionExpiredStateUrl, + starkSessionLogoutStateName, + starkSessionLogoutStateUrl, + StarkStateConfigWithParams +} from "@nationalbankbelgium/stark-core"; +import { + StarkLoginPageComponent, + StarkPreloadingPageComponent, + StarkSessionExpiredPageComponent, + StarkSessionLogoutPageComponent +} from "./pages"; +import { of } from "rxjs"; + +/** + * Configuration of the route state of the application + */ +export function resolveTargetRoute( + $location: Location, + routingService: StarkRoutingService +): Promise { + // get the path of the current URL in the browser's navigation bar + const targetUrlPath: string = $location.path(); + const targetRoute: StarkStateConfigWithParams | undefined = routingService.getStateConfigByUrlPath(targetUrlPath); + + // skip any init/exit state + const initOrExitStateRegex: RegExp = new RegExp("(" + starkAppInitStateName + "|" + starkAppExitStateName + ")"); + + if (targetRoute) { + if ((targetRoute.state.$$state)().parent) { + if (!(targetRoute.state.$$state)().parent.name.match(initOrExitStateRegex)) { + return of(targetRoute).toPromise(); + } else { + return of(undefined).toPromise(); + } + } else { + return of(targetRoute).toPromise(); + } + } else { + return of(undefined).toPromise(); + } +} + +/** + * Check if targetRoute is defined and returns the name of the state OR undefined. + * @param targetRoute - returned value of resolveTargetRoute method + */ +export function resolveTargetState(targetRoute: StarkStateConfigWithParams): Promise { + return of(typeof targetRoute !== "undefined" ? targetRoute.state.name : undefined).toPromise(); +} + +/** + * Check if targetRoute is defined and returns the params of the state OR undefined. + * @param targetRoute - returned value of resolveTargetRoute method + */ +export function resolveTargetStateParams(targetRoute: StarkStateConfigWithParams): Promise { + return of(typeof targetRoute !== "undefined" ? targetRoute.paramValues : undefined).toPromise(); +} + +/** + * States defined by Session-Ui + */ +/* tslint:disable:no-duplicate-string */ +export const SESSION_MODULE_STATES: Ng2StateDeclaration[] = [ + { + name: starkAppInitStateName, // parent state for any initialization state (used to show/hide the main app component) + abstract: true, + + resolve: [ + { + token: "targetRoute", + deps: [Location, STARK_ROUTING_SERVICE], + resolveFn: resolveTargetRoute + }, + { + token: "targetState", + deps: ["targetRoute"], + resolveFn: resolveTargetState + }, + { + token: "targetStateParams", + deps: ["targetRoute"], + resolveFn: resolveTargetStateParams + } + ] + }, + { + name: starkAppExitStateName, // parent state for any exit state (used to show/hide the main app component) + abstract: true + }, + { + name: starkLoginStateName, // the parent is defined in the state's name (contains a dot) + url: starkLoginStateUrl, + views: { + "initOrExit@": { + component: StarkLoginPageComponent + } + } + }, + { + name: starkPreloadingStateName, // the parent is defined in the state's name (contains a dot) + url: starkPreloadingStateUrl, + views: { + "initOrExit@": { + component: StarkPreloadingPageComponent + } + } + }, + { + name: starkSessionExpiredStateName, // the parent is defined in the state's name (contains a dot) + url: starkSessionExpiredStateUrl, + views: { + "initOrExit@": { + component: StarkSessionExpiredPageComponent + } + } + }, + { + name: starkSessionLogoutStateName, // the parent is defined in the state's name (contains a dot) + url: starkSessionLogoutStateUrl, + views: { + "initOrExit@": { + component: StarkSessionLogoutPageComponent + } + } + } +]; diff --git a/packages/stark-ui/src/modules/session-ui/session-ui.module.ts b/packages/stark-ui/src/modules/session-ui/session-ui.module.ts new file mode 100644 index 0000000000..4b2cc29bfb --- /dev/null +++ b/packages/stark-ui/src/modules/session-ui/session-ui.module.ts @@ -0,0 +1,90 @@ +import { ApplicationInitStatus, Inject, ModuleWithProviders, NgModule, Optional, SkipSelf } from "@angular/core"; +import { + StarkLoginPageComponent, + StarkPreloadingPageComponent, + StarkSessionExpiredPageComponent, + StarkSessionLogoutPageComponent +} from "./pages"; +import { UIRouterModule } from "@uirouter/angular"; +import { TranslateModule } from "@ngx-translate/core"; +import { CommonModule, Location } from "@angular/common"; +import { SESSION_MODULE_STATES } from "./routes"; +import { from } from "rxjs"; +import { + STARK_ROUTING_SERVICE, + starkLoginStateName, + starkPreloadingStateName, + StarkRoutingService, +} from "@nationalbankbelgium/stark-core"; +import { StarkAppContainerComponent } from "./components"; +import { MatButtonModule } from "@angular/material/button"; + +@NgModule({ + declarations: [ + StarkLoginPageComponent, + StarkPreloadingPageComponent, + StarkSessionExpiredPageComponent, + StarkSessionLogoutPageComponent, + StarkAppContainerComponent + ], + exports: [ + StarkLoginPageComponent, + StarkPreloadingPageComponent, + StarkSessionExpiredPageComponent, + StarkSessionLogoutPageComponent, + StarkAppContainerComponent + ], + imports: [ + CommonModule, + UIRouterModule.forChild({ + states: SESSION_MODULE_STATES + }), + MatButtonModule, + TranslateModule + ], + providers: [ + Location + ], +}) +export class StarkSessionUiModule { + /** + * Instantiates the services only once since they should be singletons + * so the forRoot() should be called only by the AppModule + * @link https://angular.io/guide/singleton-services#forroot + * @returns a module with providers + */ + public static forRoot(): ModuleWithProviders { + return { + ngModule: StarkSessionUiModule + }; + } + + /** + * Prevents this module from being re-imported + * @link https://angular.io/guide/singleton-services#prevent-reimport-of-the-coremodule + * @param parentModule - The parent module + * @param routingService - The routing service of the application + * @param appInitStatus - A class that reflects the state of running {@link APP_INITIALIZER}s + */ + public constructor( + @Optional() + @SkipSelf() + parentModule: StarkSessionUiModule, + @Inject(STARK_ROUTING_SERVICE) routingService: StarkRoutingService, + appInitStatus: ApplicationInitStatus + ) { + if (parentModule) { + throw new Error("StarkSessionUiModule is already loaded. Import it in the AppModule only"); + } + + // this logic cannot be executed in an APP_INITIALIZER factory because the StarkRoutingService uses the StarkLoggingService + // which needs the "logging" state to be already defined in the Store (which NGRX defines internally via APP_INITIALIZER factories :p) + from(appInitStatus.donePromise).subscribe(() => { + if (ENV === "development") { + routingService.navigateTo(starkLoginStateName); + } else { + routingService.navigateTo(starkPreloadingStateName); + } + }); + } +} diff --git a/packages/stark-ui/tsconfig.spec.json b/packages/stark-ui/tsconfig.spec.json index 6435eadbb8..b31b4e4993 100644 --- a/packages/stark-ui/tsconfig.spec.json +++ b/packages/stark-ui/tsconfig.spec.json @@ -4,6 +4,7 @@ "module": "commonjs", "paths": { "@ngx-translate/*": ["../stark-core/node_modules/@ngx-translate/*"], + "@uirouter/*": ["../stark-core/node_modules/@uirouter/*"], "moment": ["../stark-core/node_modules/moment"], "@nationalbankbelgium/stark-core/testing": ["../../dist/packages/stark-core/testing"], "@nationalbankbelgium/stark-core": ["../../dist/packages/stark-core"], diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 25e535be75..adb42b5e1d 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -10,7 +10,9 @@ "paths": { "rxjs/*": ["../node_modules/rxjs/*"], "@angular/*": ["../node_modules/@angular/*"], + "@ngrx/*": ["./stark-core/node_modules/@ngrx/*"], "@ngx-translate/*": ["./stark-core/node_modules/@ngx-translate/*"], + "@uirouter/*": ["./stark-core/node_modules/@uirouter/*"], "moment": ["./stark-core/node_modules/moment"], "environments/environment": ["./stark-core/src/common/environment"], "@nationalbankbelgium/stark-*": ["./stark-*"] diff --git a/showcase/config/json-server/data.json b/showcase/config/json-server/data.json new file mode 100644 index 0000000000..c5af73a269 --- /dev/null +++ b/showcase/config/json-server/data.json @@ -0,0 +1,7 @@ +{ + "profiles": [ + { "uuid": "1", "username": "janedoe", "firstName": "Jane", "lastName": "Doe", "language": "en", "roles": ["admin"] }, + { "uuid": "2", "username": "johndoe", "firstName": "John", "lastName": "Doe", "language": "fr", "roles": ["manager"] }, + { "uuid": "3", "username": "chucknorris", "firstName": "Chuck", "lastName": "Norris", "language": "nl", "roles": ["developer"] } + ] +} diff --git a/showcase/src/app/app.component.html b/showcase/src/app/app.component.html index 5d527b12d5..66baa03ab5 100644 --- a/showcase/src/app/app.component.html +++ b/showcase/src/app/app.component.html @@ -1,159 +1,162 @@ - - - - - Home - - - Stark news - - - Action Bar - - - Breadcrumb - - - Button - - - Card - - - Collapsible - - - Colors - - - Date Picker - - - Date Range Picker - - - Dropdown - - - Example Viewer - - - Keyboard Directives - - - Language Selector - - - Logout - - - Pretty Print - - - Sidebar - - - Slider - - - Stark header - - - Table - - - Toast Message - - - Typography - - - - -
- Top content -
-
- Middle content -
-
- Bottom content -
-
- -
- Top content -
-
- Middle content -
-
- Bottom content -
-
-
-
-
-
-
-
- - -
-
-
- -
-
-
-
-

App Data

+ + + + + + Home + + + Stark news + + + Action Bar + + + Breadcrumb + + + Button + + + Card + + + Collapsible + + + Colors + + + Date Picker + + + Date Range Picker + + + Dropdown + + + Example Viewer + + + Keyboard Directives + + + Language Selector + + + Logout + + + Pretty Print + + + Sidebar + + + Slider + + + Stark header + + + Table + + + Toast Message + + + Typography + + + + +
+ Top content +
+
+ Middle content +
+
+ Bottom content +
+
+ +
+ Top content +
+
+ Middle content +
+
+ Bottom content +
+
+
+
+
+
+
+
+ +
- -
-
- - - +
+
-
- - - +
+
+
+

App Data

+
+ + +
+
+ + + +
+
+ + + +
-
-
-

Stark

+
+

Stark

+
+
+
+
+ +
-
-
-
- -
-
-
+ + diff --git a/showcase/src/app/app.component.ts b/showcase/src/app/app.component.ts index 8a03a22006..5fe2a8f64a 100644 --- a/showcase/src/app/app.component.ts +++ b/showcase/src/app/app.component.ts @@ -16,16 +16,12 @@ import { AppState } from "./app.service"; templateUrl: "./app.component.html" }) export class AppComponent implements OnInit { - public appState: AppState; - public constructor( - appState: AppState, + public appState: AppState, @Inject(STARK_APP_SIDEBAR_SERVICE) public sidebarService: StarkAppSidebarService, @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, @Inject(STARK_ROUTING_SERVICE) public routingService: StarkRoutingService - ) { - this.appState = appState; - } + ) {} public ngOnInit(): void { this.logger.debug("Initial App State", this.appState.state); diff --git a/showcase/src/app/app.module.ts b/showcase/src/app/app.module.ts index b170a356d5..58d7a3b0bf 100644 --- a/showcase/src/app/app.module.ts +++ b/showcase/src/app/app.module.ts @@ -1,7 +1,7 @@ -import { Inject, NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from "@angular/core"; +import { APP_INITIALIZER, Inject, NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from "@angular/core"; import { BrowserModule, DomSanitizer } from "@angular/platform-browser"; import { FormsModule } from "@angular/forms"; -import { UIRouterModule } from "@uirouter/angular"; +import { UIRouter, UIRouterModule } from "@uirouter/angular"; import { NgIdleModule } from "@ng-idle/core"; import { NgIdleKeepaliveModule } from "@ng-idle/keepalive"; import { ActionReducer, ActionReducerMap, MetaReducer, StoreModule } from "@ngrx/store"; @@ -10,7 +10,7 @@ import { EffectsModule } from "@ngrx/effects"; import { storeFreeze } from "ngrx-store-freeze"; import { storeLogger } from "ngrx-store-logger"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { MatIconRegistry, MatIconModule } from "@angular/material/icon"; +import { MatIconModule, MatIconRegistry } from "@angular/material/icon"; import { MatButtonModule } from "@angular/material/button"; import { MatButtonToggleModule } from "@angular/material/button-toggle"; import { MatCardModule } from "@angular/material/card"; @@ -20,6 +20,7 @@ import { MatSidenavModule } from "@angular/material/sidenav"; import { MatTooltipModule } from "@angular/material/tooltip"; import { SharedModule } from "./shared/shared.module"; import { DateAdapter } from "@angular/material/core"; +import { filter } from "rxjs/operators"; import { STARK_APP_CONFIG, @@ -32,15 +33,16 @@ import { StarkApplicationMetadata, StarkApplicationMetadataImpl, StarkHttpModule, - StarkLoggingModule, StarkLoggingActionTypes, + StarkLoggingModule, StarkMockData, StarkRoutingModule, StarkSessionModule, StarkSessionService, StarkSettingsModule, StarkSettingsService, - StarkUser + StarkUser, + StarkUserModule } from "@nationalbankbelgium/stark-core"; import { @@ -48,12 +50,13 @@ import { StarkAppLogoutModule, StarkAppSidebarModule, StarkBreadcrumbModule, + StarkDatePickerModule, StarkLanguageSelectorModule, + StarkSessionUiModule, StarkSvgViewBoxModule, - StarkDatePickerModule, StarkToastNotificationModule } from "@nationalbankbelgium/stark-ui"; -import { routerConfigFn } from "./router.config"; +import { logRegisteredStates, routerConfigFn } from "./router.config"; import { registerMaterialIconSet } from "./material-icons.config"; import { Deserialize } from "cerialize"; /* @@ -93,10 +96,10 @@ export function starkAppConfigFactory(): StarkApplicationConfig { const applicationConfig: StarkApplicationConfig = Deserialize(config, StarkApplicationConfigImpl); - applicationConfig.rootStateUrl = "home"; + applicationConfig.rootStateUrl = "/"; applicationConfig.rootStateName = ""; applicationConfig.homeStateName = "home"; - applicationConfig.errorStateName = ""; + applicationConfig.errorStateName = "otherwise"; applicationConfig.angularDebugInfoEnabled = true; //DEVELOPMENT; applicationConfig.debugLoggingEnabled = true; //DEVELOPMENT; applicationConfig.routerLoggingEnabled = true; //DEVELOPMENT; @@ -113,10 +116,15 @@ export function starkAppMetadataFactory(): StarkApplicationMetadata { // TODO: where to put this factory function? export function starkMockDataFactory(): StarkMockData { - return { - whatever: "dummy prop", - profiles: [] - }; + if (ENV === "development") { + return require("../../config/json-server/data.json"); + } else { + return {}; + } +} + +export function initRouterLog(router: UIRouter): Function { + return () => logRegisteredStates(router.stateService.get()); } // Application Redux State @@ -173,7 +181,7 @@ export const metaReducers: MetaReducer[] = ENV !== "production" ? [logger UIRouterModule.forRoot({ states: APP_STATES, useHash: !Boolean(history.pushState), - otherwise: { state: "otherwise" }, + otherwise: "otherwise", config: routerConfigFn }), TranslateModule.forRoot(), @@ -184,6 +192,7 @@ export const metaReducers: MetaReducer[] = ENV !== "production" ? [logger StarkSessionModule.forRoot(), StarkSettingsModule.forRoot(), StarkRoutingModule.forRoot(), + StarkUserModule.forRoot(), SharedModule, DemoModule, NewsModule, @@ -198,7 +207,8 @@ export const metaReducers: MetaReducer[] = ENV !== "production" ? [logger delay: 5000, position: "top right", actionClasses: [] - }) + }), + StarkSessionUiModule.forRoot() ], /** * Expose our Services and Providers into Angular's dependency injection. @@ -209,7 +219,8 @@ export const metaReducers: MetaReducer[] = ENV !== "production" ? [logger { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }, // needed for ui-router { provide: STARK_APP_CONFIG, useFactory: starkAppConfigFactory }, { provide: STARK_APP_METADATA, useFactory: starkAppMetadataFactory }, - { provide: STARK_MOCK_DATA, useFactory: starkMockDataFactory } + { provide: STARK_MOCK_DATA, useFactory: starkMockDataFactory }, + { provide: APP_INITIALIZER, useFactory: initRouterLog, multi: true, deps: [UIRouter] } ] }) export class AppModule { @@ -226,16 +237,12 @@ export class AppModule { this.settingsService.initializeSettings(); - const user: StarkUser = { - uuid: "abc123", - username: "John", - firstName: "Doe", - lastName: "Smith", - roles: ["dummy role"] - }; - - const devAuthenticationHeaders: Map = getAuthenticationHeaders(user); - this.sessionService.setDevAuthenticationHeaders(devAuthenticationHeaders); - this.sessionService.login(user); + sessionService + .getCurrentUser() + .pipe(filter((user?: StarkUser): user is StarkUser => typeof user !== "undefined")) + .subscribe((user: StarkUser) => { + const devAuthenticationHeaders: Map = getAuthenticationHeaders(user); + this.sessionService.setDevAuthenticationHeaders(devAuthenticationHeaders); + }); } } diff --git a/showcase/src/app/home/home.component.spec.ts b/showcase/src/app/home/home.component.spec.ts index bbc7f276c4..746fdf16a6 100644 --- a/showcase/src/app/home/home.component.spec.ts +++ b/showcase/src/app/home/home.component.spec.ts @@ -16,7 +16,6 @@ import { MockStarkHttpService, MockStarkLoggingService } from "@nationalbankbelg /** * Load the implementations that should be tested. */ -import { AppState } from "../app.service"; import { HomeComponent } from "./home.component"; import SpyObj = jasmine.SpyObj; @@ -47,7 +46,6 @@ describe(`Home`, () => { schemas: [NO_ERRORS_SCHEMA], imports: [StoreModule.forRoot({}), HttpClientTestingModule], providers: [ - AppState, { provide: STARK_APP_CONFIG, useValue: mockStarkAppConfig }, { provide: STARK_HTTP_SERVICE, useValue: MockStarkHttpService }, { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() } diff --git a/showcase/src/app/home/home.component.ts b/showcase/src/app/home/home.component.ts index 1655c078ae..dfcc538424 100644 --- a/showcase/src/app/home/home.component.ts +++ b/showcase/src/app/home/home.component.ts @@ -1,6 +1,5 @@ import { Component, Inject, OnInit } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkErrorImpl, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { AppState } from "../app.service"; @Component({ selector: "home", // @@ -8,7 +7,7 @@ import { AppState } from "../app.service"; templateUrl: "./home.component.html" }) export class HomeComponent implements OnInit { - public constructor(public appState: AppState, @Inject(STARK_LOGGING_SERVICE) public loggingService: StarkLoggingService) {} + public constructor(@Inject(STARK_LOGGING_SERVICE) public loggingService: StarkLoggingService) {} public ngOnInit(): void { this.loggingService.debug("hello from `Home` component"); diff --git a/showcase/src/app/router.config.ts b/showcase/src/app/router.config.ts index a69ad9e23c..1f54028251 100644 --- a/showcase/src/app/router.config.ts +++ b/showcase/src/app/router.config.ts @@ -1,6 +1,6 @@ import { UIRouter, Category, StateDeclaration } from "@uirouter/core"; -function logRegisteredStates(registeredStates: StateDeclaration[]): void { +export function logRegisteredStates(registeredStates: StateDeclaration[]): void { let message: string = "============= Registered Ui-Router states: ==============\n"; for (const state of registeredStates) { @@ -21,7 +21,6 @@ export function routerConfigFn(router: UIRouter): void { // if (ENV === "development") { // router.plugin(Visualizer); // Visualizer should be imported from "@uirouter/visualizer" // } - logRegisteredStates(router.stateService.get()); } export function routerChildConfigFn(router: UIRouter): void { diff --git a/showcase/src/index.html b/showcase/src/index.html index a08eae20bf..09027b0ba0 100644 --- a/showcase/src/index.html +++ b/showcase/src/index.html @@ -37,19 +37,6 @@ + Loading... <% if (htmlWebpackPlugin.options.dllFiles) { %> diff --git a/showcase/src/styles/_stark-styles.scss b/showcase/src/styles/_stark-styles.scss index 9bb9af9768..c7b2f5d207 100644 --- a/showcase/src/styles/_stark-styles.scss +++ b/showcase/src/styles/_stark-styles.scss @@ -24,3 +24,9 @@ IMPORTANT: Stark styles are provided as SCSS styles so they should be imported i @import "~@nationalbankbelgium/stark-ui/src/modules/toast-notification/components/toast-notification.component"; @import "~@nationalbankbelgium/stark-ui/src/modules/toast-notification/components/toast-notification-theme"; @import "~@nationalbankbelgium/stark-ui/src/modules/dropdown/components/dropdown.component"; + +/* Stark session-ui pages */ +@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/login/login-page.component"; +@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/preloading/preloading-page.component"; +@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/session-expired/session-expired-page.component"; +@import "~@nationalbankbelgium/stark-ui/src/modules/session-ui/pages/session-logout/session-logout-page.component";