web-dev-qa-db-de.com

Angular 5 canActivate-Umleitung zur Anmeldung bei Browseraktualisierung

Angular 5 Authentifizierungs-App mit anglefire2 und firebase. Die App funktioniert einwandfrei mit In-App-Links, z. Weiterleitung zum Dashboard nach Anmeldung oder Link zu einer anderen Seite (Komponente) über einen Button/Link in der App. Wenn ich jedoch auf der Seite " http: // localhost: 4300/dashboard " auf die Browseraktualisierung (Chrome) tippe, werde ich zurück zur Anmeldeseite geleitet. Die Verwendung von BACK/NEXT im Browser funktioniert einwandfrei - aber ich vermute, weil ich nicht ausdrücklich nach einer bestimmten Route frage.

Ich habe eine NavBar, die über ein Abonnement feststellt, ob ich angemeldet bin oder nicht (siehe Screenshot oben rechts ...) - und das alles funktioniert einwandfrei.

Login page

Ich vermute, dass bei einer Browseraktualisierung oder einer direkten URL-Navigation versucht wird, die Seite zu laden, bevor festgestellt wird, ob ich bereits authentifiziert bin oder nicht. Die dev-Konsole schlägt dies aus den console.log-Anweisungen vor, die ich in die Navigationsleisten-Komponente eingefügt habe, und die Tatsache, dass sie "undefiniert" sind, bevor Angular core angibt, dass wir im dev-Modus ausgeführt werden:

Developer Tools Console

Anwendungsrouten:

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

Die canActivate() -Methode wird direkt bei der Seitenaktualisierung aufgerufen. Es wird also immer false zurückgegeben:

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
}

Die Lösung:

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

jetzt zurück in den wenig geänderten AuthService: (habe nur den/relevanten Code hier geändert)

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);
      }
    }
  }

}

Hinweis: Ich habe diesen Code in das Antwortfeld geschrieben und in meinem Gehirn zusammengestellt. Es können also Bugs existieren. Ich weiß auch nicht, ob dies wirklich die beste Vorgehensweise ist. Aber die Idee sollte klar sein ?!

Basierend auf dem Angular Routing Guide

Es scheint, als gäbe es ähnliche Probleme/Lösungen: Angular 2 AuthGuard + Firebase Auth

12