0

I am new to Rxjs. I am trying to implement a canActivate guard for my nativescript-angular app. Basically, if the user has JWT token in the storage, it will try to check for the JWT validity from the server, and land on the homepage if the token still valid, or redirect to the login page if its invalid. The following code works fine when the server is up, however, when the server is down, it will land on an incomplete homepage because data is not available to fetch from the server. This happen because the synchronous if(this.authService.isLoggedIn()) do not wait for the asynchronous http call and return true (code below). I want it to redirect to login page instead of the homepage by blocking when the server is down, I have tried various ways (even refactoring the code) but none of them works. This and this are quite similar to my case, but the solutions are not helpful. Here is my code:

Edit: auth.service.ts (Updated)

import { getString, setString, remove } from 'application-settings';

interface JWTResponse {
  status: string,
  success: string,
  user: any
};
export class AuthService {
  tokenKey: string = 'JWT';
  isAuthenticated: Boolean = false;
  username: Subject<string> = new Subject<string>();
  authToken: string = undefined;

  constructor(private http: HttpClient) { }

  loadUserCredentials() { //call from header component
    if(getString(this.tokenKey)){ //throw error for undefined
      var credentials = JSON.parse(getString(this.tokenKey));
      if (credentials && credentials.username != undefined) {
        this.useCredentials(credentials);
        if (this.authToken)
          this.checkJWTtoken();
      }  
    }
  }  

  checkJWTtoken() {
    this.http.get<JWTResponse>(baseURL + 'users/checkJWTtoken') //call server asynchronously, bypassed by synchronous code in canActivate function
    .subscribe(res => {
      this.sendUsername(res.user.username);
    },
    err => {
      this.destroyUserCredentials();
    })
  }  

  useCredentials(credentials: any) {
    this.isAuthenticated = true;
    this.authToken = credentials.token;
    this.sendUsername(credentials.username);
  }  

  sendUsername(name: string) {
    this.username.next(name);
  }  

  isLoggedIn(): Boolean{
    return this.isAuthenticated;
  }

  destroyUserCredentials() {
    this.authToken = undefined;
    this.clearUsername();
    this.isAuthenticated = false;
    // remove("username");
    remove(this.tokenKey); 
  }

  clearUsername() {
    this.username.next(undefined);
  }  

auth-guard.service.ts

 canActivate(route: ActivatedRouteSnapshot,
        state:RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean{

            this.authService.loadUserCredentials(); //skip to next line while waiting for server response
            if(this.authService.isLoggedIn()){ //return true due useCredentials called in the loadUserCredentials
                return true;
            }
            else{
                this.routerExtensions.navigate(["/login"], { clearHistory: true })
                return false;
            }
    }

app.routing.ts

const routes: Routes = [
    { path: "", redirectTo: "/home", pathMatch: "full" },
    { path: "login", component: LoginComponent },
    { path: "home", component: HomeComponent, canActivate: [AuthGuard] },
    { path: "test", component: TestComponent },
    // { path: "item/:id", component: ItemDetailComponent },
];

Edit2: I tried these code:

auth-guard.service.ts

canActivate(route: ActivatedRouteSnapshot,
    state:RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean{

    this.authService.loadUserCredentials();
    if(this.authService.isLoggedIn()){
        console.log("if");
        return true;
    }
    else{
        console.log("else");
        this.routerExtensions.navigate(["/login"], { clearHistory: true })
        return false;
    }
}

auth.service.ts

  async checkJWTtoken() {
    console.log("checkJWTtoken1")    
    try{
      console.log("try1")
      let res = await this.http.get<JWTResponse>(baseURL + 'users/checkJWTtoken').toPromise();
      console.log("try2")
      this.sendUsername(res.user.username);
      console.log("try3")
    }
    catch(e){
      console.log(e);
      this.destroyUserCredentials();
    }
    console.log("checkJWTtoken2")    
  }  


  // checkJWTtoken() {
  //   this.http.get<JWTResponse>(baseURL + 'users/checkJWTtoken')
  //   .subscribe(res => {
  //     // console.log("JWT Token Valid: ", JSON.stringify(res));
  //     this.sendUsername(res.user.username);
  //   },
  //   err => {
  //     console.log("JWT Token invalid: ", err);
  //     this.destroyUserCredentials();
  //   })
  // }  

and here is the console output:

JS: Angular is running in the development mode. Call enableProdMode() to enable the production mode.
JS: checkJWTtoken1
JS: try1
JS: if
JS: ANGULAR BOOTSTRAP DONE. 6078
JS: try2
JS: try3
JS: checkJWTtoken2

Edit3:

tsconfig.json

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "noEmitHelpers": true,
        "noEmitOnError": true,
        "lib": [
            "es6",
            "dom",
            "es2015.iterable"
        ],
        "baseUrl": ".",
        "paths": {
            "*": [
                "./node_modules/tns-core-modules/*",
                "./node_modules/*"
            ]
        }
    },
    "exclude": [
        "node_modules",
        "platforms"
    ]
}

Please advise and thanks in advance.

lanette
  • 35
  • 10

3 Answers3

1

You need to have an Observable to determine whether the user is logged in, there are many ways to do, one of which is this

Define a behaviour subject in authservice

userLoggedIn = new BehaviorSubject(null)

and emit the user logged in event

loadUserCredentials() { //call from header component
    if(getString(this.tokenKey)){ //throw error for undefined
      var credentials = JSON.parse(getString(this.tokenKey));
      if (credentials && credentials.username != undefined) {
        this.useCredentials(credentials);
        this.userLoggedIn.next(true)
         //Make sure you set it false when the user is not logged in
        if (this.authToken)
          this.checkJWTtoken();
      }  
    }
  }

and modifiy canActivate method to wait for observers

