From dd3445b84687fbf42fe822f2353af49d3afbd936 Mon Sep 17 00:00:00 2001
From: Florent POITTEVIN <poittevin.florent@gmail.com>
Date: Tue, 23 Jun 2020 15:58:30 +0200
Subject: [PATCH] fix: DLCM-1489 in archive detail avoid download when not
 possible and check rigth to disable button accordingly (todo on sarch list
 table)

---
 .../data-file/deposit-data-file.state.ts      |  2 +-
 .../features/deposit/stores/deposit.state.ts  |  2 +-
 .../home-archive-detail.presentational.html   | 74 ++++++++++--------
 .../home-archive-detail.presentational.scss   |  4 +
 .../home-archive-detail.presentational.ts     | 77 ++++++++++++++++++-
 src/app/features/home/stores/home.action.ts   |  2 +-
 src/app/features/home/stores/home.state.ts    |  8 +-
 src/app/shared/services/download.service.ts   | 63 ++++++++-------
 src/app/shared/services/security.service.ts   | 11 +++
 src/assets/i18n/de.json                       |  4 +
 src/assets/i18n/en.json                       |  4 +
 src/assets/i18n/fr.json                       |  4 +
 12 files changed, 186 insertions(+), 69 deletions(-)

diff --git a/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts b/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts
index 52ccb771a..30aa97068 100644
--- a/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts
+++ b/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts
@@ -215,7 +215,7 @@ export class DepositDataFileState extends CompositionState<DepositDataFileStateM
   }
 
   @Action(DepositDataFileAction.Download)
