0
votes

I'm trying to protect authenticated routes using AuthGuard middleware. When I implement this middleware into a route. Those routes doesn't work after I activate guard.

App routing module

const routes: Routes = [
  {
    path: 'posts',
    canLoad: [AuthGuardMiddleware],
    loadChildren: () => import('./modules/posts/posts.module').then(m => m.PostsModule)
  },
  {
    path: 'users',
    canLoad: [AuthGuardMiddleware],
    loadChildren: () => import('./modules/users/users.module').then(m => m.UsersModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: PreloadAllModules
  })],
  exports: [RouterModule]
})

and AuthGuardMiddleware

@Injectable({
  providedIn: 'root'
})
export class AuthGuardMiddleware implements CanActivate, CanLoad {
  constructor(private authService: AuthService, private router: Router) {
  }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> {
    return this.authService.user.pipe(map(user => {
      if (user) {
        return true;
      }
      return this.router.createUrlTree(['/login']);
    }));
  }

  canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.user.pipe(map(user => {
      if (user) {
        return true;
      }
      this.router.navigate(['/']);
      return false;
    }));
  }
}

How can I fix it ? Note: AuthService::user type is BehaviorSubject initialized as null

UPDATE

I realized that when I clean up my app component and remove conditional router outlet, everything start to work fine. I still don't know what exactly causing this error.

app component

export class AppComponent implements OnInit {
  currentUrl: string;
  isAuthRoute = false;

  constructor(private router: Router) {
  }

  ngOnInit() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationEnd) {
        this.currentUrl = event.url;
        this.isAuthRoute = ['/login', '/register']
          .indexOf(this.currentUrl) !== -1;
      }
    });
  }
}

app component view

<div *ngIf="!isAuthRoute">
  <div class="row page-wrapper">
    <div class="col-md-3 col-md-pull-9">
      <app-sidebar></app-sidebar>
    </div>
    <div class="col-md-9 col-md-push-3 content-area">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

<div style="height: 80vh" class="d-flex justify-content-center align-items-center" *ngIf="isAuthRoute">
  <router-outlet ></router-outlet>
</div>
2
Based on your solution, maybe you should pass your logic to show/hide the router-outlet to a lazy-load module (the guards would decide if it could be loaded/activated). Would it be possible that the router-outlet needs to be there from the beginning? I've heard of an issue like that with angular-dart. But... before jumping to this conclusion try adding an extra module that would work as a shell for all the rest of your app and would be loaded/activated only for authorized users. It would be equivalent to your *ngIf logic. - julianobrasil
Well actually ngIf logic here for only to display one-page login page and hide the sidebar. all authenticated components encapsulated as an separate module. - Teoman Tıngır
Yeah, I got that... I'm suggesting you move that logic to a guard of a new lazy-loaded shell module. That would solve the problem if it is indeed related to the dynamically created router-outlet. - julianobrasil
Take a look at this angular-dart issue apparently similar to what you have. - julianobrasil
@julianobrasil it also would work, thank you for all your advices but I will go with PierreDuc s answer - Teoman Tıngır

2 Answers

1
votes

Your AuthService.user returns an Observable stream and it never completes. As far as I know, the canLoad and canActivate need a completed Observable. You can make it so by using the take(1) operator:

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> {
    return this.authService.user.pipe(
       map(user => {
        if (user) {
          return true;
        }
        return this.router.createUrlTree(['/login']);
      }),
      take(1)
    );
  }

  canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.user.pipe(
      map(user => {
        if (user) {
          return true;
        }
        this.router.navigate(['/']);
        return false;
      }),
      take(1)
    );
  }

I guess another reason this can happen, is because you are using the PreloadAllModules strategy. This will load all modules immediately, but I guess this is done when the user is not logged in yet and therefore the guard will return false and the module won't load, and won't be retried.

So you need to rethink if you want to use the canLoad guard this way, if you are using the PreloadAllModules. Perhaps the canActivate guard will be enough.

You can also implement a custom preload strategy, which somehow waits for the user to login to load it


With your additional information about the conditional router-outlet, it makes sense, because first the component will be placed in one router-outlet, but after the routing is completed, the other router-outlet becomes visible, but nothing has been placed in there :). You can update your template by utilizing the ngTemplateOutlet directive to circumvent this:

<ng-template #outlet>
  <router-outlet ></router-outlet>
</ng-template>

<div *ngIf="!isAuthRoute">
  <div class="row page-wrapper">
    <div class="col-md-3 col-md-pull-9">
      <app-sidebar></app-sidebar>
    </div>
    <div class="col-md-9 col-md-push-3 content-area">
      <ng-component [ngTemplateOutlet]="outlet"></ng-component>
    </div>
  </div>
</div>

<div style="height: 80vh" class="d-flex justify-content-center align-items-center" *ngIf="isAuthRoute">
  <ng-component [ngTemplateOutlet]="outlet"></ng-component>
</div>

This way you make sure that only one router-outlet is placed in the AppComponent, which is the way it should be done

0
votes

I can't see where you use AuthGuard in your AppRoutingModule.

Try to apply it to any route, e.g.:

{
    path: 'posts',
    canLoad: [AuthGuard],
    loadChildren: () => import('./modules/posts/posts.module').then(m => m.PostsModule),
},