Search code examples
javascriptangularhttp-redirectangular-router

Angular 4, Router 4.0.0: routerLink or router.navigate programatically doesn't redirect to child routes


I'm trying to create a link to get redirected after login(/signin route) to a /dashboard/overview child route without any luck. I click the link and I get no errors nor any response. I can see the path on the bottom bar of the browser that is correct and If I enter the url manually I access the correct page, the route is /dashboard/overview. One thing I'm not sure if it has anything to do is that the route is AuthGuarded.

I tried both programatically after sign in redirect the user to a dashboard, I can even see the 'redirecting to dashboard' message on chrome console

onSignin(form: NgForm){
    const email = form.value.email;
    const password = form.value.password;
    this.user = {email, password};
     this.authServiceSubscription = this.authService.signinUser(this.user).subscribe(
       (response) => {
         //redirect to dashboard
        const loginResultCode = response.login_result_code;
         if (loginResultCode == "SUCCESS") {
                       console.log("Sponsor logged in");
                       this.authService.changeStatusToAuthenticated();
                       //redirect to dashboard
                       console.log('Redirecting to dashboard');
                       this.router.navigate(['/dashboard/overview']);
                   } else {
                       console.log("There were errors with the data");
                       //present errors to the user
                       this.errorMessage = "Los datos de autenticación son incorrectos. Intente nuevamente";
                   }
       },
       (error) => { console.log("Error Login", error); this.errorMessage = "Hubo un error interno, intente de nuevo mas tarde";}
       );
  }

And also creating a routerLink, but it doesn't work either, nothing happens, not even an error in the console:

  <li><a style="cursor: pointer;" routerLink="/dashboard/overview">Go To Dashboard</a></li>

Here's my routing file:

const appRoutes: Routes = [
  { path: '', redirectTo: '/', pathMatch:'full'},
  { path: '', component: MainComponent },
  { path: 'signin', component:SigninComponent},
  { path: 'signup', component: SignupComponent},
  { path: 'dashboard', canActivate:[AuthGuard],component: DashboardComponent,
    children: [
      { path: '', redirectTo:'/dashboard/overview', pathMatch: 'full'},
      { path: 'overview', component: OverviewCampaignsComponent },
      { path: 'active', component: ActiveCampaignsComponent},
      { path: 'history', component: HistoryCampaignsComponent}
    ] },
  { path: 'not-found', component: ErrorPageComponent },
  { path: '**', redirectTo: '/not-found' }

]

I even put a console.log on the ngOnInit of the Dashboard component to see if the component gets created, or in the overview component but I didn't have any luck, I couldn't see any messages on console when navigating programatically nor with routerLink. I did get the message when I accessed manually as I stated above. Any ideas? Thank you very much

EDIT: Apparently is a problem with the authguard I'm applying to the dashboard route, this is the AuthGuard file, could it be that it's not catching some error or maybe the returned values are not the ones that should be??:

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


@Injectable()
export class AuthGuard implements CanActivate {

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

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated().map(isAuth => {
      if (isAuth){
        console.log("Auth Guard approves the access");
        return true;
      }
      else {
        console.log('AuthGuard Denying route, redirecting to signin');
        this.router.navigate(['/signin']);
        return false;
      }
    });
  }
}

the isAuthenticated() method on the authService just returns an observable with the auth state of the user. I wonder if there's a race condition or something...cause that observable gets set initially by making an http async request....If I put a console.log in isAuthenticated method it gets logged on the console. If I put a console.log inside the map on the authguard function in or out the if it doesn't get logged so for some reason that code is not being executed....

