53

I have an application that I am building that implements CanActivate on the dashboard route. It works fine except on page reload, I check a flag in the user service to see if a user is logged in or not. By default this flag is false which kicks the user out to login. Also on page reload I am trying to fetch user data with a token in localStorage, if fetch is successful, I want them to be able to stay on the dashboard. The problem is that I am seeing a glimpse of login and having to manually redirect them to the dashboard. Is there any way to fix this to where the authGuard doesn't do anything until after it checks the API? Code is here: https://github.com/judsonmusic/tfl

dashboard:

import { Component, ViewChild } from '@angular/core';
import { LoginComponent } from "../login.component";
import { UserService } from "../user.service";
import { SimpleChartComponent } from "../charts/simpleChart.component";
import { AppleChartComponent } from "../charts/appleChart.component";
import { BarChartComponent } from "../charts/barChart.component";
import { DonutChartComponent } from "../charts/donutChart.component";
import { AlertComponent } from 'ng2-bootstrap/ng2-bootstrap';
import { ModalDemoComponent } from "../modals/modalDemoComponent";
import { NgInitHelperComponent } from "../helpers/nginit.helper.component";
import { ModalDirective } from "ng2-bootstrap/ng2-bootstrap";
import { MODAL_DIRECTIVES, BS_VIEW_PROVIDERS } from 'ng2-bootstrap/ng2-bootstrap';


@Component({
  selector: 'dashboard',
  templateUrl: '/app/components/dashboard/dashboard.component.html',
  providers: [UserService, BS_VIEW_PROVIDERS],
  directives: [SimpleChartComponent, AppleChartComponent, BarChartComponent, DonutChartComponent, AlertComponent, ModalDemoComponent, NgInitHelperComponent, ModalDirective]
})
export class DashboardComponent  {

  public areas: any;

  constructor() {

    this.areas = [
      "Spiritual",
      "Habits",
      "Relationships",
      "Emotional",
      "Eating Habits",
      "Relaxation",
      "Exercise",
      "Medical",
      "Financial",
      "Play",
      "Work/ Life Balance",
      "Home Environment",
      "Intellectual Well-being",
      "Self Image",
      "Work Satisfaction"
    ]

  }
}

Routes:

import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './components/about.component';
import { PageNotFoundComponent } from "./components/pageNotFound.component";
import { HomeComponent } from "./components/home.component";
import { DashboardComponent } from "./components/dashboard/dashboard.component";
import { SurveyComponent } from "./components/survey/survey.component";
import { ResourcesComponent } from "./components/resources.component";
import { LogoutComponent } from "./components/logout.component";
import { AuthGuard } from "./components/auth-guard.service";
import { loginRoutes, authProviders } from './login.routing';
import { LoginComponent } from "./components/login.component";

const appRoutes:Routes = [
  { path: '', component: HomeComponent },
  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },
  { path: 'logout', component: LogoutComponent },
  { path: 'resources', component: ResourcesComponent },
  { path: 'survey', component: SurveyComponent },
  { path: 'about', component: AboutComponent },
  { path: 'login', component: LoginComponent },
  { path: '**', component: PageNotFoundComponent }
];

export const appRoutingProviders: any[] = [
  authProviders
];
export const routing = RouterModule.forRoot(appRoutes);

login route:

import { Routes }         from '@angular/router';
import { AuthGuard }      from './components/auth-guard.service';
import { AuthService }    from './components/auth.service';
import { LoginComponent } from './components/login.component';
export const loginRoutes: Routes = [
  { path: 'login', component: LoginComponent }
];
export const authProviders = [
  AuthGuard,
  AuthService
];
BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Judson Terrell
  • 4,204
  • 2
  • 29
  • 45

7 Answers7

96

