optimize user data export process
This commit is contained in:
+4
-9
@@ -153,15 +153,6 @@ func startWebServer(c *cli.Context) error {
|
||||
apiRoute.POST("/register.json", bindApi(api.Users.UserRegisterHandler))
|
||||
}
|
||||
|
||||
if config.EnableDataExport {
|
||||
dataRoute := apiRoute.Group("/data")
|
||||
dataRoute.Use(bindMiddleware(middlewares.HeaderInQueryString))
|
||||
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||
{
|
||||
dataRoute.GET("/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
|
||||
}
|
||||
}
|
||||
|
||||
apiRoute.GET("/logout.json", bindApi(api.Tokens.TokenRevokeCurrentHandler))
|
||||
|
||||
apiV1Route := apiRoute.Group("/v1")
|
||||
@@ -190,6 +181,10 @@ func startWebServer(c *cli.Context) error {
|
||||
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
|
||||
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
|
||||
|
||||
if config.EnableDataExport {
|
||||
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
|
||||
}
|
||||
|
||||
// Accounts
|
||||
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
|
||||
apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler))
|
||||
|
||||
@@ -37,21 +37,6 @@ func JWTAuthorization(c *core.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token
|
||||
func JWTAuthorizationByQueryString(c *core.Context) {
|
||||
token, exists := c.GetQuery(tokenQueryStringParam)
|
||||
|
||||
if !exists {
|
||||
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] no token provided")
|
||||
utils.PrintJsonErrorResult(c, errs.ErrUnauthorizedAccess)
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Header.Set("Authorization", token)
|
||||
|
||||
JWTAuthorization(c)
|
||||
}
|
||||
|
||||
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
|
||||
func JWTTwoFactorAuthorization(c *core.Context) {
|
||||
claims, err := getTokenClaims(c)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
)
|
||||
|
||||
const utcOffsetQueryStringParam = "utc_offset"
|
||||
|
||||
// HeaderInQueryString puts some headers from query string
|
||||
func HeaderInQueryString(c *core.Context) {
|
||||
utcOffset, exists := c.GetQuery(utcOffsetQueryStringParam)
|
||||
|
||||
if exists {
|
||||
c.Request.Header.Set(core.ClientTimezoneOffsetHeaderName, utcOffset)
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,9 @@ export default {
|
||||
getUserDataStatistics: () => {
|
||||
return axios.get('v1/data/statistics.json');
|
||||
},
|
||||
getExportedUserData: () => {
|
||||
return axios.get('v1/data/export.csv');
|
||||
},
|
||||
clearData: ({ password }) => {
|
||||
return axios.post('v1/data/clear.json', {
|
||||
password
|
||||
|
||||
@@ -41,6 +41,10 @@ export default {
|
||||
'long': 'm/d/yyyy hh::mm A',
|
||||
},
|
||||
},
|
||||
'dataExport': {
|
||||
'defaultExportFilename': 'ezBookkeeping_export_data',
|
||||
'exportFilename': 'ezBookkeeping_{nickname}_export_data'
|
||||
},
|
||||
'datetime': {
|
||||
'Monday': {
|
||||
'min': 'Mo',
|
||||
@@ -591,6 +595,7 @@ export default {
|
||||
'transaction tag name is empty': 'Transaction tag title is empty',
|
||||
'transaction tag name already exists': 'Transaction tag title already exists',
|
||||
'transaction tag is in use and cannot be deleted': 'Transaction tag is in use and it cannot be deleted',
|
||||
'data export not allowed': 'User data export is not allowed',
|
||||
'query items cannot be empty': 'There are no query items',
|
||||
'query items too much': 'There are too many query items',
|
||||
'query items have invalid item': 'There is invalid item in query items',
|
||||
@@ -901,6 +906,10 @@ export default {
|
||||
'Unable to get user statistics data': 'Unable to get user statistics data',
|
||||
'Export Data': 'Export Data',
|
||||
'Clear User Data': 'Clear User Data',
|
||||
'Are you sure you want to export all data to csv file?': 'Are you sure you want to export all data to csv file?',
|
||||
'It may take a long time, please wait for a few minutes.': 'It may take a long time, please wait for a few minutes.',
|
||||
'Unable to get exported user data': 'Unable to get exported user data',
|
||||
'Save Data': 'Save Data',
|
||||
'Are you sure you want to clear all data?': 'Are you sure you want to clear all data?',
|
||||
'You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.': 'You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.',
|
||||
'All user data has been cleared': 'All user data has been cleared',
|
||||
|
||||
@@ -41,6 +41,10 @@ export default {
|
||||
'long': 'yyyy年m月d日 HH::mm',
|
||||
},
|
||||
},
|
||||
'dataExport': {
|
||||
'defaultExportFilename': 'ezBookkeeping_导出数据',
|
||||
'exportFilename': 'ezBookkeeping_{nickname}_导出数据'
|
||||
},
|
||||
'datetime': {
|
||||
'Monday': {
|
||||
'min': '一',
|
||||
@@ -591,6 +595,7 @@ export default {
|
||||
'transaction tag name is empty': '交易标签标题不能为空',
|
||||
'transaction tag name already exists': '交易标签标题已经存在',
|
||||
'transaction tag is in use and cannot be deleted': '交易标签正在被使用,无法删除',
|
||||
'data export not allowed': '不允许用户数据导出',
|
||||
'query items cannot be empty': '请求项目不能为空',
|
||||
'query items too much': '请求项目过多',
|
||||
'query items have invalid item': '请求项目中有非法项目',
|
||||
@@ -901,6 +906,10 @@ export default {
|
||||
'Unable to get user statistics data': '无法获取用户统计数据',
|
||||
'Export Data': '导出数据',
|
||||
'Clear User Data': '清除用户数据',
|
||||
'Are you sure you want to export all data to csv file?': '您确定要导出所有数据到 csv 文件?',
|
||||
'It may take a long time, please wait for a few minutes.': '这可能花费一些时间,请稍等几分钟。',
|
||||
'Unable to get exported user data': '无法获取导出的用户数据',
|
||||
'Save Data': '保存数据',
|
||||
'Are you sure you want to clear all data?': '您确定要清除所有数据?',
|
||||
'You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.': '您不能撤销该操作。该操作将会清除您的账户、分类、标签以及交易数据。请输入您当前的密码以确认。',
|
||||
'All user data has been cleared': '用户所有数据已经清空',
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
getCurrentUserProfile,
|
||||
updateUserProfile,
|
||||
getUserDataStatistics,
|
||||
getExportedUserData,
|
||||
clearUserData,
|
||||
clearUserInfoState,
|
||||
resetState,
|
||||
@@ -947,6 +948,7 @@ const stores = {
|
||||
getCurrentUserProfile,
|
||||
updateUserProfile,
|
||||
getUserDataStatistics,
|
||||
getExportedUserData,
|
||||
clearUserData,
|
||||
clearUserInfoState,
|
||||
resetState,
|
||||
|
||||
@@ -307,6 +307,30 @@ export function getUserDataStatistics() {
|
||||
});
|
||||
}
|
||||
|
||||
export function getExportedUserData() {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.getExportedUserData().then(response => {
|
||||
if (response && response.headers && response.headers['content-type'] !== 'text/csv') {
|
||||
reject({ message: 'Unable to get exported user data' });
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: response.headers['content-type'] });
|
||||
resolve(blob);
|
||||
}).catch(error => {
|
||||
logger.error('failed to get user statistics data', error);
|
||||
|
||||
if (error.response.headers['content-type'] === 'text/text' && error.response && error.response.data) {
|
||||
reject({ message: 'error.' + error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to get exported user data' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function clearUserData(context, { password }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.clearData({
|
||||
|
||||
@@ -27,12 +27,28 @@
|
||||
<f7-card>
|
||||
<f7-card-content class="no-safe-areas" :padding="false">
|
||||
<f7-list>
|
||||
<f7-list-button external no-chevron target="_blank" :link="`${$constants.api.baseUrlPath}/data/export.csv?token=${$user.getToken()}&utc_offset=${currentTimezoneOffsetMinutes}`" v-if="isDataExportingEnabled">{{ $t('Export Data') }}</f7-list-button>
|
||||
<f7-list-button @click="exportedData = null; showExportDataSheet = true" v-if="isDataExportingEnabled">{{ $t('Export Data') }}</f7-list-button>
|
||||
<f7-list-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
|
||||
</f7-list>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
|
||||
<f7-sheet style="height:auto" :opened="showExportDataSheet" @sheet:closed="showExportDataSheet = false; exportedData = null;">
|
||||
<f7-page-content>
|
||||
<div class="display-flex padding justify-content-space-between align-items-center">
|
||||
<div style="font-size: 18px"><b>{{ $t('Are you sure you want to export all data to csv file?') }}</b></div>
|
||||
</div>
|
||||
<div class="padding-horizontal padding-bottom">
|
||||
<p class="no-margin-top margin-bottom-half">{{ $t('It may take a long time, please wait for a few minutes.') }}</p>
|
||||
<f7-button large fill :class="{ 'disabled': exportingData }" :text="$t('Continue')" @click="exportData" v-if="!exportedData"></f7-button>
|
||||
<f7-button large fill external :text="$t('Save Data')" :download="exportFileName" :href="exportedData" target="_blank" v-if="exportedData"></f7-button>
|
||||
<div class="margin-top text-align-center">
|
||||
<f7-link :class="{ 'disabled': exportingData }" @click="showExportDataSheet = false" :text="$t('Cancel')"></f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</f7-page-content>
|
||||
</f7-sheet>
|
||||
|
||||
<password-input-sheet :title="$t('Are you sure you want to clear all data?')"
|
||||
:hint="$t('You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.')"
|
||||
:show.sync="showInputPasswordSheetForClearData"
|
||||
@@ -51,8 +67,11 @@ export default {
|
||||
loading: true,
|
||||
loadingError: null,
|
||||
dataStatistics: null,
|
||||
exportingData: false,
|
||||
exportedData: null,
|
||||
currentPasswordForClearData: '',
|
||||
clearingData: false,
|
||||
showExportDataSheet: false,
|
||||
showInputPasswordSheetForClearData: false,
|
||||
};
|
||||
},
|
||||
@@ -62,7 +81,18 @@ export default {
|
||||
},
|
||||
isDataExportingEnabled() {
|
||||
return this.$settings.isDataExportingEnabled();
|
||||
},
|
||||
exportFileName() {
|
||||
const nickname = this.$store.getters.currentUserNickname;
|
||||
|
||||
if (nickname) {
|
||||
return this.$t('dataExport.exportFilename', {
|
||||
nickname: nickname
|
||||
}) + '.csv';
|
||||
}
|
||||
|
||||
return this.$t('dataExport.defaultExportFilename') + '.csv';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
@@ -85,6 +115,26 @@ export default {
|
||||
onPageAfterIn() {
|
||||
this.$routeBackOnError('loadingError');
|
||||
},
|
||||
exportData() {
|
||||
const self = this;
|
||||
|
||||
self.$showLoading();
|
||||
self.exportingData = true;
|
||||
|
||||
self.$store.dispatch('getExportedUserData').then(data => {
|
||||
self.exportedData = URL.createObjectURL(data);
|
||||
self.exportingData = false;
|
||||
self.$hideLoading();
|
||||
}).catch(error => {
|
||||
self.exportedData = null;
|
||||
self.exportingData = false;
|
||||
self.$hideLoading();
|
||||
|
||||
if (!error.processed) {
|
||||
self.$toast(error.message || error);
|
||||
}
|
||||
});
|
||||
},
|
||||
clearData(password) {
|
||||
const self = this;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user