auth.service.ts

    import { Injectable, OnInit } from '@angular/core';
    import { Router } from '@angular/router';
    import { Http, Response, RequestOptions, Headers } from '@angular/http';
    import 'rxjs/add/operator/map';
    import {Observable, Subject} from "rxjs/Rx";

    @Injectable()
    export class AuthService implements OnInit {
     userIsAuthenticated = new Subject();
      constructor(private router: Router, private http: Http) {
        this.ngOnInit();
      }

      private getHeaders(){
          let headers = new Headers();
          headers.append('Content-Type', 'application/json');
          headers.append('Accept', 'application/json');
          headers.append('Authorization','Bearer');
          return headers;
      }

      ngOnInit(){

        this.changeStatusToUnauthenticated();
        //initial check with the server
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });

       this.http.get('http://localhost:3000/api/sponsor/check/login',options)
          .map(response => {
        console.log("Execute this");
            if (response.status === 200) {
              console.log("execute also this");
              this.changeStatusToAuthenticated();
              return Observable.of(true);
            }
          }
        ).catch((err)=>{
          //maybe add in the future if the code is 403 then send him to login otherwise send him elsewhere
          if(err.status === 403){
            console.log('Forbidden 403');
    //        If I want to redirect the user uncomment this line
    //        this.router.navigate(['/signin']);
          }
          this.changeStatusToUnauthenticated();
          return Observable.of(false);
        }).subscribe((isAuth)=>{
           console.log("Initial refresh auth state ", isAuth);
        });

      }

      isAuthenticated(): Observable<boolean> {
  if(this.userIsAuthenticated){
 //if I change this line for return Observable.of(true) it works
   return this.userIsAuthenticated;
 }else{
   return Observable.of(false);
 }
  }

      logout() {
        console.log('logging out');
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
        return this.http.get('http://localhost:3000/api/sponsor/logout/', options).map(res=>res.json())
        .subscribe(
            (response) => {
              //redirect to dashboard
             const logoutResultCode = response.code;
              if (logoutResultCode == "200") {
                            console.log("Sponsor logged out successfully");
                            //redirect to dashboard
                            this.changeStatusToUnauthenticated();
                            this.router.navigate(['/signin']);
                        }
            },
            (error) => {
              console.log("Error Logout- Header", error);
              //check for 403 if it's forbidden or a connection error
              this.changeStatusToUnauthenticated();
              this.router.navigate(['/signin']);}
          );

      }

      signinUser(user) {
        console.log("Logging user");
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
        return this.http.post('http://localhost:3000/api/sponsor/login/', user, options).map(
          response => response.json());
      }

      registerUser(user) {
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
        return this.http.post('http://localhost:3000/api/sponsor/register/', user, options).map(
          response => response.json());
      }

      changeStatusToUnauthenticated(){
        this.userIsAuthenticated.next(false);
      }

      changeStatusToAuthenticated(){
        this.userIsAuthenticated.next(true);
      }


    }

EDIT 2: I used Behaviour Subject instead of Subject on the authService cause it lets me get the last emitted value which is a pretty cool feature compared to the regular subject in which you have to subscribe, which sometimes is not enough. More details on my answer below.


