1

I have a simple Ionic 4/5 tabbed application (I started from their tab starter). I would like to make one of the tabs guarded by authentication. Here's what I want to achieve:

  • have one of the tabs reserved for logged in users, profile management, account management, etc..
  • so when the user clicks the third tab, the default page is 'profile' which is guarded by an AuthGuard, if no user is present in ionic storage then redirect to the Login/Register page instead

Here's what I tried so far:

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  static STORAGE_KEY = 'auth-user';

  user: BehaviorSubject<User> = new BehaviorSubject(null);
  redirectUrl: string;

  constructor(
    @Inject(ROUTES_CONFIG)
    private readonly ROUTES: AppRoutes,
    private readonly router: Router,
    private readonly storage: Storage,
    private readonly platform: Platform,
    private readonly http: HttpClient
  ) {
    this.platform.ready().then(() => {
      this.checkUser();
    });
  }

  async checkUser() {
    const user = await this.storage.get(AuthService.STORAGE_KEY) as User;
    if (user) {
      this.user.next(user);
    }
  }

  login(credentials): Observable<any> {
    const loginObservable = this.http.post(`http://localhost:3000/auth/login`, credentials);

    loginObservable.subscribe(async (user: User) => {
      await this.storage.set(AuthService.STORAGE_KEY, user);
      this.user.next(user);
      this.router.navigateByUrl(this.redirectUrl || this.ROUTES.AUTH);
    });

    return loginObservable;
  }

  async logout(): Promise<void> {
    await this.storage.remove(AuthService.STORAGE_KEY);
    this.user.next(null);
    this.router.navigateByUrl(this.ROUTES.LOGIN);
  }

  isLoggedIn(): Observable<User> {
    return this.user;
  }

}

And the guard:

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<boolean> | Observable<boolean> | boolean {
    let url: string = state.url;

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

    return this.authService
      .isLoggedIn()
      .pipe(
        skip(1),
        map(user => {
          if (user) {
            console.log('authenticated');
            return true;
          }

          console.error('Not authenticated, redirecting to login');
          this.router.navigateByUrl(this.ROUTES.LOGIN);
          return false;
        })
      )
  }

The problem with this is once authenticated the authguard never goes into the map. I put the skip(1) there because I wanted to skip the initial null value. How can I "defer" the AuthGuard until my AuthService checks if the user is present in the storage, because after all that's what I want to do.

Attila Kling
  • 1,717
  • 4
  • 18
  • 32

1 Answers1

0

I couldn't solve it this way, but I could solve it another way, here's how:

Previously I had

  • tab1 module, tab2 module, tab3 module (profile, login, register)
  • profile page was guarded by AuthGuard. If no user was found redirected to login
  • AuthService -> was called from AuthGuard, help state of user as BehaviorObject -> see my original question

This is how I changed

  • tab1 module, tab2 module same
  • tab3 module (profile page)
  • login, register is in the root module now
  • created APP_INITIALIZER where I check if there is a user in the storage
  • created a new UserService just to store the user
  • AuthService just manages redirects, login, register

The -related- codes

app.module.ts -> in providers

    BootstrappingService,
    {
      provide: APP_INITIALIZER,
      useFactory: (bootstrappingService: BootstrappingService) =>
        () => bootstrappingService.initApp(),
      deps: [BootstrappingService],
      multi: true
    },

BootstrappingService.ts

@Injectable()
export class BootstrappingService {

  constructor(
    private readonly storage: Storage,
    private readonly platform: Platform,
    private readonly userService: UserService
  ) {}

  async initApp() {
    await this.platform.ready();
    const user: User = await this.storage.get(AuthService.STORAGE_KEY);
    if (user) {
      this.userService.user.next(user);
    }
  }
}

UserService.ts -> holding authenticated (user) state

@Injectable({
  providedIn: 'root'
})
export class UserService {

  user: BehaviorSubject<User> = new BehaviorSubject(null);

  constructor() { }
}

AuthService.ts

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  static STORAGE_KEY = 'auth-user';

  redirectUrl: string;

  constructor(
    @Inject(ROUTES_CONFIG)
    private readonly ROUTES: AppRoutes,
    private readonly router: Router,
    private readonly storage: Storage,
    private readonly userService: UserService,
    private readonly http: HttpClient
  ) {}

  login(credentials): Observable<any> {
    const loginObservable = this.http.post(`http://localhost:3000/auth/login`, credentials);
    loginObservable.subscribe(async (user: User) => {
      await this.storage.set(AuthService.STORAGE_KEY, user);
      this.userService.user.next(user);
      this.router.navigateByUrl(this.redirectUrl || this.ROUTES.PROFILE);
    });
    return loginObservable;
  }

  async logout(): Promise<void> {
    await this.storage.remove(AuthService.STORAGE_KEY);
    this.userService.user.next(null);
    this.router.navigateByUrl(this.ROUTES.HOME);
  }
}

and finally AuthGuard.ts

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    @Inject(ROUTES_CONFIG)
    private readonly ROUTES: AppRoutes,
    private readonly userService: UserService,
    private readonly authService: AuthService,
    private readonly router: Router
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<boolean> | Observable<boolean> | boolean {
    let url: string = state.url;

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

    return this.userService.user
      .asObservable()
      .pipe(
        map((user: User) => {
          if (user) {
            console.log('authenticated');
            return true;
          }

          console.error('Not authenticated, redirecting to login', user);
          this.router.navigateByUrl(this.ROUTES.LOGIN);
          return false;
        })
      )
  }
}

Now my protected third tab seems to handle the logged-in/logged-out state properly. Hitting refresh on a protected route works also.

Attila Kling
  • 1,717
  • 4
  • 18
  • 32