Skip to content

Commit 68780d4

Browse files
committed
[DSC-1860] Provide a setting to use a different REST url during SSR execution
1 parent f81924c commit 68780d4

10 files changed

Lines changed: 292 additions & 28 deletions

server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ extendEnvironmentWithAppConfig(environment, appConfig);
8383
// Create a DOM window object based on the template
8484
const _window = domino.createWindow(indexHtml);
8585

86+
// The REST server base URL
87+
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
88+
8689
// Assign the DOM window and document objects to the global object
8790
(_window as any).screen = {deviceXDPI: 0, logicalXDPI: 0};
8891
(global as any).window = _window;
@@ -185,7 +188,7 @@ export function app() {
185188
* Proxy the sitemaps
186189
*/
187190
router.use('/sitemap**', createProxyMiddleware({
188-
target: `${environment.rest.baseUrl}/sitemaps`,
191+
target: `${REST_BASE_URL}/sitemaps`,
189192
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
190193
changeOrigin: true
191194
}));
@@ -194,7 +197,7 @@ export function app() {
194197
* Proxy the linksets
195198
*/
196199
router.use('/signposting**', createProxyMiddleware({
197-
target: `${environment.rest.baseUrl}`,
200+
target: `${REST_BASE_URL}`,
198201
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
199202
changeOrigin: true
200203
}));
@@ -645,7 +648,7 @@ function clientHealthCheck(req, res) {
645648
* The callback function to serve health check requests
646649
*/
647650
function healthCheck(req, res) {
648-
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
651+
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
649652
axios.get(baseUrl)
650653
.then((response) => {
651654
res.status(response.status).send(response.data);

src/app/app.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { RootModule } from './root.module';
3333
import { NuMarkdownModule } from '@ng-util/markdown';
3434
import { FooterModule } from './footer/footer.module';
3535
import { SocialModule } from './social/social.module';
36+
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
3637

3738
export function getConfig() {
3839
return environment;
@@ -109,6 +110,11 @@ const PROVIDERS = [
109110
useClass: LogInterceptor,
110111
multi: true
111112
},
113+
{
114+
provide: HTTP_INTERCEPTORS,
115+
useClass: DspaceRestInterceptor,
116+
multi: true
117+
},
112118
// register the dynamic matcher used by form. MUST be provided by the app module
113119
...DYNAMIC_MATCHER_PROVIDERS,
114120
];
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
3+
import { DspaceRestInterceptor } from './dspace-rest.interceptor';
4+
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
5+
import { DspaceRestService } from './dspace-rest.service';
6+
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
7+
import { PLATFORM_ID } from '@angular/core';
8+
9+
describe('DspaceRestInterceptor', () => {
10+
let httpMock: HttpTestingController;
11+
let httpClient: HttpClient;
12+
const appConfig: Partial<AppConfig> = {
13+
rest: {
14+
ssl: false,
15+
host: 'localhost',
16+
port: 8080,
17+
nameSpace: '/server',
18+
baseUrl: 'http://api.example.com/server',
19+
}
20+
};
21+
const appConfigWithSSR: Partial<AppConfig> = {
22+
rest: {
23+
ssl: false,
24+
host: 'localhost',
25+
port: 8080,
26+
nameSpace: '/server',
27+
baseUrl: 'http://api.example.com/server',
28+
ssrBaseUrl: 'http://ssr.example.com/server',
29+
}
30+
};
31+
32+
describe('When SSR base URL is not set ', () => {
33+
describe('and it\'s in the browser', () => {
34+
beforeEach(() => {
35+
TestBed.configureTestingModule({
36+
imports: [HttpClientTestingModule],
37+
providers: [
38+
DspaceRestService,
39+
{
40+
provide: HTTP_INTERCEPTORS,
41+
useClass: DspaceRestInterceptor,
42+
multi: true,
43+
},
44+
{ provide: APP_CONFIG, useValue: appConfig },
45+
{ provide: PLATFORM_ID, useValue: 'browser' }
46+
],
47+
});
48+
49+
httpMock = TestBed.inject(HttpTestingController);
50+
httpClient = TestBed.inject(HttpClient);
51+
});
52+
53+
it('should not modify the request', () => {
54+
const url = 'http://api.example.com/server/items';
55+
httpClient.get(url).subscribe((response) => {
56+
expect(response).toBeTruthy();
57+
});
58+
59+
const req = httpMock.expectOne(url);
60+
expect(req.request.url).toBe(url);
61+
req.flush({});
62+
httpMock.verify();
63+
});
64+
});
65+
66+
describe('and it\'s in SSR mode', () => {
67+
beforeEach(() => {
68+
TestBed.configureTestingModule({
69+
imports: [HttpClientTestingModule],
70+
providers: [
71+
DspaceRestService,
72+
{
73+
provide: HTTP_INTERCEPTORS,
74+
useClass: DspaceRestInterceptor,
75+
multi: true,
76+
},
77+
{ provide: APP_CONFIG, useValue: appConfig },
78+
{ provide: PLATFORM_ID, useValue: 'server' }
79+
],
80+
});
81+
82+
httpMock = TestBed.inject(HttpTestingController);
83+
httpClient = TestBed.inject(HttpClient);
84+
});
85+
86+
it('should not replace the base URL', () => {
87+
const url = 'http://api.example.com/server/items';
88+
89+
httpClient.get(url).subscribe((response) => {
90+
expect(response).toBeTruthy();
91+
});
92+
93+
const req = httpMock.expectOne(url);
94+
expect(req.request.url).toBe(url);
95+
req.flush({});
96+
httpMock.verify();
97+
});
98+
});
99+
});
100+
101+
describe('When SSR base URL is set ', () => {
102+
describe('and it\'s in the browser', () => {
103+
beforeEach(() => {
104+
TestBed.configureTestingModule({
105+
imports: [HttpClientTestingModule],
106+
providers: [
107+
DspaceRestService,
108+
{
109+
provide: HTTP_INTERCEPTORS,
110+
useClass: DspaceRestInterceptor,
111+
multi: true,
112+
},
113+
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
114+
{ provide: PLATFORM_ID, useValue: 'browser' }
115+
],
116+
});
117+
118+
httpMock = TestBed.inject(HttpTestingController);
119+
httpClient = TestBed.inject(HttpClient);
120+
});
121+
122+
it('should not modify the request', () => {
123+
const url = 'http://api.example.com/server/items';
124+
httpClient.get(url).subscribe((response) => {
125+
expect(response).toBeTruthy();
126+
});
127+
128+
const req = httpMock.expectOne(url);
129+
expect(req.request.url).toBe(url);
130+
req.flush({});
131+
httpMock.verify();
132+
});
133+
});
134+
135+
describe('and it\'s in SSR mode', () => {
136+
beforeEach(() => {
137+
TestBed.configureTestingModule({
138+
imports: [HttpClientTestingModule],
139+
providers: [
140+
DspaceRestService,
141+
{
142+
provide: HTTP_INTERCEPTORS,
143+
useClass: DspaceRestInterceptor,
144+
multi: true,
145+
},
146+
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
147+
{ provide: PLATFORM_ID, useValue: 'server' }
148+
],
149+
});
150+
151+
httpMock = TestBed.inject(HttpTestingController);
152+
httpClient = TestBed.inject(HttpClient);
153+
});
154+
155+
it('should replace the base URL', () => {
156+
const url = 'http://api.example.com/server/items';
157+
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
158+
159+
httpClient.get(url).subscribe((response) => {
160+
expect(response).toBeTruthy();
161+
});
162+
163+
const req = httpMock.expectOne(ssrBaseUrl + '/items');
164+
expect(req.request.url).toBe(ssrBaseUrl + '/items');
165+
req.flush({});
166+
httpMock.verify();
167+
});
168+
169+
it('should not replace any query param containing the base URL', () => {
170+
const url = 'http://api.example.com/server/items?url=http://api.example.com/server/item/1';
171+
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
172+
173+
httpClient.get(url).subscribe((response) => {
174+
expect(response).toBeTruthy();
175+
});
176+
177+
const req = httpMock.expectOne(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
178+
expect(req.request.url).toBe(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
179+
req.flush({});
180+
httpMock.verify();
181+
});
182+
});
183+
});
184+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
2+
import { isPlatformBrowser } from '@angular/common';
3+
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
4+
import { Observable } from 'rxjs';
5+
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
6+
import { isEmpty } from '../../shared/empty.util';
7+
8+
@Injectable()
9+
/**
10+
* This Interceptor is used to use the configured base URL for the request made during SSR execution
11+
*/
12+
export class DspaceRestInterceptor implements HttpInterceptor {
13+
14+
/**
15+
* Contains the configured application base URL
16+
* @protected
17+
*/
18+
protected baseUrl: string;
19+
protected ssrBaseUrl: string;
20+
21+
constructor(
22+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
23+
@Inject(PLATFORM_ID) private platformId: string,
24+
) {
25+
this.baseUrl = this.appConfig.rest.baseUrl;
26+
this.ssrBaseUrl = this.appConfig.rest.ssrBaseUrl;
27+
}
28+
29+
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
30+
if (isPlatformBrowser(this.platformId) || isEmpty(this.ssrBaseUrl) || this.baseUrl === this.ssrBaseUrl) {
31+
return next.handle(request);
32+
}
33+
34+
// Different SSR Base URL specified so replace it in the current request url
35+
let newRequest: HttpRequest<any>;
36+
const url = request.url.replace(this.baseUrl, this.ssrBaseUrl);
37+
newRequest = request.clone({ url });
38+
return next.handle(newRequest);
39+
}
40+
}

src/app/core/services/server-hard-redirect.service.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { TestBed } from '@angular/core/testing';
22
import { ServerHardRedirectService } from './server-hard-redirect.service';
3+
import { environment } from '../../../environments/environment.test';
34

45
describe('ServerHardRedirectService', () => {
56

67
const mockRequest = jasmine.createSpyObj(['get']);
78
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
89

9-
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
10+
const service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
1011
const origin = 'https://test-host.com:4000';
1112

1213
beforeEach(() => {

src/app/core/services/server-hard-redirect.service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@angular/core';
22
import { Request, Response } from 'express';
33
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
44
import { HardRedirectService } from './hard-redirect.service';
5+
import { isNotEmpty } from '../../shared/empty.util';
6+
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
57

68
/**
79
* Service for performing hard redirects within the server app module
@@ -10,6 +12,7 @@ import { HardRedirectService } from './hard-redirect.service';
1012
export class ServerHardRedirectService extends HardRedirectService {
1113

1214
constructor(
15+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
1316
@Inject(REQUEST) protected req: Request,
1417
@Inject(RESPONSE) protected res: Response,
1518
) {
@@ -25,17 +28,22 @@ export class ServerHardRedirectService extends HardRedirectService {
2528
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
2629
*/
2730
redirect(url: string, statusCode?: number) {
28-
2931
if (url === this.req.url) {
3032
return;
3133
}
3234

35+
let redirectUrl = url;
36+
// If redirect url contains SSR base url then replace with public base url
37+
if (isNotEmpty(this.appConfig.rest.ssrBaseUrl) && this.appConfig.rest.baseUrl !== this.appConfig.rest.ssrBaseUrl) {
38+
redirectUrl = url.replace(this.appConfig.rest.ssrBaseUrl, this.appConfig.rest.baseUrl);
39+
}
40+
3341
if (this.res.finished) {
3442
const req: any = this.req;
3543
req._r_count = (req._r_count || 0) + 1;
3644

3745
console.warn('Attempted to redirect on a finished response. From',
38-
this.req.url, 'to', url);
46+
this.req.url, 'to', redirectUrl);
3947

4048
if (req._r_count > 10) {
4149
console.error('Detected a redirection loop. killing the nodejs process');
@@ -49,9 +57,9 @@ export class ServerHardRedirectService extends HardRedirectService {
4957
status = 302;
5058
}
5159

52-
console.info(`Redirecting from ${this.req.url} to ${url} with ${status}`);
60+
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
5361

54-
this.res.redirect(status, url);
62+
this.res.redirect(status, redirectUrl);
5563
this.res.end();
5664
// I haven't found a way to correctly stop Angular rendering.
5765
// So we just let it end its work, though we have already closed

src/app/thumbnail/thumbnail.component.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
1+
import { Component, Inject, Input, OnChanges, PLATFORM_ID, SimpleChanges } from '@angular/core';
2+
import { isPlatformBrowser } from '@angular/common';
23
import { Bitstream } from '../core/shared/bitstream.model';
34
import { hasNoValue, hasValue } from '../shared/empty.util';
45
import { RemoteData } from '../core/data/remote-data';
@@ -60,6 +61,7 @@ export class ThumbnailComponent implements OnChanges {
6061
isLoading$ = new BehaviorSubject(true);
6162

6263
constructor(
64+
@Inject(PLATFORM_ID) private platformID: any,
6365
protected auth: AuthService,
6466
protected authorizationService: AuthorizationDataService,
6567
protected fileService: FileService,
@@ -71,16 +73,18 @@ export class ThumbnailComponent implements OnChanges {
7173
* Use a default image if no actual image is available.
7274
*/
7375
ngOnChanges(changes: SimpleChanges): void {
74-
if (hasNoValue(this.thumbnail)) {
75-
this.setSrc(this.defaultImage);
76-
return;
77-
}
76+
if (isPlatformBrowser(this.platformID)) {
77+
if (hasNoValue(this.thumbnail)) {
78+
this.setSrc(this.defaultImage);
79+
return;
80+
}
7881

79-
const src = this.contentHref;
80-
if (hasValue(src)) {
81-
this.setSrc(src);
82-
} else {
83-
this.setSrc(this.defaultImage);
82+
const src = this.contentHref;
83+
if (hasValue(src)) {
84+
this.setSrc(src);
85+
} else {
86+
this.setSrc(this.defaultImage);
87+
}
8488
}
8589
}
8690

0 commit comments

Comments
 (0)