From b999fdfd9b630be09a4d988f3c7f5bff8128c7e7 Mon Sep 17 00:00:00 2001
From: Florent Lavelle <flavelle@neogeo.fr>
Date: Fri, 12 Jan 2024 08:44:07 +0000
Subject: [PATCH] Support personal data dl [REDMINE_ISSUE-17891]

---
 README.md                             |   1 +
 package.json                          |   2 +-
 src/api/personalDataAPI.js            |  64 +++++++++++
 src/assets/locales/en.json            |   4 +
 src/assets/locales/fr.json            |   4 +
 src/components/PersonalDataExport.vue | 158 ++++++++++++++++++++++++++
 src/utils.js                          |  14 +++
 src/views/UserProfile.vue             |  18 ++-
 vue.config.js                         |   7 ++
 9 files changed, 269 insertions(+), 3 deletions(-)
 create mode 100644 src/api/personalDataAPI.js
 create mode 100644 src/components/PersonalDataExport.vue
 create mode 100644 src/utils.js

diff --git a/README.md b/README.md
index 1924874..ea989e0 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ VUE_APP_LOGO=@/assets/logo.png
 VUE_APP_LOGIN_API_PATH=/login
 VUE_APP_ORGANISATION_API_PATH=/organisation/
 VUE_APP_USERGROUP_API_PATH=/usergroup/
+VUE_APP_PERSONAL_DATA_API_PATH=/api/personal-data/
 
 # AUTH
 VUE_APP_LOGIN_API_USERNAME=admin
diff --git a/package.json b/package.json
index 962f5c5..1a1bfc6 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
   },
   "dependencies": {
     "axios": "^1.5.0",
-    "bootstrap-vue": "^2.21.2",
+    "bootstrap-vue": "^2.22.0",
     "core-js": "^3.6.5",
     "corejs-typeahead": "^1.3.1",
     "lodash": "^4.17.21",
diff --git a/src/api/personalDataAPI.js b/src/api/personalDataAPI.js
new file mode 100644
index 0000000..7f8a870
--- /dev/null
+++ b/src/api/personalDataAPI.js
@@ -0,0 +1,64 @@
+import axios from 'axios';
+import i18n from '@/i18n';
+
+const DEV_AUTH = process.env.NODE_ENV === 'development' ? true : false;
+
+const AUTH = {
+  username: process.env.VUE_APP_LOGIN_API_USERNAME,
+  password: process.env.VUE_APP_LOGIN_API_PASSWORD
+};
+
+if (!DEV_AUTH) {
+  axios.defaults.headers.common['X-CSRFToken'] = (name => {
+    var re = new RegExp(name + "=([^;]+)");
+    var value = re.exec(document.cookie);
+    return (value != null) ? unescape(value[1]) : null;
+  })('csrftoken');
+}
+
+const path = require('path');
+const DOMAIN = process.env.VUE_APP_DOMAIN;
+const PERSONAL_DATA_URL = process.env.VUE_APP_PERSONAL_DATA_API_PATH
+
+const personalDataAPI = {
+  async postPersonalDataDemand() {
+    const url = new URL(path.join(`${i18n.locale}${PERSONAL_DATA_URL}`, 'demands/'), DOMAIN);
+    const response = await axios.post(
+      url,
+      null,
+      { ...DEV_AUTH && { auth: AUTH } }
+    );
+    if (response.status === 200) {
+      return response.data;
+    }
+    return false;
+  },
+
+  async getPersonalDataDemands() {
+    const url = new URL(path.join(`${i18n.locale}${PERSONAL_DATA_URL}`, 'demands/'), DOMAIN);
+    const response = await axios.get(
+      url,
+      { ...DEV_AUTH && { auth: AUTH } }
+    );
+    if (response.status === 200) {
+      return response.data;
+    }
+    return false;
+  },
+
+  // async downloadPersonalData(url) {
+  //   const response = await axios.get(
+  //     url,
+  //     {
+  //       responseType: 'blob',
+  //       ...DEV_AUTH && { auth: AUTH }
+  //     }
+  //   );
+  //   if (response.status === 200) {
+  //     return response.data;
+  //   }
+  //   return false;
+  // }
+}
+
+export default personalDataAPI;
diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index 5a136cd..34ad0da 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -172,6 +172,10 @@
           "Your password cannot be entirely numerical."
         ]
       }
