浏览代码

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 <kmitrovic@itekako.com>
Reviewed-on: https://git.bubblev.org/bubblev/bubble-web/pulls/2
pull/4/head
Kristijan Mitrovic 4 年前
committed by jonathan
父节点
当前提交
8582fc2745
共有 6 个文件被更改,包括 147 次插入20 次删除
  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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

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

正在加载...
取消
保存