0

I am having an angular app and spring boot in the backend using BasicAuth for my authentication.

When the session on the server runs out I am getting a 401 and in the httpInterceptor I am then redirecting to the logout page.

On some components I have a form and when the user has not saved the changes, there is a canDeactive guard with a modal pop-up, warning the user of the unsaved changes. The problem is now, when the session is invalid, the guard fires (since the angular app does not know yet of the invalid session) and I am stuck on the pop-up. My goal would be to check if the session is valid, before the guard is fired (or evaluate in the guard itself, if the session is valid). My idea was to call a isAuthenticated function in the backend and in the guard navigate to logout, on a 401 and else show the dialog, but since the call is async it won´t work and I did not figure out how to do it.

component.ts

createChangeGuard() {
    setTimeout(() => {
      this.myObjectOrig$ = new BehaviorSubject(this.bookingForm.value).asObservable();
      this.isDirty$ = this.bookingForm.valueChanges.pipe(dirtyCheck(this.myObjectOrig$));
      this.myObjectOrig$.pipe(untilDestroyed(this)).subscribe(state => { this.bookingForm.patchValue(state, { emitEvent: false }); });
    }, 1000);
  }

dirty-check component

export interface DirtyComponent {
  isDirty$: Observable<boolean>;
  // isPrinting: boolean;
  hasValidSession$: Observable<boolean>;
}

export function dirtyCheck<U>( source: Observable<U> ) {
  let subscription: Subscription;
  let isDirty = false;
  return function <T>( valueChanges: Observable<T> ): Observable<boolean> {

    const isDirty$ = combineLatest(
      [source,
      valueChanges]
    ).pipe(
      debounceTime(300),
      map(( [a, b] ) => { return isDirty = isEqual(a, b) === false;
      }),
      finalize(() => subscription.unsubscribe()),
      startWith(false),
      shareReplay({ bufferSize: 1, refCount: true }),
    );

    subscription = fromEvent(window, 'beforeunload').subscribe(event => {
        isDirty && (event.returnValue = false) && event.preventDefault();
    });
    return isDirty$;
  };
}

deactivate Guard

export class DirtyCheckGuard implements CanDeactivate<DirtyComponent> {
   ....

    constructor( private modalService: NzModalService,
                 .....
                  ) {
    }

    contentNormal = 'Die Änderungen wurden nicht gespeichert. Wollen Sie wirklich die Seite verlassen?';
    contentPrint = 'Die Änderungen wurden noch nicht gespeichert. Trotzdem drucken?';
    canDeactivate( component: DirtyComponent, currentRoute: ActivatedRouteSnapshot ): Observable<boolean> | boolean {
      let formIsDirty = false;
      ...some logic to set formIsDirty...
      return component.isDirty$.pipe(switchMap(dirty => {
        let navigate;
        if ( (dirty === false && !formIsDirty ) ) {
          // update status to status before the booking went into edit mode since no change was made
            this.checkPage(currentRoute.data.origin);
          return of(true);
        } else if (dirty === true || (dirty === false && formIsDirty)) {
        return this.modalService.create({
          nzTitle: 'Not saved',
          nzContent: content,
          nzFooter: [
            {
              label: 'Abbrechen',
              onClick: () => {navigate = false; this.modalService.closeAll(); }
            },
            {
              label: confirmText,
              type: 'primary',
              onClick: () => { navigate = true; this.checkPage(currentRoute.data.origin); this.modalService.closeAll(); }
            }
          ]
        }).afterClose.pipe(map(() => navigate ));

      }}), take(1));
    }
  }

interceptor

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
     const requestRoute = this.router.url;
       return next.handle(request).pipe(retry(1), catchError((error: HttpErrorResponse) => {
         // this.errorCount = true;|| this.authenticationService.isLoggedIn() === false
         if (error.status === 401 ) {
             this.authenticationService.logout(requestRoute);
           }
         if (error.error instanceof ErrorEvent) {
               // A client-side or network error occurred. Handle it accordingly.
               console.error('An error occurred:', error.error.message);
             } else {
               console.error('An error occurred:', error);
               // The backend returned an unsuccessful response code.
               // The response body may contain clues as to what went wrong,
               if (error.error !== null){
                   this.showErrorNotification(error.status, error.error?.errorCode, error.error?.errorText);
                   console.error(`Backend returned code ${error.status}, body was: ${error.error?.errorText}}`);
                 }
             }
         return throwError(error.error);
       })
       );

authentication service

async isAuthenticated(){
    const serverUrl = this.baseURL + 'isValidSession';
    const response = await this.http.get(serverUrl, httpOptions).toPromise();
    return response.toString();
 }
D. K.
  • 151
  • 2
  • 13

1 Answers1

0

If you have to validate session on each canActivate. You need to have a api for that.

lets say you have that api in AuthService

async isSessionValid() {
   var res = await this.http.get('url').toPromise();
   return res;
}

then use this promise returned as first statement of guard using same await strategy. if this returns true proceed furter, if this returns false return false from guard.

Aakash Garg
  • 10,649
  • 2
  • 7
  • 25
  • this unfortunately does not work, since the guard fires, before even communicating with the server, showing the pop-up and then do the navigation – D. K. May 30 '20 at 08:15
  • you are calling this.authenticationService.logout(requestRoute); In logout method before navigation add that flag i mentioned above to sessionStorage. and in your deactivate guard use the logic i gave above. – Aakash Garg May 30 '20 at 08:36
  • I tried that, but it is not working, problem is, as I said, that the interceptor does not fire, until a call to the backend is made. The guard gives me the pop-up before navigation, so the interceptor does not fire, only when I confirm the pop-up because then I call a function in the backend – D. K. May 30 '20 at 08:43
  • ahhh got it, can you give your code to validate session? – Aakash Garg May 30 '20 at 08:45
  • I added the service, basically I was trying to do something like here: https://stackoverflow.com/questions/43953007/angular-2-wait-for-method-observable-to-complete?rq=1 (accepted answer) – D. K. May 30 '20 at 08:56