+    },
+    "personalDataExport": {
+      "title": "Personal data export",
+      "newDemand": "New request for export of personal data"
     }
   },
   "validationEmail": {
diff --git a/src/assets/locales/fr.json b/src/assets/locales/fr.json
index 863e5e2..3570941 100644
--- a/src/assets/locales/fr.json
+++ b/src/assets/locales/fr.json
@@ -173,6 +173,10 @@
           "Votre mot de passe ne peut pas être entièrement numérique."
         ]
       }
+    },
+    "personalDataExport": {
+      "title": "Export des données personnelles",
+      "newDemand": "Nouvelle demande d'export des données personnelles"
     }
   },
   "validationEmail": {
diff --git a/src/components/PersonalDataExport.vue b/src/components/PersonalDataExport.vue
new file mode 100644
index 0000000..3549669
--- /dev/null
+++ b/src/components/PersonalDataExport.vue
@@ -0,0 +1,158 @@
+<template>
+  <div id="export_data">
+    <b-overlay
+      :show="loading"
+      rounded="lg"
+      :style="'padding: 5px;'"
+      variant="white"
+      :spinner-small="true"
+      spinner-type="grow"
+      spinner-variant="primary"
+    >
+      <div
+        v-if="lastDemand && !isLastDemandFinished"
+        class="export_data-demande in-progress"
+      >
+        <div>
+          La demande d'export de vos données personnelles du 
+          <i>{{ new Date(lastDemand.creation_date).toLocaleDateString() }}</i> à 
+          <i>{{ new Date(lastDemand.creation_date).toLocaleTimeString([], { hour: '2-digit', minute:'2-digit' }) }}</i> 
+          est en attente.
+        </div>
+      </div>
+      <div
+        v-else-if="lastDemand && isLastDemandFinished"
+        class="export_data-demande"
+      >
+        <div>
+          La demande d'export de vos données personnelles du 
+          <i>{{ new Date(lastDemand.creation_date).toLocaleDateString() }}</i> à 
+          <i>{{ new Date(lastDemand.creation_date).toLocaleTimeString([], { hour: '2-digit', minute:'2-digit' }) }}</i> 
+          a aboutie.
+        </div>
+        <div style="display: flex; justify-content: center;">
+          <b-button
+            variant="success"
+            @click="downloadPersonalData"
+          >
+            Télécharger
+          </b-button>
+        </div>
+        
+      </div>
+
+      <b-button
+        id="export_data-button"
+        variant="primary"
+        :disabled="!isLastDemandFinished"
+        @click="demandPersonalData"
+      >
+        {{ $t('profile.personalDataExport.newDemand') }}
+      </b-button>
+    </b-overlay>
+  </div>
+</template>
+
+<script>
+import personalDataAPI from '@/api/personalDataAPI.js';
+import { downloadFile } from '@/utils';
+
+export default {
+  name: 'PersonalDataExport',
+
+  data() {
+    return {
+      loading: false,
+      lastDemand: null
+    };
+  },
+
+  computed: {
+    isLastDemandFinished() {
+      if (this.lastDemand && !this.lastDemand.stop_date) {
+        return false;
+      }
+      return true;
+    }
+  },
+
+  created() {
+    this.getDemands();
+  },
+
+  methods: {
+    async demandPersonalData() {
+      try {
+        this.loading = true;
+        const newDemand = await personalDataAPI.postPersonalDataDemand();
+        this.lastDemand = newDemand;
+        this.loading = false;
+        await this.getDemands();
+      } catch {
+        this.loading = false;
+      }
+    },
+
+    async getDemands() {
+      try {
+        this.loading = true;
+        const demands = await personalDataAPI.getPersonalDataDemands();
+        if (demands && demands.results && demands.results.length) {
+          this.lastDemand = demands.results[0];
+        }
+        this.loading = false;
+      } catch {
+        this.loading = false;
+      }
+    },
+
+    async downloadPersonalData() {
+      try {
+        if (this.lastDemand && this.lastDemand.download_href) {
+          this.loading = true;
+          const link = document.createElement('a');
+          link.href = this.lastDemand.download_href;
+          link.setAttribute('download', `personal_data-${this.lastDemand.id}`);
+          link.setAttribute('target', '_blank');
+          document.body.appendChild(link);
+          link.click();
+          link.remove();
+          this.loading = false;
+        }
+      } catch {
+        this.loading = false;
+      }
+    }
+  }
+};
+</script>
+
+<style scoped lang="less">
+#export_data {
+  width: 100%;
+  .export_data-demande {
+    margin: 0 2px 1.5rem;
+    padding: 0.5rem 0.75rem;
+    background-color: #effff4;
+    border-radius: 2px;
+    box-shadow: 0px 1px 4px rgba(14, 31, 53, 0.12), 0px 4px 8px rgba(14, 31, 53, 0.08);
+    color: #495057;
+    font-weight: 600;
+    text-align: center;
+    button {
+      margin: 0.5rem;
+      font-weight: 600;
+    }
+  }
+  .export_data-demande.in-progress {
+    background-color: #fdf6e4;
+  }
+  #export_data-button {
+    width: 100%;
+    font-size: 1rem;
+    border: 2px solid #9BD0FF;
+    border-radius: 8px;
+    font-weight: 600;
+  }
+}
+</style>
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..959f1d2
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,14 @@
+export function downloadFile(file) {
+  const fileName =
+    file.name && file.name.split('/').length && file.name.split('/').slice(-1)[0].split('.').length ?
+      file.name.split('/').slice(-1)[0].split('.')[0] :
+      'file';
+  const href = URL.createObjectURL(file.url);
+  const link = document.createElement('a');
+  link.href = href;
+  link.setAttribute('download', `${fileName}.${file.url.type === 'application/x-zip-compressed' ? 'zip' : 'pdf'}`);
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  URL.revokeObjectURL(href);
+}
\ No newline at end of file
diff --git a/src/views/UserProfile.vue b/src/views/UserProfile.vue
index 9b5cc6f..99e7c4e 100644
--- a/src/views/UserProfile.vue
+++ b/src/views/UserProfile.vue
@@ -18,6 +18,7 @@
           <span class="sub-title">{{ $t('profile.subtitle') }}</span>
         </h4>
         <hr class="divider">
