mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-09 19:13:08 +00:00
71504: short-lived token for downloading files through FileService
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models';
|
import {
|
||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
AuthGetRequest,
|
||||||
|
AuthPostRequest,
|
||||||
|
GetRequest,
|
||||||
|
PostRequest,
|
||||||
|
RestRequest,
|
||||||
|
TokenPostRequest
|
||||||
|
} from '../data/request.models';
|
||||||
|
import { AuthStatusResponse, ErrorResponse, RestResponse, TokenResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
@@ -15,6 +22,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
protected linkName = 'authn';
|
protected linkName = 'authn';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
||||||
|
|
||||||
constructor(protected halService: HALEndpointService,
|
constructor(protected halService: HALEndpointService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -67,4 +75,19 @@ export class AuthRequestService {
|
|||||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||||
distinctUntilChanged());
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST request to retrieve a short-lived token which provides download access of restricted files
|
||||||
|
*/
|
||||||
|
public getShortlivedToken(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(`${this.linkName}/${this.shortlivedtokensEndpoint}`).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new TokenPostRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||||
|
switchMap((request: PostRequest) => this.requestService.getByUUID(request.uuid)),
|
||||||
|
getResponseFromEntry(),
|
||||||
|
map((response: TokenResponse) => response.token)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -546,4 +546,14 @@ export class AuthService {
|
|||||||
return this.getImpersonateID() === epersonId;
|
return this.getImpersonateID() === epersonId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a short-lived token for appending to download urls of restricted files
|
||||||
|
* Returns null if the user isn't authenticated
|
||||||
|
*/
|
||||||
|
getShortlivedToken(): Observable<string> {
|
||||||
|
return this.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
23
src/app/core/auth/token-response-parsing.service.ts
Normal file
23
src/app/core/auth/token-response-parsing.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ResponseParsingService } from '../data/parsing.service';
|
||||||
|
import { RestRequest } from '../data/request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { RestResponse, TokenResponse } from '../cache/response.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a token string
|
||||||
|
* wrapped in a TokenResponse
|
||||||
|
*/
|
||||||
|
export class TokenResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload.token) && (data.statusCode === 200)) {
|
||||||
|
return new TokenResponse(data.payload.token, true, data.statusCode, data.statusText);
|
||||||
|
} else {
|
||||||
|
return new TokenResponse(null, false, data.statusCode, data.statusText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
src/app/core/cache/response.models.ts
vendored
14
src/app/core/cache/response.models.ts
vendored
@@ -211,6 +211,20 @@ export class AuthStatusResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A REST Response containing a token
|
||||||
|
*/
|
||||||
|
export class TokenResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public token: string,
|
||||||
|
public isSuccessful: boolean,
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string
|
||||||
|
) {
|
||||||
|
super(isSuccessful, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntegrationSuccessResponse extends RestResponse {
|
export class IntegrationSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public dataDefinition: PaginatedList<IntegrationModel>,
|
public dataDefinition: PaginatedList<IntegrationModel>,
|
||||||
|
@@ -145,6 +145,7 @@ import { Version } from './shared/version.model';
|
|||||||
import { VersionHistory } from './shared/version-history.model';
|
import { VersionHistory } from './shared/version-history.model';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
|
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -264,6 +265,7 @@ const PROVIDERS = [
|
|||||||
LicenseDataService,
|
LicenseDataService,
|
||||||
ItemTypeDataService,
|
ItemTypeDataService,
|
||||||
WorkflowActionDataService,
|
WorkflowActionDataService,
|
||||||
|
TokenResponseParsingService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -20,6 +20,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -241,6 +242,15 @@ export class AuthGetRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A POST request for retrieving a token
|
||||||
|
*/
|
||||||
|
export class TokenPostRequest extends PostRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return TokenResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntegrationRequest extends GetRequest {
|
export class IntegrationRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string) {
|
constructor(uuid: string, href: string) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
|
|
||||||
import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides utility methods to save files on the client-side.
|
* Provides utility methods to save files on the client-side.
|
||||||
@@ -12,22 +12,20 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileService {
|
export class FileService {
|
||||||
constructor(
|
constructor(
|
||||||
private restService: DSpaceRESTv2Service
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
private authService: AuthService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a HTTP Get request to download a file
|
* Combines an URL with a short-lived token and sets the current URL to the newly created one
|
||||||
*
|
*
|
||||||
* @param url
|
* @param url
|
||||||
* file url
|
* file url
|
||||||
*/
|
*/
|
||||||
downloadFile(url: string) {
|
downloadFile(url: string) {
|
||||||
const headers = new HttpHeaders();
|
this.authService.getShortlivedToken().pipe(take(1)).subscribe((token) => {
|
||||||
const options: HttpOptions = Object.create({headers, responseType: 'blob'});
|
this._window.nativeWindow.location.href = hasValue(token) ? new URLCombiner(url, `?token=${token}`).toString() : url;
|
||||||
return this.restService.request(RestRequestMethod.GET, url, null, options)
|
});
|
||||||
.subscribe((data) => {
|
|
||||||
saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user