Skip to content

Commit 968249e

Browse files
Merge pull request #62 from shiftcode/#61-add-origin-interceptor
#61 add origin interceptor
2 parents 4cbd1b1 + ff48d91 commit 968249e

File tree

7 files changed

+155
-3
lines changed

7 files changed

+155
-3
lines changed

libs/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shiftcode/ngx-components",
3-
"version": "12.0.0",
3+
"version": "12.0.1-pr61.0",
44
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
55
"license": "MIT",
66
"author": "shiftcode GmbH <team@shiftcode.ch>",
@@ -25,7 +25,7 @@
2525
"@angular/forms": "^20.0.0",
2626
"@angular/router": "^20.0.0",
2727
"@shiftcode/logger": "^3.0.0",
28-
"@shiftcode/ngx-core": "^12.0.0 || ^12.0.0-pr56",
28+
"@shiftcode/ngx-core": "^12.0.0 || ^12.1.0-pr61",
2929
"rxjs": "^6.5.3 || ^7.4.0"
3030
}
3131
}

libs/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shiftcode/ngx-core",
3-
"version": "12.0.0",
3+
"version": "12.1.0-pr61.1",
44
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
55
"license": "MIT",
66
"author": "shiftcode GmbH <team@shiftcode.ch>",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { HttpClient, HttpRequest, provideHttpClient, withInterceptors } from '@angular/common/http'
2+
import { ensureOriginInterceptor } from './ensure-origin.interceptor'
3+
import { TestBed } from '@angular/core/testing'
4+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'
5+
import { ORIGIN } from './origin.token'
6+
7+
describe('ensureOriginInterceptor', () => {
8+
const origin = 'https://example.com'
9+
10+
beforeEach(() => {
11+
TestBed.configureTestingModule({
12+
providers: [
13+
{ provide: ORIGIN, useValue: origin },
14+
provideHttpClient(withInterceptors([ensureOriginInterceptor])),
15+
provideHttpClientTesting(),
16+
],
17+
})
18+
})
19+
20+
it('should prepend origin to relative URLs', () => {
21+
const req = new HttpRequest('GET', '/api/data')
22+
23+
const nextSpy = jest.fn()
24+
TestBed.runInInjectionContext(() => ensureOriginInterceptor(req, nextSpy))
25+
expect(nextSpy).toHaveBeenCalledWith(expect.objectContaining({ url: `${origin}/api/data` }))
26+
})
27+
28+
it('should not modify absolute URLs', () => {
29+
const req = new HttpRequest('GET', 'https://other.com/api/data')
30+
31+
const nextSpy = jest.fn()
32+
TestBed.runInInjectionContext(() => ensureOriginInterceptor(req, nextSpy))
33+
expect(nextSpy).toHaveBeenCalledWith(req)
34+
})
35+
36+
it('should work integrated with HttpClient', () => {
37+
const controller = TestBed.inject(HttpTestingController)
38+
const httpClient = TestBed.inject(HttpClient)
39+
40+
httpClient.get('/api/data').subscribe()
41+
42+
const req = controller.expectOne(`${origin}/api/data`)
43+
expect(req.request.url).toBe(`${origin}/api/data`)
44+
req.flush('ok')
45+
})
46+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { HttpInterceptorFn } from '@angular/common/http'
2+
import { inject } from '@angular/core'
3+
import { ORIGIN } from './origin.token'
4+
5+
/**
6+
* An interceptor function that prepends the origin to requests with a relative url.
7+
* it is important that ssr and browser both use the origin for requests otherwise we get different TransferState keys
8+
* and the ssr state is not found in the browser.
9+
*/
10+
export const ensureOriginInterceptor: HttpInterceptorFn = (req, next) => {
11+
if (req.url.startsWith('/')) {
12+
const origin = inject(ORIGIN)
13+
14+
const clonedReq = req.clone({ url: `${origin}${req.url}` })
15+
return next(clonedReq)
16+
}
17+
return next(req)
18+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { PLATFORM_ID } from '@angular/core'
2+
import { provideOriginFromEnv } from './provide-origin-from-env.function'
3+
import { ORIGIN } from './origin.token'
4+
import { TestBed } from '@angular/core/testing'
5+
6+
describe('provideOriginFromEnv', () => {
7+
const ENV_KEY = 'TEST_ORIGIN'
8+
9+
let originalEnv: NodeJS.ProcessEnv
10+
11+
beforeEach(() => {
12+
originalEnv = { ...process.env }
13+
})
14+
15+
afterEach(() => {
16+
process.env = originalEnv
17+
})
18+
19+
function setup(platformId: 'browser' | 'server') {
20+
TestBed.configureTestingModule({
21+
providers: [{ provide: PLATFORM_ID, useValue: platformId }, provideOriginFromEnv(ENV_KEY)],
22+
})
23+
}
24+
25+
it('should provide the ORIGIN token with a valid origin from env', () => {
26+
setup('server')
27+
28+
const validOrigin = 'https://example.valid.com'
29+
30+
process.env[ENV_KEY] = validOrigin
31+
32+
expect(TestBed.inject(ORIGIN)).toBe(validOrigin)
33+
})
34+
35+
it('should throw if not running on the server', () => {
36+
setup('browser')
37+
38+
process.env[ENV_KEY] = 'https://example.valid.com'
39+
40+
expect(() => TestBed.inject(ORIGIN)).toThrowError(`provideOriginFromEnv can only be used on the server`)
41+
})
42+
43+
it('should throw if env var is not defined', () => {
44+
setup('server')
45+
delete process.env[ENV_KEY]
46+
expect(() => TestBed.inject(ORIGIN)).toThrowError(`Env var ${ENV_KEY} needs to be defined`)
47+
})
48+
49+
it('should throw if env var is not a valid origin', () => {
50+
setup('server')
51+
process.env[ENV_KEY] = 'invalid-origin'
52+
expect(() => TestBed.inject(ORIGIN)).toThrowError(`Env var ${ENV_KEY} is not a valid origin: invalid-origin`)
53+
})
54+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { inject, PLATFORM_ID, Provider } from '@angular/core'
2+
import { ORIGIN } from './origin.token'
3+
import { isPlatformServer } from '@angular/common'
4+
5+
declare const process: { env: Record<string, undefined | string> }
6+
7+
/**
8+
* Provides the {@link ORIGIN} token from the environment variable.
9+
*/
10+
export function provideOriginFromEnv(key: string): Provider[] {
11+
return [
12+
{
13+
provide: ORIGIN,
14+
useFactory: () => {
15+
if (!isPlatformServer(inject(PLATFORM_ID))) {
16+
throw new Error(`${provideOriginFromEnv.name} can only be used on the server`)
17+
}
18+
19+
const value = process.env[key]?.trim()
20+
if (!value) {
21+
throw new Error(`Env var ${key} needs to be defined`)
22+
}
23+
try {
24+
new URL(value)
25+
return value
26+
} catch (error) {
27+
throw new Error(`Env var ${key} is not a valid origin: ${value}`, { cause: error })
28+
}
29+
},
30+
},
31+
]
32+
}

libs/core/src/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export * from './lib/logger/remote/with-remote-transport.function'
4747

4848
// origin
4949
export * from './lib/origin/origin.token'
50+
export * from './lib/origin/ensure-origin.interceptor'
51+
export * from './lib/origin/provide-origin-from-env.function'
5052

5153
// resize
5254
export * from './lib/resize/resize.service'

0 commit comments

Comments
 (0)