optimize user data export process

This commit is contained in:
MaysWind
2023-04-02 23:18:05 +08:00
parent 44ca940ca3
commit 33250d2f3d
9 changed files with 103 additions and 42 deletions
+4 -9
View File
@@ -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))
-15
View File
@@ -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)
-16
View File
@@ -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)
}
}
+3
View File
@@ -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
+9
View File
@@ -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',
+9
View File
@@ -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': '用户所有数据已经清空',
+2
View File
@@ -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,
+24
View File
@@ -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({
+51 -1
View File
@@ -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;