Skip to content

Commit 0d127cd

Browse files
Merge branch 'main-cris' into task/main-cris/DSC-2309
2 parents d62b4c6 + 0b1462c commit 0d127cd

63 files changed

Lines changed: 672 additions & 220 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dspace-angular",
3-
"version": "2024.02.01-SNAPSHOT",
3+
"version": "2024.02.02-SNAPSHOT",
44
"scripts": {
55
"ng": "ng",
66
"config:watch": "nodemon",

server.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
6767
// Set path fir IIIF viewer.
6868
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
6969

70+
const miradorHtml = join(IIIF_VIEWER, '/mirador/index.html');
71+
7072
const indexHtml = join(DIST_FOLDER, 'index.html');
7173

7274
const cookieParser = require('cookie-parser');
@@ -88,8 +90,10 @@ const _window = domino.createWindow(indexHtml);
8890
// The REST server base URL
8991
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
9092

93+
const IIIF_ALLOWED_ORIGINS = environment.rest.allowedOrigins || [];
94+
9195
// Assign the DOM window and document objects to the global object
92-
(_window as any).screen = {deviceXDPI: 0, logicalXDPI: 0};
96+
(_window as any).screen = { deviceXDPI: 0, logicalXDPI: 0 };
9397
(global as any).window = _window;
9498
(global as any).document = _window.document;
9599
(global as any).navigator = _window.navigator;
@@ -211,6 +215,35 @@ export function app() {
211215
*/
212216
router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
213217

218+
/*
219+
* Adapt headers to allow embedding of IIIF viewer in authorized pages
220+
*/
221+
server.get('/iiif/mirador/index.html', (req, res) => {
222+
const referer = req.headers.referer;
223+
224+
if (referer && !referer.startsWith('/')) {
225+
try {
226+
const origin = new URL(referer).origin;
227+
if (IIIF_ALLOWED_ORIGINS.includes(origin)) {
228+
console.info('Found allowed origin, setting headers for IIIF viewer');
229+
// CORS header
230+
res.setHeader('Access-Control-Allow-Origin', origin);
231+
// CSP for iframe embedding
232+
res.setHeader('Content-Security-Policy', `frame-ancestors ${origin};`);
233+
console.info('Headers have been set ', res.getHeader('Access-Control-Allow-Origin'), res.getHeader('Content-Security-Policy'));
234+
}
235+
} catch (error) {
236+
console.error('An error occurred setting security headers in response:', error);
237+
}
238+
}
239+
240+
res.sendFile(miradorHtml, (err) => {
241+
if (err) {
242+
res.status(500).send('Internal Server Error');
243+
}
244+
});
245+
});
246+
214247
/**
215248
* Checking server status
216249
*/
@@ -283,6 +316,10 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
283316
],
284317
})
285318
.then((html) => {
319+
if (res.writableEnded || res.headersSent || res.finished) {
320+
return;
321+
}
322+
286323
if (hasValue(html)) {
287324
// Replace REST URL with UI URL
288325
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
@@ -646,10 +683,10 @@ function start() {
646683
* The callback function to serve client health check requests
647684
*/
648685
function clientHealthCheck(req, res) {
649-
const isServerHealthy = true;
650-
if (isServerHealthy) {
651-
res.status(200).json({ status: 'UP' });
652-
}
686+
const isServerHealthy = true;
687+
if (isServerHealthy) {
688+
res.status(200).json({ status: 'UP' });
689+
}
653690
}
654691

655692
/*
@@ -667,6 +704,8 @@ function healthCheck(req, res) {
667704
});
668705
});
669706
}
707+
708+
670709
// Webpack will replace 'require' with '__webpack_require__'
671710
// '__non_webpack_require__' is a proxy to Node 'require'
672711
// The below code is to ensure that the server is run only when not requiring the bundle.

src/app/bitstream-page/bitstream-download-redirect.guard.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ describe('BitstreamDownloadRedirectGuard', () => {
169169
it('should redirect to the content link', waitForAsync(() => {
170170
TestBed.runInInjectionContext(() => {
171171
resolver(route, state).subscribe(() => {
172-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
172+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link', null, true);
173173
},
174174
);
175175
});
@@ -183,7 +183,7 @@ describe('BitstreamDownloadRedirectGuard', () => {
183183
it('should redirect to an updated content link', waitForAsync(() => {
184184
TestBed.runInInjectionContext(() => {
185185
resolver(route, state).subscribe(() => {
186-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
186+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers', null, true);
187187
});
188188
});
189189
}));

src/app/bitstream-page/bitstream-download-redirect.guard.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const bitstreamDownloadRedirectGuard: CanActivateFn = (
4747
): Observable<UrlTree | boolean> => {
4848

4949
const bitstreamId = route.params.id;
50+
const accessToken: string = route.queryParams.accessToken;
5051

5152
return bitstreamDataService.findById(bitstreamId, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW).pipe(
5253
getFirstCompletedRemoteData(),
@@ -77,16 +78,23 @@ export const bitstreamDownloadRedirectGuard: CanActivateFn = (
7778
}),
7879
map(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
7980
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
80-
hardRedirectService.redirect(fileLink);
81+
hardRedirectService.redirect(fileLink, null, true);
8182
return false;
82-
} else if (isAuthorized && !isLoggedIn) {
83-
hardRedirectService.redirect(bitstream._links.content.href);
83+
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
84+
hardRedirectService.redirect(bitstream._links.content.href, null, true);
8485
return false;
85-
} else if (!isAuthorized && isLoggedIn) {
86-
return router.createUrlTree([getForbiddenRoute()]);
87-
} else if (!isAuthorized && !isLoggedIn) {
88-
auth.setRedirectUrl(router.url);
89-
return router.createUrlTree(['login']);
86+
} else if (!isAuthorized) {
87+
// Either we have an access token, or we are logged in, or we are not logged in.
88+
// For now, the access token does not care if we are logged in or not.
89+
if (hasValue(accessToken)) {
90+
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken, null, true);
91+
return false;
92+
} else if (isLoggedIn) {
93+
return router.createUrlTree([getForbiddenRoute()]);
94+
} else if (!isLoggedIn) {
95+
auth.setRedirectUrl(router.url);
96+
return router.createUrlTree(['login']);
97+
}
9098
}
9199
}),
92100
);

src/app/core/auth/server-auth-request.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { Injectable } from '@angular/core';
77
import { Observable } from 'rxjs';
88
import { map } from 'rxjs/operators';
9+
import { RESTURLCombiner } from 'src/app/core/url-combiner/rest-url-combiner';
910

1011
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
1112
import { PostRequest } from '../data/request.models';
@@ -42,8 +43,8 @@ export class ServerAuthRequestService extends AuthRequestService {
4243
* @protected
4344
*/
4445
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
45-
// First do a call to the root endpoint in order to get an XSRF token
46-
return this.httpClient.get(this.halService.getRootHref(), { observe: 'response' }).pipe(
46+
// First do a call to the csrf endpoint in order to get an XSRF token
47+
return this.httpClient.get(new RESTURLCombiner('/security/csrf').toString(), { observe: 'response' }).pipe(
4748
// retrieve the XSRF token from the response header
4849
map((response: HttpResponse<any>) => response.headers.get(XSRF_RESPONSE_HEADER)),
4950
map((xsrfToken: string) => {

src/app/core/data/feature-authorization/feature-id.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export enum FeatureID {
4141
EPersonForgotPassword = 'epersonForgotPassword',
4242
ShowClaimItem = 'showClaimItem',
4343
CanCorrectItem = 'canCorrectItem',
44+
CanViewInWorkflowSinceStatistics = 'canViewInWorkflowSinceStatistics',
4445
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export abstract class HardRedirectService {
1313
* the page to redirect to
1414
* @param statusCode
1515
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
16+
* @param shouldSetCorsHeader
17+
* optional to prevent CORS error on redirect
1618
*/
17-
abstract redirect(url: string, statusCode?: number);
19+
abstract redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean);
1820

1921
/**
2022
* Get the current route, with query params included

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ describe('ServerHardRedirectService', () => {
88
const mockRequest = jasmine.createSpyObj(['get']);
99
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
1010

11-
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
11+
const serverResponseService = jasmine.createSpyObj('ServerResponseService', {
12+
setHeader: jasmine.createSpy('setHeader'),
13+
});
14+
15+
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse, serverResponseService);
1216
const origin = 'https://test-host.com:4000';
1317

1418
beforeEach(() => {
1519
mockRequest.protocol = 'https';
20+
mockRequest.path = '/bitstreams/test-uuid/download';
1621
mockRequest.headers = {
1722
host: 'test-host.com:4000',
1823
};
@@ -76,7 +81,7 @@ describe('ServerHardRedirectService', () => {
7681
ssrBaseUrl: 'https://private-url:4000/server',
7782
baseUrl: 'https://public-url/server',
7883
} } };
79-
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse);
84+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse, serverResponseService);
8085

8186
beforeEach(() => {
8287
service.redirect(redirect);
@@ -88,4 +93,21 @@ describe('ServerHardRedirectService', () => {
8893
});
8994
});
9095

96+
describe('Should add cors header on download path', () => {
97+
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
98+
const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
99+
ssrBaseUrl: 'https://private-url:4000/server',
100+
baseUrl: 'https://public-url/server',
101+
} } };
102+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse, serverResponseService);
103+
104+
beforeEach(() => {
105+
service.redirect(redirect, null, true);
106+
});
107+
108+
it('should set header', () => {
109+
expect(serverResponseService.setHeader).toHaveBeenCalled();
110+
});
111+
});
112+
91113
});

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '../../../express.tokens';
1818
import { isNotEmpty } from '../../shared/empty.util';
1919
import { HardRedirectService } from './hard-redirect.service';
20+
import { ServerResponseService } from './server-response.service';
2021

2122
/**
2223
* Service for performing hard redirects within the server app module
@@ -28,6 +29,7 @@ export class ServerHardRedirectService extends HardRedirectService {
2829
@Inject(APP_CONFIG) protected appConfig: AppConfig,
2930
@Inject(REQUEST) protected req: Request,
3031
@Inject(RESPONSE) protected res: Response,
32+
private responseService: ServerResponseService,
3133
) {
3234
super();
3335
}
@@ -39,8 +41,9 @@ export class ServerHardRedirectService extends HardRedirectService {
3941
* the page to redirect to
4042
* @param statusCode
4143
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
44+
* @param shouldSetCorsHeader
4245
*/
43-
redirect(url: string, statusCode?: number) {
46+
redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean) {
4447
if (url === this.req.url) {
4548
return;
4649
}
@@ -70,6 +73,10 @@ export class ServerHardRedirectService extends HardRedirectService {
7073
status = 302;
7174
}
7275

76+
if (shouldSetCorsHeader) {
77+
this.setCorsHeader();
78+
}
79+
7380
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
7481

7582
this.res.redirect(status, redirectUrl);
@@ -96,4 +103,12 @@ export class ServerHardRedirectService extends HardRedirectService {
96103
getCurrentOrigin(): string {
97104
return this.req.protocol + '://' + this.req.headers.host;
98105
}
106+
107+
/**
108+
* Set CORS header to allow embedding of redirected content.
109+
* The actual security header will be set by the rest
110+
*/
111+
setCorsHeader() {
112+
this.responseService.setHeader('Access-Control-Allow-Origin', '*');
113+
}
99114
}

src/app/item-page/item-page.resolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ export const itemPageResolver: ResolveFn<RemoteData<Item>> = (
7575
map((rd: RemoteData<Item>) => {
7676
store.dispatch(new ResolvedAction(state.url, rd.payload));
7777
if (rd.hasSucceeded && hasValue(rd.payload)) {
78+
const itemRoute = router.parseUrl(getItemPageRoute(rd.payload)).toString();
7879
// Check if custom url not empty and if the current id parameter is different from the custom url redirect to custom url
7980
if (hasValue(rd.payload.metadata) && isNotEmpty(rd.payload.metadata['cris.customurl'])) {
8081
if (route.params.id !== rd.payload.metadata['cris.customurl'][0].value) {
81-
const newUrl = state.url.replace(route.params.id, rd.payload.metadata['cris.customurl'][0].value);
82+
const newUrl = itemRoute.replace(route.params.id, rd.payload.metadata['cris.customurl'][0].value);
8283
router.navigateByUrl(newUrl);
8384
}
8485
} else {
@@ -88,7 +89,6 @@ export const itemPageResolver: ResolveFn<RemoteData<Item>> = (
8889
// or semicolons) and thisRoute has been encoded with that function. If we want to compare
8990
// it with itemRoute, we have to run itemRoute through Angular's version as well to ensure
9091
// the same characters are encoded the same way.
91-
const itemRoute = router.parseUrl(getItemPageRoute(rd.payload)).toString();
9292

9393
if (!thisRoute.startsWith(itemRoute)) {
9494
const itemId = rd.payload.uuid;

0 commit comments

Comments
 (0)