#2 Add support for downloading account data

Merged
jonathan merged 22 commits from kris/download_account into master 4 years ago
  1. +1
    -0
      src/_helpers/router.js
  2. +29
    -5
      src/_helpers/util.js
  3. +23
    -1
      src/_services/user.service.js
  4. +42
    -1
      src/_store/account.module.js
  5. +1
    -9
      src/account/DevicesPage.vue
  6. +51
    -4
      src/account/profile/PolicyPage.vue

+ 1
- 0
src/_helpers/router.js View File

@@ -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 },


+ 29
- 5
src/_helpers/util.js View File

@@ -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<data.length; i++) {


+ 23
- 1
src/_services/user.service.js View File

@@ -12,6 +12,8 @@ export const userService = {
register,
searchAccounts,
getMe,
requestAccountDownload,
downloadAccount,
getUserById,
getPolicyByUserId,
updatePolicyByUserId,
@@ -91,6 +93,26 @@ function getMe(messages, errors) {
).then(util.handleCrudResponse(messages, errors));
}

function requestAccountDownload(messages, errors) {
return fetch(`${config.apiUrl}/me/download`, util.postWithAuth()).then(util.handlePlaintextResponse(messages, errors));
}

function downloadAccount(token, messages, errors) {
const fileName = (util.currentUser().name || util.currentUser().uuid || token) + ".json";

return fetch(`${config.apiUrl}/me/download/${token}`, util.postWithAuth())
.then(util.handlePlaintextResponse(messages, errors))
.then(text => {
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;
});
};
}
}

+ 42
- 1
src/_store/account.module.js View File

@@ -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));
}
};



+ 1
- 9
src/account/DevicesPage.vue View File

@@ -34,7 +34,7 @@

<hr/>

<button v-if="vpnConfBase64" @click="downloadURI('data:text/plain;base64,'+vpnConfBase64, 'vpn.conf')">{{messages.message_device_vpn_download_conf}}</button>
<button v-if="vpnConfBase64" @click="util.downloadURI('data:text/plain;base64,'+vpnConfBase64, 'vpn.conf')">{{messages.message_device_vpn_download_conf}}</button>
<div v-if="errors.has('deviceVpnConf')" class="invalid-feedback d-block">{{ errors.first('deviceVpnConf') }}</div>

<hr/>
@@ -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();


+ 51
- 4
src/account/profile/PolicyPage.vue View File

@@ -1,6 +1,14 @@
<!-- Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ -->
<template>
<div>
<div v-if="showDownloadMessages && status.downloadingAccount">
<div v-if="actionStatus.requesting" :class="`alert alert-info`">{{ messages.downloading_notice }}</div>
<div v-if="!actionStatus.requesting && actionStatus.type === 'download' && actionStatus.error"
class="invalid-feedback d-block alert alert-danger">
{{ messages.downloading_failed }}
</div>
</div>

<div v-if="inboundAction" :class="`alert ${inboundAction.alertType}`">
{{messages['message_inbound_'+inboundAction.actionType]}}
{{messages['message_inbound_'+inboundAction.status]}}
@@ -10,6 +18,27 @@

<h2>{{messages.form_title_account_policy}}<span v-if="this.me === false"> - {{this.userId}}</span></h2>
<form @submit.prevent="updatePolicy">
<span v-if="me && currentUser.name != 'root'">
<hr/>
<div class="form-group">
<label htmlFor="downloadAccountBtn">{{messages.field_label_account_download}}</label>
<button class="btn btn-primary" name="downloadAccountBtn" :disabled="actionStatus.requesting"
v-on:click="clickRequestAccountDownload()">
{{ messages.button_label_account_download }}
</button>
<img v-show="loading()" :src="loadingImgSrc" />
<div v-if="showDownloadMessages && !actionStatus.requesting">
<div v-if="actionStatus.type === 'requestDownload' && actionStatus.error"
class="invalid-feedback d-block alert alert-danger">
{{ actionStatus.error }}
</div>
<div v-if="status.requestAccountDownloadRequestSent" class="alert alert-info">
{{messages.field_label_account_download_requested_notice}}
</div>
</div>
</div>
</span>

<hr/>
<div class="form-group">
<label htmlFor="deletionPolicy">{{messages.field_label_policy_account_deletion}}</label>
@@ -388,11 +417,12 @@
verifyingContact: null,
inboundAction: null,
watchedPolicy: null,
showDownloadMessages: false,
loadingImgSrc: loadingImgSrc
}
},
computed: {
...mapState('account', ['actionStatus']),
...mapState('account', ['actionStatus', 'status']),
...mapState('system', [
'messages', 'accountDeletionOptions', 'timeDurationOptions', 'timeDurationOptionsReversed',
'contactTypes', 'detectedLocale', 'countries'
@@ -427,9 +457,12 @@
}
},
methods: {
...mapActions('account', ['approveAction', 'denyAction', 'sendAuthenticatorCode', 'resendVerificationCode']),
...mapActions('account', [
'approveAction', 'denyAction', 'sendAuthenticatorCode', 'resendVerificationCode',
'requestAccountDownload', 'downloadAccount'
]),
...mapActions('users', [
'getPolicyByUserId', 'updatePolicyByUserId', 'addPolicyContactByUserId', 'removePolicyContactByUserId',
'getPolicyByUserId', 'updatePolicyByUserId', 'addPolicyContactByUserId', 'removePolicyContactByUserId'
]),
...mapGetters('users', ['loading']),
isAuthenticator(val) { return window.isAuthenticator(val); },
@@ -521,6 +554,12 @@
errors: this.errors
});
},
clickRequestAccountDownload() {
this.errors.clear();
this.showDownloadMessages = true;
this.requestAccountDownload({ messages: this.messages, errors: this.errors });
return false; // do not follow the click
},
startVerifyContact(contact) {
// console.log('startVerifyContact: '+JSON.stringify(contact));
this.verifyingContact = contact.uuid;
@@ -654,6 +693,14 @@
// console.log('PolicyPage.created: $route.params='+JSON.stringify(this.$route.query));
this.inboundAction = util.setInboundAction(this.$route);
this.newContactSmsCountry = countryFromLocale(this.detectedLocale);

this.showDownloadMessages = false;
if (this.$route.query.hasOwnProperty('download')) {
this.showDownloadMessages = true;
this.downloadAccount({
token: this.$route.query.download, messages: this.messages, errors: this.errors
});
}
}
};
</script>
</script>

Loading…
Cancel
Save