From 8582fc274570d85ffe6af8ade5e8ae1d06001cd6 Mon Sep 17 00:00:00 2001 From: Kristijan Mitrovic Date: Tue, 5 May 2020 15:34:11 +0000 Subject: [PATCH] Add support for downloading account data (#2) Replace loading img src in one new place Merge branch 'master' into kris/download_account # Conflicts: # src/account/profile/PolicyPage.vue Rename require with request Shorthen redirect route definition Properly show request account download error Take just first error in case of a list Reset download messages on page load Reuse same download URI method Show download account file status on page Better indent Use state's new property instead of status' property Beautify JSON before downloading it as file Force download of account data json as file Load account data as JSON on request in background Remove not needed status flag Hide download account button for root and other users Fix error and description line for new account download button Better log line on HTTP 422 error Fix log lines' prefix Remove not needed comma Ass initial support for downloading account data Use proper available errData instead of data Co-authored-by: Kristijan Mitrovic Reviewed-on: https://git.bubblev.org/bubblev/bubble-web/pulls/2 --- src/_helpers/router.js | 1 + src/_helpers/util.js | 34 +++++++++++++++--- src/_services/user.service.js | 24 ++++++++++++- src/_store/account.module.js | 43 ++++++++++++++++++++++- src/account/DevicesPage.vue | 10 +----- src/account/profile/PolicyPage.vue | 55 +++++++++++++++++++++++++++--- 6 files changed, 147 insertions(+), 20 deletions(-) diff --git a/src/_helpers/router.js b/src/_helpers/router.js index aee0e2f..00a4bc9 100644 --- a/src/_helpers/router.js +++ b/src/_helpers/router.js @@ -69,6 +69,7 @@ export const router = new Router({ { path: '/me', component: ProfilePage }, { path: '/me/policy', component: PolicyPage }, + { path: '/me/download/:uuid', redirect: r => ({ path: '/me/policy', query: { download: r.params.uuid } }) }, { path: '/me/action', component: ActionPage }, { path: '/me/changePassword', component: ChangePasswordPage }, { path: '/me/setPassword/:code', component: SetPasswordPage }, diff --git a/src/_helpers/util.js b/src/_helpers/util.js index 236e2a0..5b718d0 100644 --- a/src/_helpers/util.js +++ b/src/_helpers/util.js @@ -155,18 +155,20 @@ export const util = { return function (response) { return response.text().then(text => { if (!response.ok) { + let errData = JSON.parse('' + text) || text; + if (Array.isArray(errData)) errData = errData[0]; + if (response.status === 404) { // todo: show nicer error message - const errData = JSON.parse(''+text); - console.log('handleCrudResponse: received 404: ' + text); + console.log('handlePlaintextResponse: received 404: ' + (errData.resource || errData)); } else if (response.status === 422) { - const errData = JSON.parse(''+text); - console.log('handleCrudResponse: received 422, error: ' + text); + console.log('handlePlaintextResponse: received 422, error: ' + + ((errData.message + ": " + errData.invalidValue) || errData)); util.setValidationErrors(errData, messages, errors); } - const error = (data && data.message) || response.statusText; + const error = errData.message || errData || response.statusText; return Promise.reject(error); } return text; @@ -174,6 +176,28 @@ export const util = { } }, + handleDataToDownloadAsFile: function(fileName, mimeType) { + return function(data) { + // Original taken from: https://javascript.info/blob#blob-as-url + const uri = URL.createObjectURL(new Blob([data], {type: mimeType})); + try { + util.downloadURI(uri, fileName); + } catch(err) { + return Promise.reject(err); + } finally { + URL.revokeObjectURL(uri); + } + return 'ok'; + }; + }, + + downloadURI: function(uri, name) { + const link = document.createElement("a"); + link.download = name; + link.href = uri; + link.click(); + }, + setValidationErrors: function(data, messages, errors, enableTotpModal) { const errs = []; for (let i=0; i { + try { + return JSON.stringify(JSON.parse(text, (_, v) => typeof v === 'string' ? (JSON.parse(v) || v) : v), + null, '\t'); + } catch(err) { + Promise.reject(text || err); + } + }) + .then(util.handleDataToDownloadAsFile(fileName, 'application/json')); +} + function getUserById(userId, messages, errors) { return fetch(`${config.apiUrl}/users/${userId}`, util.getWithAuth()).then(util.handleCrudResponse(messages, errors)); } @@ -227,4 +249,4 @@ function handleAuthResponse(messages, errors) { return data; }); }; -} \ No newline at end of file +} diff --git a/src/_store/account.module.js b/src/_store/account.module.js index 7ebba7f..3adcd9b 100644 --- a/src/_store/account.module.js +++ b/src/_store/account.module.js @@ -19,7 +19,9 @@ const defaultStatus = { authenticating: false, sendingVerification: false, sendingResetPasswordMessage: false, - registrationError: null + registrationError: null, + requestAccountDownloadRequestSent: false, + downloadingAccount: false }; const state = { @@ -179,6 +181,18 @@ const actions = { }, error => commit('resendVerificationCodeFailure', error) ); + }, + requestAccountDownload({ commit }, {messages, errors}) { + commit('requestAccountDownloadRequest'); + userService.requestAccountDownload(messages, errors) + .then(ok => commit('requestAccountDownloadSuccess'), + error => commit('requestAccountDownloadFailure', error)); + }, + downloadAccount({ commit }, {token, messages, errors}) { + commit('downloadAccountRequest'); + userService.downloadAccount(token, messages, errors) + .then(ok => commit('downloadAccountSuccess'), + error => commit('downloadAccountFailure', error)); } }; @@ -367,6 +381,33 @@ const mutations = { resendVerificationCodeFailure(state, error) { state.status = Object.assign({}, state.status, {sendingVerification: false}); state.actionStatus = { error: error, type: 'verify' }; + }, + + requestAccountDownloadRequest(state) { + state.status = Object.assign({}, state.status, + { downloadingAccount: false, requestAccountDownloadRequestSent: false }); + state.actionStatus = { requesting: true, type: 'requestDownload' }; + }, + requestAccountDownloadSuccess(state) { + state.status = Object.assign({}, state.status, { requestAccountDownloadRequestSent: true }); + state.actionStatus = { success: true, type: 'requestDownload' }; + }, + requestAccountDownloadFailure(state, error) { + state.actionStatus = { error: error, type: 'requestDownload' }; + console.log('requestAccountDownloadFailure: ' + JSON.stringify(error)); + }, + + downloadAccountRequest(state) { + state.status = Object.assign({}, state.status, { downloadingAccount: true }); + state.actionStatus = { requesting: true, type: 'download' }; + }, + downloadAccountSuccess(state) { + state.status = Object.assign({}, state.status, { downloadingAccount: false }); + state.actionStatus = { success: true, type: 'download' }; + }, + downloadAccountFailure(state, error) { + state.actionStatus = { error: error, type: 'download' }; + console.log('downloadAccountFailure: ' + JSON.stringify(error)); } }; diff --git a/src/account/DevicesPage.vue b/src/account/DevicesPage.vue index e4ed7c7..ce83ec8 100644 --- a/src/account/DevicesPage.vue +++ b/src/account/DevicesPage.vue @@ -34,7 +34,7 @@
- +
{{ errors.first('deviceVpnConf') }}

@@ -225,14 +225,6 @@ }, hideDeviceHelp () { this.displayDeviceHelp = {}; }, - downloadURI(uri, name) { // adapted from https://stackoverflow.com/a/15832662/1251543 - let link = document.createElement("a"); - link.download = name; - link.href = uri; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }, mitmOn () { this.mitmLoading = true; this.errors.clear(); diff --git a/src/account/profile/PolicyPage.vue b/src/account/profile/PolicyPage.vue index f7cbb57..33b4a73 100644 --- a/src/account/profile/PolicyPage.vue +++ b/src/account/profile/PolicyPage.vue @@ -1,6 +1,14 @@