mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
[DURACOM-288] Provide a setting to use a different REST url during SSR execution
This commit is contained in:
@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
|
||||
// extend environment with app config for server
|
||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||
|
||||
// The REST server base URL
|
||||
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
|
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app() {
|
||||
|
||||
@@ -156,7 +159,7 @@ export function app() {
|
||||
* Proxy the sitemaps
|
||||
*/
|
||||
router.use('/sitemap**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}/sitemaps`,
|
||||
target: `${REST_BASE_URL}/sitemaps`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true,
|
||||
}));
|
||||
@@ -165,7 +168,7 @@ export function app() {
|
||||
* Proxy the linksets
|
||||
*/
|
||||
router.use('/signposting**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}`,
|
||||
target: `${REST_BASE_URL}`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true,
|
||||
}));
|
||||
@@ -623,7 +626,7 @@ function start() {
|
||||
* The callback function to serve health check requests
|
||||
*/
|
||||
function healthCheck(req, res) {
|
||||
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
|
||||
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
|
||||
axios.get(baseUrl)
|
||||
.then((response) => {
|
||||
res.status(response.status).send(response.data);
|
||||
|
@@ -54,6 +54,7 @@ import {
|
||||
} from './app-routes';
|
||||
import { BROWSE_BY_DECORATOR_MAP } from './browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
||||
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
|
||||
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
||||
import { LogInterceptor } from './core/log/log.interceptor';
|
||||
import {
|
||||
@@ -148,6 +149,11 @@ export const commonAppConfig: ApplicationConfig = {
|
||||
useClass: LogInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DspaceRestInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||
...DYNAMIC_MATCHER_PROVIDERS,
|
||||
provideCore(),
|
||||
|
194
src/app/core/dspace-rest/dspace-rest.interceptor.spec.ts
Normal file
194
src/app/core/dspace-rest/dspace-rest.interceptor.spec.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
HttpClient,
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing';
|
||||
import { PLATFORM_ID } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { DspaceRestInterceptor } from './dspace-rest.interceptor';
|
||||
import { DspaceRestService } from './dspace-rest.service';
|
||||
|
||||
describe('DspaceRestInterceptor', () => {
|
||||
let httpMock: HttpTestingController;
|
||||
let httpClient: HttpClient;
|
||||
const appConfig: Partial<AppConfig> = {
|
||||
rest: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
nameSpace: '/server',
|
||||
baseUrl: 'http://api.example.com/server',
|
||||
},
|
||||
};
|
||||
const appConfigWithSSR: Partial<AppConfig> = {
|
||||
rest: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
nameSpace: '/server',
|
||||
baseUrl: 'http://api.example.com/server',
|
||||
ssrBaseUrl: 'http://ssr.example.com/server',
|
||||
},
|
||||
};
|
||||
|
||||
describe('When SSR base URL is not set ', () => {
|
||||
describe('and it\'s in the browser', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
DspaceRestService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DspaceRestInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: APP_CONFIG, useValue: appConfig },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
});
|
||||
|
||||
it('should not modify the request', () => {
|
||||
const url = 'http://api.example.com/server/items';
|
||||
httpClient.get(url).subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
expect(req.request.url).toBe(url);
|
||||
req.flush({});
|
||||
httpMock.verify();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and it\'s in SSR mode', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
DspaceRestService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DspaceRestInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: APP_CONFIG, useValue: appConfig },
|
||||
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||
],
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
});
|
||||
|
||||
it('should not replace the base URL', () => {
|
||||
const url = 'http://api.example.com/server/items';
|
||||
|
||||
httpClient.get(url).subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
expect(req.request.url).toBe(url);
|
||||
req.flush({});
|
||||
httpMock.verify();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When SSR base URL is set ', () => {
|
||||
describe('and it\'s in the browser', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
DspaceRestService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DspaceRestInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
});
|
||||
|
||||
it('should not modify the request', () => {
|
||||
const url = 'http://api.example.com/server/items';
|
||||
httpClient.get(url).subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(url);
|
||||
expect(req.request.url).toBe(url);
|
||||
req.flush({});
|
||||
httpMock.verify();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and it\'s in SSR mode', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
DspaceRestService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DspaceRestInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
|
||||
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||
],
|
||||
});
|
||||
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
});
|
||||
|
||||
it('should replace the base URL', () => {
|
||||
const url = 'http://api.example.com/server/items';
|
||||
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
|
||||
|
||||
httpClient.get(url).subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(ssrBaseUrl + '/items');
|
||||
expect(req.request.url).toBe(ssrBaseUrl + '/items');
|
||||
req.flush({});
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should not replace any query param containing the base URL', () => {
|
||||
const url = 'http://api.example.com/server/items?url=http://api.example.com/server/item/1';
|
||||
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
|
||||
|
||||
httpClient.get(url).subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
|
||||
expect(req.request.url).toBe(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
|
||||
req.flush({});
|
||||
httpMock.verify();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
52
src/app/core/dspace-rest/dspace-rest.interceptor.ts
Normal file
52
src/app/core/dspace-rest/dspace-rest.interceptor.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import {
|
||||
HttpEvent,
|
||||
HttpHandler,
|
||||
HttpInterceptor,
|
||||
HttpRequest,
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { isEmpty } from '../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* This Interceptor is used to use the configured base URL for the request made during SSR execution
|
||||
*/
|
||||
export class DspaceRestInterceptor implements HttpInterceptor {
|
||||
|
||||
/**
|
||||
* Contains the configured application base URL
|
||||
* @protected
|
||||
*/
|
||||
protected baseUrl: string;
|
||||
protected ssrBaseUrl: string;
|
||||
|
||||
constructor(
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
@Inject(PLATFORM_ID) private platformId: string,
|
||||
) {
|
||||
this.baseUrl = this.appConfig.rest.baseUrl;
|
||||
this.ssrBaseUrl = this.appConfig.rest.ssrBaseUrl;
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
if (isPlatformBrowser(this.platformId) || isEmpty(this.ssrBaseUrl) || this.baseUrl === this.ssrBaseUrl) {
|
||||
return next.handle(request);
|
||||
}
|
||||
|
||||
// Different SSR Base URL specified so replace it in the current request url
|
||||
const url = request.url.replace(this.baseUrl, this.ssrBaseUrl);
|
||||
const newRequest: HttpRequest<any> = request.clone({ url });
|
||||
return next.handle(newRequest);
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { environment } from '../../../environments/environment.test';
|
||||
import { ServerHardRedirectService } from './server-hard-redirect.service';
|
||||
|
||||
describe('ServerHardRedirectService', () => {
|
||||
@@ -7,7 +8,7 @@ describe('ServerHardRedirectService', () => {
|
||||
const mockRequest = jasmine.createSpyObj(['get']);
|
||||
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
|
||||
|
||||
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
|
||||
const service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
|
||||
const origin = 'https://test-host.com:4000';
|
||||
|
||||
beforeEach(() => {
|
||||
|
@@ -7,10 +7,15 @@ import {
|
||||
Response,
|
||||
} from 'express';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import {
|
||||
REQUEST,
|
||||
RESPONSE,
|
||||
} from '../../../express.tokens';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { HardRedirectService } from './hard-redirect.service';
|
||||
|
||||
/**
|
||||
@@ -20,6 +25,7 @@ import { HardRedirectService } from './hard-redirect.service';
|
||||
export class ServerHardRedirectService extends HardRedirectService {
|
||||
|
||||
constructor(
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
@Inject(REQUEST) protected req: Request,
|
||||
@Inject(RESPONSE) protected res: Response,
|
||||
) {
|
||||
@@ -35,17 +41,22 @@ export class ServerHardRedirectService extends HardRedirectService {
|
||||
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
|
||||
*/
|
||||
redirect(url: string, statusCode?: number) {
|
||||
|
||||
if (url === this.req.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
let redirectUrl = url;
|
||||
// If redirect url contains SSR base url then replace with public base url
|
||||
if (isNotEmpty(this.appConfig.rest.ssrBaseUrl) && this.appConfig.rest.baseUrl !== this.appConfig.rest.ssrBaseUrl) {
|
||||
redirectUrl = url.replace(this.appConfig.rest.ssrBaseUrl, this.appConfig.rest.baseUrl);
|
||||
}
|
||||
|
||||
if (this.res.finished) {
|
||||
const req: any = this.req;
|
||||
req._r_count = (req._r_count || 0) + 1;
|
||||
|
||||
console.warn('Attempted to redirect on a finished response. From',
|
||||
this.req.url, 'to', url);
|
||||
this.req.url, 'to', redirectUrl);
|
||||
|
||||
if (req._r_count > 10) {
|
||||
console.error('Detected a redirection loop. killing the nodejs process');
|
||||
@@ -59,9 +70,9 @@ export class ServerHardRedirectService extends HardRedirectService {
|
||||
status = 302;
|
||||
}
|
||||
|
||||
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
||||
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
|
||||
|
||||
this.res.redirect(status, url);
|
||||
this.res.redirect(status, redirectUrl);
|
||||
this.res.end();
|
||||
// I haven't found a way to correctly stop Angular rendering.
|
||||
// So we just let it end its work, though we have already closed
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CommonModule,
|
||||
isPlatformBrowser,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
PLATFORM_ID,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -76,6 +81,7 @@ export class ThumbnailComponent implements OnChanges {
|
||||
isLoading = true;
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformID: any,
|
||||
protected auth: AuthService,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected fileService: FileService,
|
||||
@@ -87,16 +93,18 @@ export class ThumbnailComponent implements OnChanges {
|
||||
* Use a default image if no actual image is available.
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (hasNoValue(this.thumbnail)) {
|
||||
this.setSrc(this.defaultImage);
|
||||
return;
|
||||
}
|
||||
if (isPlatformBrowser(this.platformID)) {
|
||||
if (hasNoValue(this.thumbnail)) {
|
||||
this.setSrc(this.defaultImage);
|
||||
return;
|
||||
}
|
||||
|
||||
const src = this.contentHref;
|
||||
if (hasValue(src)) {
|
||||
this.setSrc(src);
|
||||
} else {
|
||||
this.setSrc(this.defaultImage);
|
||||
const src = this.contentHref;
|
||||
if (hasValue(src)) {
|
||||
this.setSrc(src);
|
||||
} else {
|
||||
this.setSrc(this.defaultImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,4 +6,6 @@ export class ServerConfig implements Config {
|
||||
public port: number;
|
||||
public nameSpace: string;
|
||||
public baseUrl?: string;
|
||||
public ssrBaseUrl?: string;
|
||||
public hasSsrBaseUrl?: boolean;
|
||||
}
|
||||
|
@@ -144,15 +144,20 @@ export class BrowserInitService extends InitService {
|
||||
* @private
|
||||
*/
|
||||
private async loadAppState(): Promise<boolean> {
|
||||
const state = this.transferState.get<any>(InitService.NGRX_STATE, null);
|
||||
this.transferState.remove(InitService.NGRX_STATE);
|
||||
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
|
||||
return lastValueFrom(
|
||||
this.store.select(coreSelector).pipe(
|
||||
find((core: any) => isNotEmpty(core)),
|
||||
map(() => true),
|
||||
),
|
||||
);
|
||||
// The app state can be transferred only when SSR and CSR are using the same base url for the REST API
|
||||
if (!this.appConfig.rest.hasSsrBaseUrl) {
|
||||
const state = this.transferState.get<any>(InitService.NGRX_STATE, null);
|
||||
this.transferState.remove(InitService.NGRX_STATE);
|
||||
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
|
||||
return lastValueFrom(
|
||||
this.store.select(coreSelector).pipe(
|
||||
find((core: any) => isNotEmpty(core)),
|
||||
map(() => true),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
private trackAuthTokenExpiration(): void {
|
||||
|
@@ -21,6 +21,7 @@ import { LocaleService } from '../../app/core/locale/locale.service';
|
||||
import { HeadTagService } from '../../app/core/metadata/head-tag.service';
|
||||
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||
import { InitService } from '../../app/init.service';
|
||||
import { isNotEmpty } from '../../app/shared/empty.util';
|
||||
import { MenuService } from '../../app/shared/menu/menu.service';
|
||||
import { ThemeService } from '../../app/shared/theme-support/theme.service';
|
||||
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||
@@ -100,6 +101,14 @@ export class ServerInitService extends InitService {
|
||||
}
|
||||
|
||||
private saveAppConfigForCSR(): void {
|
||||
this.transferState.set<AppConfig>(APP_CONFIG_STATE, environment as AppConfig);
|
||||
if (isNotEmpty(environment.rest.ssrBaseUrl) && environment.rest.baseUrl !== environment.rest.ssrBaseUrl) {
|
||||
// Avoid to transfer ssrBaseUrl in order to prevent security issues
|
||||
const config: AppConfig = Object.assign({}, environment as AppConfig, {
|
||||
rest: Object.assign({}, environment.rest, { ssrBaseUrl: '', hasSsrBaseUrl: true }),
|
||||
});
|
||||
this.transferState.set<AppConfig>(APP_CONFIG_STATE, config);
|
||||
} else {
|
||||
this.transferState.set<AppConfig>(APP_CONFIG_STATE, environment as AppConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user