diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.html b/src/app/item-page/access-by-token/item-access-by-token-page.component.html
new file mode 100644
index 0000000000..a99f01858b
--- /dev/null
+++ b/src/app/item-page/access-by-token/item-access-by-token-page.component.html
@@ -0,0 +1,8 @@
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.scss b/src/app/item-page/access-by-token/item-access-by-token-page.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.spec.ts b/src/app/item-page/access-by-token/item-access-by-token-page.component.spec.ts
new file mode 100644
index 0000000000..e270b34669
--- /dev/null
+++ b/src/app/item-page/access-by-token/item-access-by-token-page.component.spec.ts
@@ -0,0 +1,299 @@
+import { KeyValuePipe } from '@angular/common';
+import { PLATFORM_ID } from '@angular/core';
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+} from '@angular/core/testing';
+import {
+ ActivatedRoute,
+ Router,
+} from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Store } from '@ngrx/store';
+import { TranslateModule } from '@ngx-translate/core';
+import {
+ BehaviorSubject,
+ of as observableOf,
+} from 'rxjs';
+
+import { getForbiddenRoute } from '../../app-routing-paths';
+import { AuthService } from '../../core/auth/auth.service';
+import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
+import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
+import { ItemDataService } from '../../core/data/item-data.service';
+import { ItemRequestDataService } from '../../core/data/item-request-data.service';
+import { SignpostingDataService } from '../../core/data/signposting-data.service';
+import { LinkHeadService } from '../../core/services/link-head.service';
+import { ServerResponseService } from '../../core/services/server-response.service';
+import { Item } from '../../core/shared/item.model';
+import { ItemRequest } from '../../core/shared/item-request.model';
+import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
+import { ItemAccessByTokenPageComponent } from './item-access-by-token-page.component';
+import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component';
+
+describe('ItemAccessByTokenPageComponent', () => {
+ let component: ItemAccessByTokenPageComponent;
+ let fixture: ComponentFixture;
+ let itemRequestService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
+ let authorizationService: AuthorizationDataService;
+ authorizationService = jasmine.createSpyObj('authorizationService', {
+ isAuthorized: observableOf(false),
+ });
+ let signpostingDataService: SignpostingDataService;
+
+ const mocklink = {
+ href: 'http://test.org',
+ rel: 'test',
+ type: 'test',
+ };
+
+ const mocklink2 = {
+ href: 'http://test2.org',
+ rel: 'test',
+ type: 'test',
+ };
+ signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
+ getLinks: observableOf([mocklink, mocklink2]),
+ });
+ const linkHeadService = jasmine.createSpyObj('linkHeadService', {
+ addTag: '',
+ });
+
+ const mockItem = Object.assign(new Item(), {
+ uuid: 'test-item-uuid',
+ id: 'test-item-id',
+ metadata: {
+ 'dspace.entity.type': [{
+ value: 'Publication',
+ language: 'en',
+ place: 0,
+ authority: null,
+ confidence: -1,
+ }],
+ },
+ _links: {
+ self: { href: 'obj-selflink' },
+ },
+ });
+
+ const mockItemRequest = Object.assign(new ItemRequest(), {
+ token: 'valid-token',
+ accessToken: 'valid-token',
+ itemId: mockItem.uuid,
+ });
+
+ const queryParams = { accessToken: 'valid-token' };
+ const mockActivatedRoute = {
+ queryParams: new BehaviorSubject(queryParams),
+ data: observableOf({
+ dso: createSuccessfulRemoteDataObject(mockItem),
+ }),
+ params: observableOf({ itemId: mockItem.uuid, queryParams: [ { accessToken: 'valid-token' } ] }),
+ children: [],
+ };
+ itemRequestService = jasmine.createSpyObj('ItemRequestDataService', {
+ getSanitizedRequestByAccessToken: observableOf(createSuccessfulRemoteDataObject(mockItemRequest)),
+ });
+ router = jasmine.createSpyObj('Router', ['navigateByUrl'], {
+ events: observableOf([]),
+ });
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ KeyValuePipe,
+ ],
+ providers: [
+ { provide: ItemRequestDataService, useValue: itemRequestService },
+ { provide: Router, useValue: router },
+ { provide: ActivatedRoute, useValue: mockActivatedRoute },
+ { provide: ItemDataService, useValue: {} },
+ { provide: AuthorizationDataService, useValue: authorizationService },
+ { provide: ServerResponseService, useValue: {} },
+ { provide: SignpostingDataService, useValue: signpostingDataService },
+ { provide: LinkHeadService, useValue: linkHeadService },
+ { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
+ { provide: PLATFORM_ID, useValue: 'browser' },
+ KeyValuePipe,
+ {
+ provide: Store,
+ useValue: {
+ pipe: () => observableOf({}),
+ dispatch: () => {
+ },
+ select: () => observableOf({}),
+ },
+ },
+ {
+ provide: AuthService, useValue: {
+ isAuthenticated: () => observableOf(true),
+ },
+ },
+ ],
+ }).overrideComponent(ItemAccessByTokenPageComponent, {
+ set: {
+ template: '',
+ },
+ }).overrideComponent(ItemAccessByTokenViewComponent, {
+ set: {
+ template: '',
+ },
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ });
+
+ /**
+ * Tests in this component are concerned only with successful access token processing (or error handling)
+ * and a resulting item request object. Testing of template elements is out of scope and left for child components.
+ */
+ describe('ngOnInit - basic component testing', () => {
+ it('should find valid access token and sanitize it', fakeAsync(() => {
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ KeyValuePipe,
+ RouterTestingModule.withRoutes([]),
+ ],
+ providers: [
+ { provide: ItemRequestDataService, useValue: itemRequestService },
+ { provide: Router, useValue: router },
+ { provide: ActivatedRoute, useValue: mockActivatedRoute },
+ { provide: AuthService, useValue: {} },
+ { provide: ItemDataService, useValue: {} },
+ { provide: AuthorizationDataService, useValue: authorizationService },
+ { provide: ServerResponseService, useValue: {} },
+ { provide: SignpostingDataService, useValue: signpostingDataService },
+ { provide: LinkHeadService, useValue: linkHeadService },
+ { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
+ { provide: PLATFORM_ID, useValue: 'browser' },
+ KeyValuePipe,
+ {
+ provide: Store,
+ useValue: {
+ pipe: () => observableOf({}),
+ dispatch: () => {},
+ select: () => observableOf({}),
+ },
+ },
+ { provide: AuthService, useValue: {
+ isAuthenticated: () => observableOf(false ) },
+ },
+ ],
+ }).overrideComponent(ItemAccessByTokenViewComponent, {
+ set: { template: '' } } ).compileComponents();
+
+ fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ expect(itemRequestService.getSanitizedRequestByAccessToken).toHaveBeenCalledWith('valid-token');
+
+ }));
+
+ it('should process valid access token and load item request', fakeAsync(() => {
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ KeyValuePipe,
+ RouterTestingModule.withRoutes([]),
+ ],
+ providers: [
+ { provide: ItemRequestDataService, useValue: itemRequestService },
+ { provide: Router, useValue: router },
+ { provide: ActivatedRoute, useValue: mockActivatedRoute },
+ { provide: AuthService, useValue: {} },
+ { provide: ItemDataService, useValue: {} },
+ { provide: AuthorizationDataService, useValue: authorizationService },
+ { provide: ServerResponseService, useValue: {} },
+ { provide: SignpostingDataService, useValue: signpostingDataService },
+ { provide: LinkHeadService, useValue: linkHeadService },
+ { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
+ { provide: PLATFORM_ID, useValue: 'browser' },
+ KeyValuePipe,
+ {
+ provide: Store,
+ useValue: {
+ pipe: () => observableOf({}),
+ dispatch: () => {},
+ select: () => observableOf({}),
+ },
+ },
+ { provide: AuthService, useValue: {
+ isAuthenticated: () => observableOf(false ) },
+ },
+ ],
+ }).overrideComponent(ItemAccessByTokenViewComponent, {
+ set: { template: '' } } ).compileComponents();
+
+ fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ component.itemRequest$.subscribe((request) => {
+ expect(request).toBeTruthy();
+ });
+ }));
+
+ it('should redirect to forbidden route when access token is missing', fakeAsync(() => {
+ const routeWithoutToken = {
+ queryParams: observableOf({}),
+ data: observableOf({
+ dso: createSuccessfulRemoteDataObject(mockItem),
+ }),
+ params: observableOf({ itemId: mockItem.uuid }),
+ children: [],
+ };
+
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ KeyValuePipe,
+ ],
+ providers: [
+ { provide: ItemRequestDataService, useValue: itemRequestService },
+ { provide: Router, useValue: router },
+ { provide: ActivatedRoute, useValue: routeWithoutToken },
+ { provide: AuthService, useValue: {} },
+ { provide: ItemDataService, useValue: {} },
+ { provide: AuthorizationDataService, useValue: authorizationService },
+ { provide: ServerResponseService, useValue: {} },
+ { provide: SignpostingDataService, useValue: signpostingDataService },
+ { provide: LinkHeadService, useValue: linkHeadService },
+ { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
+ { provide: PLATFORM_ID, useValue: 'browser' },
+ {
+ provide: Store,
+ useValue: {
+ pipe: () => observableOf({}),
+ dispatch: () => {},
+ select: () => observableOf({}),
+ },
+ },
+ { provide: AuthService, useValue: {
+ isAuthenticated: () => observableOf(false ) },
+ },
+ ],
+ }).overrideComponent(ItemAccessByTokenViewComponent, {
+ set: {
+ template: '',
+ } })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: false });
+ }));
+ });
+});
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.ts b/src/app/item-page/access-by-token/item-access-by-token-page.component.ts
new file mode 100644
index 0000000000..5250ac37a2
--- /dev/null
+++ b/src/app/item-page/access-by-token/item-access-by-token-page.component.ts
@@ -0,0 +1,178 @@
+import {
+ AsyncPipe,
+ KeyValuePipe,
+ Location,
+ NgForOf,
+ NgIf,
+} from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Inject,
+ OnDestroy,
+ OnInit,
+ PLATFORM_ID,
+} from '@angular/core';
+import {
+ ActivatedRoute,
+ Router,
+ RouterLink,
+} from '@angular/router';
+import { TranslateModule } from '@ngx-translate/core';
+import { Observable } from 'rxjs';
+import {
+ filter,
+ map,
+ switchMap,
+ take,
+ tap,
+} from 'rxjs/operators';
+
+import { getForbiddenRoute } from '../../app-routing-paths';
+import { AuthService } from '../../core/auth/auth.service';
+import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
+import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
+import { ItemDataService } from '../../core/data/item-data.service';
+import { ItemRequestDataService } from '../../core/data/item-request-data.service';
+import { SignpostingDataService } from '../../core/data/signposting-data.service';
+import { LinkHeadService } from '../../core/services/link-head.service';
+import { ServerResponseService } from '../../core/services/server-response.service';
+import { redirectOn4xx } from '../../core/shared/authorized.operators';
+import { ItemRequest } from '../../core/shared/item-request.model';
+import {
+ getFirstCompletedRemoteData,
+ getFirstSucceededRemoteDataPayload,
+} from '../../core/shared/operators';
+import { fadeInOut } from '../../shared/animations/fade';
+import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
+import { hasValue } from '../../shared/empty.util';
+import { ErrorComponent } from '../../shared/error/error.component';
+import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
+import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
+import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component';
+import { VarDirective } from '../../shared/utils/var.directive';
+import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
+import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component';
+import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
+import { CollectionsComponent } from '../field-components/collections/collections.component';
+import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component';
+import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component';
+import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component';
+import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component';
+import { ItemPageAbstractFieldComponent } from '../simple/field-components/specific-field/abstract/item-page-abstract-field.component';
+import { ItemPageDateFieldComponent } from '../simple/field-components/specific-field/date/item-page-date-field.component';
+import { GenericItemPageFieldComponent } from '../simple/field-components/specific-field/generic/generic-item-page-field.component';
+import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
+import { ItemPageUriFieldComponent } from '../simple/field-components/specific-field/uri/item-page-uri-field.component';
+import { ItemPageComponent } from '../simple/item-page.component';
+import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component';
+import { ItemVersionsComponent } from '../versions/item-versions.component';
+import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
+import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component';
+import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component';
+
+@Component({
+ selector: 'ds-access-by-token-item-page',
+ styleUrls: ['./item-access-by-token-page.component.scss'],
+ templateUrl: './item-access-by-token-page.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [fadeInOut],
+ standalone: true,
+ imports: [
+ ErrorComponent,
+ ThemedLoadingComponent,
+ TranslateModule,
+ ThemedFullFileSectionComponent,
+ CollectionsComponent,
+ ItemVersionsComponent,
+ NgIf,
+ NgForOf,
+ AsyncPipe,
+ KeyValuePipe,
+ RouterLink,
+ ThemedItemPageTitleFieldComponent,
+ DsoEditMenuComponent,
+ ItemVersionsNoticeComponent,
+ ViewTrackerComponent,
+ ThemedItemAlertsComponent,
+ VarDirective,
+ ItemSecureFileSectionComponent,
+ GenericItemPageFieldComponent,
+ ItemPageAbstractFieldComponent,
+ ItemPageDateFieldComponent,
+ ItemPageUriFieldComponent,
+ MetadataFieldWrapperComponent,
+ MiradorViewerComponent,
+ ThemedFileSectionComponent,
+ ThemedMediaViewerComponent,
+ ThemedMetadataRepresentationListComponent,
+ ThemedResultsBackButtonComponent,
+ ThemedThumbnailComponent,
+ ItemAccessByTokenViewComponent,
+ ],
+})
+export class ItemAccessByTokenPageComponent extends ItemPageComponent implements OnInit, OnDestroy {
+
+ itemRequest$: Observable;
+
+ constructor(
+ protected route: ActivatedRoute,
+ protected router: Router,
+ protected items: ItemDataService,
+ protected authService: AuthService,
+ protected authorizationService: AuthorizationDataService,
+ protected _location: Location,
+ protected responseService: ServerResponseService,
+ protected signpostingDataService: SignpostingDataService,
+ protected linkHeadService: LinkHeadService,
+ protected notifyInfoService: NotifyInfoService,
+ private itemRequestDataService: ItemRequestDataService,
+ @Inject(PLATFORM_ID) protected platformId: string,
+ ) {
+ super(route, router, items, authorizationService, responseService, signpostingDataService, linkHeadService, notifyInfoService, platformId);
+ }
+
+ protected readonly hasValue = hasValue;
+
+ /**
+ * Initialise this component
+ * 1. take the access token from the query params and complete the stream
+ * 2. test for access token or redirect to forbidden page
+ * 3. get the sanitized token, make sure it is valid (if not, redirect to forbidden page)
+ * 4. return observable to itemRequest$ for the view to subscribe to
+ */
+ ngOnInit(): void {
+ this.itemRequest$ = this.route.queryParams.pipe(
+ take(1),
+ map(params => {
+ if (!hasValue(params?.accessToken)) {
+ this.router.navigateByUrl(getForbiddenRoute(), { skipLocationChange: false });
+ return null;
+ }
+ return params.accessToken;
+ }),
+ filter(token => hasValue(token)),
+ switchMap(token => this.itemRequestDataService.getSanitizedRequestByAccessToken(token)),
+ getFirstCompletedRemoteData(),
+ redirectOn4xx(this.router, this.authService),
+ getFirstSucceededRemoteDataPayload(),
+ tap(request => {
+ if (!hasValue(request)) {
+ this.router.navigateByUrl(getForbiddenRoute());
+ }
+ }),
+ );
+
+ // Call item page component initialization.
+ super.ngOnInit();
+ }
+
+ /**
+ * Navigate back in browser history.
+ */
+ back() {
+ this._location.back();
+ }
+
+}
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.html b/src/app/item-page/access-by-token/item-access-by-token-view.component.html
new file mode 100644
index 0000000000..047966e615
--- /dev/null
+++ b/src/app/item-page/access-by-token/item-access-by-token-view.component.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
{{'bitstream-request-a-copy.access-by-token.warning' | translate}}
+
0">{{ 'bitstream-request-a-copy.access-by-token.expiry-label' | translate }} {{ getAccessPeriodEndDate() }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.scss b/src/app/item-page/access-by-token/item-access-by-token-view.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.spec.ts b/src/app/item-page/access-by-token/item-access-by-token-view.component.spec.ts
new file mode 100644
index 0000000000..78e4544b6c
--- /dev/null
+++ b/src/app/item-page/access-by-token/item-access-by-token-view.component.spec.ts
@@ -0,0 +1,213 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import {
+ ComponentFixture,
+ TestBed,
+ waitForAsync,
+} from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { provideMockStore } from '@ngrx/store/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import {
+ of as observableOf,
+ of,
+} from 'rxjs';
+
+import {
+ APP_CONFIG,
+ APP_DATA_SERVICES_MAP,
+} from '../../../config/app-config.interface';
+import { environment } from '../../../environments/environment';
+import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
+import { ItemRequestDataService } from '../../core/data/item-request-data.service';
+import { Bitstream } from '../../core/shared/bitstream.model';
+import { Item } from '../../core/shared/item.model';
+import { ItemRequest } from '../../core/shared/item-request.model';
+import { ITEM_REQUEST } from '../../core/shared/item-request.resource-type';
+import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
+import { ErrorComponent } from '../../shared/error/error.component';
+import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
+import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
+import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component';
+import { RouterLinkDirectiveStub } from '../../shared/testing/router-link-directive.stub';
+import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
+import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
+import { CollectionsComponent } from '../field-components/collections/collections.component';
+import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component';
+import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component';
+import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component';
+import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component';
+import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component';
+import { ItemVersionsComponent } from '../versions/item-versions.component';
+import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
+import { ItemSecureFileDownloadLinkComponent } from './field-components/file-download-link/item-secure-file-download-link.component';
+import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component';
+import { ItemSecureMediaViewerComponent } from './field-components/media-viewer/item-secure-media-viewer.component';
+import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component';
+
+
+describe('ItemAccessByTokenViewComponent', () => {
+ let authorizationService: AuthorizationDataService;
+ let itemRequestDataService: ItemRequestDataService;
+ let bitstream: Bitstream;
+ let item: Item;
+ let itemRequest: ItemRequest;
+ let component: ItemAccessByTokenViewComponent;
+ let fixture: ComponentFixture;
+ let routeStub: any;
+
+ function init() {
+ itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
+ canDownload: observableOf(true),
+ });
+ bitstream = Object.assign(new Bitstream(), {
+ uuid: 'bitstreamUuid',
+ });
+ item = Object.assign(new Item(), {
+ uuid: 'itemUuid',
+ metadata: {
+ 'dspace.entity.type': [
+ {
+ value: 'Publication',
+ },
+ ],
+ },
+ _links: {
+ self: { href: 'obj-selflink' },
+ },
+ });
+ routeStub = {
+ data: observableOf({
+ dso: createSuccessfulRemoteDataObject(item),
+ }),
+ children: [],
+ };
+
+ const mockItemRequest: ItemRequest = Object.assign(new ItemRequest(), {
+
+ });
+ itemRequest = Object.assign(new ItemRequest(),
+ {
+ itemId: item.uuid,
+ bitstreamId: bitstream.uuid,
+ allfiles: false,
+ requestEmail: 'user@name.org',
+ requestName: 'User Name',
+ requestMessage: 'I would like to request a copy',
+ accessPeriod: 3600,
+ decisionDate: new Date().toISOString(),
+ token: 'test-token',
+ type: ITEM_REQUEST,
+ requestDate: new Date().toISOString(),
+ accessToken: 'test-token',
+ expires: null,
+ acceptRequest: true,
+ });
+ }
+
+ function initTestbed() {
+
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(), ItemSecureFileDownloadLinkComponent,
+ RouterLinkDirectiveStub,
+ ],
+ providers: [
+ { provide: AuthorizationDataService, useValue: authorizationService },
+ { provide: ActivatedRoute, useValue: routeStub },
+ { provide: RouterLinkDirectiveStub },
+ { provide: ItemRequestDataService, useValue: itemRequestDataService },
+ provideMockStore(),
+ { provide: APP_DATA_SERVICES_MAP, useValue: {} },
+ { provide: APP_CONFIG, useValue: environment },
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ }).overrideComponent(ItemAccessByTokenViewComponent, {
+ remove: {
+ imports: [
+ ErrorComponent,
+ ThemedLoadingComponent,
+ ThemedFullFileSectionComponent,
+ CollectionsComponent,
+ ItemVersionsComponent,
+ DsoEditMenuComponent,
+ ItemVersionsNoticeComponent,
+ ViewTrackerComponent,
+ ThemedItemAlertsComponent,
+ ItemSecureFileSectionComponent,
+ MiradorViewerComponent,
+ ThemedFileSectionComponent,
+ ThemedMediaViewerComponent,
+ ThemedMetadataRepresentationListComponent,
+ ThemedResultsBackButtonComponent,
+ ItemSecureMediaViewerComponent,
+ ],
+ },
+ }).compileComponents();
+ }
+
+ const mockItem = Object.assign(new Item(), {
+ uuid: 'test-item-uuid',
+ id: 'test-item-id',
+ });
+
+
+
+
+ beforeEach(waitForAsync(() => {
+ init();
+ authorizationService = jasmine.createSpyObj('authorizationService', {
+ isAuthorized: observableOf(true),
+ });
+ initTestbed();
+ }));
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ItemAccessByTokenViewComponent);
+ component = fixture.componentInstance;
+ component.object = item;
+ component.itemRequest$ = of(itemRequest);
+ component.itemRequestSubject.next(itemRequest);
+ fixture.detectChanges();
+ });
+
+ describe('Component and inputs initialised properly', () => {
+ it('should initialize with valid ItemRequest input', () => {
+ //component.itemRequestSubject.next(itemRequest);
+ component.itemRequest$.subscribe(request => {
+ expect(request).toBeDefined();
+ expect(request.accessPeriod).toBe(3600);
+ expect(request.token).toBe('test-token');
+ expect(request.requestName).toBe('User Name');
+ expect(request.requestEmail).toBe('user@name.org');
+ expect(request.requestMessage).toBe('I would like to request a copy');
+ expect(request.allfiles).toBe(false);
+ expect(request.bitstreamId).toBe(bitstream.uuid);
+ expect(request.acceptRequest).toBe(true);
+ });
+ });
+ });
+
+ describe('getAccessPeriodEndDate', () => {
+ it('should calculate correct end date based on decision date and access period', () => {
+ const testDecisionDate = '2024-01-01T00:00:00Z';
+ const testAccessPeriod = 3600;
+
+ const testRequest = {
+ ...itemRequest,
+ decisionDate: testDecisionDate,
+ accessPeriod: testAccessPeriod,
+ };
+ component.itemRequest$ = of(testRequest);
+ component.itemRequestSubject.next(testRequest);
+ const expectedDate = new Date(testDecisionDate);
+ expectedDate.setUTCSeconds(expectedDate.getUTCSeconds() + testAccessPeriod);
+
+ expect(component.getAccessPeriodEndDate()).toEqual(expectedDate);
+ });
+
+ it('should return undefined when access period is 0', () => {
+ component.itemRequestSubject.next({ ...itemRequest, accessPeriod: 0 });
+ expect(component.getAccessPeriodEndDate()).toBeUndefined();
+ });
+ });
+});
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.ts b/src/app/item-page/access-by-token/item-access-by-token-view.component.ts
new file mode 100644
index 0000000000..61f7679cfc
--- /dev/null
+++ b/src/app/item-page/access-by-token/item-access-by-token-view.component.ts
@@ -0,0 +1,128 @@
+import {
+ AsyncPipe,
+ KeyValuePipe,
+ NgForOf,
+ NgIf,
+} from '@angular/common';
+import {
+ Component,
+ Input,
+ OnInit,
+} from '@angular/core';
+import {
+ Router,
+ RouterLink,
+} from '@angular/router';
+import { TranslateModule } from '@ngx-translate/core';
+import {
+ BehaviorSubject,
+ Observable,
+} from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { RouteService } from '../../core/services/route.service';
+import { ItemRequest } from '../../core/shared/item-request.model';
+import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
+import { hasValue } from '../../shared/empty.util';
+import { ErrorComponent } from '../../shared/error/error.component';
+import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
+import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
+import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component';
+import { VarDirective } from '../../shared/utils/var.directive';
+import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
+import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component';
+import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
+import { CollectionsComponent } from '../field-components/collections/collections.component';
+import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component';
+import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component';
+import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component';
+import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component';
+import { ItemPageAbstractFieldComponent } from '../simple/field-components/specific-field/abstract/item-page-abstract-field.component';
+import { ItemPageDateFieldComponent } from '../simple/field-components/specific-field/date/item-page-date-field.component';
+import { GenericItemPageFieldComponent } from '../simple/field-components/specific-field/generic/generic-item-page-field.component';
+import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
+import { ItemPageUriFieldComponent } from '../simple/field-components/specific-field/uri/item-page-uri-field.component';
+import { ItemComponent } from '../simple/item-types/shared/item.component';
+import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component';
+import { ItemVersionsComponent } from '../versions/item-versions.component';
+import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
+import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component';
+import { ItemSecureMediaViewerComponent } from './field-components/media-viewer/item-secure-media-viewer.component';
+
+@Component({
+ selector: 'ds-item-access-by-token-view',
+ styleUrls: ['./item-access-by-token-view.component.scss'],
+ templateUrl: './item-access-by-token-view.component.html',
+ standalone: true,
+ imports: [
+ ErrorComponent,
+ ThemedLoadingComponent,
+ TranslateModule,
+ ThemedFullFileSectionComponent,
+ CollectionsComponent,
+ ItemVersionsComponent,
+ NgIf,
+ NgForOf,
+ AsyncPipe,
+ KeyValuePipe,
+ RouterLink,
+ ThemedItemPageTitleFieldComponent,
+ DsoEditMenuComponent,
+ ItemVersionsNoticeComponent,
+ ViewTrackerComponent,
+ ThemedItemAlertsComponent,
+ VarDirective,
+ ItemSecureFileSectionComponent,
+ GenericItemPageFieldComponent,
+ ItemPageAbstractFieldComponent,
+ ItemPageDateFieldComponent,
+ ItemPageUriFieldComponent,
+ MetadataFieldWrapperComponent,
+ MiradorViewerComponent,
+ ThemedFileSectionComponent,
+ ThemedMediaViewerComponent,
+ ThemedMetadataRepresentationListComponent,
+ ThemedResultsBackButtonComponent,
+ ThemedThumbnailComponent,
+ ItemSecureMediaViewerComponent,
+ //ItemPageTitleFieldComponent,
+ //ThumbnailComponent,
+ //MetadataRepresentationListComponent,
+ ],
+})
+export class ItemAccessByTokenViewComponent extends ItemComponent implements OnInit {
+
+ @Input() itemRequest$: Observable;
+ itemRequestSubject = new BehaviorSubject(null);
+ expiryDate: Date;
+
+ constructor(
+ protected routeService: RouteService,
+ protected router: Router,
+ ) {
+ super(routeService, router);
+ }
+
+ protected readonly hasValue = hasValue;
+
+ ngOnInit(): void {
+ this.itemRequest$.pipe(
+ filter(request => hasValue(request)),
+ ).subscribe(request => {
+ this.itemRequestSubject.next(request);
+ super.ngOnInit();
+ });
+
+
+ }
+
+ getAccessPeriodEndDate(): Date {
+ const request = this.itemRequestSubject.getValue();
+ // Set expiry, if not 0
+ if (hasValue(request) && request.accessPeriod > 0) {
+ const date = new Date(request.decisionDate);
+ date.setUTCSeconds(date.getUTCSeconds() + request.accessPeriod);
+ return date;
+ }
+ }
+}
diff --git a/src/app/shared/testing/router-link-directive.stub.ts b/src/app/shared/testing/router-link-directive.stub.ts
index b3e2b09fb3..35d668c1bf 100644
--- a/src/app/shared/testing/router-link-directive.stub.ts
+++ b/src/app/shared/testing/router-link-directive.stub.ts
@@ -11,4 +11,5 @@ import {
})
export class RouterLinkDirectiveStub {
@Input() routerLink: any;
+ @Input() queryParams: any;
}