Skip to content

Commit 59e6c4a

Browse files
authored
Merge pull request #3953 from 4Science/task/dspace-8_x/DURACOM-288
[Port dspace-8_x] Provide a setting to use a different REST url during SSR execution
2 parents 173f1d4 + 4b61ba4 commit 59e6c4a

18 files changed

+695
-266
lines changed

config/config.example.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ ui:
1919

2020
# Angular Server Side Rendering (SSR) settings
2121
ssr:
22+
# Enable request performance profiling data collection and printing the results in the server console.
23+
# Defaults to false. Enabling in production is NOT recommended
24+
enablePerformanceProfiler: false
2225
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
2326
# Determining which styles are critical is a relatively expensive operation; this option is
2427
# disabled (false) by default to boost server performance at the expense of loading smoothness.
@@ -35,6 +38,16 @@ ssr:
3538
# If set to true the component will be included in the HTML returned from the server side rendering.
3639
# If set to false the component will not be included in the HTML returned from the server side rendering.
3740
enableBrowseComponent: false
41+
# Enable state transfer from the server-side application to the client-side application.
42+
# Defaults to true.
43+
# Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
44+
# Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
45+
# ensure that users always use the most up-to-date state.
46+
transferState: true
47+
# When a different REST base URL is used for the server-side application, the generated state contains references to
48+
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
49+
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
50+
replaceRestUrl: true
3851

3952
# The REST API server settings
4053
# NOTE: these settings define which (publicly available) REST API to use. They are usually
@@ -45,6 +58,9 @@ rest:
4558
port: 443
4659
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
4760
nameSpace: /server
61+
# Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
62+
# server namespace (uncomment to use it).
63+
#ssrBaseUrl: http://localhost:8080/server
4864

4965
# Caching settings
5066
cache:

server.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
8181
// extend environment with app config for server
8282
extendEnvironmentWithAppConfig(environment, appConfig);
8383

84+
// The REST server base URL
85+
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
86+
8487
// The Express app is exported so that it can be used by serverless Functions.
8588
export function app() {
8689

@@ -156,7 +159,7 @@ export function app() {
156159
* Proxy the sitemaps
157160
*/
158161
router.use('/sitemap**', createProxyMiddleware({
159-
target: `${environment.rest.baseUrl}/sitemaps`,
162+
target: `${REST_BASE_URL}/sitemaps`,
160163
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
161164
changeOrigin: true,
162165
}));
@@ -165,7 +168,7 @@ export function app() {
165168
* Proxy the linksets
166169
*/
167170
router.use('/signposting**', createProxyMiddleware({
168-
target: `${environment.rest.baseUrl}`,
171+
target: `${REST_BASE_URL}`,
169172
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
170173
changeOrigin: true,
171174
}));
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
266269
})
267270
.then((html) => {
268271
if (hasValue(html)) {
272+
// Replace REST URL with UI URL
273+
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
274+
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
275+
}
276+
269277
// save server side rendered page to cache (if any are enabled)
270278
saveToCache(req, html);
271279
if (sendToUser) {
@@ -623,7 +631,7 @@ function start() {
623631
* The callback function to serve health check requests
624632
*/
625633
function healthCheck(req, res) {
626-
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
634+
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
627635
axios.get(baseUrl)
628636
.then((response) => {
629637
res.status(response.status).send(response.data);

src/app/app.config.ts

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

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { TestBed } from '@angular/core/testing';
22

3+
import { environment } from '../../../environments/environment.test';
34
import { ServerHardRedirectService } from './server-hard-redirect.service';
45

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

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

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

1314
beforeEach(() => {
@@ -68,4 +69,23 @@ describe('ServerHardRedirectService', () => {
6869
});
6970
});
7071

72+
describe('when SSR base url is set', () => {
73+
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
74+
const replacedUrl = 'https://public-url/server/api/bitstreams/uuid';
75+
const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
76+
ssrBaseUrl: 'https://private-url:4000/server',
77+
baseUrl: 'https://public-url/server',
78+
} } };
79+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse);
80+
81+
beforeEach(() => {
82+
service.redirect(redirect);
83+
});
84+
85+
it('should perform a 302 redirect', () => {
86+
expect(mockResponse.redirect).toHaveBeenCalledWith(302, replacedUrl);
87+
expect(mockResponse.end).toHaveBeenCalled();
88+
});
89+
});
90+
7191
});

0 commit comments

Comments
 (0)