In AuthGuard do the following:

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate() {
    if (/*user is logged in*/) {
      this.router.navigate(['/dashboard']);
      return true;
    } else {
      this.router.navigate(['/Login']);
    }
    return false;
  }
}
rashfmnb
  • 9,959
  • 4
  • 33
  • 44
  • 9
    Instead of hardcoding '/dashboard' is there a way to pull the intended route from the router? – Kirby Dec 12 '17 at 21:36
  • 4
    @Kirby return true, without redirecting – Markus Pscheidt Apr 04 '18 at 13:54
  • 1
    @Kirby For that, see this [question](https://stackoverflow.com/q/39747246/1056283) – Paul Siersma May 31 '18 at 14:08
  • 2
    Its better to keep redirection logic away from when the guard resolves to true. This is because if you're in some route other than dashboard on which also auth guard is applied, then a refresh of the page would redirect you to dashboard. – Dipendu Paul Mar 26 '20 at 11:39
13

Here's how to correctly handle redirects in a guard by using an UrlTree

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivateChild {
  constructor(
    private authService: AuthService,
    private logger: NGXLogger,
    private router: Router
  ) {}

  canActivateChild(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> {

    return this.authService.isLoggedIn().pipe(
      map(isLoggedIn => {
        if (!isLoggedIn) {
          return this.router.parseUrl('/login');
        }

        return true;
      })
    );
  }
}

Big thanks to Angular In Depth for the explanation!

michelepatrassi
  • 2,016
  • 18
  • 32
6

You can now return a UrlTree from an AuthGuard, or a boolean true / false.

Kind of amazed nobody has mentioned this yet! Sorry no example right now, but the idea is pretty simple.

Samuel Liew
  • 76,741
  • 107
  • 159
  • 260
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • A fuller example would be great. The linked article has a simple "authGuard as a redirect" example but attempting to integrate it with an existing Angular project (8) my root url just spins infinitely, apparently in some loop. Clearly the problem is on my end. – J E Carter II Jul 30 '19 at 14:44
  • There’s tonnes of examples of CanActivate out there and it’s no different except you return a URL tree. When you say spins do you think you’re getting an infinite loop? If you put a console.log statement inside the route guard how many times does it get printed ? – Simon_Weaver Jul 30 '19 at 15:45
  • Yes, it went into an infinite loop and your suggestion for the console statement paid off by helping me see what was happening but I couldn't get the guard to function properly so for the moment I'm bypassing it. I'll check around for some more current version canActivate examples. – J E Carter II Jul 30 '19 at 16:31
  • Sounds like you maybe have the CanActivate in either the wrong place or two places. It will run every time you go to that URL and if you’re redirecting to a child of the original URL it will run again (they are inherited). I think returning a tree is only intended to be used if authorization fails so by definition it would fail again if where you redirect to triggers the same CanActivate. – Simon_Weaver Jul 30 '19 at 18:11
  • That makes sense. I was using it for success and last night it occurred to me that it maybe is meant to be used as a sub router of sorts. Following your advise I'm closer. Success returns true now only, and the infinite loop is gone, but neither does it continue to the path the authGaurd is the canActivate handler for. I can work around this for now with a check in the target for the user token and reroute to login path if absent, so it's not a show stopper. Probably I have some other code causing the problem. – J E Carter II Jul 31 '19 at 11:00
  • I got it working @Simon_Weaver. The trouble was with an improperly subscribed service in the guarded route target preventing the final render. So returning a UrlTree for a fail and `true` for success is working well with that sorted. – J E Carter II Jul 31 '19 at 14:05
4

I actually changed my service to this and it works:

import { Injectable }             from '@angular/core';
import { CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot }    from '@angular/router';
import { AuthService }            from './auth.service';
import {UserService} from "./user.service";

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

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {



    if (this.authService.isLoggedIn){
      console.log('ATUH GUARD SAYD THEY ARE ALREADY LOGGED IN!');
      return true;


    }else {


      this.userService.getUser().subscribe((user) => {

        console.log('AUTH GUARD GETTING USER', user);

        if (user._id) {
        this.authService.isLoggedIn = true;
        // Store the attempted URL for redirecting
        this.authService.redirectUrl = state.url;
        this.router.navigate(['/dashboard']);
        return true;
        }else{
          console.log('Validation Failed.');
          localStorage.clear();
          this.router.navigate(['/login']);
          return false;
        }


      }, (error) => {
        console.log('There was an error.');
        this.router.navigate(['/login']);
        return false

      });

    }


  }
}
Judson Terrell
  • 4,204
  • 2
  • 29
  • 45
  • This isn't good at all sorry! You must return either a boolean or an observable. You can't subscribe to something and then return true/false inside that subscription because it won't go anywhere. This may kind of work but you're not using AuthGuard properly. Instead you should `return userService.getUser().pipe(...)` and can use `tap` or `switchMap` inside to run your logic. – Simon_Weaver Apr 04 '19 at 05:39
0

I solved it like this and used it in my AuthGuard

isLoggedIn(): Observable<boolean> {
return this.afAuth.authState
  .pipe(
    take(1),
    map(user => {
        return !!user;
      },
      () => {
        return false;
      }
    ),
    tap(loggedIn => {
        if (!loggedIn) {
          this.router.navigate(['/']);
        }
      }
    ));
}
daniel gi
  • 396
  • 1
  • 7
  • 19
0

This is what I did for canActivate

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    // ensure the user is properly logged in and the url he's requesting is within his right
    if (this.authSvc.getRole().trim().length > 0 && this.authSvc.getToken().trim().length > 0
      && state.url.includes(this.authSvc.getRole().trim())) {
      let url: string;
      // base on user's role send to different url to check
      if (this.authSvc.getRole().trim() === 'client') {
        url = ClientAccessRequestUrl;
      } else {
        url = AdminAccessRequestUrl;
      }
      return this.http.post<AccessRequestResponse>(url, {
        token: this.authSvc.getToken(),
      }).pipe(map(response => {
        console.log('response is:', response);
        // check successful then let go
        if (response.reply === true) {
          return true;
          // unless go to visitor site
        } else {
          return this.router.createUrlTree(['/visitor']);
        }
      }));
    } else {
      return this.router.createUrlTree(['/visitor']);
    }
  }
