I am trying to set/read if a user is authenticated in my application. I have things working, but as I am learning more about RxJs, I am trying to refactor things to be more declarative.
My code samples below are 98% working. When I log in, I am seeing my logging statement in my service as expected with the correct value(s):
console.log('setting auth status...', status); // true
However, I'm not seeing the logging in my component:
console.log('reading isAuthenticated$ status... ', response) <-- not seeing this when logging in
If I refresh my page, I am seeing Hello World
as expected. Since I am subscribing using the async
pipe, I thought I would see the ui update without having to refresh.
If I log out, (click a button). I am seeing all logging statements as expected every time--even without refreshing.
console.log('setting auth status...', status); // false
console.log('reading isAuthenticated$ status... ', response) // false
This makes me think there is something wrong how I am emitting or reading isAuthenticated$
observable.
How can I read the isAuthenticated$
observable in my template without refreshing?
Here is my service:
// auth.service.ts
export class AuthService {
private readonly baseURL = environment.baseURL;
private isAuthenticatedSubject = new BehaviorSubject<boolean>(
this.hasAccessToken()
);
isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
...
signInWithEmailPassword(
emailPasswordCredentials: EmailPasswordCredentials
) {
return this.httpClient
.post<Response>(
`${this.baseURL}/v1/auth/signin`,
emailPasswordCredentials
)
.pipe(
map((authResponse) => {
// set auth cookie
this.setAuthenticatedStatus(true);
})
catchError((err) => {
return throwError(err);
})
);
}
...
setAuthenticatedStatus(status: boolean): void {
console.log('setting auth status...', status);
this.isAuthenticatedSubject.next(status); // true|false
}
signOut(): void {
this.router.navigate(['/signin']).then(() => {
this.setAuthenticatedStatus(false);
});
}
}
Here is what my component looks like:
// app.component.ts
isAuthenticated$ = this.authService.isAuthenticated$.pipe(
tap((response) =>
console.log('reading isAuthenticated$ status... ', response)
),
catchError((err) => {
this.message = err;
return EMPTY;
})
);
Finally, here is my template:
<div *ngIf="isAuthenticated$ | async">Hello World!</div>
EDIT
I have a login.component
that handles the email/password button click. It looks like this:
signInWithEmailPassword(form: FormGroup): void {
if (form.invalid) {
return;
}
...
this.authService.signInWithEmailPassword(this.form.value).subscribe({
next: (response) => {
this.router.navigate(['/dashboard']).then(() => {
// this.authService.setAuthenticatedStatus(true); // This doesn't seem to impact anything
});
},
error: (err) => {
this.handleHttpError(err);
},
complete: () => {
//...
},
});
}
I thought that by setting that in my login.component
I could update that subject. It all works after I refresh the page. The nav shows...but if I just login... it's like I'm not "watching" on my app.component
that observable or something.
EDIT/UPDATE
I have tried to set up a stackblitz to help illustrate what I'm running into. I am not sure how to mock the auth part to return a dummy response, but hopefully this will help.
First and foremost I want to thank everyone for their suggestions & answers. They really helped me think through things to find what I was doing wrong.
In the end, I created a stripped-down Stackblitz of what I was trying to do. It was interesting that everything seemed to work as expected, and after several days of chasing, I decided that there was something somewhere in my application that was causing my trouble.
I started a new application, and everything now is working as expected.
Here is what my application looks like, hopefully this will help someone else...
Here is the login.component
:
// login.component.ts
public signInWithEmailPassword(form: FormGroup): void {
if (form.invalid) {
return;
}
this.authService.signInWithEmailPassword(this.form.value).subscribe({
next: (response: AuthResponse) => {
this.router.navigate(['/dashboard']).then(() => {
//...
});
},
error: (err) => {
console.log('err: ', err);
},
complete: () => {
//...
},
});
}
Here is my app.component
// app.component.ts
export class AppComponent {
message: string | undefined;
isAuthenticated$ = this.authService.isAuthenticated$.pipe(
catchError((err) => (this.message = err))
);
constructor(
private readonly authService: AuthService
) {
//...
}
}
Here is what my template looks like:
// app.template
<div>
<app-desktop-sidebar *ngIf="isAuthenticated$ | async"></app-desktop-sidebar>
<div>
<router-outlet></router-outlet>
</div>
</div>
Here is my auth.service
:
// auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { catchError, Observable, throwError, tap, BehaviorSubject } from 'rxjs';
import { EmailPasswordCredentials } from './models/email-password-credentials.model';
import { AuthResponse } from './models/auth-response.interface';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly baseURL = environment.baseURL;
private isLoadingSubject = new BehaviorSubject<boolean>(false);
private isAuthenticatedSubject = new BehaviorSubject<boolean>(
this.hasAccessToken()
);
public isLoading$ = this.isLoadingSubject.asObservable();
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
constructor(
private readonly httpClient: HttpClient,
) {
//...
}
public signInWithEmailPassword(
emailPasswordCredentials: EmailPasswordCredentials
): Observable<AuthResponse> {
this.setLoadingStatus(true);
return this.httpClient
.post<AuthResponse>(
`${this.baseURL}/v1/auth/signin`,
emailPasswordCredentials
)
.pipe(
tap((response) => {
...
this.setLoadingStatus(false);
this.isAuthenticatedSubject.next(true);
}),
catchError((err) => {
this.setLoadingStatus(false);
return throwError(err);
})
);
}
public signOut() {
this.isAuthenticatedSubject.next(false);
}
}