From 076be863003af4c571ece6daadceb7d6d3defd36 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Wed, 15 Sep 2021 18:32:32 +0200
Subject: [PATCH] Download file for private dataset (Authorization)

---
 client/src/app/auth/auth.actions.ts           |  1 +
 client/src/app/auth/auth.effects.ts           | 13 ++++++++++
 client/src/app/auth/init.keycloak.ts          |  3 +++
 .../result/datatable-tab.component.html       |  1 -
 .../result/datatable-tab.component.ts         |  1 -
 .../components/result/download.component.html |  8 +++---
 .../components/result/download.component.ts   | 16 +++++++++++-
 .../search/containers/result.component.html   |  1 -
 .../search/containers/result.component.ts     |  2 --
 .../datatable/datatable.component.html        |  1 -
 .../datatable/datatable.component.ts          |  1 -
 .../renderer/download-renderer.component.html |  2 +-
 .../renderer/download-renderer.component.ts   | 26 ++++++++++++++-----
 conf-dev/public_key                           |  2 +-
 docker-compose.yml                            |  2 +-
 15 files changed, 58 insertions(+), 22 deletions(-)

diff --git a/client/src/app/auth/auth.actions.ts b/client/src/app/auth/auth.actions.ts
index 4103bceb..e661950b 100644
--- a/client/src/app/auth/auth.actions.ts
+++ b/client/src/app/auth/auth.actions.ts
@@ -14,6 +14,7 @@ import { UserProfile } from './user-profile.model';
 export const login = createAction('[Auth] Login');
 export const logout = createAction('[Auth] Logout');
 export const authSuccess = createAction('[Auth] Auth Success');
+export const authRefreshSuccess = createAction('[Auth] Auth Refresh Success');
 export const loadUserProfileSuccess = createAction('[Auth] Load User Profile Success', props<{ userProfile: UserProfile }>());
 export const loadUserRoleSuccess = createAction('[Auth] Load User Roles Success', props<{ userRoles: string[] }>());
 export const loadTokenSuccess = createAction('[Auth] Load Token Success', props<{ token: string }>());
diff --git a/client/src/app/auth/auth.effects.ts b/client/src/app/auth/auth.effects.ts
index a663b0a0..d063947d 100644
--- a/client/src/app/auth/auth.effects.ts
+++ b/client/src/app/auth/auth.effects.ts
@@ -60,6 +60,19 @@ export class AuthEffects {
         )
     );
 
