I am using Angular 16 with Google Identity and google one tap, to make users being able to login:
https://developers.google.com/identity/gsi/web/guides/overview
Currently I am working on the logout feature of the application, where I want to revoke the token in my .net backend.
To do that I want to dispatch an action when the user does that.
This is what I have tried:
This is revokeToken() from AuthApiService, where I send the information to the backend, which use the email (a string) to logout the user. It takes in a string email, which it should get when I dispatch the action.
revokeToken(email: string) {
const header = new HttpHeaders().set('Content-type', 'application/json');
return this.httpClient.delete(`api/v1/Auth/RevokeToken/` + email, { headers: header, withCredentials: true });
}
Therefore, I have created actions:
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { HttpErrorResponse } from '@angular/common/http';
export const AuthActions = createActionGroup({
source: 'Auth',
events: {
'Logout Init': emptyProps(),
'Logout Success': emptyProps(),
'Logout Failed': props<{ error: HttpErrorResponse }>(),
},
});
and reducers to implement this:
import { createFeature, createReducer, on } from '@ngrx/store';
import { AuthActions } from './auth.actions';
import { LoadingState } from '../../shared/models/loading-state.model';
export interface AuthState {
token: string;
email: string;
fullName: string;
}
export const initialState: AuthState = {
token: '',
email: '',
fullName: '',
};
export const authFeature = createFeature({
name: 'auth',
reducer: createReducer(
initialState,
on(AuthActions.logoutInit, (state) => ({
...state,
...LoadingState.loading(),
})),
on(AuthActions.logoutSuccess, (state) => ({
...state,
token: '',
email: '',
fullName: '',
...LoadingState.loaded(),
})),
on(AuthActions.logoutFailed, (state, { error }) => ({
...state,
...LoadingState.failed(error),
})),
)
})
and I now need the effect. This is what I have tried:
revokeTokenAtLogout$ = createEffect(() => {
return this.actions$.pipe(
ofType(AuthActions.logoutInit),
concatLatestFrom(() => this.store.select(selectEmail)),
map(([action, email]) => {
this.authApiService.revokeToken(email).pipe(
AuthActions.logoutSuccess(),
catchError(error => of(AuthActions.logoutFailed({error})))
)
})
)
})
Which I know does not work.
I know what it should do though.
I am hopelessy stuck with it and been looking at it for days..
What I need help with is how to implement the effect correctly, so that it works as intended.
Can anybody help me with this?
EDIT:
The solution for this problem is:
@Injectable()
export class AuthEffects {
constructor(
private readonly actions$: Actions,
private readonly store: Store,
private readonly authApiService: AuthApiService,
private router: Router,
) {}
revokeTokenAtLogout$ = createEffect(() => {
return this.actions$.pipe(
ofType(AuthActions.logoutInit),
concatLatestFrom(() => this.store.select(selectEmail)),
switchMap(([, email]) =>
this.authApiService.revokeToken(email).pipe(
map(() => AuthActions.logoutSuccess()),
catchError(error => of(AuthActions.logoutFailed({ error })))
),
),
);
});
logoutNavigate$ = createEffect(() => {
return this.actions$.pipe(
ofType(AuthActions.logoutSuccess),
tap(() => {
this.router.navigate(['login']).then(() => window.location.reload());
})
);
}, { dispatch: false })
}
When asking questions please define "does not work". Because, to me, the code matches the description:
revokeTokenAtLogout$ = createEffect(() => {
return this.actions$.pipe(
// ✅ It must listen on logoutInit,
ofType(AuthActions.logoutInit),
// ✅ then get the current state of email from the store, concatLatestFrom(() => this.store.select(selectEmail)),
map(([action, email]) => {
// ✅ then give that to the revokeToken as we call it,
this.authApiService.revokeToken(email).pipe(
// ✅ if it succeedes, logout and navigate back to the login screen.,
// ⚠️ but wrap it in a `map`
map(() => AuthActions.logoutSuccess()),
// ✅ if it fails, return the logoutFailed action.
catchError(error => of(AuthActions.logoutFailed({error})))
)
})
)
})
What we don't see but can affect the outcome:
email
property is not set to a valueLoadingState.abc()
methods in the reducer