Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dsp-app): Resolves DEV-2687 #1336

Merged
merged 8 commits into from
Jan 11, 2024
2 changes: 0 additions & 2 deletions apps/dsp-app/src/app/app.module.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -196,7 +195,6 @@ export function httpLoaderFactory(httpClient: HttpClient) {
AppComponent,
ArchiveComponent,
AudioComponent,
AuthGuardComponent,
AvTimelineComponent,
DescriptionComponent,
BooleanValueComponent,
Expand Down
37 changes: 24 additions & 13 deletions 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,
Expand All @@ -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,
Expand All @@ -28,6 +30,9 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginFormComponent implements OnInit, OnDestroy {
get isLoggedIn$(): Observable<boolean> {
return this._authService.isSessionValid$();
}
/**
* set whether or not you want icons to display in the input fields
*
Expand Down Expand Up @@ -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
) {}

/**
Expand All @@ -109,7 +117,7 @@ export class LoginFormComponent implements OnInit, OnDestroy {
*/
ngOnInit() {
this.buildLoginForm();
this.returnUrl = this.getReturnUrl() || '/';
this.returnUrl = this.getReturnUrl();
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -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)),
irmastnt marked this conversation as resolved.
Show resolved Hide resolved
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]);
}
});
}
},
Expand Down
14 changes: 0 additions & 14 deletions apps/dsp-app/src/app/main/guard/auth-guard.component.ts

This file was deleted.

11 changes: 5 additions & 6 deletions apps/dsp-app/src/app/main/guard/auth.guard.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand All @@ -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}`;
}
}
Expand Up @@ -13,8 +13,6 @@ import { map } from 'rxjs/operators';
providedIn: 'root',
})
export class OntologyClassInstanceGuard implements CanActivate {
isLoggedIn$: Observable<boolean> = this.authService.isLoggedIn$;

@Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable<boolean>;
@Select(UserSelectors.userProjects) userProjects$: Observable<StoredProject[]>;

Expand All @@ -26,12 +24,12 @@ export class OntologyClassInstanceGuard implements CanActivate {

canActivate(activatedRoute: ActivatedRouteSnapshot): Observable<boolean> {
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;
}
Expand Down
11 changes: 9 additions & 2 deletions 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';
Expand All @@ -22,13 +23,19 @@ export class UserMenuComponent implements OnInit, OnDestroy {

private ngUnsubscribe: Subject<void> = new Subject<void>();

isLoggedIn$: Observable<boolean> = this._authService.isLoggedIn$;
get isLoggedIn$(): Observable<boolean> {
return this._authService.isSessionValid$();
}

@Select(UserSelectors.user) user$: Observable<User>;
@Select(UserSelectors.isSysAdmin) isSysAdmin$: Observable<User>;

systemLink = RouteConstants.system;

constructor(private _authService: AuthService) {}
constructor(
private _authService: AuthService,
private _router: Router
) {}

ngOnInit() {
this.navigation = [
Expand Down
@@ -1,4 +1,5 @@
import { inject, Injectable } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
BuildTag,
BuildTagToken,
Expand Down Expand Up @@ -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();
}
}
}
});
});
}
});
}
Expand Down
Expand Up @@ -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();
}
});
}

/**
Expand Down
35 changes: 12 additions & 23 deletions 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<boolean>(this.isLoggedIn());
private _dspApiConnection = inject(DspApiConnectionToken);

isLoggedIn$ = this._isLoggedIn$.asObservable();

get tokenUser() {
return this.getTokenUser();
}
Expand All @@ -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) {
Expand All @@ -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<boolean> {
isSessionValid$(forceLogout: boolean = false): Observable<boolean> {
// mix of checks with session.validation and this.authenticate
const accessToken = this.getAccessToken();
if (accessToken) {
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -106,14 +103,6 @@ export class AuthService {
}
}

loadUser(username: string): Observable<UserStateModel> {
return this.store.dispatch(new LoadUserAction(username)).pipe(
tap(() => {
this._isLoggedIn$.next(true);
})
);
}

/**
* Login user
* @param identifier can be the email or the username
Expand Down Expand Up @@ -186,7 +175,6 @@ export class AuthService {
}

doLogoutUser() {
this._isLoggedIn$.next(false);
this.removeTokens();
this.store.dispatch([
new LogUserOutAction(),
Expand All @@ -195,6 +183,7 @@ export class AuthService {
new ClearOntologiesAction(),
]);
clearTimeout(this.tokenRefreshIntervalId);
this.router.navigate([RouteConstants.home], { replaceUrl: true });
}

isLoggedIn() {
Expand Down
5 changes: 3 additions & 2 deletions 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';
Expand Down Expand Up @@ -83,6 +82,8 @@ export class UserState {

@Action(SetUserAction)
setUser(ctx: StateContext<UserStateModel>, { user }: SetUserAction) {
if (!user) return;

const state = ctx.getState();
const userIndex = state.allUsers.findIndex(u => u.id === user.id);
if (userIndex > -1) {
Expand Down