Sfoglia il codice sorgente

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 anni fa
committed by jonathan
parent
commit
8582fc2745
6 ha cambiato i file con 147 aggiunte e 20 eliminazioni
  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 Vedi 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 Vedi 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 Vedi 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 Vedi 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 Vedi 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 Vedi 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>

Caricamento…
Annulla
Salva