I'm building an authentication app using the PEAN stack (i.e., PostgreSQL - ExpressJS - Angular - NodeJS).
I check for user sign-in status as follows:
- On the backend, check the session cookie to see if the
user
property exists in thereq.session
object.
server.js
/* ... */
app.post('/api/get-signin-status', async (req, res) => {
try {
if (req.session.user) {
return res.status(200).json({ message: 'User logged in' });
} else {
return res.status(400).json({ message: 'User logged out' });
}
} catch {
return res.status(500).json({ message: 'Internal server error' });
}
});
/* ... */
- Send an HTTP POST request to the
api/get-signin-status
endpoint with optional data and include a cookie in the request.
auth.service.ts
/* ... */
getSignInStatus(data?: any) {
return this.http.post(this.authUrl + 'api/get-signin-status', data, {
withCredentials: true,
});
}
/* ... */
- Intercept any HTTP request and provide an observable
interceptorResponse$
for subscribing to the response of intercepted requests.
interceptor.service.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { AuthService } from 'src/app/auth/services/auth.service';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
private interceptorResponse$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const signInStatusObserver = {
next: (x: any) => {
this.interceptorResponse$.next({ success: true, response: x });
},
error: (err: any) => {
this.interceptorResponse$.next({ success: false, response: err });
},
};
this.authService.getSignInStatus().subscribe(signInStatusObserver);
return next.handle(httpRequest);
}
getInterceptorResponse(): Observable<any> {
return this.interceptorResponse$.asObservable();
}
constructor(private authService: AuthService) {}
}
- On the frontend, subscribe to the
interceptorResponse
observable from theInterceptorService
and log the response to the console.
header.component.ts
import { Component, OnInit } from '@angular/core';
import { InterceptorService } from '../auth/services/interceptor.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
interceptorResponse: any;
constructor(
private interceptorService: InterceptorService
) {
this.interceptorService.getInterceptorResponse().subscribe((response: any) => {
console.log(response);
this.interceptorResponse = response;
if (response) {
console.log('Interceptor response success:', response.response);
} else {
console.log('Interceptor response is null');
}
});
}
ngOnInit(): void {}
}
Problem
According to the StackOverflow answer, I should use BehaviorSubject
. The problem is that in the console, I always get the following:
But if I console log next
and error
like this:
interceptor.service.ts
/* ... */
const signInStatusObserver = {
next: (x: any) => {
console.log(x);
this.interceptorResponse$.next({ success: true, response: x });
},
error: (err: any) => {
console.log(err.error.message);
this.interceptorResponse$.next({ success: false, response: err });
},
};
/* ... */
I see the expected {message: 'User logged in'}
in the console, as shown in the screenshot below. This means that the backend correctly passes sign-in status to the frontend.
Question
Why does the Angular interceptor always return null
(i.e., the initial value) using BehaviorSubject
, not the updated value?
EDIT 1
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HeaderComponent } from './header/header.component';
import { AppComponent } from './app.component';
import { FooterComponent } from './footer/footer.component';
import { AppRoutingModule } from './app-routing.module';
import { RoutingComponents } from './app-routing.module';
import { SharedModule } from './shared/shared.module';
import { HttpClientModule } from '@angular/common/http';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { CodeInputModule } from 'angular-code-input';
import { IfSignedOut } from './auth/guards/if-signed-out.guard';
import { IfSignedIn } from './auth/guards/if-signed-in.guard';
import { InterceptorService } from './auth/services/interceptor.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
declarations: [HeaderComponent, AppComponent, FooterComponent, RoutingComponents],
imports: [BrowserModule, BrowserAnimationsModule, AppRoutingModule, SharedModule, HttpClientModule, MatMenuModule, MatSidenavModule, CodeInputModule],
providers: [IfSignedOut, IfSignedIn, { provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true }],
bootstrap: [AppComponent],
})
export class AppModule {}
EDIT 2
With the help of @VonC I managed to get the whole thing working as expected. Here's what I did.
- I removed my initial code in
server.js
because the interceptor will now depend on theapi/get-user
endpoint, notapi/get-signin-status
like before. Consequently, I don't needapp.post('/api/get-signin-status', () => {})
anymore. The reason why I now use theapi/get-user
endpoint is because both did the same thing (i.e., checked the session cookie to see if theuser
property exists in thereq.session
object), which means that only one is enough for my auth app. There's no need to check the session cookie twice.
server.js
/* ... */
/* Removed */
/*
app.post('/api/get-signin-status', async (req, res) => {
try {
if (req.session.user) {
return res.status(200).json({ message: 'User logged in' });
} else {
return res.status(400).json({ message: 'User logged out' });
}
} catch {
return res.status(500).json({ message: 'Internal server error' });
}
});
*/
/* ... */
- I removed my initial code in
auth.service.ts
and added the code as @VonC suggested.
auth.service.ts
/* ... */
/* Removed */
/*
getSignInStatus(data?: any) {
return this.http.post(this.authUrl + 'api/get-signin-status', data, {
withCredentials: true,
});
}
*/
/* Added */
private signInStatus$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
getSignInStatusObserver(): Observable<any> {
return this.signInStatus$.asObservable();
}
setSignInStatus(status: any): void {
this.signInStatus$.next(status);
}
/* ... */
- I removed my initial code in
interceptor.service.ts
and added the code as @VonC suggested. Note: I changed the endpoint fromapi/get-signin-status
toapi/get-user
.
interceptor.service.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/services/auth.service';
@Injectable({
providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && httpRequest.url.endsWith('api/get-user')) {
this.authService.setSignInStatus({ success: true, response: event.body });
}
}),
catchError((err: any) => {
if (httpRequest.url.endsWith('api/get-user')) {
this.authService.setSignInStatus({ success: false, response: err });
}
return throwError(err);
})
);
}
constructor(private authService: AuthService) {}
}
- I removed my initial code in
header.component.ts
and added the code as @VonC suggested.
header.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from 'src/app/auth/services/auth.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
signInStatus: any;
constructor(private authService: AuthService, public publicAuthService: AuthService, private signOutRouter: Router, private snackBar: MatSnackBar) {
this.authService.getSignInStatusObserver().subscribe((response: any) => {
this.signInStatus = response;
if (response) {
console.log('Sign in status success:', response.response);
} else {
console.log('Sign in status is null');
}
});
}
ngOnInit(): void {}
}
Now I can finally show elements in header.component.html
depending on sign-in status coming from the backend as follows:
header.component.html
<div *ngIf="signInStatus">Show this element if the user is signed in</div>
<div *ngIf="!signInStatus">Show this element if the user is signed out</div>