+
         <ValidationObserver ref="form" v-slot="{ handleSubmit }">
           <b-overlay
             :show="loadingUserInformation"
@@ -402,6 +403,10 @@
             </div>
           </b-overlay>
         </ValidationObserver>
+
+        <h5>{{ $t('profile.personalDataExport.title') }}</h5>
+        <hr class="divider">
+        <PersonalDataExport />
       </div>
     </div>
     <small class="footer">
@@ -420,6 +425,8 @@ import i18n from '@/i18n';
 import Swal from "sweetalert2";
 import "sweetalert2/dist/sweetalert2.min.css";
 
+import PersonalDataExport from '../components/PersonalDataExport.vue';
+
 import {
   ValidationObserver,
   ValidationProvider,
@@ -455,6 +462,7 @@ export default {
   name: 'UserProfile',
 
   components: {
+    PersonalDataExport,
     ValidationObserver,
     ValidationProvider,
   },
@@ -679,12 +687,18 @@ export default {
       border-top: 2px solid #373b3d;
     }
 
+    #export_data {
+      margin-top: 24px;
+    }
+
     h5 {
-      color: #6b7479;
+      margin-bottom: 20px;
+      margin-top: 40px;
+      color: #373b3d;
     }
 
     form {
-      margin-top: 32px;
+      margin-top: 20px;
 
       h5 {
         margin-bottom: 20px;
diff --git a/vue.config.js b/vue.config.js
index 0aeb41b..741f7bd 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -12,4 +12,11 @@ module.exports = {
       },
     },
   },
+  configureWebpack: {
+    resolve: {
+      alias: {
+        'vue$': 'vue/dist/vue.esm.js'
+      }
+    }
+  }
 };
-- 
GitLab