-  download(ctx: StateContext<DepositDataFileStateModel>, action: DepositDataFileAction.Download): Observable<void> {
+  download(ctx: StateContext<DepositDataFileStateModel>, action: DepositDataFileAction.Download): Observable<boolean> {
     const url = `${this._urlResource}/${action.parentId}/${ApiResourceNameEnum.DATAFILE}/${action.dataFile.resId}/${ApiActionEnum.DL}`;
     return this.downloadService.download(url, action.dataFile.fileName, action.dataFile.fileSize);
   }
diff --git a/src/app/features/deposit/stores/deposit.state.ts b/src/app/features/deposit/stores/deposit.state.ts
index ac4b7b6b7..e2277c356 100644
--- a/src/app/features/deposit/stores/deposit.state.ts
+++ b/src/app/features/deposit/stores/deposit.state.ts
@@ -845,7 +845,7 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> {
   }
 
   @Action(DepositAction.Download)
-  download(ctx: StateContext<DepositStateModel>, action: DepositAction.Download): Observable<void | Blob> {
+  download(ctx: StateContext<DepositStateModel>, action: DepositAction.Download): Observable<boolean> {
     let fileName = this._DEPOSIT_FILE_DOWNLOAD_PREFIX + action.id;
     let url = `${this._urlResource}/${action.id}/${ApiActionEnum.DL}`;
     if (isNotNullNorUndefined(action.fullFolderName)) {
diff --git a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html
index c6e8466f0..cb11d9655 100644
--- a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html
+++ b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html
@@ -81,40 +81,48 @@
   </div>
 </dl>
 
-<button *ngIf="isLoggedIn || isPublicMetadata()"
-        mat-flat-button
-        [dlcmButtonSpinner]="isLoadingPrepareDownload"
-        color="primary"
-        solidifyShortCuts
-        (onEnter)="download(archiveMetadata)"
-        (click)="download(archiveMetadata)"
->
-  <dlcm-shared-icon [iconName]="iconNameEnum.download"></dlcm-shared-icon>
-  {{'homePage.archive.detail.download' | translate}}
-</button>
+<div class="button-wrapper">
+  <div [matTooltip]="getTooltipDownload(!(isDownloadAuthorized() | solidifyAsync)) | translate">
+    <button *ngIf="isLoggedIn"
+            mat-flat-button
+            [dlcmButtonSpinner]="isLoadingPrepareDownload"
+            color="primary"
+            solidifyShortCuts
+            [disabled]="!(isDownloadAuthorized() | solidifyAsync)"
+            (onEnter)="download(archiveMetadata)"
+            (click)="download(archiveMetadata)"
+    >
+      <dlcm-shared-icon [iconName]="iconNameEnum.download"></dlcm-shared-icon>
+      {{'homePage.archive.detail.download' | translate}}
+    </button>
+  </div>
 
-<button *ngIf="isLoggedIn"
-        mat-flat-button
-        color="primary"
-        class="add-to-cart-button"
-        solidifyShortCuts
-        (onEnter)="addToCart(archiveMetadata)"
-        (click)="addToCart(archiveMetadata)"
+  <div [matTooltip]="getTooltipDownload(!(isDownloadAuthorized() | solidifyAsync)) | translate">
+    <button *ngIf="isLoggedIn"
+            mat-flat-button
+            class="add-to-cart-button"
+            color="primary"
+            solidifyShortCuts
+            [disabled]="!(isDownloadAuthorized() | solidifyAsync)"
+            (onEnter)="addToCart(archiveMetadata)"
+            (click)="addToCart(archiveMetadata)"
 
->
-  <dlcm-shared-icon [iconName]="iconNameEnum.addToCart"></dlcm-shared-icon>
-  {{'homePage.archive.detail.button.addToCart' | translate}}
-</button>
+    >
+      <dlcm-shared-icon [iconName]="iconNameEnum.addToCart"></dlcm-shared-icon>
+      {{'homePage.archive.detail.button.addToCart' | translate}}
+    </button>
+  </div>
 
-<button *ngIf="isLoggedIn && isRestrictedOrClosedAccessLevel()"
-        mat-flat-button
-        color="primary"
-        class="add-to-cart-button"
-        solidifyShortCuts
-        (onEnter)="askAccess(archiveMetadata)"
-        (click)="askAccess(archiveMetadata)"
+  <button *ngIf="isAskAccessAvailable() | solidifyAsync"
+          mat-flat-button
+          color="primary"
+          class="add-to-cart-button"
+          solidifyShortCuts
+          (onEnter)="askAccess(archiveMetadata)"
+          (click)="askAccess(archiveMetadata)"
 
->
-  <dlcm-shared-icon [iconName]="iconNameEnum.sendRequest"></dlcm-shared-icon>
-  {{'homePage.archive.detail.button.askAccess' | translate}}
-</button>
+  >
+    <dlcm-shared-icon [iconName]="iconNameEnum.sendRequest"></dlcm-shared-icon>
+    {{'homePage.archive.detail.button.askAccess' | translate}}
+  </button>
+</div>
diff --git a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss
index 70b258d69..f4bf9ba41 100644
--- a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss
+++ b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss
@@ -80,4 +80,8 @@ $padding-bottom-desktop: 20px;
   .add-to-cart-button {
     margin-left: 10px;
   }
+
+  .button-wrapper {
+    display: flex;
+  }
 }
diff --git a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts
index 5f0b178ad..c833212e1 100644
--- a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts
+++ b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts
@@ -11,11 +11,14 @@ import {ArchiveMetadata} from "@app/shared/models/business/archive-metadata.mode
 import {MetadataUtil} from "@app/shared/utils/metadata.util";
 import {Enums} from "@enums";
 import {GetShortDoiWrapper} from "@shared/components/presentationals/shared-doi-menu/shared-doi-menu.presentational";
+import {ObservableOrPromiseOrValue} from "@shared/models/extra-button-toolbar.model";
 import {FileSizePipe} from "@shared/pipes/file-size.pipe";
+import {SecurityService} from "@shared/services/security.service";
 import {
   BehaviorSubject,
   Observable,
 } from "rxjs";
+import {map} from "rxjs/operators";
 import {
   DateUtil,
   EnumUtil,
@@ -69,6 +72,14 @@ export class HomeArchiveDetailPresentational extends SharedAbstractPresentationa
     return TypeInfoEnum;
   }
 
+  constructor(protected readonly _securityService: SecurityService) {
+    super();
+  }
+
+  get orgUnitResId(): string {
+    return MetadataUtil.getOrganizationalUnitResId(this.archiveMetadata.metadata);
+  }
+
   ngOnInit(): void {
     const metadata = this.archiveMetadata.metadata;
     this.fileSizePipe = new FileSizePipe();
@@ -183,12 +194,72 @@ export class HomeArchiveDetailPresentational extends SharedAbstractPresentationa
     return MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata) === Enums.Deposit.AccessEnum.PUBLIC;
   }
 
-  isRestrictedOrClosedAccessLevel(): boolean {
+  isRestrictedMetadata(): boolean {
+    if (isNullOrUndefined(this.archiveMetadata)) {
+      return false;
+    }
+    return MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata) === Enums.Deposit.AccessEnum.RESTRICTED;
+  }
+
+  isClosedMetadata(): boolean {
     if (isNullOrUndefined(this.archiveMetadata)) {
       return false;
     }
-    const accessLevel = MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata);
-    return accessLevel === Enums.Deposit.AccessEnum.RESTRICTED || accessLevel === Enums.Deposit.AccessEnum.CLOSED;
+    return MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata) === Enums.Deposit.AccessEnum.CLOSED;
+  }
+
+  isDownloadAuthorized(): ObservableOrPromiseOrValue<boolean> {
+    if (this.isPublicMetadata()) {
+      return true;
+    }
+    if (!this.isLoggedIn) {
+      return false;
+    }
+
+    if (this._securityService.isRootOrAdmin()) {
+      return true;
+    }
+
+    if (this.isRestrictedMetadata()) {
+      return this._securityService.isMemberOfOrgUnit(this.orgUnitResId);
+    }
+
+    if (this.isClosedMetadata()) {
+      return this._securityService.isStewardOfOrgUnit(this.orgUnitResId);
+    }
+  }
+
+  isAskAccessAvailable(): ObservableOrPromiseOrValue<boolean> {
+    if (!this.isLoggedIn) {
+      return false;
+    }
+    if (this.isPublicMetadata()) {
+      return false;
+    }
+    const isMemberOfOrgUnit = this._securityService.isMemberOfOrgUnit(this.orgUnitResId);
+    if (!isMemberOfOrgUnit) {
+      return true;
+    }
+    if (this.isRestrictedMetadata()) {
+      // FOR RESTRICTED : NEED TO BE MEMBER
+      return true;
+    }
+    if (this.isClosedMetadata()) {
+      // FOR CLOSED : NEED TO BE STEWARD / OR MEMBER OF LIST (not ready on backend)
+      return this._securityService.isStewardOfOrgUnit(this.orgUnitResId).pipe(map(result => !result));
+    }
+  }
+
+  getTooltipDownload(isDisabled: boolean): undefined | string {
+    if (this.isPublicMetadata() || !isDisabled) {
+      return undefined;
+    }
+    if (this.isRestrictedMetadata()) {
+      return MARK_AS_TRANSLATABLE("homePage.archive.detail.buttonDisabledReason.needToBeMember");
+    }
+    if (this.isClosedMetadata()) {
+      return MARK_AS_TRANSLATABLE("homePage.archive.detail.buttonDisabledReason.needToBeSteward");
+    }
   }
 }
 
