diff --git a/apps/dsp-app/src/app/app.module.ts b/apps/dsp-app/src/app/app.module.ts index bdebcf3344..61f6dc5972 100644 --- a/apps/dsp-app/src/app/app.module.ts +++ b/apps/dsp-app/src/app/app.module.ts @@ -56,7 +56,6 @@ import { DisableContextMenuDirective } from './main/directive/disable-context-me import { InvalidControlScrollDirective } from './main/directive/invalid-control-scroll.directive'; import { FooterComponent } from './main/footer/footer.component'; import { GridComponent } from './main/grid/grid.component'; -import { AuthGuardComponent } from './main/guard/auth-guard.component'; import { HeaderComponent } from './main/header/header.component'; import { HelpComponent } from './main/help/help.component'; import { AuthInterceptor } from './main/http-interceptors/auth-interceptor'; @@ -196,7 +195,6 @@ export function httpLoaderFactory(httpClient: HttpClient) { AppComponent, ArchiveComponent, AudioComponent, - AuthGuardComponent, AvTimelineComponent, DescriptionComponent, BooleanValueComponent, diff --git a/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts b/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts index d260905f43..6315d39db2 100644 --- a/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts +++ b/apps/dsp-app/src/app/main/action/login-form/login-form.component.ts @@ -1,9 +1,10 @@ -import { Location } from '@angular/common'; +import { DOCUMENT, Location } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, + Inject, Input, OnDestroy, OnInit, @@ -12,9 +13,10 @@ import { import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { AuthError, AuthService } from '@dasch-swiss/vre/shared/app-session'; -import { UserStateModel } from '@dasch-swiss/vre/shared/app-state'; -import { Subject } from 'rxjs'; -import { map, take, takeLast } from 'rxjs/operators'; +import { LoadUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { Actions, Store, ofActionSuccessful } from '@ngxs/store'; +import { Observable, Subject, combineLatest } from 'rxjs'; +import { take, takeLast } from 'rxjs/operators'; import { ComponentCommunicationEventService, EmitEvent, @@ -28,6 +30,9 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class LoginFormComponent implements OnInit, OnDestroy { + get isLoggedIn$(): Observable { + return this._authService.isSessionValid$(); + } /** * set whether or not you want icons to display in the input fields * @@ -99,7 +104,10 @@ export class LoginFormComponent implements OnInit, OnDestroy { private _authService: AuthService, private route: ActivatedRoute, private location: Location, - private cd: ChangeDetectorRef + private cd: ChangeDetectorRef, + private _actions$: Actions, + private _store: Store, + @Inject(DOCUMENT) private document: Document ) {} /** @@ -109,7 +117,7 @@ export class LoginFormComponent implements OnInit, OnDestroy { */ ngOnInit() { this.buildLoginForm(); - this.returnUrl = this.getReturnUrl() || '/'; + this.returnUrl = this.getReturnUrl(); } ngOnDestroy(): void { @@ -142,16 +150,19 @@ export class LoginFormComponent implements OnInit, OnDestroy { next: loginResult => { if (loginResult) { this._componentCommsService.emit(new EmitEvent(Events.loginSuccess, true)); - - return this._authService - .loadUser(identifier) + this._store.dispatch(new LoadUserAction(identifier)); + return combineLatest([ + this._actions$.pipe(ofActionSuccessful(LoadUserAction)), + this._store.select(UserSelectors.user), + ]) .pipe(take(1)) - .pipe(map((result: any) => result.user)) - .subscribe((user: UserStateModel) => { + .subscribe(([action, user]) => { this.loading = false; - this._authService.loginSuccessfulEvent.emit(user.user); + this._authService.loginSuccessfulEvent.emit(user); this.cd.markForCheck(); - this.router.navigate([this.returnUrl]); + if (this.returnUrl) { + this.router.navigate([this.returnUrl]); + } }); } }, diff --git a/apps/dsp-app/src/app/main/guard/auth-guard.component.ts b/apps/dsp-app/src/app/main/guard/auth-guard.component.ts deleted file mode 100644 index b55e63c896..0000000000 --- a/apps/dsp-app/src/app/main/guard/auth-guard.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { Router } from '@angular/router'; -import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; - -// empty component used as a redirect when the user logs in -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', -}) -export class AuthGuardComponent { - constructor(private router: Router) { - this.router.navigate([RouteConstants.home], { replaceUrl: true }); - } -} diff --git a/apps/dsp-app/src/app/main/guard/auth.guard.ts b/apps/dsp-app/src/app/main/guard/auth.guard.ts index 95f68da044..ae86b1adfa 100644 --- a/apps/dsp-app/src/app/main/guard/auth.guard.ts +++ b/apps/dsp-app/src/app/main/guard/auth.guard.ts @@ -2,9 +2,10 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; import { ReadUser } from '@dasch-swiss/dsp-js'; +import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { AuthService } from '@dasch-swiss/vre/shared/app-session'; -import { CurrentPageSelectors, SetUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; -import { Actions, ofActionCompleted, Select, Store } from '@ngxs/store'; +import { SetUserAction, UserSelectors } from '@dasch-swiss/vre/shared/app-state'; +import { Actions, Select, Store, ofActionCompleted } from '@ngxs/store'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; @@ -32,7 +33,7 @@ export class AuthGuard implements CanActivate { return this.store.dispatch(new SetUserAction(user)); } }), - switchMap(() => this._authService.isLoggedIn$), + switchMap(() => this._authService.isSessionValid$(true)), map(isLoggedIn => { if (isLoggedIn) { return true; @@ -45,8 +46,6 @@ export class AuthGuard implements CanActivate { } private _goToHomePage() { - this.document.defaultView.location.href = - `${this.document.defaultView.location.href}?` + - `returnLink=${this.store.selectSnapshot(CurrentPageSelectors.loginReturnLink)}`; + this.document.defaultView.location.href = `${RouteConstants.home}?returnLink=${this.document.defaultView.location.href}`; } } diff --git a/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts b/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts index 5171de6cff..e3fe0a6afe 100644 --- a/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts +++ b/apps/dsp-app/src/app/main/guard/ontology-class-instance.guard.ts @@ -13,8 +13,6 @@ import { map } from 'rxjs/operators'; providedIn: 'root', }) export class OntologyClassInstanceGuard implements CanActivate { - isLoggedIn$: Observable = this.authService.isLoggedIn$; - @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; @Select(UserSelectors.userProjects) userProjects$: Observable; @@ -26,12 +24,12 @@ export class OntologyClassInstanceGuard implements CanActivate { canActivate(activatedRoute: ActivatedRouteSnapshot): Observable { const instanceId = activatedRoute.params[RouteConstants.instanceParameter]; - return combineLatest([this.isLoggedIn$, this.isSysAdmin$, this.userProjects$]).pipe( - map(([isLoggedIn, isSysAdmin, userProjects]) => { + return combineLatest([this.authService.isSessionValid$(), this.isSysAdmin$, this.userProjects$]).pipe( + map(([isSessionValid, isSysAdmin, userProjects]) => { const projectUuid = activatedRoute.parent.params[RouteConstants.uuidParameter]; const isAddInstance = instanceId === RouteConstants.addClassInstance; - if (!isLoggedIn && isAddInstance) { + if (!isSessionValid && isAddInstance) { this.router.navigateByUrl(`/${RouteConstants.project}/${projectUuid}`); return false; } diff --git a/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts b/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts index b6d50c31d1..3f4fa7a3be 100644 --- a/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts +++ b/apps/dsp-app/src/app/user/user-menu/user-menu.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatMenuTrigger } from '@angular/material/menu'; +import { Router } from '@angular/router'; import { User } from '@dasch-swiss/dsp-js'; import { RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { AuthService } from '@dasch-swiss/vre/shared/app-session'; @@ -22,13 +23,19 @@ export class UserMenuComponent implements OnInit, OnDestroy { private ngUnsubscribe: Subject = new Subject(); - isLoggedIn$: Observable = this._authService.isLoggedIn$; + get isLoggedIn$(): Observable { + return this._authService.isSessionValid$(); + } + @Select(UserSelectors.user) user$: Observable; @Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable; systemLink = RouteConstants.system; - constructor(private _authService: AuthService) {} + constructor( + private _authService: AuthService, + private _router: Router + ) {} ngOnInit() { this.navigation = [ diff --git a/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts b/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts index c2ee770fd2..d4d74498be 100644 --- a/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts +++ b/libs/vre/shared/app-analytics/src/lib/datadog-rum/datadog-rum.service.ts @@ -1,4 +1,5 @@ import { inject, Injectable } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { BuildTag, BuildTagToken, @@ -47,16 +48,19 @@ export class DatadogRumService { }); // depending on the session state, activate or deactivate the user - this.authService.isLoggedIn$.subscribe((isLoggedIn: boolean) => { - if (isLoggedIn) { - if (this.authService.tokenUser) { - const id: string = uuidv5(this.authService.tokenUser, uuidv5.URL); - this.setActiveUser(id); - } else { - this.removeActiveUser(); + this.authService + .isSessionValid$() + .pipe(takeUntilDestroyed()) + .subscribe((isSessionValid: boolean) => { + if (isSessionValid) { + if (this.authService.tokenUser) { + const id: string = uuidv5(this.authService.tokenUser, uuidv5.URL); + this.setActiveUser(id); + } else { + this.removeActiveUser(); + } } - } - }); + }); } }); } diff --git a/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts b/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts index 02a7ee9491..599c19c0a1 100644 --- a/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts +++ b/libs/vre/shared/app-analytics/src/lib/pendo-analytics/pendo-analytics.service.ts @@ -12,13 +12,16 @@ export class PendoAnalyticsService { private environment: string = this.config.environment; constructor() { - this.authService.isLoggedIn$.pipe(takeUntilDestroyed()).subscribe((isLoggedIn: boolean) => { - if (isLoggedIn) { - this.setActiveUser(this.authService.tokenUser); - } else { - this.removeActiveUser(); - } - }); + this.authService + .isSessionValid$() + .pipe(takeUntilDestroyed()) + .subscribe((isSessionValid: boolean) => { + if (isSessionValid) { + this.setActiveUser(this.authService.tokenUser); + } else { + this.removeActiveUser(); + } + }); } /** diff --git a/libs/vre/shared/app-session/src/lib/auth.service.ts b/libs/vre/shared/app-session/src/lib/auth.service.ts index 4ae0ece8e4..50b9cefc95 100644 --- a/libs/vre/shared/app-session/src/lib/auth.service.ts +++ b/libs/vre/shared/app-session/src/lib/auth.service.ts @@ -1,30 +1,24 @@ import { EventEmitter, Injectable, Output, inject } from '@angular/core'; import { Router } from '@angular/router'; import { ApiResponseData, ApiResponseError, CredentialsResponse, LoginResponse, User } from '@dasch-swiss/dsp-js'; -import { Auth, DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; -import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; +import { Auth, DspApiConnectionToken, RouteConstants } from '@dasch-swiss/vre/shared/app-config'; import { - LoadUserAction, - ClearProjectsAction, - LogUserOutAction, - UserStateModel, ClearListsAction, ClearOntologiesAction, + ClearProjectsAction, + LogUserOutAction, } from '@dasch-swiss/vre/shared/app-state'; import { Store } from '@ngxs/store'; import jwt_decode, { JwtPayload } from 'jwt-decode'; -import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; -import { catchError, tap, switchMap, map, takeLast, take } from 'rxjs/operators'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map, switchMap, take, takeLast, tap } from 'rxjs/operators'; import { LoginError, ServerError } from './error'; @Injectable({ providedIn: 'root' }) export class AuthService { private tokenRefreshIntervalId: any; - private _isLoggedIn$ = new BehaviorSubject(this.isLoggedIn()); private _dspApiConnection = inject(DspApiConnectionToken); - isLoggedIn$ = this._isLoggedIn$.asObservable(); - get tokenUser() { return this.getTokenUser(); } @@ -36,7 +30,7 @@ export class AuthService { private router: Router // private intervalWrapper: IntervalWrapperService ) { // check if the (possibly) existing session is still valid and if not, destroy it - this.isSessionValid() + this.isSessionValid$() .pipe(takeLast(1)) .subscribe(valid => { if (!valid) { @@ -54,7 +48,7 @@ export class AuthService { * If a json web token exists, it doesn't mean that the knora api credentials are still valid. * */ - isSessionValid(): Observable { + isSessionValid$(forceLogout: boolean = false): Observable { // mix of checks with session.validation and this.authenticate const accessToken = this.getAccessToken(); if (accessToken) { @@ -83,7 +77,10 @@ export class AuthService { } else { // no session found; update knora api connection with empty jwt this._dspApiConnection.v2.jsonWebToken = ''; - this.doLogoutUser(); + if (forceLogout) { + this.doLogoutUser(); + } + return of(false); } } @@ -106,14 +103,6 @@ export class AuthService { } } - loadUser(username: string): Observable { - return this.store.dispatch(new LoadUserAction(username)).pipe( - tap(() => { - this._isLoggedIn$.next(true); - }) - ); - } - /** * Login user * @param identifier can be the email or the username @@ -186,7 +175,6 @@ export class AuthService { } doLogoutUser() { - this._isLoggedIn$.next(false); this.removeTokens(); this.store.dispatch([ new LogUserOutAction(), @@ -195,6 +183,7 @@ export class AuthService { new ClearOntologiesAction(), ]); clearTimeout(this.tokenRefreshIntervalId); + this.router.navigate([RouteConstants.home], { replaceUrl: true }); } isLoggedIn() { diff --git a/libs/vre/shared/app-state/src/lib/user/user.state.ts b/libs/vre/shared/app-state/src/lib/user/user.state.ts index 2433e7771d..84c4f81754 100644 --- a/libs/vre/shared/app-state/src/lib/user/user.state.ts +++ b/libs/vre/shared/app-state/src/lib/user/user.state.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { ApiResponseError, Constants, ReadUser } from '@dasch-swiss/dsp-js'; import { UserApiService } from '@dasch-swiss/vre/shared/app-api'; -import { DspApiConnectionToken } from '@dasch-swiss/vre/shared/app-config'; import { AppErrorHandler } from '@dasch-swiss/vre/shared/app-error-handler'; import { Action, State, StateContext } from '@ngxs/store'; import { of } from 'rxjs'; @@ -83,6 +82,8 @@ export class UserState { @Action(SetUserAction) setUser(ctx: StateContext, { user }: SetUserAction) { + if (!user) return; + const state = ctx.getState(); const userIndex = state.allUsers.findIndex(u => u.id === user.id); if (userIndex > -1) {