Qin Chenfeng
  • 451
  • 6
  • 5
0

The best way to do redirects after authentication is structuring the logic as shown below;

  1. in the AuthGuard,
canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree 
    {

      // keep the requsted url for redirect after login
      let url: string = state.url;

      // call the authentication function
      var authenticated = this.http.isAuthenticated();
      
      var subject = new Subject<boolean>();

      authenticated.subscribe(

      (res) => {
        //if not authenticated redirect to the login route with the initial route attached as an query param 'returnUrl'
        if(!res.successState) {
          this.router.navigate(['/login'], {queryParams: {returnUrl: url}});
        }else{

        // the user is authenticated just go to the requested route

          subject.next(res.successState);

        }

      });

      return subject.asObservable();

    }


  1. in the login route
  loginAction(data: any){
    // if the auth works fine the go the route requested before the inconviniences :)
    if(data.successState){

    // get the query params to see if there was a route requested earlier or they just typed in the login route directly
    this.route.queryParams.subscribe(params => {
      // console.log(params); //returnUrl

      if(params.returnUrl){

        // pearse the url to get the path and query params
        let urlTree: UrlTree = this.router.parseUrl(params.returnUrl);
        let thePathArray : any[] = [];
        // populate it with urlTree.root.children.primary.segments[i].path;
        for(let i = 0; i < urlTree.root.children.primary.segments.length; i++){
          thePathArray.push(urlTree.root.children.primary.segments[i].path);
        }
        let the_params = urlTree.queryParams;

        this.router.navigate(thePathArray, {queryParams: the_params});

      }else{

        this.router.navigate(['']);

      }
    });

    }else{
    // tell them about it and let them try again or reset the password 
    }

 }

That should work perfectly. it will even preserve query params for the initial request.

THANKS

Muasya
  • 44
  • 4