Colin Ragnarök
Colin Ragnarök

Reputation: 1062

Page has to be refreshed in order to get data from the api

I have a working auth. flow. There is just one strange bug. When I log in with a first user I first have to refresh the page in order to get data in my view. When I log out with this user and a next user logs in I still have the data of user1 in my view. If I log out again with user2 and log in with user3 I have the data of user2 in my view. So it seems like its always one behind. I tried to fix it with with destroying the subscription, but that didn't solve the problem. Also when I think that bug begins when I have to refresh my page in order to get my first users data, to destroy the subscription cannot be the errors solution.

This is my code:

auth.service: Where I post the users credentials, give back a token, and store it and the user_id in order to get the authenticated users data in the view.

import { Storage } from '@ionic/storage';
import { JwtHelperService } from '@auth0/angular-jwt';


export const TOKEN_KEY = 'access_token';
export const USERNAME_KEY = 'username_key';
export const USER_ID = 'user_id';
...
user = null;
refreshToken = null;
authenticationState = new BehaviorSubject(false);

  constructor(private storage: Storage, private helper: JwtHelperService) {
      this.checkToken();
   }

   checkToken() {
       this.storage.get(TOKEN_KEY).then(access => {
           if (access) {
               this.user = this.helper.decodeToken(access);
               this.authenticationState.next(true);
           }
       });
   }

   apilogin(username: string, password: string) {
    return this.http.post<any>(`${this.url}`, { username, password })
    .pipe(
        tap(res => {
            this.storage.set(TOKEN_KEY, res['access']);
            this.storage.set(USERNAME_KEY, username);
            this.storage.set(USER_ID, this.user['user_id']);
            this.user = this.helper.decodeToken(res['access']);
            console.log('my user: ', this.user);
            this.authenticationState.next(true);
        }));
}

apilogout() {
    this.storage.remove(USER_ID);
    this.storage.remove(USERNAME_KEY);
    this.storage.remove(TOKEN_KEY).then(() => {
    this.authenticationState.next(false);
   });
}

page.ts: here I get the data that I display in my view. (user service just retrieves a single user. In the end I destroy the subscription)

import { Storage } from '@ionic/storage';
import { USER_ID } from 'src/app/services/auth.service';
import { SubscriptionLike } from 'rxjs';

  information = null;
  id: number;
  key: string;
  subscription: SubscriptionLike;

  constructor(private storage: Storage,  private activatedRoute: ActivatedRoute,
              private userService: UserService, private authService: AuthService) { }

  ngOnInit() {
    // How to get just the authenticated api?
      if (this.authService.authenticationState) {
      console.log(this.storage);
      this.storage.get(USER_ID).then(val => {
        this.id = val;
        this.subscription = this.userService.getUserDetails(this.id).subscribe(result => {
          this.information = result;
          console.log(this.information);
        });
      });
    }

  }

ngOnDestroy() {
this.subscription.unsubscribe();
this.information = null;
}

login.ts (handles my routing to the main page)

 // get return url from route parameters or default to '/'
    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
...

apiSubmit() {
  console.log('Hello World');
  this.submitted = true;

  // if form is invalid => stop
  if (this.loginForm.invalid) {
      return;
  }
  this.isLoading = true;
  this.loadingEl.present();
  this.authService.apilogin(
  this.f.username,
  this.f.password)
      .pipe(tap(x => this.loadingEl.dismiss()),
      )
      .subscribe(
        data => {
          console.log('0');
          this.router.navigate([this.returnUrl]);
        },
        error => {
          console.log('1');
          this.loadingEl.dismiss();
          this.error = error;
          this.isLoading = false;
        }
      );
}

authGuard

export class AuthGuard implements CanActivate {
  constructor(
      private router: Router,
      private authService: AuthService
  ) { }


  canActivate(): boolean {
    return this.authService.isAuthenticated();
  }
} 

Upvotes: 1

Views: 94

Answers (1)

AVJT82
AVJT82

Reputation: 73357

Instead of if (this.authService.authenticationState) { you should actually subscribe to this observable. Also we need to remember that setting or getting or removing from ionic Storage is actually asynchronous. We need to wait that the action has been performed before doing anything else. Also I suggest to instead of calling next on your observable, just call checkToken() that can do the check and then call next on the BehaviorSubject.

This should work:

Service:

import { BehaviorSubject, of, forkJoin } from 'rxjs';
import { tap, switchMap, map } from 'rxjs/operators';

// ...

private authenticationState = new BehaviorSubject(false);
public authenticationState$ = this.authenticationState.asObservable();

checkToken() {
  this.storage.get(TOKEN_KEY).then(access => {
    if (access) {
      this.authenticationState.next(true);
    } else {
      this.authenticationState.next(false);
    }
  });
}

apilogin(username: string, password: string) {
  return this.http.post<any>(`${this.url}`, { username, password }).pipe(
    // switch to inner observable
    switchMap((data: any) => {
      // run all in paralell
      return forkJoin(
        this.storage.set(TOKEN_KEY, 'access'),
        this.storage.set(USERNAME_KEY, 'username'),
        this.storage.set(USER_ID, 'id'),
      )
    }),
    // now we know for sure storage values have been set,
    // therefore call checkToken()
    tap(() => this.checkToken()),
  )
}

// seems you are not currently subscribing to this function in the
// component, so I guess you can subscribe here, but I'd subscribe in comp
apilogout() {
  forkJoin(
    this.storage.remove(USER_ID),
    this.storage.remove(REFRESH_TOKEN_KEY),
    this.storage.remove(USERNAME_KEY),
    this.storage.remove(TOKEN_KEY)
  ).subscribe(() => this.checkToken())
}

Then the component would subscribe to your BehaviorSubject and do whatever you need to do:

ngOnInit(): void {
  this.sub = this.authService.authenticationState$.pipe(
    switchMap((data) => {
      return data ? this.storage.get(USER_ID) : of(null)
    }),
    switchMap((id: any) => {
      this.id = id;
      return id ? this.userService.getUserDetails(id) : of(null)
    })
  ).subscribe(val => {
    // do stuff!
  })
}

ngOnDestroy() {
  this.sub.unsubscribe();
}

PS. DON'T use any, type your data to models :)

Upvotes: 3

Related Questions