diff --git a/cmd/webserver.go b/cmd/webserver.go index a2b31630..6936ecf5 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -185,6 +185,9 @@ func startWebServer(c *cli.Context) error { apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler)) } + // Data + apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler)) + // Overview apiV1Route.GET("/overviews/transaction.json", bindApi(api.Overviews.TransactionOverviewHandler)) diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 96d54405..61fdc271 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -113,6 +113,56 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, return []byte(result), fileName, nil } +// ClearDataHandler deletes all user data +func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *errs.Error) { + var clearDataReq models.ClearDataRequest + err := c.ShouldBindJSON(&clearDataReq) + + if err != nil { + log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + user, err := a.users.GetUserById(uid) + + if err != nil { + if !errs.IsCustomError(err) { + log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error()) + } + + return nil, errs.ErrUserNotFound + } + + if !a.users.IsPasswordEqualsUserPassword(clearDataReq.Password, user) { + return nil, errs.ErrUserPasswordWrong + } + + err = a.transactions.DeleteAllTransactions(uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error()) + return nil, errs.ErrOperationFailed + } + + err = a.categories.DeleteAllCategories(uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error()) + return nil, errs.ErrOperationFailed + } + + err = a.tags.DeleteAllTags(uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error()) + return nil, errs.ErrOperationFailed + } + + log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid) + return true, nil +} + func (a *DataManagementsApi) getCSVFormatData(c *core.Context, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) (string, error) { var ret strings.Builder diff --git a/pkg/models/data_management.go b/pkg/models/data_management.go new file mode 100644 index 00000000..3e61c11a --- /dev/null +++ b/pkg/models/data_management.go @@ -0,0 +1,6 @@ +package models + +// ClearDataRequest represents all parameters of clear user data request +type ClearDataRequest struct { + Password string `json:"password" binding:"omitempty,min=6,max=128"` +} diff --git a/pkg/services/transaction_categories.go b/pkg/services/transaction_categories.go index 847bc58d..17e3822f 100644 --- a/pkg/services/transaction_categories.go +++ b/pkg/services/transaction_categories.go @@ -316,6 +316,38 @@ func (s *TransactionCategoryService) DeleteCategory(uid int64, categoryId int64) }) } +// DeleteAllCategories deletes all existed transaction categories from database +func (s *TransactionCategoryService) DeleteAllCategories(uid int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionCategory{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { + exists, err := sess.Cols("uid", "deleted", "category_id").Where("uid=? AND deleted=? AND category_id<>?", uid, false, 0).Limit(1).Exist(&models.Transaction{}) + + if err != nil { + return err + } else if exists { + return errs.ErrTransactionCategoryInUseCannotBeDeleted + } + + _, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } + + return nil + }) +} + // GetCategoryMapByList returns a transaction category map by a list func (s *TransactionCategoryService) GetCategoryMapByList(categories []*models.TransactionCategory) map[int64]*models.TransactionCategory { categoryMap := make(map[int64]*models.TransactionCategory) diff --git a/pkg/services/transaction_tags.go b/pkg/services/transaction_tags.go index 90664bf4..b55bdfb1 100644 --- a/pkg/services/transaction_tags.go +++ b/pkg/services/transaction_tags.go @@ -252,6 +252,38 @@ func (s *TransactionTagService) DeleteTag(uid int64, tagId int64) error { }) } +// DeleteAllTags deletes all existed transaction tags from database +func (s *TransactionTagService) DeleteAllTags(uid int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionTag{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { + exists, err := sess.Cols("uid", "deleted").Where("uid=? AND deleted=?", uid, false).Limit(1).Exist(&models.TransactionTagIndex{}) + + if err != nil { + return err + } else if exists { + return errs.ErrTransactionTagInUseCannotBeDeleted + } + + _, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } + + return nil + }) +} + // ExistsTagName returns whether the given tag name exists func (s *TransactionTagService) ExistsTagName(uid int64, name string) (bool, error) { if name == "" { diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 4d0dd382..bc772e95 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -908,6 +908,56 @@ func (s *TransactionService) DeleteTransaction(uid int64, transactionId int64) e }) } +// DeleteAllTransactions deletes all existed transactions from database +func (s *TransactionService) DeleteAllTransactions(uid int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.Transaction{ + Deleted: true, + DeletedUnixTime: now, + } + + tagIndexUpdateModel := &models.TransactionTagIndex{ + Deleted: true, + DeletedUnixTime: now, + } + + accountUpdateModel := &models.Account{ + Balance: 0, + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { + // Update all transaction to deleted + _, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } + + // Update all transaction tag index to deleted + _, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(tagIndexUpdateModel) + + if err != nil { + return err + } + + // Update all account table to deleted + _, err = sess.Cols("balance", "deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(accountUpdateModel) + + if err != nil { + return err + } + + return nil + }) +} + // GetRelatedTransferTransaction returns the related transaction for transfer transaction func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *models.Transaction, relatedTransactionId int64) *models.Transaction { var relatedType models.TransactionDbType diff --git a/src/lib/services.js b/src/lib/services.js index 1a4db829..3cf7205f 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -159,6 +159,11 @@ export default { password }); }, + clearData: ({ password }) => { + return axios.post('v1/data/clear.json', { + password + }); + }, getTransactionOverview: ( { today, thisWeek, thisMonth, thisYear } ) => { const queryParams = [];