mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #716 from atmire/Authorization-for-downloads-of-restricted-bitstreams
Authorization for downloads of restricted bitstreams
This commit is contained in:
@@ -21,9 +21,9 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<a [href]="file._links.content.href" [download]="file.name">
|
||||
<ds-file-download-link [href]="file._links.content.href" [download]="file.name">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</a>
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||
<div class="file-section">
|
||||
<a *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
||||
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
||||
<span>{{file?.name}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||
</a>
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</ds-metadata-field-wrapper>
|
||||
</ng-container>
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models';
|
||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||
import {
|
||||
AuthGetRequest,
|
||||
AuthPostRequest,
|
||||
GetRequest,
|
||||
PostRequest,
|
||||
RestRequest,
|
||||
TokenPostRequest
|
||||
} from '../data/request.models';
|
||||
import { AuthStatusResponse, ErrorResponse, TokenResponse } from '../cache/response.models';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
@@ -15,6 +21,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
export class AuthRequestService {
|
||||
protected linkName = 'authn';
|
||||
protected browseEndpoint = '';
|
||||
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
||||
|
||||
constructor(protected halService: HALEndpointService,
|
||||
protected requestService: RequestService,
|
||||
@@ -67,4 +74,19 @@ export class AuthRequestService {
|
||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,14 @@
|
||||
import { async, inject, TestBed } from '@angular/core/testing';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { authReducer, AuthState } from './auth.reducer';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
||||
import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
@@ -49,6 +46,7 @@ describe('AuthService test', () => {
|
||||
let storage: CookieService;
|
||||
let token: AuthTokenInfo;
|
||||
let authenticatedState;
|
||||
let unAuthenticatedState;
|
||||
let linkService;
|
||||
|
||||
function init() {
|
||||
@@ -67,6 +65,13 @@ describe('AuthService test', () => {
|
||||
authToken: token,
|
||||
user: EPersonMock
|
||||
};
|
||||
unAuthenticatedState = {
|
||||
authenticated: false,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: undefined,
|
||||
user: undefined
|
||||
};
|
||||
authRequest = new AuthRequestServiceStub();
|
||||
routeStub = new ActivatedRouteStub();
|
||||
linkService = {
|
||||
@@ -214,6 +219,12 @@ describe('AuthService test', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the shortlived token when user is logged in', () => {
|
||||
authService.getShortlivedToken().subscribe((shortlivedToken: string) => {
|
||||
expect(shortlivedToken).toEqual(authRequest.mockShortLivedToken);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return token object when it is valid', () => {
|
||||
authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => {
|
||||
expect(tokenState).toBe(token);
|
||||
@@ -448,4 +459,44 @@ describe('AuthService test', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is not logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({ authReducer }, {
|
||||
runtimeChecks: {
|
||||
strictStateImmutability: false,
|
||||
strictActionImmutability: false
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthRequestService, useValue: authRequest },
|
||||
{ provide: REQUEST, useValue: {} },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: RemoteDataBuildService, useValue: linkService },
|
||||
CookieService,
|
||||
AuthService
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = unAuthenticatedState;
|
||||
});
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||
}));
|
||||
|
||||
it('should return null for the shortlived token', () => {
|
||||
authService.getShortlivedToken().subscribe((shortlivedToken: string) => {
|
||||
expect(shortlivedToken).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -534,4 +534,14 @@ export class AuthService {
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
45
src/app/core/auth/token-response-parsing.service.spec.ts
Normal file
45
src/app/core/auth/token-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { TokenResponseParsingService } from './token-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { TokenResponse } from '../cache/response.models';
|
||||
|
||||
describe('TokenResponseParsingService', () => {
|
||||
let service: TokenResponseParsingService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new TokenResponseParsingService();
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('should return a TokenResponse containing the token', () => {
|
||||
const data = {
|
||||
payload: {
|
||||
token: 'valid-token'
|
||||
},
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
} as DSpaceRESTV2Response;
|
||||
const expected = new TokenResponse(data.payload.token, true, 200, 'OK');
|
||||
expect(service.parse(undefined, data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an empty TokenResponse when payload doesn\'t contain a token', () => {
|
||||
const data = {
|
||||
payload: {},
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
} as DSpaceRESTV2Response;
|
||||
const expected = new TokenResponse(null, false, 200, 'OK');
|
||||
expect(service.parse(undefined, data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an error TokenResponse when the response failed', () => {
|
||||
const data = {
|
||||
payload: {},
|
||||
statusCode: 400,
|
||||
statusText: 'BAD REQUEST'
|
||||
} as DSpaceRESTV2Response;
|
||||
const expected = new TokenResponse(null, false, 400, 'BAD REQUEST');
|
||||
expect(service.parse(undefined, data)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
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
@@ -167,6 +167,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 {
|
||||
constructor(
|
||||
public dataDefinition: PaginatedList<IntegrationModel>,
|
||||
|
@@ -143,6 +143,7 @@ import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||
import { Registration } from './shared/registration.model';
|
||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -259,6 +260,7 @@ const PROVIDERS = [
|
||||
WorkflowActionDataService,
|
||||
MetadataSchemaDataService,
|
||||
MetadataFieldDataService,
|
||||
TokenResponseParsingService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
|
@@ -18,6 +18,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
@@ -239,6 +240,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 {
|
||||
constructor(uuid: string, href: string) {
|
||||
super(uuid, href);
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { 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 { Inject, Injectable } from '@angular/core';
|
||||
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.
|
||||
@@ -12,22 +12,20 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
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
|
||||
* file url
|
||||
*/
|
||||
downloadFile(url: string) {
|
||||
const headers = new HttpHeaders();
|
||||
const options: HttpOptions = Object.create({headers, responseType: 'blob'});
|
||||
return this.restService.request(RestRequestMethod.GET, url, null, options)
|
||||
.subscribe((data) => {
|
||||
saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data));
|
||||
});
|
||||
this.authService.getShortlivedToken().pipe(take(1)).subscribe((token) => {
|
||||
this._window.nativeWindow.location.href = hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<a *ngIf="!(isAuthenticated$ | async)" [href]="href" [download]="download"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||
<a *ngIf="(isAuthenticated$ | async)" [href]="href" [download]="download" (click)="downloadFile()"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||
|
||||
<ng-template #content>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
@@ -0,0 +1,57 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FileDownloadLinkComponent } from './file-download-link.component';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('FileDownloadLinkComponent', () => {
|
||||
let component: FileDownloadLinkComponent;
|
||||
let fixture: ComponentFixture<FileDownloadLinkComponent>;
|
||||
|
||||
let authService: AuthService;
|
||||
let fileService: FileService;
|
||||
let href: string;
|
||||
|
||||
function init() {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
fileService = jasmine.createSpyObj('fileService', ['downloadFile']);
|
||||
href = 'test-download-file-link';
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FileDownloadLinkComponent ],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: FileService, useValue: fileService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.href = href;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('downloadFile', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = component.downloadFile();
|
||||
});
|
||||
|
||||
it('should call fileService.downloadFile with the provided href', () => {
|
||||
expect(fileService.downloadFile).toHaveBeenCalledWith(href);
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,48 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-file-download-link',
|
||||
templateUrl: './file-download-link.component.html',
|
||||
styleUrls: ['./file-download-link.component.scss']
|
||||
})
|
||||
/**
|
||||
* Component displaying a download link
|
||||
* When the user is authenticated, a short-lived token retrieved from the REST API is added to the download link,
|
||||
* ensuring the user is authorized to download the file.
|
||||
*/
|
||||
export class FileDownloadLinkComponent implements OnInit {
|
||||
/**
|
||||
* Href to link to
|
||||
*/
|
||||
@Input() href: string;
|
||||
|
||||
/**
|
||||
* Optional file name for the download
|
||||
*/
|
||||
@Input() download: string;
|
||||
|
||||
/**
|
||||
* Whether or not the current user is authenticated
|
||||
*/
|
||||
isAuthenticated$: Observable<boolean>;
|
||||
|
||||
constructor(private fileService: FileService,
|
||||
private authService: AuthService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.isAuthenticated$ = this.authService.isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a download of the file
|
||||
* Return false to ensure the original href is displayed when the user hovers over the link
|
||||
*/
|
||||
downloadFile(): boolean {
|
||||
this.fileService.downloadFile(this.href);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -202,6 +202,7 @@ import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/reso
|
||||
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
||||
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
||||
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
||||
import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component';
|
||||
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
|
||||
|
||||
const MODULES = [
|
||||
@@ -388,6 +389,7 @@ const COMPONENTS = [
|
||||
EpersonGroupListComponent,
|
||||
EpersonSearchBoxComponent,
|
||||
GroupSearchBoxComponent,
|
||||
FileDownloadLinkComponent,
|
||||
CollectionDropdownComponent
|
||||
];
|
||||
|
||||
@@ -461,7 +463,8 @@ const ENTRY_COMPONENTS = [
|
||||
ClaimedTaskActionsApproveComponent,
|
||||
ClaimedTaskActionsRejectComponent,
|
||||
ClaimedTaskActionsReturnToPoolComponent,
|
||||
ClaimedTaskActionsEditMetadataComponent
|
||||
ClaimedTaskActionsEditMetadataComponent,
|
||||
FileDownloadLinkComponent,
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
@@ -9,6 +9,7 @@ import { EPersonMock } from './eperson.mock';
|
||||
export class AuthRequestServiceStub {
|
||||
protected mockUser: EPerson = EPersonMock;
|
||||
protected mockTokenInfo = new AuthTokenInfo('test_token');
|
||||
protected mockShortLivedToken = 'test-shortlived-token';
|
||||
|
||||
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
||||
const authStatusStub: AuthStatus = new AuthStatus();
|
||||
@@ -82,4 +83,8 @@ export class AuthRequestServiceStub {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public getShortlivedToken() {
|
||||
return observableOf(this.mockShortLivedToken);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user