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> - <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> - <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> @@ -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'"> </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