Преглед на файлове

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>

Зареждане…
Отказ
Запис