'Anglar 13 route guard (canActivate) only works on first page load

Solution found - since the solution is much shoerter than the problem, I add it here: The problem was not the guard (even though I optimized the guard by using map / switchMap operator instead of new Obeservable), but the Subject, that I used to get user data in the component. I replaced Subject with ReplaySubject in the user service, because I had a late subscription in the ngOnInit hook.

In an Angular 13 app, I have set up the folowwing route canActivate guard to a account route:

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  Router,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import { UserService } from './user.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private userService: UserService, private route: Router) {
  }

  isLoggedIn(): Observable<boolean | UrlTree> {
    return this.userService.checkLoginStatus().pipe(
      map((status) => {
        if (status.status === 'logged in') {
          return true;
        }
        this.route.navigate(['/login']);
        return false;
      })
    );
  }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree>
     { 
         return this.isLoggedIn() 
     }
}

However, this only works when I reaoad the page in browser, but not when I navigate there with the app by clicking a router link. I can see in the network tab that the checkLoginStatus request is done and gets 'logged in' as return value, however the page just keeps blank. Can anyone spot the error in my canActivate implementation? I made sure that the block with subscriber.next(true); is executed, but I guess what I do there is somehow wrong?

Thanks!

EDIT:

Thank you for your answers so far. Unfortunately there seems to be something very wrong, because even if I implement canActivate like this:


  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | boolean
    | UrlTree
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree> {
    // return this.isLoggedIn()
    return true;
  }

...which should result in always granting access to that page, even then the Page remains blank. So I'm adding the user service to this post:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, Subject, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { User } from '../models/user.model';
@Injectable()
export class UserService {
  private userURL = 'https://phoenix:4001/user';
  public user = new Subject<User | null>();

  constructor(private http: HttpClient) {
    this.checkLoginStatus().subscribe(status => {
      if (status.status === 'logged in') {
        this.completeLogin();
      }
    })
  }

  public logout() {
    this.http
      .get<{ message?: string }>(this.userURL + '/logout', {
        withCredentials: true,
      })
      .pipe(catchError(this.handleNotAuthorized.bind(this)))
      .subscribe((resp) => {
        if (resp?.message === 'logged out') {
          this.user.next(null);
        }
      });
  }

  public login(params: { name: string; password: string }) {
    this.http
      .get<{ message?: string; token?: string }>(`${this.userURL}/login`, {
        withCredentials: true,
        params,
      })
      .pipe(catchError(this.handleNotAuthorized.bind(this)))
      .subscribe((response) => {
        if (response.hasOwnProperty('token')) {
          this.completeLogin();
        }
      });
  }

  public register(params: {
    name: string;
    password: string;
    firstName?: string;
    lastName?: string;
  }) {
    this.http
      .post<{ message?: string; token?: string }>(
        `${this.userURL}/register`,
        params,
        {
          withCredentials: true,
        }
      )
      .pipe(catchError(this.handleNotAuthorized.bind(this)))
      .subscribe((response) => {
        if (response.hasOwnProperty('token')) {
          this.completeLogin();
        }
      });
  }

  checkLoginStatus() {
    return this.http
      .get<{status: string}>(this.userURL + '/check', { withCredentials: true })
      .pipe(catchError(this.handleNotAuthorized.bind(this)));
  }

  private getUser(): Observable<User> {
    return this.http
      .get<User>(this.userURL, { withCredentials: true })
      .pipe(catchError(this.handleNotAuthorized.bind(this)));
  }

  private completeLogin() {
    this.getUser().subscribe((user) => {
      this.user.next(user);
    });
  }

  private handleNotAuthorized(error: any) {
    if (error?.error?.message === 'Unauthorized') {
      this.user.next(null);
      return throwError(() => {});
    }
    let errorMessage = '';
    if (error.error instanceof ErrorEvent) {
      // Get client-side error
      errorMessage = error.error.message;
    } else {
      // Get server-side error
      errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    return throwError(() => {
      return errorMessage;
    });
  }
}

The Account component is very simple:

import { Component, OnInit } from '@angular/core';
import { User } from 'src/app/models/user.model';
import { UserService } from 'src/app/services/user.service';

@Component({
  selector: 'app-account',
  templateUrl: './account.component.html'
})
export class AccountComponent implements OnInit {
  userData: User | null = null;
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.userService.user.subscribe(user => {
      if(user) {
        this.userData = user;
      }
    })
  }

}

My router looks like this:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './services/auth-guard.service';
import { ContentResolver } from './services/content-resolver.service';
import { ProductResolver } from './services/product-resolver.service';
import { AboutComponent } from './views/about/about.component';
import { AccountComponent } from './views/account/account.component';
import { ContentEngagementComponent } from './views/content-engagement/content-engagement.component';
import { DirectiveComponent } from './views/directive/directive.component';
import { HomeComponent } from './views/home/home.component';
import { LoginComponent } from './views/login/login.component';
import { ProductComponent } from './views/product/product.component';
import { ShopComponent } from './views/shop/shop.component';
import { TeaserComponent } from './views/teaser/teaser.component';
import { ThankYouComponent } from './views/thank-you/thank-you.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
    pathMatch: 'full',
    resolve: [ContentResolver],
  },
  {
    path: 'shop/:id',
    component: ProductComponent,
    resolve: [ProductResolver],
  },
  {
    path: 'shop',
    component: ShopComponent,
    resolve: [ContentResolver, ProductResolver],
  },
  {
    path: 'about',
    component: AboutComponent,
    resolve: [ContentResolver],
  },
  {
    path: 'login',
    component: LoginComponent,
  },
  {
    path: 'content-engagement',
    component: ContentEngagementComponent,
  },
  {
    path: 'teaser',
    component: TeaserComponent,
  },
  {
    path: 'account',
    component: AccountComponent,
    canActivate: [AuthGuard]
  },
  {
    path: 'thankyou',
    component: ThankYouComponent,
  },
  {
    path: 'directives',
    component: DirectiveComponent,
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}



Solution 1:[1]

isLogged method returns Observable which does not complete in your code, It has to be completed.

 isLoggedIn(): Observable<boolean | UrlTree> {
    return new Observable((subscriber) => {
      this.userService.checkLoginStatus().subscribe((status) => {
        if (status.status === 'logged in') {
          subscriber.next(true);
        } else {
          subscriber.next(this.route.parseUrl('/login'));
        }
        subscriber.complete();
      });
    });
  }

Solution 2:[2]

Little bit of code cleanup. No need to create a new observable when you already have it, just pipe map the existing one and return it. Based on your code i am assuming you want to send user to /login if they are not logged in trying to access a route that it guards.

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  Router,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import { UserService } from './user.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private userService: UserService, private route: Router) {
  }

  isLoggedIn(): Observable<boolean | UrlTree> {
    return this.userService.checkLoginStatus().pipe(
        map(status) => {
          if (status.status === 'logged in') {
            return true;
          } 
            route.navigate(['/login\']);
            return false;
          }
      }));
    });
  }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree>
     { 
         return this.isLoggedIn() 
     }
}

Solution 3:[3]

You not return an observable<bool | UrlTree> but a new Observable<Subscription>, try to do this way.

  isLoggedIn(): Observable<boolean | UrlTree> {
    return this.userService.checkLoginStatus()
        .pipe(
             switchMap((status) => {
                 if (status.status === 'logged in') {
                   return of(true);
                 } else {
                   this.route.parseUrl('/login');
                   return of(false);
                 }
             })
        );
   }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Chellappan வ
Solution 2 Henrik Bøgelund Lavstsen
Solution 3 Den