Solution

  • In the end the problem was in what the authService returned on the isAuthenticated() method, I was not returning a resolved value apparently according to the logs, so the authguard got stucked before being able to resolve the route to the component. I solved my question by searching on the rxjs documentation. I found the BehaviorSubject https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md

    It lets you get the last value emmited so I can return an Observable.of(userIsAuthenticated.getValue()) and deliver it to the AuthGuard and it works perfectly now. I added the logic that if the last emitted value was false then I do a dummy request to decide if the user should be sent to the login screen. Then this goes hand in hand with changing the value of the BehaviourSubject to false if I get an http forbidden response on EVERY request I make to the server. This things combined will assure consistency between frontend and backend traditional sessions, avoiding the expired session on the backend and non-expired state on the frontend. Hope this helps someone. The code:

    auth.service.ts

            @Injectable()
            export class AuthService implements OnInit {
             userIsAuthenticated= new BehaviorSubject(null);
              constructor(private router: Router, private http: Http) {
                this.ngOnInit();
              }
    
              private getHeaders(){
                  let headers = new Headers();
                  headers.append('Content-Type', 'application/json');
                  headers.append('Accept', 'application/json');
                  headers.append('Authorization','Bearer');
                  return headers;
              }
    
    
          ngOnInit() {
            //initial check with the server
            this.doAuthCheck();
    
          }
    
          doAuthCheck(): Observable<boolean> {
    
            let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
    
            return this.http.get('http://localhost:3000/api/check/login', options)
              .map(response => {
                if (response.status === 200) {
                  this.changeStatusToAuthenticated();
                  return Observable.of(true);
                }
              }
              ).catch((err) => {
                //maybe add in the future if the code is 403 then send him to login otherwise send him elsewhere
                if (err.status === 403) {
                  console.log('Forbidden 403');
                  //        If I want to redirect the user uncomment this line
                  //        this.router.navigate(['/signin']);
                }
                this.changeStatusToUnauthenticated();
                return Observable.of(false);
              });
          }
    
          isAuthenticated(): Observable<boolean> {
            const isAuth = this.userIsAuthenticated.getValue();
            if (isAuth) {
              return Observable.of(isAuth);
            } else {
              return this.doAuthCheck();
            }
          }
    
    
      logout() {
        console.log('logging out');
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
        return this.http.get('http://localhost:3000/api/logout/', options).map(res => res.json())
          .subscribe(
          (response) => {
            //redirect to dashboard
            const logoutResultCode = response.code;
            if (logoutResultCode == "200") {
              console.log("logged out successfully");
              //redirect to dashboard
              this.changeStatusToUnauthenticated();
              this.router.navigate(['/signin']);
            }
          },
          (error) => {
            console.log("Error Logout- Header", error);
            //check for 403 if it's forbidden or a connection error
            this.changeStatusToUnauthenticated();
            this.router.navigate(['/signin']);
          }
          );
    
      }
    
    
    
      signinUser(user) {
        console.log("Logging user");
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
        return this.http.post('http://localhost:3000/api/login/', user, options).map(
          response => response.json());
      }
    
      registerUser(user) {
        let options = new RequestOptions({ headers: this.getHeaders(), withCredentials: true });
        return this.http.post('http://localhost:3000/api/register/', user, options).map(
          response => response.json());
      }
    
    changeStatusToUnauthenticated() {
        this.userIsAuthenticated.next(false);
      }
    
      changeStatusToAuthenticated() {
        this.userIsAuthenticated.next(true);
      }
    
            }
    

    auth-guard.service.ts

    import { Injectable } from '@angular/core';
    import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
    import { AuthService } from './auth.service';
    import { Observable } from 'rxjs/Observable';
    
    
    @Injectable()
    export class AuthGuard implements CanActivate {
    
      constructor(private authService: AuthService, private router: Router) { }
    
      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
        return this.authService.isAuthenticated().map(isAuth => {
          console.log("is Authenticated",isAuth);
          if (isAuth){
            console.log("Auth Guard approves the access");
            return true;
          }
          else {
            console.log('AuthGuard Denying route, redirecting to signin');
            this.router.navigate(['/signin']);
            return false;
          }
        });
      }
    }
    

    routes file

    const appRoutes: Routes = [
      { path: '', redirectTo: '/', pathMatch:'full'},
      { path: '', component: MainComponent },
      { path: 'signin', component:SigninComponent},
      { path: 'signup', component: SignupComponent},
      { path: 'dashboard', canActivate:[AuthGuard],component: DashboardComponent,
        children: [
          { path: '', redirectTo:'/dashboard/overview', pathMatch: 'full'},
          { path: 'overview', component: OverviewCampaignsComponent },
          { path: 'active', component: ActiveCampaignsComponent},
          { path: 'history', component: HistoryCampaignsComponent}
        ] },
      { path: 'not-found', component: ErrorPageComponent },
      { path: '**', redirectTo: '/not-found' }
    
    ]