I am trying to implement canActivate
in Angular 2/4 using RxJS observables. I have already read another SO question on this. With the following code, my canActivate
method works only once, when the app launches, but hello
is never printed again when the isLoggedIn
observable triggers new values.
canActivate(): Observable<boolean> {
return this.authService.isLoggedIn().map(isLoggedIn => {
console.log('hello');
if (!isLoggedIn) {
this.router.navigate(['/login']);
}
return isLoggedIn;
}).first();
}
or this version is not working as well:
canActivate(): Observable<boolean> {
return this.authService.isLoggedIn().map(isLoggedIn => {
console.log('hello');
if (isLoggedIn) {
this.router.navigate(['/']);
}
return !isLoggedIn;
});
}
However, it works fine with this code:
canActivate(): Observable<boolean> {
return Observable.create(obs => {
this.authService.isLoggedIn().map(isLoggedIn => {
console.log('hello');
if (isLoggedIn) {
this.router.navigate(['/']);
}
return !isLoggedIn;
}).subscribe(isLoggedIn => obs.next(isLoggedIn));
});
}
What am I doing wrong in the first piece of code ?
EDIT: here is the isLoggedIn
implementation
@LocalStorage(AuthService.JWT_TOKEN_KEY)
private readonly token: string;
private tokenStream: Subject<string>;
public isLoggedIn(): Observable<boolean> {
if (!this.tokenStream) {
this.tokenStream = new BehaviorSubject(this.token);
this.storage.observe(AuthService.JWT_TOKEN_KEY)
.subscribe(token => this.tokenStream.next(token));
}
return this.tokenStream.map(token => {
return token != null
});
}
that uses ngx-webstorage
. and RxJS BehaviorSubject
.
This was one of the things I struggled with when switching from AngularJs's promises to Angular's Observable pattern. You see promises are pull notifications and observables are push notifications. As such, you have to rethink your AuthService so that it uses a push pattern. I kept thinking in terms of pulling even when I wrote working Observables. I couldn't stop thinking in terms of pulling.
With a promise pattern it was easier. When the AuthService was created it would either create a promise that resolved to "not logged in" or it would create an async promise that would "restore logged state". You could then have a method named isLoggedIn()
that would return that promise. That allowed you to easily handle delays between showing user data and when you receive user data.
Now, we switch to Observables and the verb "is" needs to be changed to "when". Making this small change helps you re-think how things are going to work. So lets rename "isLoggedIn" to "whenLoggedIn()" which will be an Observable that emits data when a user authenticates.
class AuthService {
private logIns: Subject = new Subject<UserData>();
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
// example
AuthService.whenLoggedIn().subscribe(console.log);
AuthService.setUser(new UserData());
When a user passed to setUser
it's emitted to subscribes that a new user has been authenticated.
The above introduces several problems that need to be fixed.
whenLoggedIn
will listen for new users forever. The pull stream is never completed.setUser
is lost after being pushed to subscribers.We can fix some of this by switching from Subject
to BehaviorSubject
.
class AuthService {
private logIns: Subject = new BehaviorSubject<UserData>(null);
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
// example
AuthService.whenLoggedIn().first().subscribe(console.log);
AuthService.setUser(new UserData());
This is much closer to what we want.
BehaviorSubject
will always emit the last value for each new subscription.whenLoggedIn().first()
was added to subscribe and auto unsubscribe after the first value is received. If we didn't use BehaviorSubject
this would block until someone called setUser
which might never happen.BehaviorSubject
doesn't work for AuthService and I'll demonstrate with this sample code here.
class AuthService {
private logIns: Subject = new BehaviorSubject<UserData>(null);
public constructor(userSessionToken:string, tokenService: TokenService) {
if(userSessionToken) {
tokenService.create(userSessionToken).subscribe((user:UserData) => {
this.logIns.next(user);
});
}
}
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
Here's how the problem would appear in your code.
// example
let auth = new AuthService("my_token", tokenService);
auth.whenLoggedIn().first().subscribe(console.log);
The above creates a new AuthService with a token to restore the user session, but when it runs the console just prints "null".
This happens because BehaviorSubject
is created with an initial value of null
, and the operation to restore the user session is going to happen later after the HTTP call is complete. AuthService will continue to emit null
until the session is restored, but that's a problem when you want to use route activators.
We want to remember the current user, but not emit anything until we know if there is a user or not. ReplaySubject
is the answer to this problem.
Here's how you would use it.
class AuthService {
private logIns: Subject<UserData> = new ReplaySubject(1);
public constructor(userSessionToken:string, tokenService: TokenService) {
if(userSessionToken) {
tokenService.create(userSessionToken).subscribe((user:UserData) => {
this.logIns.next(user);
}, ()=> {
this.logIns.next(null);
console.error('could not restore session');
});
} else {
this.logIns.next(null);
}
}
public setUser(user: UserData) {
this.logIns.next(user);
}
public whenLoggedIn(): Observable<UserData> {
return this.logIns;
}
}
// example
let auth = new AuthService("my_token", tokenService);
auth.whenLoggedIn().first().subscribe(console.log);
The above will not wait until the first value is emitted by whenLoggedIn
. It will get the first
value and unsubscribe.
ReplaySubject
works because it remembers the 1
items or emits nothing. It's the nothing part that is important. When we use AuthService in canActivate
we want to wait until a user state is known.
This now makes it a lot easier to write a user guard that redirects to a login screen or allows the route to change.
class UserGuard implements CanActivate {
public constructor(private auth: AuthService, private router: Router) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.auth.whenLoggedIn()
.first()
.do((user:UserData) => {
if(user === null) {
this.router.navigate('/login');
}
})
.map((user:UserData) => !!user);
}
This will yield an Observable of true or false if there is a user session. It will also block router change until that state is known (i.e. are we fetching data from the server?).
It will also redirect the router to the login screen if there is no user data.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With