Skip to content

Commit 4f77f39

Browse files
authored
feat(ngx-auth): Add interceptor and authenticated http client
1 parent 541bcf7 commit 4f77f39

File tree

15 files changed

+452
-13
lines changed

15 files changed

+452
-13
lines changed

apps/docs/src/app/pages/docs/angular/authentication/implementation/index.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ keyword: ImplementationPage
44

55
## Setup
66

7-
In order to use `ngx-auth`, we use the `provideNgxAuthenticationConfiguration` provider that requires an implementation of the `NgxAuthenticationAbstractService` to provide the service to the application. We advise you to provide this in the root of your application.
7+
In order to use `ngx-auth`, we use the `provideNgxAuthenticationConfiguration` provider that requires an implementation of the `NgxAuthenticationAbstractService` to provide the service to the application. We advise you to provide this in the `bootstrapApplication` of your project.
88

99
```ts
1010
providers: [
@@ -68,6 +68,55 @@ The services provides access to the user, session and metadata by providing the
6868

6969
By default, features are provided on a user basis. However, we are aware that certain features might be set globally regardless of the authentication status of the user. You can do so be calling the `setGlobalFeatures` method. These features will always be available to any user, anonymous or not.
7070

71+
## NgxAuthenticatedHttpClient
72+
73+
Additionally, you can use the `NgxAuthenticatedHttpClient` to further enhance your developer experience. Given its opinionated nature, it's entirely optional withing the `ngx-auth` package.
74+
75+
By providing additional `httpClientConfiguration` to the `provideNgxAuthenticationConfiguration` function, as seen below, we can now use the `HttpClient` wrapper. We no longer have to provide the `HttpClient` itself by calling `provideHttpClient`, this is done automatically by the configuration provider.
76+
77+
```ts
78+
providers: [
79+
...,
80+
provideNgxAuthenticationConfiguration({
81+
service: YourAuthenticationService,
82+
httpClientConfiguration: {
83+
baseUrl: () => environment.baseUrl,
84+
interceptors: [MyCustomInterceptor],
85+
authenticatedCallHandler: MyAuthenticatedCallHandler
86+
}
87+
})
88+
]
89+
90+
```
91+
92+
The configuration has three optional items. `baseUrl` is a function that will be called at injection time and provides the base url we'll add to each request url. We do this according to the `baseurl/request-url` pattern.
93+
94+
Using the optional `interceptors`, we can provide other custom made interceptors to the application. By providing `authenticatedCallHandler` we can make changes to any request that is considered an authenticated request, which will be explained further below.
95+
96+
The `NgxAuthenticatedHttpClient` comes with two added benefits. On one hand, it automatically adds a base-url to all of your request calls. On the other, it integrates with the `NgxAuthenticatedHttpInterceptor` which will automatically call your interceptor function whenever a call with authentication is made.
97+
98+
Our `HttpClient` wrapper provides an implementation for `get`, `post`, `push`, `patch` and `delete` calls. On top of that, it also provides an extra method, `download`, which adds extra information to a `GET` request specifically for downloading `Blobs`. In the example below you see can see the `get` method in action.
99+
100+
```ts
101+
const httpClient = inject(NgxAuthenticatedHttpClient);
102+
103+
public getData(): Observable<Data> {
104+
return httpClient.get<Data>('get-data')
105+
}
106+
```
107+
108+
In the example above we do a simple get call to fetch data from an end-point. We have the ability to pass params as the second parameter.
109+
110+
As this is an opinionated wrapper, we assume that most calls are made within an authenticated state. Because of that, the base assumption is that we can set the `withCredentials` flag to true by default. In the example below, we show how you can overwrite that.
111+
112+
```ts
113+
const httpClient = inject(NgxAuthenticatedHttpClient);
114+
115+
public loginUser(data: HttParams): Observable<Data> {
116+
return httpClient.post<Data>('login', data, false )
117+
}
118+
```
119+
71120
## Guards
72121
73122
All guards depend on being provided specific data in the `data` block of the route. We strongly advise to type your routes as the provided `NgxAuthenticatedRoute` instead of the default `Route`. This way, you'll ensure that your routes always provide the necessary data for the guards.
@@ -127,9 +176,7 @@ The `ngxHasFeature`and `ngxHasPermission` directives have added options to handl
127176
<p *ngxHasPermission="['Admin']; shouldHavePermission: false">
128177
I will be shown if the user is not an Admin!
129178
</p>
130-
<p *ngxIsAuthenticated="true">
131-
I will be shown if the user is authenticated!
132-
</p>
179+
<p *ngxIsAuthenticated="true">I will be shown if the user is authenticated!</p>
133180
```
134181
135182
## Pipes

