diff --git a/chutney/ui/src/app/core/components/login/login.component.scss b/chutney/ui/src/app/core/components/login/login.component.scss
index bfdfd89f8..fbf0853e3 100644
--- a/chutney/ui/src/app/core/components/login/login.component.scss
+++ b/chutney/ui/src/app/core/components/login/login.component.scss
@@ -60,3 +60,10 @@
align-items: center;
}
}
+
+.ssoImage {
+ height: 4rem;
+ border-radius: 10px;
+ border: solid 1px #d3d7d7;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
+}
diff --git a/chutney/ui/src/app/core/components/login/login.component.ts b/chutney/ui/src/app/core/components/login/login.component.ts
index 65bee858f..ed6e25470 100644
--- a/chutney/ui/src/app/core/components/login/login.component.ts
+++ b/chutney/ui/src/app/core/components/login/login.component.ts
@@ -11,6 +11,7 @@ import { Subscription } from 'rxjs';
import { AlertService } from '@shared';
import { InfoService, LoginService } from '@core/services';
+import { SsoService } from '@core/services/sso.service';
@Component({
selector: 'chutney-login',
@@ -35,6 +36,7 @@ export class LoginComponent implements OnDestroy, OnInit {
private infoService: InfoService,
private route: ActivatedRoute,
private alertService: AlertService,
+ private ssoService: SsoService
) {
this.paramsSubscription = this.route.params.subscribe(params => {
this.action = params['action'];
@@ -78,4 +80,20 @@ export class LoginComponent implements OnDestroy, OnInit {
}
);
}
+
+ connectSso() {
+ this.ssoService.login()
+ }
+
+ getSsoProviderName() {
+ return this.ssoService.getSsoProviderName()
+ }
+
+ displaySsoButton() {
+ return this.ssoService.getEnableSso
+ }
+
+ getSsoProviderImageUrl() {
+ return this.ssoService.getSsoProviderImageUrl()
+ }
}
diff --git a/chutney/ui/src/app/core/core.module.ts b/chutney/ui/src/app/core/core.module.ts
index 8d6a394d3..b97c48315 100644
--- a/chutney/ui/src/app/core/core.module.ts
+++ b/chutney/ui/src/app/core/core.module.ts
@@ -9,12 +9,13 @@ import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { RouterModule } from '@angular/router';
import { FormsModule } from '@angular/forms';
-import { HttpClientModule } from '@angular/common/http';
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { LoginComponent } from './components/login/login.component';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { ParentComponent } from './components/parent/parent.component';
import { DROPDOWN_SETTINGS, DropdownSettings } from '@core/model/dropdown-settings';
+import { OAuth2ContentTypeInterceptor } from '@core/services/oauth2-content-type-interceptor.service';
@NgModule({
declarations: [
@@ -30,7 +31,8 @@ import { DROPDOWN_SETTINGS, DropdownSettings } from '@core/model/dropdown-settin
TranslateModule
],
providers: [
- {provide: DROPDOWN_SETTINGS, useClass: DropdownSettings}
+ {provide: DROPDOWN_SETTINGS, useClass: DropdownSettings},
+ {provide: HTTP_INTERCEPTORS, useClass: OAuth2ContentTypeInterceptor, multi: true }
]
})
diff --git a/chutney/ui/src/app/core/guards/auth.guard.ts b/chutney/ui/src/app/core/guards/auth.guard.ts
index 3b532dafc..ba22b18a5 100644
--- a/chutney/ui/src/app/core/guards/auth.guard.ts
+++ b/chutney/ui/src/app/core/guards/auth.guard.ts
@@ -7,31 +7,11 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
-import { TranslateService } from '@ngx-translate/core';
-
import { LoginService } from '@core/services';
-import { AlertService } from '@shared';
-import { Authorization } from '@model';
-export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
- const translateService = inject(TranslateService);
+export const authGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const loginService = inject(LoginService);
- const alertService = inject(AlertService);
-
const requestURL = state.url !== undefined ? state.url : '';
- const unauthorizedMessage = translateService.instant('login.unauthorized')
- if (!loginService.isAuthenticated()) {
- loginService.initLogin(requestURL);
- return false;
- }
-
- const authorizations: Array
= route.data['authorizations'] || [];
- if (loginService.hasAuthorization(authorizations)) {
- return true;
- } else {
- alertService.error(unauthorizedMessage, {timeOut: 0, extendedTimeOut: 0, closeButton: true});
- loginService.navigateAfterLogin();
- return false;
- }
+ return loginService.isAuthorized(requestURL, route)
}
diff --git a/chutney/ui/src/app/core/services/login.service.ts b/chutney/ui/src/app/core/services/login.service.ts
index 991b5c1a4..b737568a9 100644
--- a/chutney/ui/src/app/core/services/login.service.ts
+++ b/chutney/ui/src/app/core/services/login.service.ts
@@ -7,134 +7,189 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
-import { Router } from '@angular/router';
-import { BehaviorSubject, Observable } from 'rxjs';
-import { delay, tap } from 'rxjs/operators';
+import { ActivatedRouteSnapshot, Router } from '@angular/router';
+import { BehaviorSubject, firstValueFrom, Observable, timeout } from 'rxjs';
+import { catchError, delay, filter, first, tap } from 'rxjs/operators';
import { environment } from '@env/environment';
import { Authorization, User } from '@model';
import { contains, intersection, isNullOrBlankString } from '@shared/tools';
+import { SsoService } from '@core/services/sso.service';
+import { TranslateService } from '@ngx-translate/core';
+import { AlertService } from '@shared';
@Injectable({
- providedIn: 'root'
+ providedIn: 'root'
})
export class LoginService {
- private url = '/api/v1/user';
- private loginUrl = this.url + '/login';
- private NO_USER = new User('');
- private user$: BehaviorSubject = new BehaviorSubject(this.NO_USER);
-
- constructor(
- private http: HttpClient,
- private router: Router
- ) { }
-
- initLogin(url?: string) {
- this.currentUser(true).pipe(
- tap(user => this.setUser(user))
- ).subscribe(
- () => this.navigateAfterLogin(url),
- () => {
- const nextUrl = this.nullifyLoginUrl(url);
- const queryParams: Object = isNullOrBlankString(nextUrl) ? {} : { queryParams: { url: nextUrl } };
- this.router.navigate(['login'], queryParams);
+ private url = '/api/v1/user';
+ private loginUrl = this.url + '/login';
+ private NO_USER = new User('');
+ private user$: BehaviorSubject = new BehaviorSubject(this.NO_USER);
+
+ constructor(
+ private http: HttpClient,
+ private router: Router,
+ private ssoService: SsoService,
+ private translateService: TranslateService,
+ private alertService: AlertService
+ ) {
+ }
+
+ async isAuthorized(requestURL: string, route: ActivatedRouteSnapshot) {
+ const unauthorizedMessage = this.translateService.instant('login.unauthorized')
+ if (!this.isAuthenticated()) {
+ if (this.oauth2Token) {
+ await this.initLoginWithToken(requestURL)
+ } else {
+ await firstValueFrom(this.ssoService.tokenLoaded$.pipe(
+ timeout(1000),
+ filter(tokenLoaded => tokenLoaded === true),
+ first(),
+ catchError(async error => this.initLogin(requestURL))));
+ if (this.oauth2Token) {
+ await this.initLoginWithToken(requestURL)
+ } else {
+ await this.initLogin(requestURL);
+ return false;
+ }
+ }
+ }
+ const authorizations: Array = route.data['authorizations'] || [];
+ if (this.hasAuthorization(authorizations)) {
+ return true;
+ } else {
+ this.alertService.error(unauthorizedMessage, {timeOut: 0, extendedTimeOut: 0, closeButton: true});
+ this.navigateAfterLogin();
+ return false;
}
- );
- }
+ }
+
+ private async initLoginWithToken(requestURL: string) {
+ await firstValueFrom(this.initLoginObservable(requestURL, {
+ Authorization: 'Bearer ' + this.oauth2Token,
+ }));
+ }
+
+ async initLogin(url?: string, headers: HttpHeaders | {
+ [header: string]: string | string[];
+ } = {}) {
+ await firstValueFrom(this.initLoginObservable(url, headers))
+ }
+
+ initLoginObservable(url?: string, headers?: HttpHeaders | {
+ [header: string]: string | string[];
+ }) {
+ return this.currentUser(true, headers).pipe(
+ tap(user => this.setUser(user)),
+ tap(_ => this.navigateAfterLogin(url)),
+ catchError(error => {
+ const nextUrl = this.nullifyLoginUrl(url);
+ const queryParams: Object = isNullOrBlankString(nextUrl) ? {} : {queryParams: {url: nextUrl}};
+ this.router.navigate(['login'], queryParams);
+ this.alertService.error("Unauthorized, you've been disconnected", {timeOut: 0, extendedTimeOut: 0, closeButton: true});
+ return error
+ })
+ );
+ }
- login(username: string, password: string): Observable {
- if (isNullOrBlankString(username) && isNullOrBlankString(password)) {
- return this.currentUser().pipe(
- tap(user => this.setUser(user))
- );
+ get oauth2Token(): string {
+ return this.ssoService.accessToken
}
- const body = new URLSearchParams();
- body.set('username', username);
- body.set('password', password);
+ login(username: string, password: string): Observable {
+ if (isNullOrBlankString(username) && isNullOrBlankString(password)) {
+ return this.currentUser().pipe(
+ tap(user => this.setUser(user))
+ );
+ }
+
+ const body = new URLSearchParams();
+ body.set('username', username);
+ body.set('password', password);
- const options = {
- headers: new HttpHeaders()
+ const options = {
+ headers: new HttpHeaders()
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('no-intercept-error', '')
- };
+ };
- return this.http.post(environment.backend + this.loginUrl, body.toString(), options)
- .pipe(
- tap(user => this.setUser(user))
- );
- }
+ return this.http.post(environment.backend + this.loginUrl, body.toString(), options)
+ .pipe(
+ tap(user => this.setUser(user))
+ );
+ }
- navigateAfterLogin(url?: string) {
- const nextUrl = this.nullifyLoginUrl(url);
- if (this.isAuthenticated()) {
- const user: User = this.user$.getValue();
- this.router.navigateByUrl(nextUrl ? nextUrl : this.defaultForwardUrl(user));
- } else {
- this.router.navigateByUrl('/login');
- }
- }
-
- logout() {
- this.http.post(environment.backend + this.url + '/logout', null).pipe(
- tap(() => this.setUser(this.NO_USER)),
- delay(500)
- ).subscribe(
- () => {
+ navigateAfterLogin(url?: string) {
+ const nextUrl = this.nullifyLoginUrl(url);
+ if (this.isAuthenticated()) {
+ const user: User = this.user$.getValue();
+ this.router.navigateByUrl(nextUrl ? nextUrl : this.defaultForwardUrl(user));
+ } else {
this.router.navigateByUrl('/login');
}
- );
- }
-
- getUser(): Observable {
- return this.user$;
- }
-
- isAuthenticated(): boolean {
- const user: User = this.user$.getValue();
- return this.NO_USER !== user;
- }
-
- hasAuthorization(authorization: Array | Authorization = [], u: User = null): boolean {
- const user: User = u || this.user$.getValue();
- const auth = [].concat(authorization);
- if (user != this.NO_USER) {
- return auth.length == 0 || intersection(user.authorizations, auth).length > 0;
- }
- return false;
- }
-
- isLoginUrl(url: string): boolean {
- return url.includes(this.loginUrl);
- }
-
- private setUser(user: User) {
- this.user$.next(user);
- }
-
- private currentUser(skipInterceptor: boolean = false): Observable {
- const options = {
- headers: { 'no-intercept-error': ''}
- };
- return this.http.get(environment.backend + this.url, skipInterceptor ? options : {});
- }
-
- private defaultForwardUrl(user: User): string {
- const authorizations = user.authorizations;
- if (authorizations) {
- if (contains(authorizations, Authorization.SCENARIO_READ)) return '/scenario';
- if (contains(authorizations, Authorization.CAMPAIGN_READ)) return '/campaign';
- if (contains(authorizations, Authorization.ENVIRONMENT_ACCESS)) return '/targets';
- if (contains(authorizations, Authorization.GLOBAL_VAR_READ)) return '/variable';
- if (contains(authorizations, Authorization.DATASET_READ)) return '/dataset';
- if (contains(authorizations, Authorization.ADMIN_ACCESS)) return '/';
- }
-
- return '/login';
- }
-
- private nullifyLoginUrl(url: string): string {
- return url && url !== '/login' ? url : null;
- }
+ }
+
+ logout() {
+ this.http.post(environment.backend + this.url + '/logout', null).pipe(
+ tap(() => this.setUser(this.NO_USER)),
+ tap(() => this.ssoService.logout()),
+ delay(500)
+ ).subscribe(
+ () => {
+ this.router.navigateByUrl('/login');
+ }
+ );
+ }
+
+ getUser(): Observable {
+ return this.user$;
+ }
+
+ isAuthenticated(): boolean {
+ const user: User = this.user$.getValue();
+ return this.NO_USER !== user;
+ }
+
+ hasAuthorization(authorization: Array | Authorization = [], u: User = null): boolean {
+ const user: User = u || this.user$.getValue();
+ const auth = [].concat(authorization);
+ if (user != this.NO_USER) {
+ return auth.length == 0 || intersection(user.authorizations, auth).length > 0;
+ }
+ return false;
+ }
+
+ currentUser(skipInterceptor: boolean = false, headers: HttpHeaders | {
+ [header: string]: string | string[];
+ } = {}): Observable {
+ const headersInterceptor = skipInterceptor ? {'no-intercept-error': ''} : {}
+ const options = {
+ headers: {...headersInterceptor, ...headers}
+ };
+ return this.http.get(environment.backend + this.url, options);
+ }
+
+ private setUser(user: User) {
+ this.user$.next(user);
+ }
+
+ private defaultForwardUrl(user: User): string {
+ const authorizations = user.authorizations;
+ if (authorizations) {
+ if (contains(authorizations, Authorization.SCENARIO_READ)) return '/scenario';
+ if (contains(authorizations, Authorization.CAMPAIGN_READ)) return '/campaign';
+ if (contains(authorizations, Authorization.ENVIRONMENT_ACCESS)) return '/targets';
+ if (contains(authorizations, Authorization.GLOBAL_VAR_READ)) return '/variable';
+ if (contains(authorizations, Authorization.DATASET_READ)) return '/dataset';
+ if (contains(authorizations, Authorization.ADMIN_ACCESS)) return '/';
+ }
+
+ return '/login';
+ }
+
+ private nullifyLoginUrl(url: string): string {
+ return url && url !== '/login' ? url : null;
+ }
}
diff --git a/chutney/ui/src/app/core/services/oauth2-content-type-interceptor.service.ts b/chutney/ui/src/app/core/services/oauth2-content-type-interceptor.service.ts
new file mode 100644
index 000000000..e959aeabf
--- /dev/null
+++ b/chutney/ui/src/app/core/services/oauth2-content-type-interceptor.service.ts
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: 2017-2024 Enedis
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { Injectable } from '@angular/core';
+import { SsoService } from '@core/services/sso.service';
+
+@Injectable()
+export class OAuth2ContentTypeInterceptor implements HttpInterceptor {
+
+ constructor(private ssoService: SsoService) {}
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ const isTokenEndpoint = this.ssoService.headers && req.url.startsWith(this.ssoService.tokenEndpoint);
+ if (isTokenEndpoint) {
+ const modifiedReq = req.clone({
+ setHeaders: this.ssoService.headers
+ });
+ return next.handle(modifiedReq);
+ }
+ return next.handle(req);
+ }
+}
diff --git a/chutney/ui/src/app/core/services/sso.service.ts b/chutney/ui/src/app/core/services/sso.service.ts
new file mode 100644
index 000000000..8bcbaef12
--- /dev/null
+++ b/chutney/ui/src/app/core/services/sso.service.ts
@@ -0,0 +1,119 @@
+/*
+ * SPDX-FileCopyrightText: 2017-2024 Enedis
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+import { HttpClient } from '@angular/common/http';
+import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
+import { environment } from '@env/environment';
+import { BehaviorSubject, map, tap } from 'rxjs';
+import { Injectable } from '@angular/core';
+import { filter, switchMap } from 'rxjs/operators';
+
+interface SsoAuthConfig {
+ issuer: string,
+ clientId: string,
+ clientSecret: string,
+ responseType: string,
+ scope: string,
+ redirectBaseUrl: string,
+ ssoProviderName: string,
+ ssoProviderImageUrl: string,
+ headers: { [name: string]: string | string[]; },
+ additionalQueryParams: { [name: string]: string | string[]; }
+ oidc: boolean
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SsoService {
+
+ private resourceUrl = '/api/v1/sso/config';
+
+ private ssoConfig: SsoAuthConfig
+
+ private tokenLoadedSubject = new BehaviorSubject(false);
+ public tokenLoaded$ = this.tokenLoadedSubject.asObservable();
+ private enableSso = false
+
+
+ constructor(private oauthService: OAuthService, private http: HttpClient) {
+ this.oauthService.events
+ .pipe(filter(e => e.type === 'token_received'))
+ .subscribe(() => {
+ this.tokenLoadedSubject.next(true);
+ });
+ }
+
+ fetchSsoConfig(): void {
+ this.http.get(environment.backend + this.resourceUrl).pipe(
+ map(ssoConfig => {
+ this.ssoConfig = ssoConfig
+ return {
+ issuer: ssoConfig.issuer,
+ clientId: ssoConfig.clientId,
+ responseType: ssoConfig.responseType,
+ scope: ssoConfig.scope,
+ redirectUri: ssoConfig.redirectBaseUrl + '/',
+ dummyClientSecret: ssoConfig.clientSecret,
+ oidc: ssoConfig.oidc,
+ useHttpBasicAuth: true,
+ postLogoutRedirectUri: ssoConfig.redirectBaseUrl + '/',
+ sessionChecksEnabled: true,
+ logoutUrl: ssoConfig.redirectBaseUrl + '/',
+ customQueryParams: ssoConfig.additionalQueryParams,
+ useIdTokenHintForSilentRefresh: true,
+ redirectUriAsPostLogoutRedirectUriFallback: true,
+ } as AuthConfig
+ }),
+ tap(ssoConfig => this.oauthService.configure(ssoConfig)),
+ switchMap(() => this.oauthService.loadDiscoveryDocumentAndTryLogin()),
+ tap(res => this.enableSso = res),
+ filter(() => this.oauthService.hasValidAccessToken() ),
+ tap(() => this.tokenLoadedSubject.next(true) )
+ ).subscribe()
+ }
+
+ login() {
+ this.oauthService.initCodeFlow();
+ }
+
+ logout() {
+ if (this.idToken) {
+ this.oauthService.logOut({
+ 'id_token_hint': this.idToken
+ });
+ }
+ }
+
+ getSsoProviderName() {
+ return this.ssoConfig?.ssoProviderName
+ }
+
+ getSsoProviderImageUrl() {
+ return this.ssoConfig?.ssoProviderImageUrl
+ }
+
+ get accessToken(): string {
+ return this.oauthService.getAccessToken();
+ }
+
+ get idToken(): string {
+ return this.oauthService.getIdToken();
+ }
+
+ get tokenEndpoint(): string {
+ return this.oauthService.tokenEndpoint;
+ }
+
+ get headers() {
+ return this.ssoConfig?.headers
+ }
+
+ get getEnableSso() {
+ return this.enableSso
+ }
+}
diff --git a/chutney/ui/src/app/modules/dataset/components/dataset-list/dataset-list.component.spec.ts b/chutney/ui/src/app/modules/dataset/components/dataset-list/dataset-list.component.spec.ts
index 6c8962e84..8ce9bf9e7 100644
--- a/chutney/ui/src/app/modules/dataset/components/dataset-list/dataset-list.component.spec.ts
+++ b/chutney/ui/src/app/modules/dataset/components/dataset-list/dataset-list.component.spec.ts
@@ -17,15 +17,21 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DatasetListComponent } from './dataset-list.component';
import { DataSetService } from '@core/services';
-import { of } from 'rxjs';
+import { of, Subject } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgMultiSelectDropDownModule } from 'ng-multiselect-dropdown';
import { DROPDOWN_SETTINGS, DropdownSettings } from '@core/model/dropdown-settings';
import { RouterModule } from '@angular/router';
+import { OAuthService } from "angular-oauth2-oidc";
+import { AlertService } from '@shared';
describe('DatasetListComponent', () => {
+
+ const eventsSubject = new Subject();
const dataSetService = jasmine.createSpyObj('DataSetService', ['findAll']);
+ const oAuthService = jasmine.createSpyObj('OAuthService', ['loadDiscoveryDocumentAndTryLogin', 'configure', 'initCodeFlow', 'logOut', 'getAccessToken'], {events: eventsSubject.asObservable()});
+ const alertService = jasmine.createSpyObj('AlertService', ['error']);
dataSetService.findAll.and.returnValue(of([]));
beforeEach(waitForAsync(() => {
TestBed.resetTestingModule();
@@ -48,6 +54,8 @@ describe('DatasetListComponent', () => {
],
providers: [
{ provide: DataSetService, useValue: dataSetService },
+ { provide: AlertService, useValue: alertService },
+ { provide: OAuthService, useValue: oAuthService },
{provide: DROPDOWN_SETTINGS, useClass: DropdownSettings}
]
}).compileComponents();
diff --git a/chutney/ui/src/app/modules/scenarios/components/search-list/scenarios.component.spec.ts b/chutney/ui/src/app/modules/scenarios/components/search-list/scenarios.component.spec.ts
index 72d80b9df..4a7d048f5 100644
--- a/chutney/ui/src/app/modules/scenarios/components/search-list/scenarios.component.spec.ts
+++ b/chutney/ui/src/app/modules/scenarios/components/search-list/scenarios.component.spec.ts
@@ -15,7 +15,7 @@ import { MoleculesModule } from '../../../../molecules/molecules.module';
import { MomentModule } from 'ngx-moment';
import { NgbModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap';
-import { EMPTY, of } from 'rxjs';
+import { EMPTY, of, Subject } from 'rxjs';
import { ScenarioIndex } from '@core/model';
import { ScenarioService } from '@core/services';
@@ -26,6 +26,8 @@ import { ActivatedRouteStub } from '../../../../testing/activated-route-stub';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NgMultiSelectDropDownModule } from 'ng-multiselect-dropdown';
import { DROPDOWN_SETTINGS, DropdownSettings } from '@core/model/dropdown-settings';
+import { OAuthService } from "angular-oauth2-oidc";
+import { AlertService } from '@shared';
function getScenarios(html: HTMLElement) {
return html.querySelectorAll('.scenario-title');
@@ -41,7 +43,10 @@ describe('ScenariosComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.resetTestingModule();
+ const eventsSubject = new Subject();
const scenarioService = jasmine.createSpyObj('ScenarioService', ['findScenarios', 'search']);
+ const oAuthService = jasmine.createSpyObj('OAuthService', ['loadDiscoveryDocumentAndTryLogin', 'configure', 'initCodeFlow', 'logOut', 'getAccessToken'], {events: eventsSubject.asObservable()});
+ const alertService = jasmine.createSpyObj('AlertService', ['error']);
const jiraPluginService = jasmine.createSpyObj('JiraPluginService', ['findScenarios', 'findCampaigns']);
const jiraPluginConfigurationService = jasmine.createSpyObj('JiraPluginConfigurationService', ['getUrl']);
const mockScenarioIndex = [new ScenarioIndex('1', 'title1', 'description', 'source', new Date(), new Date(), 1, 'guest', [], []),
@@ -70,6 +75,8 @@ describe('ScenariosComponent', () => {
providers: [
NgbPopoverConfig,
{provide: ScenarioService, useValue: scenarioService},
+ {provide: OAuthService, useValue: oAuthService},
+ {provide: AlertService, useValue: alertService},
{provide: JiraPluginService, useValue: jiraPluginService},
{provide: JiraPluginConfigurationService, useValue: jiraPluginConfigurationService},
{provide: ActivatedRoute, useValue: activatedRouteStub},
diff --git a/chutney/ui/src/app/shared/error-interceptor.service.ts b/chutney/ui/src/app/shared/error-interceptor.service.ts
index f096830bd..1479ba985 100644
--- a/chutney/ui/src/app/shared/error-interceptor.service.ts
+++ b/chutney/ui/src/app/shared/error-interceptor.service.ts
@@ -9,8 +9,8 @@ import { Injectable } from '@angular/core';
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
-import { EMPTY, Observable, throwError } from 'rxjs';
-import { catchError } from 'rxjs/operators';
+import { EMPTY, from, Observable, throwError } from 'rxjs';
+import { catchError, switchMap } from 'rxjs/operators';
import { LoginService } from '@core/services';
import { AlertService } from '@shared';
@@ -45,11 +45,13 @@ export class ErrorInterceptor implements HttpInterceptor {
if (this.loginService.isAuthenticated()) {
this.loginService.logout();
this.alertService.error(this.sessionExpiredMessage, { timeOut: 0, extendedTimeOut: 0, closeButton: true });
+ return EMPTY
} else {
const requestURL = this.router.url !== undefined ? this.router.url : '';
- this.loginService.initLogin(requestURL);
+ return from(this.loginService.initLogin(requestURL)).pipe(
+ switchMap(() => EMPTY)
+ );
}
- return EMPTY;
}
}
return throwError(err);