Skip to content

Commit 3f6d27e

Browse files
authored
Merge pull request #435 from medizininformatik-initiative/428-refactor-http-interceptor-to-separate-concerns
Add AuthToken and HttpError interceptors for improved HTTP request ha…
2 parents 40550c8 + 479448c commit 3f6d27e

File tree

4 files changed

+83
-133
lines changed

4 files changed

+83
-133
lines changed

src/app/app.module.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { APP_INITIALIZER } from '@angular/core';
1+
import { APP_INITIALIZER, ErrorHandler } from '@angular/core';
22
import { AppComponent } from './app.component';
3-
import { AppConfigService } from './config/app-config.service';
43
import { AppRoutingModule } from './app-routing.module';
4+
import { AuthTokenInterceptor } from './core/interceptors/AuthToken.interceptor';
55
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
66
import { BrowserModule } from '@angular/platform-browser';
7+
import { CoreInitService } from './CoreInit.service';
78
import { CoreModule } from './core/core.module';
89
import { DataProtectionComponent } from './site/data-protection/data-protection.component';
910
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
11+
import { HttpErrorInterceptor } from './core/interceptors/HttpError.interceptor';
1012
import { LayoutModule } from './layout/layout.module';
1113
import { NgModule } from '@angular/core';
12-
import { OAuthInitService } from './core/auth/oauth-init.service';
13-
import { OAuthInterceptor } from './core/interceptors/oauth.interceptor';
14+
import { SharedModule } from './shared/shared.module';
1415
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
1516
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
16-
import { CoreInitService } from './CoreInit.service';
1717

1818
export const HttpLoaderFactory = (http: HttpClient): TranslateHttpLoader =>
1919
new TranslateHttpLoader(http);
@@ -27,6 +27,7 @@ export const HttpLoaderFactory = (http: HttpClient): TranslateHttpLoader =>
2727
LayoutModule,
2828
AppRoutingModule,
2929
HttpClientModule,
30+
SharedModule,
3031
TranslateModule.forRoot({
3132
defaultLanguage: 'de',
3233
loader: {
@@ -45,7 +46,12 @@ export const HttpLoaderFactory = (http: HttpClient): TranslateHttpLoader =>
4546
},
4647
{
4748
provide: HTTP_INTERCEPTORS,
48-
useClass: OAuthInterceptor,
49+
useClass: AuthTokenInterceptor,
50+
multi: true,
51+
},
52+
{
53+
provide: HTTP_INTERCEPTORS,
54+
useClass: HttpErrorInterceptor,
4955
multi: true,
5056
},
5157
],
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Injectable } from '@angular/core';
2+
import { OAuthService } from 'angular-oauth2-oidc';
3+
import { Observable } from 'rxjs';
4+
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
5+
6+
/**
7+
* HTTP interceptor that automatically adds OAuth bearer tokens to outgoing requests.
8+
* Excludes certain URLs from token injection (e.g., static assets) and validates
9+
* token availability before attaching authorization headers.
10+
*/
11+
@Injectable()
12+
export class AuthTokenInterceptor implements HttpInterceptor {
13+
/**
14+
* List of URL patterns that should be excluded from token injection.
15+
*/
16+
private readonly excludedUrls = ['assets', '/assets'];
17+
18+
/**
19+
* Pre-compiled regular expressions for URL matching.
20+
*/
21+
private readonly excludedUrlsRegEx = this.excludedUrls.map((url) => new RegExp('^' + url, 'i'));
22+
23+
constructor(private oauthService: OAuthService) {}
24+
25+
/**
26+
* Intercepts HTTP requests and conditionally adds Authorization header with bearer token.
27+
* @param req - The outgoing HTTP request to potentially modify
28+
* @param next - The next handler in the interceptor chain
29+
* @returns Observable of HTTP events from the modified or original request
30+
*/
31+
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
32+
const shouldExclude = this.excludedUrlsRegEx.some((regex) => regex.test(req.url));
33+
if (shouldExclude) {
34+
return next.handle(req);
35+
}
36+
37+
const modifiedRequest = this.attachTokenIfAvailable(req);
38+
return next.handle(modifiedRequest);
39+
}
40+
41+
/**
42+
* Creates a cloned request with Authorization header when token exists.
43+
* @param req - The original HTTP request
44+
* @returns Modified request with token or original request if no token available
45+
*/
46+
private attachTokenIfAvailable(req: HttpRequest<any>): HttpRequest<any> {
47+
const token = this.getValidToken();
48+
if (!token) {
49+
console.warn('No valid token found, request will no have Authorization header');
50+
return req;
51+
}
52+
const header = `Bearer ${token}`;
53+
return req.clone({
54+
setHeaders: { Authorization: header },
55+
});
56+
}
57+
58+
/**
59+
* Retrieves a valid, non-empty access token from the OAuth service.
60+
* @returns Access token string if valid, otherwise null
61+
*/
62+
private getValidToken(): string | null {
63+
const token = this.oauthService.getAccessToken();
64+
65+
if (token && typeof token === 'string' && this.oauthService.hasValidAccessToken()) {
66+
return token;
67+
}
68+
return null;
69+
}
70+
}

src/app/core/interceptors/oauth.interceptor.ts renamed to src/app/core/interceptors/HttpError.interceptor.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,12 @@ import {
1212
import { ErrorCodes, SnackbarService } from 'src/app/shared/service/Snackbar/Snackbar.service';
1313

1414
@Injectable()
15-
export class OAuthInterceptor implements HttpInterceptor {
16-
excludedUrls = ['assets', '/assets'];
17-
excludedUrlsRegEx = this.excludedUrls.map((url) => new RegExp('^' + url, 'i'));
18-
15+
export class HttpErrorInterceptor implements HttpInterceptor {
1916
constructor(private oauthService: OAuthService, private snackbar: SnackbarService) {}
2017

21-
private isExcluded(req: HttpRequest<any>): boolean {
22-
return this.excludedUrlsRegEx.some((toBeExcluded) => toBeExcluded.test(req.url));
23-
}
24-
2518
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
26-
if (this.isExcluded(req)) {
27-
return next.handle(req);
28-
}
29-
const token = this.getToken();
30-
if (token) {
31-
const headers = req.headers.set('Authorization', 'Bearer ' + token);
32-
req = req.clone({ headers });
33-
}
3419
return next.handle(req).pipe(
3520
catchError((error: HttpErrorResponse) => {
36-
console.error('OAuthInterceptor: Error occurred', error);
3721
if (error.status === 401) {
3822
this.oauthService.logOut();
3923
}
@@ -52,13 +36,6 @@ export class OAuthInterceptor implements HttpInterceptor {
5236
);
5337
}
5438

55-
private getToken(): string | null {
56-
if (this.oauthService.hasValidAccessToken()) {
57-
return this.oauthService.getAccessToken();
58-
}
59-
return null;
60-
}
61-
6239
public handleErrorCodes(errorCode, retryAfter?: number) {
6340
this.snackbar.displayErrorMessage(errorCode, retryAfter);
6441
}

src/app/core/interceptors/oauth.interceptor.spec.ts

Lines changed: 0 additions & 103 deletions
This file was deleted.

0 commit comments

Comments
 (0)