3

I'm building an authentication app using the PEAN stack (i.e., PostgreSQL - ExpressJS - Angular - NodeJS).

Problem

I have a problem with my auth guard:

  • If I use subscribe(), I get the expected result in the console. ✔ - WORKING
  • If I use pipe(), I get undefined in the console. ✖ - NOT WORKING

if-signed-in.guard.ts

import { CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth/auth.service';
import { inject } from '@angular/core';
import { tap, map } from 'rxjs/operators';
import { Router } from '@angular/router';

export const IfSignedIn: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);
    
  auth.getSignInStatusObserver().subscribe((res: any) => {
    console.log(res);
  });

  return auth.getSignInStatusObserver().pipe(
    tap((status) => {
      console.log(status);
    }),
    map((status) => {
      console.log(status);
      if (status) {
        if (status.success === true) {
          return true;
        } else {
          router.navigate(['/dashboard']);
          return false;
        }
      } else {
        router.navigate(['/dashboard']);
        return false;
      }
    })
  );
};

Screenshot:

Screenshot

What I've tried

I tried implementing the same logic inside subscribe():

if-signed-in.guard.ts

import { CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth/auth.service';
import { inject } from '@angular/core';
import { Router } from '@angular/router';

export const IfSignedIn: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  auth.getSignInStatusObserver().subscribe((res: any) => {
    if (res) {
      if (res.success === true) {
        return true;
      } else {
        router.navigate(['/dashboard']);
        return false;
      }
    } else {
      router.navigate(['/dashboard']);
      return false;
    }
  });
};

But I get the following error:

src/app/auth/guards/if-signed-in.guard.ts:7:14 - error TS2322: Type '(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => void' is not assignable to type 'CanActivateFn'. Type 'void' is not assignable to type 'boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree>'.

7 export const IfSignedIn: CanActivateFn = (route, state) => {

Question

Why do I get undefined from the Observable in auth guard when using pipe(), while subscribe() works but I get an error?


EDIT 1

I've tried the code suggested by @MatthieuRiegler, but I'm still getting exactly the same response as in the screenshot above.

if-signed-in.guard.ts

import { CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth/auth.service';
import { inject } from '@angular/core';
import { filter, tap, map } from 'rxjs/operators';
import { Router } from '@angular/router';

export const IfSignedIn: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  auth.getSignInStatusObserver().subscribe((res: any) => {
    console.log(res);
  });

  return auth.getSignInStatusObserver().pipe(
    filter((status) => status !== undefined),
    tap((status) => {
      console.log(status);
    }),
    map((status) => {
      console.log(status);
      if (status) {
        if (status.success === true) {
          return true;
        } else {
          router.navigate(['/dashboard']);
          return false;
        }
      } else {
        router.navigate(['/dashboard']);
        return false;
      }
    })
  );
};

EDIT 2

My app behaves strangely. Now I have the following code:

if-signed-in.guard.ts

import { CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth/auth.service';
import { inject } from '@angular/core';
import { filter, tap, map } from 'rxjs/operators';
import { Router } from '@angular/router';

export const IfSignedIn: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  return auth.getSignInStatusObserver().pipe(
    filter((status) => status !== undefined),
    tap((status) => {
      console.log('Tap: ', status);
    }),
    map((status) => {
      console.log('Map: ', status);
      if (status) {
        if (status.success === true) {
          return true;
        } else {
          router.navigate(['/dashboard']);
          return false;
        }
      } else {
        router.navigate(['/dashboard']);
        return false;
      }
    })
  );
};

If I click the Edit profile button, everything works as expected. If I type in the search bar http://localhost:4200/profile/edit-profile, it looks like something is wrong with emitting values because I receive undefined, and because the filter() works, I don't get anything in the console.

Also, the same thing happens on page refresh. It seems like the auth guard isn't working on page refresh.

Screenshot:

GIF


EDIT 3

I use an interceptor to set sign-in status as follows:

sign-in-status.service.ts

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/services/auth/auth.service';

@Injectable({
  providedIn: 'root',
})
export class SignInStatusService implements HttpInterceptor {
  intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(httpRequest).pipe(
      tap((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse && httpRequest.url.endsWith('api/get-user') && event.status === 200) {
          this.authService.setSignInStatus({ success: true, response: event.body });
        }
      }),
      catchError((err: any) => {
        if (httpRequest.url.endsWith('api/get-user')) {
          this.authService.setSignInStatus({ success: false, response: err });
        }
        return throwError(() => err);
      })
    );
  }

  constructor(private authService: AuthService) {}
}
Rok Benko
  • 14,265
  • 2
  • 24
  • 49
  • `CanActivateFn` needs to return a value (`boolean` or `Observable`) so the subscribe is not an option. – Matthieu Riegler Jul 25 '23 at 14:23
  • @MatthieuRiegler Thanks for the response. Yeah, I understand that from the error message. The problem is that `pipe()` returns `undefined`. Why? – Rok Benko Jul 25 '23 at 14:43
  • Would using a [`BehaviorSubject` instead of `Observable`](https://stackoverflow.com/q/39494058/6309) help? – VonC Jul 31 '23 at 20:31

1 Answers1

1

Your issue is elsewhere. As you can see, you get an undefined as first value of your subscribe.

By doing a subscribe without closing it, it outlives the usage of the CanActivateFn.

My guess is that you are experiencing a race condition. Your observable emit a status too late for the CanActivateFn.

Your getSignInStatusObserver shouldn't emit undefined, you filter it pipe(filter(status => status !== undefined)). That should fix your issue !

Matthieu Riegler
  • 31,918
  • 20
  • 95
  • 134
  • I've tried the code you suggested, but I'm still getting exactly the same response as in the screenshot above. I edited my question with the code I tried. – Rok Benko Jul 25 '23 at 17:46
  • What value do you get in your status ? btw you should return an `UrlTree` instead on `navigate()` + returning a `false`. – Matthieu Riegler Jul 25 '23 at 17:51
  • If I use `filter()` I don't get anything at all in the console. Nothing. If I don't use `filter()`, I get `undefined` in the console. It looks like something is wrong with emitting values because I only receive `undefined` when using `filter()` and because the filter works, I don't get anything in the console. What could be wrong? – Rok Benko Jul 26 '23 at 07:53
  • I edited my question for the second time with additional information and a GIF so you can see what's happening. I would be really grateful for your help because I've been stuck at this point for 2 weeks now. – Rok Benko Jul 26 '23 at 09:51
  • When does `getSignInStatusObserver` gets its value ? – Matthieu Riegler Jul 26 '23 at 11:13
  • The `getSignInStatusObserver` gets its value when the interceptor intercepts `status === 200` on the `api/get-user` (the backend checks for the session cookie on the `api/get-user`). So, if the cookie is present, the backend returns `200`, the interceptor intercepts that, and sets the sign-in status to `success: true`. I edited my question with `sign-in-status.service.ts`. – Rok Benko Jul 26 '23 at 11:51
  • I'm offering a bounty now. – Rok Benko Jul 27 '23 at 15:20
  • Is your repo publicly accessible ? – Matthieu Riegler Jul 27 '23 at 15:21
  • No, unfortunatelly. – Rok Benko Jul 27 '23 at 15:27
  • have you tried removing the `navigate()` and returining a `UrlTree` instead ? – Matthieu Riegler Jul 27 '23 at 15:29
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254695/discussion-between-rok-benko-and-matthieu-riegler). – Rok Benko Jul 27 '23 at 15:33