+    authRefreshSuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(authActions.authRefreshSuccess),
+            switchMap(() => from(this.keycloak.getToken())
+                .pipe(
+                    switchMap(token => [
+                        authActions.loadTokenSuccess({ token })
+                    ])
+                )
+            )
+        )
+    );
+
     openEditProfile$ =  createEffect(() =>
         this.actions$.pipe(
             ofType(authActions.openEditProfile),
diff --git a/client/src/app/auth/init.keycloak.ts b/client/src/app/auth/init.keycloak.ts
index a15e68f1..d488a979 100644
--- a/client/src/app/auth/init.keycloak.ts
+++ b/client/src/app/auth/init.keycloak.ts
@@ -24,6 +24,9 @@ export function initializeKeycloak(keycloak: KeycloakService, store: Store<{ }>,
         if (event.type === KeycloakEventType.OnAuthSuccess) {
             store.dispatch(keycloakActions.authSuccess());
         }
+        if (event.type === KeycloakEventType.OnAuthRefreshSuccess) {
+            store.dispatch(keycloakActions.authRefreshSuccess());
+        }
         if (event.type === KeycloakEventType.OnAuthRefreshError) {
             store.dispatch(keycloakActions.login());
         }
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html
index f854be32..f9f6b3aa 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.html
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.html
@@ -19,7 +19,6 @@
                 [dataIsLoading]="dataIsLoading"
                 [dataIsLoaded]="dataIsLoaded"
                 [selectedData]="selectedData"
-                [token]="token"
                 (retrieveData)="retrieveData.emit($event)"
                 (addSelectedData)="addSelectedData.emit($event)"
                 (deleteSelectedData)="deleteSelectedData.emit($event)">
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts
index 51d18381..a276dec4 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.ts
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts
@@ -33,7 +33,6 @@ export class DatatableTabComponent {
     @Input() dataIsLoading: boolean;
     @Input() dataIsLoaded: boolean;
     @Input() selectedData: any[];
-    @Input() token: string;
     @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter();
     @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
     @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
diff --git a/client/src/app/instance/search/components/result/download.component.html b/client/src/app/instance/search/components/result/download.component.html
index 4f9d9449..bca9d41f 100644
--- a/client/src/app/instance/search/components/result/download.component.html
+++ b/client/src/app/instance/search/components/result/download.component.html
@@ -18,15 +18,15 @@
                     <p>Download results just here:</p>
                 </div>
                 <div class="col">
-                    <a *ngIf="getConfigDownloadResultFormat('download_csv')" [href]="getUrl('csv')" class="btn btn-outline-primary" title="Download results in CSV format">
+                    <a *ngIf="getConfigDownloadResultFormat('download_csv')" (click)="click($event, getUrl('csv'), 'csv')" class="btn btn-outline-primary" title="Download results in CSV format">
                         <i class="fas fa-file-csv"></i> CSV
                     </a>
                     &nbsp;
-                    <a *ngIf="getConfigDownloadResultFormat('download_ascii')" [href]="getUrl('ascii')" target="_blank" class="btn btn-outline-primary" title="Download results in ASCII format">
+                    <a *ngIf="getConfigDownloadResultFormat('download_ascii')" (click)="click($event, getUrl('ascii'), 'txt')" class="btn btn-outline-primary" title="Download results in ASCII format">
                         <i class="fas fa-file"></i> ASCII
                     </a>
                     &nbsp;
-                    <a *ngIf="getConfigDownloadResultFormat('download_vo')" [href]="getUrl('votable')" target="_blank" class="btn btn-outline-primary" title="Download results in VO format">
+                    <a *ngIf="getConfigDownloadResultFormat('download_vo')" (click)="click($event, getUrl('votable'), 'xml')" class="btn btn-outline-primary" title="Download results in VO format">
                         <i class="fas fa-file"></i> VOtable
                     </a>
                     &nbsp;
@@ -41,7 +41,7 @@
                     <p>Download archive files just here:</p>
                 </div>
                 <div class="col">
-                    <a [href]="getUrlArchive()" class="btn btn-outline-primary" title="Download an archive with all files">
+                    <a (click)="click($event, getUrlArchive(), 'zip')" class="btn btn-outline-primary" title="Download an archive with all files">
                         <i class="fas fa-archive"></i> Files archive
                     </a>
                 </div>
diff --git a/client/src/app/instance/search/components/result/download.component.ts b/client/src/app/instance/search/components/result/download.component.ts
index 10607d84..b0498b15 100644
--- a/client/src/app/instance/search/components/result/download.component.ts
+++ b/client/src/app/instance/search/components/result/download.component.ts
@@ -8,6 +8,7 @@
  */
 
 import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
 
 import { Criterion, ConeSearch, criterionToString } from '../../../store/models';
 import { Dataset } from 'src/app/metamodel/models';
@@ -34,7 +35,7 @@ export class DownloadComponent {
     @Input() sampRegistered: boolean;
     @Output() broadcast: EventEmitter<string> = new EventEmitter();
 
-    constructor(private appConfig: AppConfigService) { }
+    constructor(private appConfig: AppConfigService, private http: HttpClient) { }
 
     isDownloadActivated(): boolean {
         const dataset = this.datasetList.find(dataset => dataset.name === this.datasetSelected);
@@ -91,4 +92,17 @@ export class DownloadComponent {
     broadcastVotable(): void {
         this.broadcast.emit(this.getUrl('votable'));
     }
+
+    click(event, href, extension) {
+        event.preventDefault();
+
+        this.http.get(href, {responseType: "blob"}).subscribe(
+            data => {
+                let downloadLink = document.createElement('a');
+                downloadLink.href = window.URL.createObjectURL(data);
+                downloadLink.setAttribute('download', `${this.datasetSelected}.${extension}`);
+                downloadLink.click();
+            }
+        );
+    }
 }
diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html
index e57e2aa6..052cf4ed 100644
--- a/client/src/app/instance/search/containers/result.component.html
+++ b/client/src/app/instance/search/containers/result.component.html
@@ -82,7 +82,6 @@
                 [dataIsLoading]="dataIsLoading | async"
                 [dataIsLoaded]="dataIsLoaded | async"
                 [selectedData]="selectedData | async"
-                [token]="token | async"
                 (retrieveData)="retrieveData($event)"
                 (addSelectedData)="addSearchData($event)"
                 (deleteSelectedData)="deleteSearchData($event)">
diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts
index 16a12784..9254082a 100644
--- a/client/src/app/instance/search/containers/result.component.ts
+++ b/client/src/app/instance/search/containers/result.component.ts
@@ -43,7 +43,6 @@ export class ResultComponent extends AbstractSearchComponent {
     public dataIsLoaded: Observable<boolean>;
     public selectedData: Observable<any>;
     public sampRegistered: Observable<boolean>;
-    public token: Observable<string>;
 
     private pristineSubscription: Subscription;
 
@@ -58,7 +57,6 @@ export class ResultComponent extends AbstractSearchComponent {
         this.dataIsLoaded = this.store.select(searchSelector.selectDataIsLoaded);
         this.selectedData = this.store.select(searchSelector.selectSelectedData);
         this.sampRegistered = this.store.select(sampSelector.selectRegistered);
-        this.token = this.store.select(authSelector.selectToken);
     }
 
     ngOnInit() {
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.html b/client/src/app/instance/shared-search/components/datatable/datatable.component.html
index c2509821..ecb7f572 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.html
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.html
@@ -62,7 +62,6 @@
                                 [value]="datum[attribute.label]"
                                 [datasetName]="dataset.name"
                                 [datasetPublic]="dataset.public"
-                                [token]="token"
                                 [config]="getRendererConfig(attribute)">
                             </app-download-renderer>
                         </div>
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
index 91b185a5..08038733 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
@@ -34,7 +34,6 @@ export class DatatableComponent implements OnInit {
     @Input() dataIsLoading: boolean;
     @Input() dataIsLoaded: boolean;
     @Input() selectedData: any[] = [];
-    @Input() token: string;
     @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter();
     @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
     @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html
index aef5f0ea..da864835 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.html
@@ -1,4 +1,4 @@
-<a [href]="getHref()" [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button' || config.display=='icon-text-btn')}">
+<a (click)="click($event)" [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button' || config.display=='icon-text-btn')}">
     <span *ngIf="config.display === 'icon-button' || config.display === 'icon-text-btn'" class="{{config.icon}}"></span>
     <span *ngIf="config.display === 'icon-text-btn'">&nbsp;</span>
     <span *ngIf="config.display !== 'icon-button'">{{ getText() }}</span>
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
index 40b49a0b..56475b8e 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
@@ -8,6 +8,7 @@
  */
 
 import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
 
 import { DownloadRendererConfig } from 'src/app/metamodel/models/renderers/download-renderer-config.model';
 import { getHost } from 'src/app/shared/utils';
@@ -26,10 +27,9 @@ export class DownloadRendererComponent {
     @Input() value: string;
     @Input() datasetName: string;
     @Input() datasetPublic: boolean;
-    @Input() token: string;
     @Input() config: DownloadRendererConfig;
 
-    constructor(private appConfig: AppConfigService) { }
+    constructor(private appConfig: AppConfigService, private http: HttpClient) { }
 
     /**
      * Returns link href.
@@ -37,11 +37,7 @@ export class DownloadRendererComponent {
      * @return string
      */
     getHref(): string {
-        let href = getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetName + '/' + this.value;
-        if (!this.datasetPublic && this.token) {
-            href += '?token=' + this.token;
-        }
-        return href;
+        return getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetName + '/' + this.value;
     }
 
     /**
@@ -52,4 +48,20 @@ export class DownloadRendererComponent {
     getText(): string {
         return this.config.text.replace('$value', this.value.toString());
     }
+
+    click(event) {
+        event.preventDefault();
+
+        const href = this.getHref();
+        this.http.get(href, {responseType: "blob"}).subscribe(
+            data => {
+                const filename = href.substring(href.lastIndexOf('/') + 1);
+
+                let downloadLink = document.createElement('a');
+                downloadLink.href = window.URL.createObjectURL(data);
+                downloadLink.setAttribute('download', filename);
+                downloadLink.click();
+            }
+        );
+    }
 }
diff --git a/conf-dev/public_key b/conf-dev/public_key
index 6d9fd24a..3c3ba0e8 100644
--- a/conf-dev/public_key
+++ b/conf-dev/public_key
@@ -1,3 +1,3 @@
 -----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiXxjO2Z+Mnc/L4fcY7oLIvSQXrkADSUQVnAXqfcMzfuNH/clLBMmXIHoPoIOYImIbXFMNaBAFLMWqURSaz2LDEQy5dIfllU9e3kIAV0mJeafjaN3QdxKX8TcJAnrTeQ7soooeZzen7kfPWxfjAaiEtbxo5h2xW4qup+VvADQg15C3dwBS5VV2Lc41z4prQubQNs2WT4IOVfYYhtpp+R/3IuCAFS6qfmAuBUIu8gSq/VpCqJ5Sm/YdmBZDwhyBRYOKJJlkqlNFdGPnHAkBDOL5LeU54cXCBMxPEeomtu22Kv1PIF4w2kfwUie9Qq06yWsTDwyrSAjvQ3xXHAcuAqPRQIDAQAB
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1fucBQK34tl8Gx/fefATnpWqW5PVlwMYcJAqgWvmtwNm9ZW/S5HNZZfjW1S9BOJLfudCM83WHrAwGixgHKI310YXg+6BI9qn2Gnge1GC3JtKZx6UdcxZFAYmlhY0QGL5QxGR58DkEj6l/FDrwAHyVkC5sLqDMiYsqO7CA1uJLtF8yUrCyHvI4TR+kk5ZSM94/osg6eGxGSYA89u+qhG5tz5YCFgiRUNxuAEucsz8XiEfNVAz5kdYgsR4+LtmqECfczpwcJrAu7yDxs3rQPjBqFdGqEehALHX9vq71VEYe5Id2P7ddik3byHa0a21Q0RuVhMYwGrLiMLJCXqxE1YMuwIDAQAB
 -----END PUBLIC KEY-----
diff --git a/docker-compose.yml b/docker-compose.yml
index ee412b76..482db673 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -34,7 +34,7 @@ services:
             SSO_AUTH_URL: "http://localhost:8180/auth"
             SSO_REALM: "anis"
             SSO_CLIENT_ID: "anis-client"
-            TOKEN_ENABLED: 0
+            TOKEN_ENABLED: 1
             TOKEN_PUBLIC_KEY_FILE: /mnt/public_key
             TOKEN_ADMIN_ROLE: anis_admin
         ports:
-- 
GitLab