1

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 {}

Stiegi
  • 1,074
  • 11
  • 22
  • Any console output when opening the blank page ? – Henrik Bøgelund Lavstsen May 13 '22 at 09:43
  • Unfortunately no. If I write a console.log inside the canActivate, this is logged with no problems. So the guard is executed, for some reason the rest of the app only cares if it is the inital page load... – Stiegi May 13 '22 at 09:44
  • The problem seems to be related to the ngOnInit hook in the account component: the code with in the subscrption is not executed again when I enter the route, therefore I dont receive the user info... – Stiegi May 13 '22 at 10:12

3 Answers3

3

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();
      });
    });
  }
Chellappan வ
  • 23,645
  • 3
  • 29
  • 60
  • Thank you for the hint, but it didn't make a difference. The user page remained blank upon router navigation, I edited the question to include the user service. – Stiegi May 13 '22 at 09:40
  • Can you share you router configurations? – Chellappan வ May 13 '22 at 10:01
  • sure, I will add it to the main post! – Stiegi May 13 '22 at 10:02
  • I added the router module to the main post – Stiegi May 13 '22 at 10:05
  • It seems router configure does not have an issue. Is your app navigates to other routes without issue? – Chellappan வ May 13 '22 at 10:12
  • no, but I think it is related to the ngOnInit hook in the account component - I just saw that the code within the subscription, which includes the this.userData = user; line, is not executed again when reentering this route, and therefore no user data can be rendered. So it's actually not the guard that prevents the load, but the fact that user data is missing... – Stiegi May 13 '22 at 10:16
  • from where you are getting user data in account component? – Chellappan வ May 13 '22 at 10:20
  • 1
    In you code late subscription is hapening Change your subject to ReplaySubject--> public user = new ReplaySubject(); in service – Chellappan வ May 13 '22 at 10:22
  • Thank you so much! That was it! I didn't know about ReplaySubject and I will have to read the docs about it, but after changing that, it worked! – Stiegi May 13 '22 at 10:27
  • You are welcome please read this for more information regarding late subscription https://trilon.io/blog/dealing-with-late-subscribers-in-rxjs#What-is-the-RxJS-Late-Subscriber-Problem- – Chellappan வ May 13 '22 at 11:17
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() 
     }
}
  • Thanks you, unfortunately it didn't fix my problem. I edited the question, so even just returning true didn't work... – Stiegi May 13 '22 at 09:39
1

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);
                 }
             })
        );
   }
Den
  • 650
  • 6
  • 15
  • Thanky, that's much better, I already got an answer where map operator is used. Both are much better than my new Observable approach, however it doesn't solve the problem. Even if I only return true in canActivate and nothing else, this error occurs for me... – Stiegi May 13 '22 at 10:01