    canActivate(route: ActivatedRouteSnapshot,
            state:RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean{

            return Observable.create(observer => {
               this.authService.userLoggedIn.subscribe(data => {
               if(data !== null) {
                 if(data) observer.next(true)
                 else observer.next(false)
               }
               }
        }  
Manzur Khan
  • 2,366
  • 4
  • 23
  • 44
  • Hi @Manzur, thanks for the response, I have added `userLoggedIn: Subject = new Subject()` and initialize to false in the constructor `this.userLoggedIn.next(false)` in my auth.service, and I got a blank screen in my app as a result, am I missing something? – lanette Apr 04 '18 at 08:52
  • 1
    Your auth service must be running before the canActivate Method, so you are subscribing after the emit event occured in auth service. – Manzur Khan Apr 04 '18 at 08:54
  • 1
    In this case you can use a behaviour subject, pass null as the default value and in canActivate, ignore the null values – Manzur Khan Apr 04 '18 at 08:54
  • Updated the answer – Manzur Khan Apr 04 '18 at 08:58
  • where should I call `loadUserCredentials()` in canActivate? Shouldn't I do something like `loadUserCredentials().subscribe(result => { //check userLoggedIn }` ? – lanette Apr 04 '18 at 12:45
  • Because when I call `loadUserCredentials()` inside `Observable.create()`, it still do not wait for the server call and skip to `this.authService.userLoggedIn.subscribe(data => {})` – lanette Apr 04 '18 at 12:50
  • No, you can call loadUserCredentials() in canActivate, and the userLoggedIn subscription will wait and let the canActivate know what to do – Manzur Khan Apr 05 '18 at 12:02
  • I tried to call `loadUserCredentials()` both right before `return Observable.create()` or right before `this.authService.userLoggedIn.subscribe()` but the the app screen showed up blank as mentioned, this happen when I try to return observable in canActivate, am I still missing something? Thanks. – lanette Apr 05 '18 at 15:23
0

I suggest to refactor your code to return Observable from loadUserCredentials method and it should trigger that observable only when JWT token check is done.

Also, refactor your canActivate guard to return Observable instead of static/primitive value.

Ashish Patel
  • 317
  • 1
  • 8
0

You could modify your service to make the checkJWTToken wait for http call to complete before returning

  import 'rxjs/add/operator/toPromise';

  //...
  async checkJWTtoken() {

    try
    {
        let res = await this.http.get<JWTResponse>(baseURL + 'users/checkJWTtoken').toPromise(); //call server synchronously
        this.sendUsername(res.user.username);
    }
    catch(e)
    {
        console.log(e);
        this.destroyUserCredentials();
    }
  }  

Edit When using async/await, the code just 'waits' where you use await, which in within the checkJWTToken method. BUt the code from the parent execution continues as normal

You need to use async/await up the call chain, u to where you want to really wait (which in in canActivate)

guard

async canActivate(...) {
    //..
    await this.authService.loadUserCredentials();

service

async loadUserCredentials(){
//...
   await this.checkJWTtoken();

async checkJWTtoken() {
//...
//let res = await this.http.get<JWTResponse>(baseUR

I created a stackblitz demo

David
  • 33,444
  • 11
  • 80
  • 118