0

I'm doing a basic authentication for users after login, but weird behavior is happening, where canActivate is returning false, even though it is being called in the right time.

in Login Component

private login() {
  this.authService.login(this.loginForm.valid)
  .subscribe((data: any) => { // no need for pipe and map
    console.log("First");
    this.router.navigate(['/app']);
  })
}

in auth.service:

login({ username, password }: any): Observable<any>{
    this.clearData();

    const loginData = {
      'username': username,
      'password': password
    };

    return this.http.post(environment['baseURL'] + '/api/auth/login', loginData).pipe(
      map(
        (data: any) => {
          if (data['token'] && data['user']) {
            this.isLoggedIn.next(true); 
            this.token = data['token'];
            localStorage.setItem('token', this.token);
            localStorage.setItem('usermeta', JSON.stringify(this.userData));
            return true;
          } else {
            return false;
          }
        },
        (error: any)=>{
          return of(false)
        }
      )
      
    )
  }

  getToken(): string {
    return this.token;
  }

  hasToken() {
    return this.getToken() ? true : false;
  }

in auth.guard.service

constructor(
    private router: Router,
    private authService: AuthService,) {
    console.log("SECOND - canActivate") --> this is being printed on the success of the login call
  }

 canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    console.log(this.authService.hasToken(), ">> this.authService.hasToken() <<")
    if (this.authService.hasToken()) {
      return true;
    } else {

      // not logged in so redirect to login page with the return url
      this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
      return false;
    }

  }

In console tab, it shows "First" then "Second - canActivate" which tells that the token has already took its value on success of the call, However, for some reason the value returned by hasToken() method is false

enter image description here

Ali
  • 1,633
  • 7
  • 35
  • 58
  • Th easiest way to test is instead of logging true/false, you can directly log the token, see if it's there. From the code, I can tell this.token is always empty/null, which means the if condition in your service is false, the token never get set its value – Jimmy Nov 13 '22 at 20:50
  • @Jimmy it's in the `auth.service` > `login`> `this.token = data['token'];` I can see its value in the console – Ali Nov 13 '22 at 21:34
  • You shouldn't return return of(true) , but return true instead. Map in RxJS works with data, not observables – Lonli-Lokli Nov 13 '22 at 22:50
  • if you're confident you have the token, then post a stackblitz to show the problem, making a stackblitz demo will help you verify your code again, and also helps other get the whole code instead of a small snippet, which makes debugging easier – Jimmy Nov 14 '22 at 10:01

1 Answers1

0

To be honest, I can't quite spot what's wrong but there's a few things I'd write differently, so I'll suggest those and hopefully it refactors the issue away. Also console.log is async, so don't trust it to log in the correct order.

When using RxJS I recommend leaning towards functional and not using static member variables to pass values around. You should aim to chain everything so you don't have any unexpected async side effects.

This is not tested but should be correct. Check the inline comments. Hope it helps.

private login() {
  this.authService.login(this.loginForm.valid)
  .subscribe((data: any) => { // no need for pipe and map
    console.log("First");
    this.router.navigate(['/app']);
  })
}
export class AuthService {

  public isLoggingIn = new BehaviorSubject<boolean>(false);
  public isLoggedIn = new BehaviorSubject<boolean>(false);

  constructor() { }

  // You were returning Observables, not booleans. Help TypeScript help you - don't use 'any'.
  public login(loginData: { username: string, password: string }): Observable<boolean> { 
    this.clearData();

    this.isLoggingIn.next(true);

    return this.http.post(environment['baseURL'] + '/api/auth/login', loginData)
      .pipe(
        map(
          (data: any) => {
            if (data['token'] && data['user']) {
              this.isLoggedIn.next(true);
              this.isLoggingIn.next(false);
              this.token = data['token'];
              localStorage.setItem('token', this.token);
              localStorage.setItem('usermeta', JSON.stringify(this.userData));
              return true // no 'of'
            } else {
              return false // no 'of'
            }
          }
        ),
        catchError((error: any) => {
          this.isLoggingIn.next(false);
          return of(false) // catchError expects a new observable so we do use 'of' here.
        })
      )
  }

}
export class AuthGuard implements CanActivate {

  constructor(
    private router: Router,
    private authService: AuthService
  ) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
    return this.authService.isLoggedIn.pipe(
      // always important to handle the transition state
      // skip if currently logging in (assumes a second emission once logged in)
      skipUntil(this.authService.isLoggingIn.pipe(filter(isLoggingIn => !isLoggingIn))) 
    ).pipe(
      map(isLoggedIn => {
        if (isLoggedIn) {
          return true;
        } else {
          return this.router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } })
        }
      })
    )
  }

}

Richard Dunn
  • 6,165
  • 1
  • 25
  • 36
  • 1
    I can see the refactoring you did, but still not working, the same thing is happening, the canActivate guard goes to the else and returns the 'returnUrl'.. – Ali Nov 13 '22 at 21:48