mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 17:24:26 +08:00
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))
|
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))
|
apiRoute.GET("/logout.json", bindApi(api.Tokens.TokenRevokeCurrentHandler))
|
||||||
|
|
||||||
apiV1Route := apiRoute.Group("/v1")
|
apiV1Route := apiRoute.Group("/v1")
|
||||||
@@ -190,6 +181,10 @@ func startWebServer(c *cli.Context) error {
|
|||||||
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
|
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
|
||||||
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
|
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
|
||||||
|
|
||||||
|
if config.EnableDataExport {
|
||||||
|
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
|
||||||
|
}
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
|
apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler))
|
||||||
apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler))
|
apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler))
|
||||||
|
|||||||
@@ -37,21 +37,6 @@ func JWTAuthorization(c *core.Context) {
|
|||||||
c.Next()
|
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
|
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
|
||||||
func JWTTwoFactorAuthorization(c *core.Context) {
|
func JWTTwoFactorAuthorization(c *core.Context) {
|
||||||
claims, err := getTokenClaims(c)
|
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: () => {
|
getUserDataStatistics: () => {
|
||||||
return axios.get('v1/data/statistics.json');
|
return axios.get('v1/data/statistics.json');
|
||||||
},
|
},
|
||||||
|
getExportedUserData: () => {
|
||||||
|
return axios.get('v1/data/export.csv');
|
||||||
|
},
|
||||||
clearData: ({ password }) => {
|
clearData: ({ password }) => {
|
||||||
return axios.post('v1/data/clear.json', {
|
return axios.post('v1/data/clear.json', {
|
||||||
password
|
password
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export default {
|
|||||||
'long': 'm/d/yyyy hh::mm A',
|
'long': 'm/d/yyyy hh::mm A',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'dataExport': {
|
||||||
|
'defaultExportFilename': 'ezBookkeeping_export_data',
|
||||||
|
'exportFilename': 'ezBookkeeping_{nickname}_export_data'
|
||||||
|
},
|
||||||
'datetime': {
|
'datetime': {
|
||||||
'Monday': {
|
'Monday': {
|
||||||
'min': 'Mo',
|
'min': 'Mo',
|
||||||
@@ -591,6 +595,7 @@ export default {
|
|||||||
'transaction tag name is empty': 'Transaction tag title is empty',
|
'transaction tag name is empty': 'Transaction tag title is empty',
|
||||||
'transaction tag name already exists': 'Transaction tag title already exists',
|
'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',
|
'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 cannot be empty': 'There are no query items',
|
||||||
'query items too much': 'There are too many query items',
|
'query items too much': 'There are too many query items',
|
||||||
'query items have invalid item': 'There is invalid item in 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',
|
'Unable to get user statistics data': 'Unable to get user statistics data',
|
||||||
'Export Data': 'Export Data',
|
'Export Data': 'Export Data',
|
||||||
'Clear User Data': 'Clear User 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?',
|
'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.',
|
'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',
|
'All user data has been cleared': 'All user data has been cleared',
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export default {
|
|||||||
'long': 'yyyy年m月d日 HH::mm',
|
'long': 'yyyy年m月d日 HH::mm',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'dataExport': {
|
||||||
|
'defaultExportFilename': 'ezBookkeeping_导出数据',
|
||||||
|
'exportFilename': 'ezBookkeeping_{nickname}_导出数据'
|
||||||
|
},
|
||||||
'datetime': {
|
'datetime': {
|
||||||
'Monday': {
|
'Monday': {
|
||||||
'min': '一',
|
'min': '一',
|
||||||
@@ -591,6 +595,7 @@ export default {
|
|||||||
'transaction tag name is empty': '交易标签标题不能为空',
|
'transaction tag name is empty': '交易标签标题不能为空',
|
||||||
'transaction tag name already exists': '交易标签标题已经存在',
|
'transaction tag name already exists': '交易标签标题已经存在',
|
||||||
'transaction tag is in use and cannot be deleted': '交易标签正在被使用,无法删除',
|
'transaction tag is in use and cannot be deleted': '交易标签正在被使用,无法删除',
|
||||||
|
'data export not allowed': '不允许用户数据导出',
|
||||||
'query items cannot be empty': '请求项目不能为空',
|
'query items cannot be empty': '请求项目不能为空',
|
||||||
'query items too much': '请求项目过多',
|
'query items too much': '请求项目过多',
|
||||||
'query items have invalid item': '请求项目中有非法项目',
|
'query items have invalid item': '请求项目中有非法项目',
|
||||||
@@ -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?': '您确定要导出所有数据到 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?': '您确定要清除所有数据?',
|
'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': '用户所有数据已经清空',
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
getCurrentUserProfile,
|
getCurrentUserProfile,
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
getUserDataStatistics,
|
getUserDataStatistics,
|
||||||
|
getExportedUserData,
|
||||||
clearUserData,
|
clearUserData,
|
||||||
clearUserInfoState,
|
clearUserInfoState,
|
||||||
resetState,
|
resetState,
|
||||||
@@ -947,6 +948,7 @@ const stores = {
|
|||||||
getCurrentUserProfile,
|
getCurrentUserProfile,
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
getUserDataStatistics,
|
getUserDataStatistics,
|
||||||
|
getExportedUserData,
|
||||||
clearUserData,
|
clearUserData,
|
||||||
clearUserInfoState,
|
clearUserInfoState,
|
||||||
resetState,
|
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 }) {
|
export function clearUserData(context, { password }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
services.clearData({
|
services.clearData({
|
||||||
|
|||||||
@@ -27,12 +27,28 @@
|
|||||||
<f7-card>
|
<f7-card>
|
||||||
<f7-card-content class="no-safe-areas" :padding="false">
|
<f7-card-content class="no-safe-areas" :padding="false">
|
||||||
<f7-list>
|
<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-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
</f7-card-content>
|
</f7-card-content>
|
||||||
</f7-card>
|
</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?')"
|
<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.')"
|
: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"
|
:show.sync="showInputPasswordSheetForClearData"
|
||||||
@@ -51,8 +67,11 @@ export default {
|
|||||||
loading: true,
|
loading: true,
|
||||||
loadingError: null,
|
loadingError: null,
|
||||||
dataStatistics: null,
|
dataStatistics: null,
|
||||||
|
exportingData: false,
|
||||||
|
exportedData: null,
|
||||||
currentPasswordForClearData: '',
|
currentPasswordForClearData: '',
|
||||||
clearingData: false,
|
clearingData: false,
|
||||||
|
showExportDataSheet: false,
|
||||||
showInputPasswordSheetForClearData: false,
|
showInputPasswordSheetForClearData: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -62,7 +81,18 @@ export default {
|
|||||||
},
|
},
|
||||||
isDataExportingEnabled() {
|
isDataExportingEnabled() {
|
||||||
return this.$settings.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() {
|
created() {
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -85,6 +115,26 @@ export default {
|
|||||||
onPageAfterIn() {
|
onPageAfterIn() {
|
||||||
this.$routeBackOnError('loadingError');
|
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) {
|
clearData(password) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user