diff --git a/README.md b/README.md index 1924874fb0b6664ee4b9d46d1f9c331cba06e1d8..ea989e07f13852df19dc3a6eadccda37ab24d39e 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 962f5c59973579eddcf2c02b9bc05b895967ebea..1a1bfc6968f8cb30ba063124329680536186a430 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 0000000000000000000000000000000000000000..7f8a870e69711011521706f33090836f6ef2b1c1 --- /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 5a136cdb7e13a56de89dca42e88f996ee7ee018e..34ad0da49b6e50b6e8328dac9b6556ebfdac550d 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 863e5e2a5c4c65cc7326a0c7d54de0608fda3abe..35709417a2a37f98d339b06bfa49b3edaf2833e7 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 0000000000000000000000000000000000000000..3549669674b110526f254ba34f780515a5db7869 --- /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 0000000000000000000000000000000000000000..959f1d25a9d8ed3ed93ea129601af69033529035 --- /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 9b5cc6f744dca1363f6e9ef08495e06d7a103b80..99e7c4e7c49d4ef16efb31cb9fb4f62beda6b7ee 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 0aeb41b2cd345dbfe6a2a0ef7c6b134892d23848..741f7bdc6ea257a332a343e790e96d300bf95c5d 100644 --- a/vue.config.js +++ b/vue.config.js @@ -12,4 +12,11 @@ module.exports = { }, }, }, + configureWebpack: { + resolve: { + alias: { + 'vue$': 'vue/dist/vue.esm.js' + } + } + } };