diff --git a/src/app/features/home/stores/home.action.ts b/src/app/features/home/stores/home.action.ts
index 16b828d88..3707f55f4 100644
--- a/src/app/features/home/stores/home.action.ts
+++ b/src/app/features/home/stores/home.action.ts
@@ -103,7 +103,7 @@ export namespace HomeAction {
     static readonly type: string = `[${state}] Download Success`;
   }
 
-  export class DownloadFail extends BaseSubAction<Download> {
+  export class DownloadFail extends BaseSubAction<DownloadStart> {
     static readonly type: string = `[${state}] Download Fail`;
   }
 
diff --git a/src/app/features/home/stores/home.state.ts b/src/app/features/home/stores/home.state.ts
index 6d3ea056f..3ef5271dc 100644
--- a/src/app/features/home/stores/home.state.ts
+++ b/src/app/features/home/stores/home.state.ts
@@ -391,8 +391,12 @@ export class HomeState extends BasicState<HomeStateModel> {
     const fileSize = archiveMetadata.metadata[MetadataEnum.aipSize];
 
     this.downloadService.download(`${this.getUrlMetadata(ctx)}/${archiveMetadata.resId}/${ApiActionEnum.DL}`, fileName, fileSize)
-      .subscribe(() => {
-        ctx.dispatch(new HomeAction.DownloadSuccess(action));
+      .subscribe((result) => {
+        if (result) {
+          ctx.dispatch(new HomeAction.DownloadSuccess(action));
+        } else {
+          ctx.dispatch(new HomeAction.DownloadFail(action));
+        }
       });
   }
 
diff --git a/src/app/shared/services/download.service.ts b/src/app/shared/services/download.service.ts
index 41a244281..b05c8552d 100644
--- a/src/app/shared/services/download.service.ts
+++ b/src/app/shared/services/download.service.ts
@@ -11,6 +11,7 @@ import {
 } from "rxjs";
 import {tap} from "rxjs/operators";
 import {
+  HttpStatus,
   isNullOrUndefined,
   isTrue,
   OAuth2Service,
@@ -25,7 +26,7 @@ export class DownloadService {
   constructor(private oauth2Service: OAuth2Service,
               private httpClient: HttpClient) {}
 
-  download(url: string, fileName: string, size?: number): Observable<void> {
+  download(url: string, fileName: string, size?: number): Observable<boolean> {
     if (isNullOrUndefined(streamSaver.WritableStream)) {
       streamSaver.WritableStream = WritableStream;
     }
@@ -47,34 +48,40 @@ export class DownloadService {
       requestHeaders.set("Authorization", "Bearer " + token);
     }
 
-    return from(
-      fetch(url, {
-        headers: requestHeaders,
-      }).then(res => {
-        const readableStream = res.body;
-        // const readableStream = new ReadableStream(res.body); // TODO To allow optimize version on firefox but half work and break Chrome download
-        const writableStream = streamSaver.WritableStream;
-        // Optimized way for supported browser
-        if (writableStream && readableStream.pipeTo) {
-          return readableStream.pipeTo(fileStream)
-            .then(() => {
-            });
-        }
-
-        // Standard way
-        const writer = fileStream.getWriter();
-        const reader = res.body.getReader();
-        const pump = () => reader.read()
-          .then(result => {
-            if (result.done) {
-              return writer.close();
-            } else {
-              return writer.write(result.value).then(pump);
-            }
+    const fetchResult = fetch(url, {
+      headers: requestHeaders,
+    }).then(res => {
+      if (res.status !== HttpStatus.OK) {
+        return false;
+      }
+      const readableStream = res.body;
+      // const readableStream = new ReadableStream(res.body); // TODO To allow optimize version on firefox but half work and break Chrome download
+      const writableStream = streamSaver.WritableStream;
+      // Optimized way for supported browser
+      if (writableStream && readableStream.pipeTo) {
+        return readableStream.pipeTo(fileStream)
+          .then(() => {
+            return true;
           });
-        pump();
-      }),
-    );
+      }
+
+      // Standard way
+      const writer = fileStream.getWriter();
+      const reader = res.body.getReader();
+      const pump = () => reader.read()
+        .then(result => {
+          if (result.done) {
+            writer.close();
+            return true;
+          } else {
+            writer.write(result.value).then(pump);
+            return true;
+          }
+        });
+      pump();
+    });
+
+    return from(fetchResult);
   }
 
   // Prefer use download api to stream directly on disk
diff --git a/src/app/shared/services/security.service.ts b/src/app/shared/services/security.service.ts
index 0e6a94120..daa056402 100644
--- a/src/app/shared/services/security.service.ts
+++ b/src/app/shared/services/security.service.ts
@@ -28,6 +28,7 @@ import {
 })
 export class SecurityService {
 
+  private readonly ORGUNIT_ROLE_NEED_ACCESS_TO_CLOSED_DATASET: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.STEWARD, Enums.Role.RoleEnum.MANAGER];
   private readonly ORGUNIT_ROLE_NEED_TO_EDIT: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.MANAGER];
   private readonly DEPOSIT_ROLE_NEED_TO_CREATE: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.MANAGER, Enums.Role.RoleEnum.STEWARD, Enums.Role.RoleEnum.APPROVER, Enums.Role.RoleEnum.CREATOR];
   private readonly DEPOSIT_ROLE_NEED_TO_EDIT: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.MANAGER, Enums.Role.RoleEnum.STEWARD, Enums.Role.RoleEnum.APPROVER, Enums.Role.RoleEnum.CREATOR];
@@ -59,6 +60,16 @@ export class SecurityService {
     return this.currentUserHaveRoleInListForOrgUnit(orgUnitId, this.ORGUNIT_ROLE_NEED_TO_EDIT);
   }
 
+  public isStewardOfOrgUnit(orgUnitId: string): Observable<boolean> {
+    if (this.isRootOrAdmin()) {
+      return of(true);
+    }
+    if (!this.isMemberOfOrgUnit(orgUnitId)) {
+      return of(false);
+    }
+    return this.currentUserHaveRoleInListForOrgUnit(orgUnitId, this.ORGUNIT_ROLE_NEED_ACCESS_TO_CLOSED_DATASET);
+  }
+
   public canSeeDetailDeposit(deposit: Deposit): boolean {
     if (this.isRootOrAdmin()) {
       return true;
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index 611257dbb..ee0922ea0 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -1928,6 +1928,10 @@
           "addToCart": "Zur Download-Bestellung hinzufügen",
           "askAccess": "Zugang anfordern"
         },
+        "buttonDisabledReason": {
+          "needToBeMember": "Sie müssen Mitglied der Organisationseinheit sein, um diese Aktion durchführen zu können",
+          "needToBeSteward": "Sie müssen ein Steward der Organisationseinheit sein, um diese Aktion durchzuführen."
+        },
         "datacite": "Datacite-Metadaten",
         "description": "Beschreibung",
         "dlcm": "Dlcm-Metadaten",
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 00bd8cd13..61d62fa8e 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -1928,6 +1928,10 @@
           "addToCart": "Add to download order",
           "askAccess": "Ask access"
         },
+        "buttonDisabledReason": {
+          "needToBeMember": "You must be a member of the organizational unit to do this action",
+          "needToBeSteward": "You must be Steward of the organizational unit to do this action"
+        },
         "datacite": "Datacite metadata",
         "description": "Description",
         "dlcm": "Dlcm metadata",
diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json
index d3f5fee07..face9e6fa 100644
--- a/src/assets/i18n/fr.json
+++ b/src/assets/i18n/fr.json
@@ -1928,6 +1928,10 @@
           "addToCart": "Ajouter aux téléchargements",
           "askAccess": "Demander l'accès"
         },
+        "buttonDisabledReason": {
+          "needToBeMember": "Vous devez être membre de l'unité organisationnelle pour faire cette action",
+          "needToBeSteward": "Vous devez être steward de l'unité organisationnelle pour faire cette action"
+        },
         "datacite": "Datacite métadonnées",
         "description": "Description",
         "dlcm": "Dlcm métadonnées",
-- 
GitLab