libs/angular/authentication/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"peerDependencies": {
55
"@angular/core": "^19.0.0",
66
"rxjs": "7.8.1",
7-
"@studiohyperdrive/types-auth": "^2.0.0",
8-
"@angular/router": "19.0.3"
7+
"@angular/common": "19.0.3",
8+
"obj-clean": "3.0.1"
99
},
1010
"sideEffects": false
1111
}

libs/angular/authentication/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './pipes';
55
export * from './types';
66
export * from './providers';
77
export * from './mocks';
8+
export * from './services';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { of } from 'rxjs';
3+
import { HttpRequest } from '@angular/common/http';
4+
5+
import { NgxAuthenticationInterceptorToken } from '../../tokens';
6+
import { NgxAuthenticatedHttpInterceptor } from './authentication.interceptor';
7+
8+
describe('NgxAuthenticatedHttpInterceptor', () => {
9+
let handler;
10+
const httpHandler = (request) => of(request);
11+
12+
beforeEach(() => {
13+
handler = jest.fn();
14+
15+
TestBed.configureTestingModule({
16+
imports: [],
17+
providers: [{ provide: NgxAuthenticationInterceptorToken, useValue: handler }],
18+
});
19+
});
20+
21+
it('should call the handler when the request has an withCredentials value', () => {
22+
TestBed.runInInjectionContext(() => {
23+
const request = new HttpRequest('GET', 'test', { withCredentials: true });
24+
handler.mockReturnValue(request);
25+
26+
NgxAuthenticatedHttpInterceptor(request, httpHandler).subscribe();
27+
28+
expect(handler).toHaveBeenCalled();
29+
});
30+
});
31+
32+
it('should not call the handler when the request has an withCredentials value', () => {
33+
TestBed.runInInjectionContext(() => {
34+
const request = new HttpRequest('GET', 'test', { withCredentials: false });
35+
handler.mockReturnValue(request);
36+
37+
NgxAuthenticatedHttpInterceptor(request, httpHandler).subscribe();
38+
39+
expect(handler).not.toHaveBeenCalled();
40+
});
41+
});
42+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
2+
import { Observable } from 'rxjs';
3+
import { inject } from '@angular/core';
4+
5+
import { NgxAuthenticationInterceptorToken } from '../../tokens';
6+
7+
/**
8+
* An interceptor that will handle any request that needs to be authenticated
9+
*
10+
* @param request - The provided request
11+
* @param next - The HttpHandler
12+
*/
13+
export function NgxAuthenticatedHttpInterceptor(
14+
request: HttpRequest<unknown>,
15+
next: HttpHandlerFn
16+
): Observable<HttpEvent<unknown>> {
17+
// Iben: Get the authenticatedCallHandler
18+
const authenticatedCallHandler = inject(NgxAuthenticationInterceptorToken);
19+
20+
// Iben: If the request does not need to be made in an authenticated state or if no authenticatedCallHandler was provided, we return the request as is
21+
if (!request.withCredentials || !authenticatedCallHandler) {
22+
return next(request);
23+
}
24+
25+
// Iben: Handle the authenticated call
26+
return next(authenticatedCallHandler(request));
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './authentication/authentication.interceptor';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { of } from 'rxjs';
2+
3+
export /**
4+
* Returns a mock for the NgxAuthenticatedHttpClient. By default each methods returns an empty observable
5+
*
6+
* @param configuration - Configuration to replace one of the methods with a custom method
7+
*/
8+
const NgxMockAuthenticatedHttpClient = (configuration: {
9+
download?: unknown;
10+
get?: unknown;
11+
patch?: unknown;
12+
put?: unknown;
13+
delete?: unknown;
14+
post?: unknown;
15+
}) => {
16+
const defaultFunction = () => of();
17+
return {
18+
get: configuration.get || defaultFunction,
19+
download: configuration.download || defaultFunction,
20+
delete: configuration.delete || defaultFunction,
21+
patch: configuration.patch || defaultFunction,
22+
post: configuration.post || defaultFunction,
23+
put: configuration.put || defaultFunction,
24+
};
25+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './authentication.service.mock';
22
export * from './authentication.response.mock';
3+
export * from './authenticated-http-client.mock';
Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
1-
import { Provider, Type } from '@angular/core';
2-
import { NgxAuthenticationAbstractService } from '../abstracts';
3-
import { NgxAuthenticationServiceToken } from '../tokens';
1+
import { EnvironmentProviders, Provider } from '@angular/core';
2+
import { provideHttpClient, withInterceptors } from '@angular/common/http';
3+
4+
import {
5+
NgxAuthenticationInterceptorToken,
6+
NgxAuthenticationServiceToken,
7+
NgxAuthenticationUrlHandlerToken,
8+
} from '../tokens';
9+
import { NgxAuthenticationConfiguration } from '../types';
10+
import { NgxAuthenticatedHttpInterceptor } from '../interceptors';
411
/**
512
* Configures the provided implementation of the NgxAuthenticationAbstract service to the application
613
*
714
* @param configuration - The configuration with the provided service implementation
815
*/
9-
export const provideNgxAuthenticationConfiguration = (configuration: {
10-
service: Type<NgxAuthenticationAbstractService>;
11-
}): Provider[] => {
16+
export const provideNgxAuthenticationConfiguration = (
17+
configuration: NgxAuthenticationConfiguration
18+
): Provider | EnvironmentProviders[] => {
1219
return [
1320
{
1421
provide: NgxAuthenticationServiceToken,
1522
useExisting: configuration.service,
1623
},
24+
// Iben: If the HttpClientConfiguration is provided, we assume the user wants to use the NgxAuthenticatedHttpClient
25+
...(!configuration.httpClientConfiguration
26+
? []
27+
: [
28+
{
29+
provide: NgxAuthenticationUrlHandlerToken,
30+
useValue: configuration.httpClientConfiguration.baseUrl,
31+
},
32+
{
33+
provide: NgxAuthenticationInterceptorToken,
34+
useValue: configuration.httpClientConfiguration.authenticatedCallHandler,
35+
},
36+
provideHttpClient(
37+
withInterceptors([
38+
NgxAuthenticatedHttpInterceptor,
39+
...(configuration.httpClientConfiguration.interceptors || []),
40+
])
41+
),
42+
]),
1743
];
1844
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { TestBed } from '@angular/core/testing';
3+
4+
import { of } from 'rxjs';
5+
import { NgxAuthenticationUrlHandlerToken } from '../../tokens';
6+
import { NgxAuthenticatedHttpClient } from './authenticated-http-client.service';
7+
8+
describe('NgxAuthenticatedHttpClient', () => {
9+
let service: NgxAuthenticatedHttpClient;
10+
11+
const baseUrl = () => 'www.test.be';
12+
13+
const httpClient: any = {
14+
get: jest.fn().mockReturnValue(of('test')),
15+
};
16+
17+
beforeEach(() => {
18+
TestBed.configureTestingModule({
19+
imports: [],
20+
providers: [
21+
NgxAuthenticatedHttpClient,
22+
{ provide: HttpClient, useValue: httpClient },
23+
{ provide: NgxAuthenticationUrlHandlerToken, useValue: baseUrl },
24+
],
25+
});
26+
27+
service = TestBed.inject(NgxAuthenticatedHttpClient);
28+
});
29+
30+
it('should set the base url when provided and add withCredentials', () => {
31+
TestBed.runInInjectionContext(() => {
32+
service.get<string>('api-a').subscribe();
33+
34+
expect(httpClient.get).toHaveBeenCalledWith('www.test.be/api-a', {
35+
withCredentials: true,
36+
});
37+
});
38+
});
39+
40+
it('should set the base url when provided', () => {
41+
TestBed.runInInjectionContext(() => {
42+
service.get<string>('api-a', undefined, false).subscribe();
43+
44+
expect(httpClient.get).toHaveBeenCalledWith('www.test.be/api-a', {
45+
withCredentials: false,
46+
});
47+
});
48+
});
49+
});

0 commit comments

Comments
 (0)