web-dev-qa-db-fra.com

Angular 5 canActivate redirecting to login on browser refresh

Application d'authentification Angular 5 utilisant angularfire2 et firebase. L'application fonctionne très bien en utilisant des liens intégrés à l'application, par exemple rediriger vers le tableau de bord après connexion ou lien vers une autre page (composant) via un bouton/lien dans l'application. Cependant, si sur la page " http: // localhost: 4300/dashboard ", j'appuie sur l'actualisation du navigateur (Chrome), il me redirige vers la page de connexion. L'utilisation de BACK/NEXT sur le navigateur fonctionne bien - mais je suppose que je ne demande pas spécifiquement d'aller sur un itinéraire particulier.

J'ai un NavBar qui, grâce à l'abonnement, identifie si je suis connecté ou non (voir capture d'écran en haut à droite ...) - et tout cela fonctionne bien.

Login page

Je suppose que lors de l'actualisation du navigateur ou de la navigation URL directe, il essaie de charger la page avant d'identifier si je suis déjà authentifié ou non. La console de développement suggère cela à partir des instructions console.log que j'ai insérées dans le composant nav-bar et le fait qu'elles ne soient pas définies avant Angular suggère que nous fonctionnons en mode dev:

Developer Tools Console

app.routes:

import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './views/login/login.component';
import { DashboardComponent } from './views/dashboard/dashboard.component';
import { ProfileComponent } from './views/profile/profile.component';

import { AuthGuard } from './services/auth-guard.service';

const appRoutes: Routes = [
  {
    path: '',
    component: LoginComponent
  },
  {
    path: 'dashboard',
    canActivate: [AuthGuard],
    component: DashboardComponent
  },
  {
    path: 'profile',
    canActivate: [AuthGuard],
    component: ProfileComponent
  },
  {
    path: '**',
    redirectTo: ''
  }
];

export const AppRoutes = RouterModule.forRoot(appRoutes);

auth-gaurd:

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

@Injectable()
export class AuthGuard implements CanActivate {
  status: string;

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

  canActivate() {
    this.authService.authState.subscribe(state =>
      this.status = state.toString());

    console.log('Can Activate ' + this.authService.authState);
    console.log('Can Activate ' + this.authService.isLoggedIn());
    console.log('Can Activate ' + this.status);

    if(this.authService.isLoggedIn()) {
      return true;
    }

    this.router.navigate(['/']);
    return false;
  }
}

auth.service:

import { Injectable } from '@angular/core';
import { Router } from "@angular/router";

import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase/app';
import { Observable } from 'rxjs/Observable';
import { GoogleAuthProvider, GoogleAuthProvider_Instance } from '@firebase/auth-types';
import { userInfo } from 'os';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class AuthService {
  private user: Observable<firebase.User>;
  private userDetails: firebase.User = null;

  public authState = new Subject();

  constructor(private _firebaseAuth: AngularFireAuth, private router: Router) { 
    this.user = _firebaseAuth.authState;

    this.user.subscribe((user) => {
      if (user) {
        this.userDetails = user;
        this.authState.next('Logged In');
        //console.log(this.userDetails);
      } else {
        this.userDetails = null;
        this.authState.next('Not Logged In');
      }
    });
  }

  isLoggedIn() {
    if (this.userDetails == null) {
      return false;
    } else {
      return true;
    }
  }
}

nav-bar.component:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../services/auth.service';

@Component({
  selector: 'app-nav-bar',
  templateUrl: './nav-bar.component.html',
  styleUrls: ['./nav-bar.component.css']
})
export class NavBarComponent implements OnInit {
  status: string;

  constructor(private authService: AuthService) {
    console.log('Constructor ' + this.status);
  }

  ngOnInit() {
    //this.authService.isLoggedIn().subscribe((state) => this.status = state.toString());
    this.authService.authState.subscribe(state =>
      this.status = state.toString());
    console.log('ngOnInit ' + this.status);
  }
}
10
Woody

La méthode canActivate() est appelée directement lors de l'actualisation de la page. Il renvoie donc toujours false:

canActivate() {
  this.authService.authState.subscribe(state => {
    this.status = state.toString(); // This is called async/delayed.
  });
  // so method execution proceeds

  // isLoggedIn() returns false since the login stuff in AuthService.constructor
  // is also async:    .subscribe((user) => { /* delayed login */ });
  if(this.authService.isLoggedIn()) {
    return true;
  }

  // so it comes here
  this.router.navigate(['/']); // navigating to LoginComponent
  return false;                // and canActivate returns false
}

La solution:

import { CanActivate, Router, ActivatedRouteSnapshot,
         RouterStateSnapshot } from '@angular/router';

// ...

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
  // when the user is logged in and just navigated to another route...
  if (this.authService.isLoggedIn) { return true; } 

  // proceeds if not loggedIn or F5/page refresh 

  // Store the attempted URL for redirecting later
  this.authService.redirectUrl = state.url;

  // go login page
  this.router.navigate(['/']);
  return false;
}

maintenant, de retour dans le AuthService peu modifié: (seulement ont changé/code pertinent ici)

export class AuthService {

  // new
  redirectUrl: string;

  // BehaviorSubjects have an initial value.
  // isLoggedIn is property (not function) now:
  isLoggedIn = new BehaviorSubject<boolean>(false);

  // params declared private and public in constructor become properties of the class
  constructor(private firebaseAuth: AngularFireAuth, private router: Router) {
    // so this.user is not required since it is reference to this.firebaseAuth
    this.firebaseAuth.authState.subscribe((user) => {
      if (user) {
        this.loggedIn.next(true);

        // NOW, when the callback from firebase came, and user is logged in,
        // we can navigate to the attempted URL (if exists)
        if(this.redirectUrl) {
          this.router.navigate([this.redirectUrl]);
        }
      } else {
        this.loggedIn.next(false);
      }
    }
  }

}

Remarque: J'ai écrit ce code dans la boîte de réponse et je l'ai compilé dans ma tête. Il peut donc exister des bogues. Je ne sais pas non plus si c'est réellement la meilleure pratique. Mais l'idée doit être claire?!

Basé sur le Angular Routing Guide

Il semble qu'il existe des problèmes/solutions similaires: Angular 2 AuthGuard + Firebase Auth

12
Martin Schneider