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/2pull/4/head
@@ -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 }, | |||
@@ -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++) { | |||
@@ -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; | |||
}); | |||
}; | |||
} | |||
} |
@@ -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)); | |||
} | |||
}; | |||
@@ -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(); | |||
@@ -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> |