0
votes

i don't have an login page , this is sso call

In the app component, I have dispatched an action to load the server-side token call and get the token and based on the user roles I can activate the guard in routing. if guard activates return true, I will go the dashboard page.

In the dashboard page, I have a refresh in the browser URL, can activate execute before the login action effects, therefore can activate return false and page displays blank.

In the dashboard have child routes, there also have some child links like products dashboard/products, there also have activated guard, but can activate return false before the login dispatch action effects success call.

Can anyone help me what I am doing wrong?

here is routing,

  {
    path: '',
    component: HomeComponent,
    canActivate:[AuthGuard],
    children: [
      {
        path: 'product',
        loadChildren: () => productModule
      },
      {
        path: 'order',
        loadChildren: () => orderModule
      }
    ]
  },

here is activate method: here i get the success from effects call while dispatch an action from app.component:

  canActivate(): Observable<boolean> {
     return this.store.select(appState => appState.auth.isAuthenticated)
      .pipe(map(authUser => {

        if (!authUser) {
          return authUser;
        }
        return authUser;
      }))
    }

App.componet:

ngOnInit(){



   this.onSubmit();

}

onSubmit(): void {
  const payload = {
    email: '[email protected]',
    password: 'admin'
  };
  alert('dispatch an action');
  this.store.dispatch(new LogIn(payload));
}

the problem only in a page refresh. when clicking on the router link, the logic works fine.

my initial state :

export const initialState: State = {
  isAuthenticating:false,
  isAuthenticated: false,
  user: null,
  errorMessage: null
};

screenshot of flow:enter image description here

2

2 Answers

4
votes

I might have a solution for this.

Basically you can set a property in your initial state to indicate that you're initializing:

export interface State {
  isAuthenticated: boolean;
  isInitializing: boolean;
}

const initialState: State = {
  isAuthenticated: false,
  isInitializing: true
}

And when you dispatch a login or logout, make sure to set isInitializing to false in the reducer:

const _authReducer = createReducer(
  initialState,

  on(AuthActions.setAuthenticated, (state, action) => ({
    ...state,
    isAuthenticated: true,
    isInitializing: false,
  })),

  on(AuthActions.setUnauthenticated, (state, action) => ({
    ...state,
    isAuthenticated: false,
    isInitializing: false,
  }))
);

In your guard, you can then use a filter to wait for isInitializing to be set to false, before returning information about the user authentication:

canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    console.log('Authguard running...');

    return this.store.select('auth').pipe(
      map(authState => authState.isInitializing),
      // tap((isInit) => {
      //   console.log('isInit: ' + isInit);
      // }),
      filter((isInit) => !isInit),
      switchMap(() => {
        return this.store.select('auth').pipe(
          map(authState => authState.isAuthenticated)
          // tap((isAuth) => {
          //   console.log('isAuth: ' + isAuth);
          // }),
          map((isAuth) => {
            if (isAuth) return isAuth;
            return this.router.createUrlTree(['/auth']);
          })
        );
      })
    );
  }

You can remove the comments from the tap operators if you want to monitor these values in the console :)

I'm using this solution to be able to reload on a specific url when logged in. If you try to visit that same page when not logged in you are redirected to the login.

0
votes

I think you have a problem with the asynchronism. When you refresh the page, the store should init the state with false and the guard check occurs before you get the response from your backend. There are many possible solutions, but based on the code and guard that you expose I will do the following:

Create a flag in the store to know if you are waiting the response of the authentication, like isAuthenticating then in your guard could do something like:

canActivate(): Observable<boolean> {
        return new Observable(observer => {
           this.store.select(appState => appState.auth.isAuthenticating)
               .pipe(filter(isAuthenticating => !isAuthenticating))
               .subscribe(() => {
                  this.store.select(appState => appState.auth.isAuthenticated).pipe(
                      take(1)
                  ).subscribe(isAuthenticated => {
                      observer.next(isAuthenticated);
                      observer.complete();
                  })
          })
     }
   }

I hope this help