From 560edf9fbf83d1840b8af3f1ee1521a9eb305c34 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Fri, 16 Aug 2024 23:56:23 +0800 Subject: [PATCH] code refactor --- cmd/webserver.go | 4 +- pkg/api/accounts.go | 14 ++- pkg/api/amap_api_proxies.go | 9 +- pkg/api/authorizations.go | 14 +-- pkg/api/base.go | 94 +++++++++++++++++++ pkg/api/data_managements.go | 6 +- pkg/api/exchange_rates.go | 16 ++-- pkg/api/forget_passwords.go | 10 ++- pkg/api/map_image_proxies.go | 21 +++-- pkg/api/qrcodes.go | 9 +- pkg/api/tokens.go | 14 +-- pkg/api/transaction_categories.go | 14 ++- pkg/api/transaction_templates.go | 14 ++- pkg/api/transactions.go | 14 ++- pkg/api/users.go | 125 +++++++------------------- pkg/cli/base.go | 13 +++ pkg/cli/user_data.go | 8 +- pkg/core/user_avatar_provider_type.go | 10 +++ pkg/errs/user.go | 2 + pkg/models/user.go | 25 ++---- pkg/services/base.go | 32 +++++++ pkg/services/users.go | 106 +++++++++++++++++++++- pkg/settings/setting.go | 17 ++-- pkg/settings/setting_container.go | 57 ------------ pkg/storage/storage_container.go | 30 +++---- src/locales/en.json | 2 + src/locales/zh_Hans.json | 2 + 27 files changed, 437 insertions(+), 245 deletions(-) create mode 100644 pkg/api/base.go create mode 100644 pkg/cli/base.go create mode 100644 pkg/core/user_avatar_provider_type.go diff --git a/cmd/webserver.go b/cmd/webserver.go index 05c85b42..d2201c86 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -153,7 +153,7 @@ func startWebServer(c *cli.Context) error { router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i])) } - if config.AvatarProvider == settings.InternalAvatarProvider { + if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL { avatarRoute := router.Group("/avatar") avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString)) { @@ -261,7 +261,7 @@ func startWebServer(c *cli.Context) error { apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler)) apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config)) - if config.AvatarProvider == settings.InternalAvatarProvider { + if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL { apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler)) apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler)) } diff --git a/pkg/api/accounts.go b/pkg/api/accounts.go index 700196fe..d65e7bb9 100644 --- a/pkg/api/accounts.go +++ b/pkg/api/accounts.go @@ -16,12 +16,20 @@ import ( // AccountsApi represents account api type AccountsApi struct { + ApiUsingConfig + ApiUsingDuplicateChecker accounts *services.AccountService } // Initialize an account api singleton instance var ( Accounts = &AccountsApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + container: duplicatechecker.Container, + }, accounts: services.Accounts, } ) @@ -211,8 +219,8 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) { mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1) childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq) - if settings.Container.Current.EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" { - found, remark := duplicatechecker.Container.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId) + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" { + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId) if found { log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid) @@ -256,7 +264,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) { log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId) - duplicatechecker.Container.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId)) + a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId)) accountInfoResp := mainAccount.ToAccountInfoResponse() if len(childrenAccounts) > 0 { diff --git a/pkg/api/amap_api_proxies.go b/pkg/api/amap_api_proxies.go index 09c30676..31f63445 100644 --- a/pkg/api/amap_api_proxies.go +++ b/pkg/api/amap_api_proxies.go @@ -18,11 +18,16 @@ const amapRestApiUrl = "https://restapi.amap.com/" // AmapApiProxy represents amap api proxy type AmapApiProxy struct { + ApiUsingConfig } // Initialize a amap api proxy singleton instance var ( - AmapApis = &AmapApiProxy{} + AmapApis = &AmapApiProxy{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + } ) // AmapApiProxyHandler returns amap api response @@ -38,7 +43,7 @@ func (p *AmapApiProxy) AmapApiProxyHandler(c *core.Context) (*httputil.ReversePr } director := func(req *http.Request) { - targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, settings.Container.Current.AmapApplicationSecret) + targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, p.CurrentConfig().AmapApplicationSecret) targetUrl, _ := url.Parse(targetRawUrl) oldCookies := req.Cookies() diff --git a/pkg/api/authorizations.go b/pkg/api/authorizations.go index 1abf574a..1fd707a7 100644 --- a/pkg/api/authorizations.go +++ b/pkg/api/authorizations.go @@ -13,6 +13,7 @@ import ( // AuthorizationsApi represents authorization api type AuthorizationsApi struct { + ApiUsingConfig users *services.UserService tokens *services.TokenService twoFactorAuthorizations *services.TwoFactorAuthorizationService @@ -21,6 +22,9 @@ type AuthorizationsApi struct { // Initialize a authorization api singleton instance var ( Authorizations = &AuthorizationsApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, users: services.Users, tokens: services.Tokens, twoFactorAuthorizations: services.TwoFactorAuthorizations, @@ -49,7 +53,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error) return nil, errs.ErrUserIsDisabled } - if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified { + if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified { hasValidEmailVerifyToken, err := a.tokens.ExistsValidTokenByType(c, user.Uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY) if err != nil { @@ -143,7 +147,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er return nil, errs.ErrUserIsDisabled } - if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified { + if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified { log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid) return nil, errs.ErrEmailIsNotVerified } @@ -205,7 +209,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont return nil, errs.ErrUserIsDisabled } - if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified { + if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified { log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid) return nil, errs.ErrEmailIsNotVerified } @@ -244,7 +248,7 @@ func (a *AuthorizationsApi) getAuthResponse(c *core.Context, token string, need2 return &models.AuthResponse{ Token: token, Need2FA: need2FA, - User: user.ToUserBasicInfo(), - NotificationContent: settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()), + User: a.GetUserBasicInfo(user), + NotificationContent: a.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()), } } diff --git a/pkg/api/base.go b/pkg/api/base.go new file mode 100644 index 00000000..01763103 --- /dev/null +++ b/pkg/api/base.go @@ -0,0 +1,94 @@ +package api + +import ( + "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/settings" +) + +// ApiUsingConfig represents an api that need to use config +type ApiUsingConfig struct { + container *settings.ConfigContainer +} + +// CurrentConfig returns the current config +func (a *ApiUsingConfig) CurrentConfig() *settings.Config { + return a.container.Current +} + +// GetUserBasicInfo returns the view-object of user basic info according to the user model +func (a *ApiUsingConfig) GetUserBasicInfo(user *models.User) *models.UserBasicInfo { + return user.ToUserBasicInfo(a.CurrentConfig().AvatarProvider, a.CurrentConfig().RootUrl) +} + +// GetAfterRegisterNotificationContent returns the notification content displayed each time users register +func (a *ApiUsingConfig) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string { + language := userLanguage + + if language == "" { + language = clientLanguage + } + + if !a.container.Current.AfterRegisterNotification.Enabled { + return "" + } + + if multiLanguageContent, exists := a.container.Current.AfterRegisterNotification.MultiLanguageContent[language]; exists { + return multiLanguageContent + } + + return a.container.Current.AfterRegisterNotification.DefaultContent +} + +// GetAfterLoginNotificationContent returns the notification content displayed each time users log in +func (a *ApiUsingConfig) GetAfterLoginNotificationContent(userLanguage string, clientLanguage string) string { + language := userLanguage + + if language == "" { + language = clientLanguage + } + + if !a.container.Current.AfterLoginNotification.Enabled { + return "" + } + + if multiLanguageContent, exists := a.container.Current.AfterLoginNotification.MultiLanguageContent[language]; exists { + return multiLanguageContent + } + + return a.container.Current.AfterLoginNotification.DefaultContent +} + +// GetAfterOpenNotificationContent returns the notification content displayed each time users open the app +func (a *ApiUsingConfig) GetAfterOpenNotificationContent(userLanguage string, clientLanguage string) string { + language := userLanguage + + if language == "" { + language = clientLanguage + } + + if !a.container.Current.AfterOpenNotification.Enabled { + return "" + } + + if multiLanguageContent, exists := a.container.Current.AfterOpenNotification.MultiLanguageContent[language]; exists { + return multiLanguageContent + } + + return a.container.Current.AfterOpenNotification.DefaultContent +} + +// ApiUsingDuplicateChecker represents an api that need to use duplicate checker +type ApiUsingDuplicateChecker struct { + container *duplicatechecker.DuplicateCheckerContainer +} + +// GetSubmissionRemark returns whether the same submission has been processed and related remark by the current duplicate checker +func (a *ApiUsingDuplicateChecker) GetSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) (bool, string) { + return a.container.GetSubmissionRemark(checkerType, uid, identification) +} + +// SetSubmissionRemark saves the identification and remark to in-memory cache by the current duplicate checker +func (a *ApiUsingDuplicateChecker) SetSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string) { + a.container.SetSubmissionRemark(checkerType, uid, identification, remark) +} diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 0d3c5492..65fc846b 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -19,6 +19,7 @@ const pageCountForDataExport = 1000 // DataManagementsApi represents data management api type DataManagementsApi struct { + ApiUsingConfig ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter tokens *services.TokenService @@ -33,6 +34,9 @@ type DataManagementsApi struct { // Initialize a data management api singleton instance var ( DataManagements = &DataManagementsApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{}, ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{}, tokens: services.Tokens, @@ -162,7 +166,7 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error } func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) { - if !settings.Container.Current.EnableDataExport { + if !a.CurrentConfig().EnableDataExport { return nil, "", errs.ErrDataExportNotAllowed } diff --git a/pkg/api/exchange_rates.go b/pkg/api/exchange_rates.go index a5912013..e4242906 100644 --- a/pkg/api/exchange_rates.go +++ b/pkg/api/exchange_rates.go @@ -18,11 +18,17 @@ import ( ) // ExchangeRatesApi represents exchange rate api -type ExchangeRatesApi struct{} +type ExchangeRatesApi struct { + ApiUsingConfig +} // Initialize a exchange rate api singleton instance var ( - ExchangeRates = &ExchangeRatesApi{} + ExchangeRates = &ExchangeRatesApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + } ) // LatestExchangeRateHandler returns latest exchange rate data @@ -36,9 +42,9 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err uid := c.GetCurrentUid() transport := http.DefaultTransport.(*http.Transport).Clone() - utils.SetProxyUrl(transport, settings.Container.Current.ExchangeRatesProxy) + utils.SetProxyUrl(transport, a.CurrentConfig().ExchangeRatesProxy) - if settings.Container.Current.ExchangeRatesSkipTLSVerify { + if a.CurrentConfig().ExchangeRatesSkipTLSVerify { transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } @@ -46,7 +52,7 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err client := &http.Client{ Transport: transport, - Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond, + Timeout: time.Duration(a.CurrentConfig().ExchangeRatesRequestTimeout) * time.Millisecond, } urls := dataSource.GetRequestUrls() diff --git a/pkg/api/forget_passwords.go b/pkg/api/forget_passwords.go index 777dc4ff..60fe7408 100644 --- a/pkg/api/forget_passwords.go +++ b/pkg/api/forget_passwords.go @@ -13,6 +13,7 @@ import ( // ForgetPasswordsApi represents user forget password api type ForgetPasswordsApi struct { + ApiUsingConfig users *services.UserService tokens *services.TokenService forgetPasswords *services.ForgetPasswordService @@ -21,6 +22,9 @@ type ForgetPasswordsApi struct { // Initialize a user api singleton instance var ( ForgetPasswords = &ForgetPasswordsApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, users: services.Users, tokens: services.Tokens, forgetPasswords: services.ForgetPasswords, @@ -52,12 +56,12 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) ( return nil, errs.ErrUserIsDisabled } - if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified { + if a.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified { log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid) return nil, errs.ErrEmailIsNotVerified } - if !settings.Container.Current.EnableSMTP { + if !a.CurrentConfig().EnableSMTP { return nil, errs.ErrSMTPServerNotEnabled } @@ -105,7 +109,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *er return nil, errs.ErrUserIsDisabled } - if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified { + if a.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified { log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid) return nil, errs.ErrEmailIsNotVerified } diff --git a/pkg/api/map_image_proxies.go b/pkg/api/map_image_proxies.go index 4b6ebd07..6007b17e 100644 --- a/pkg/api/map_image_proxies.go +++ b/pkg/api/map_image_proxies.go @@ -24,11 +24,16 @@ const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SE // MapImageProxy represents map image proxy type MapImageProxy struct { + ApiUsingConfig } // Initialize a map image proxy singleton instance var ( - MapImages = &MapImageProxy{} + MapImages = &MapImageProxy{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + } ) // MapTileImageProxyHandler returns map tile image @@ -47,7 +52,7 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev } else if mapProvider == settings.CartoDBMapProvider { return cartoDBMapTileImageUrlFormat, nil } else if mapProvider == settings.TomTomMapProvider { - targetUrl := tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey + targetUrl := tomtomMapTileImageUrlFormat + "?key=" + p.CurrentConfig().TomTomMapAPIKey language := c.Query("language") if language != "" { @@ -56,9 +61,9 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev return targetUrl, nil } else if mapProvider == settings.TianDiTuProvider { - return tianDiTuMapTileImageUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil + return tianDiTuMapTileImageUrlFormat + "&tk=" + p.CurrentConfig().TianDiTuAPIKey, nil } else if mapProvider == settings.CustomProvider { - return settings.Container.Current.CustomMapTileServerTileLayerUrl, nil + return p.CurrentConfig().CustomMapTileServerTileLayerUrl, nil } return "", errs.ErrParameterInvalid @@ -69,9 +74,9 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev func (p *MapImageProxy) MapAnnotationImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) { return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) { if mapProvider == settings.TianDiTuProvider { - return tianDiTuMapAnnotationUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil + return tianDiTuMapAnnotationUrlFormat + "&tk=" + p.CurrentConfig().TianDiTuAPIKey, nil } else if mapProvider == settings.CustomProvider { - return settings.Container.Current.CustomMapTileServerAnnotationLayerUrl, nil + return p.CurrentConfig().CustomMapTileServerAnnotationLayerUrl, nil } return "", errs.ErrParameterInvalid @@ -82,7 +87,7 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Co mapProvider := strings.Replace(c.Query("provider"), "-", "_", -1) targetUrl := "" - if mapProvider != settings.Container.Current.MapProvider { + if mapProvider != p.CurrentConfig().MapProvider { return nil, errs.ErrMapProviderNotCurrent } @@ -105,7 +110,7 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Co } transport := http.DefaultTransport.(*http.Transport).Clone() - utils.SetProxyUrl(transport, settings.Container.Current.MapProxy) + utils.SetProxyUrl(transport, p.CurrentConfig().MapProxy) director := func(req *http.Request) { imageRawUrl := targetUrl diff --git a/pkg/api/qrcodes.go b/pkg/api/qrcodes.go index 1c7e71cf..e7df6b6d 100644 --- a/pkg/api/qrcodes.go +++ b/pkg/api/qrcodes.go @@ -19,16 +19,21 @@ const ( // QrCodesApi represents qrcode generator api type QrCodesApi struct { + ApiUsingConfig } // Initialize a qrcode generator api singleton instance var ( - QrCodes = &QrCodesApi{} + QrCodes = &QrCodesApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + } ) // MobileUrlQrCodeHandler returns a mobile url qr code image func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *errs.Error) { - fullUrl := settings.Container.Current.RootUrl + "mobile" + fullUrl := a.CurrentConfig().RootUrl + "mobile" data, err := a.generateUrlQrCode(c, fullUrl) if err != nil { diff --git a/pkg/api/tokens.go b/pkg/api/tokens.go index 863bd245..f27461cd 100644 --- a/pkg/api/tokens.go +++ b/pkg/api/tokens.go @@ -15,6 +15,7 @@ import ( // TokensApi represents token api type TokensApi struct { + ApiUsingConfig tokens *services.TokenService users *services.UserService } @@ -22,6 +23,9 @@ type TokensApi struct { // Initialize a token api singleton instance var ( Tokens = &TokensApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, tokens: services.Tokens, users: services.Users, } @@ -180,7 +184,7 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) { now := time.Now().Unix() oldTokenClaims := c.GetTokenClaims() - if now-oldTokenClaims.IssuedAt < int64(settings.Container.Current.TokenMinRefreshInterval) { + if now-oldTokenClaims.IssuedAt < int64(a.CurrentConfig().TokenMinRefreshInterval) { log.InfofWithRequestId(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid) userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId) @@ -204,8 +208,8 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) { } refreshResp := &models.TokenRefreshResponse{ - User: user.ToUserBasicInfo(), - NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()), + User: a.GetUserBasicInfo(user), + NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()), } return refreshResp, nil @@ -233,8 +237,8 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) { refreshResp := &models.TokenRefreshResponse{ NewToken: token, OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord), - User: user.ToUserBasicInfo(), - NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()), + User: a.GetUserBasicInfo(user), + NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()), } return refreshResp, nil diff --git a/pkg/api/transaction_categories.go b/pkg/api/transaction_categories.go index 7b039248..60396fa3 100644 --- a/pkg/api/transaction_categories.go +++ b/pkg/api/transaction_categories.go @@ -17,12 +17,20 @@ import ( // TransactionCategoriesApi represents transaction category api type TransactionCategoriesApi struct { + ApiUsingConfig + ApiUsingDuplicateChecker categories *services.TransactionCategoryService } // Initialize a transaction category api singleton instance var ( TransactionCategories = &TransactionCategoriesApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + container: duplicatechecker.Container, + }, categories: services.TransactionCategories, } ) @@ -122,8 +130,8 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any, category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1) - if settings.Container.Current.EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" { - found, remark := duplicatechecker.Container.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId) + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" { + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId) if found { log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid) @@ -153,7 +161,7 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any, log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId) - duplicatechecker.Container.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId)) + a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId)) categoryResp := category.ToTransactionCategoryInfoResponse() return categoryResp, nil diff --git a/pkg/api/transaction_templates.go b/pkg/api/transaction_templates.go index ba6124f8..36740850 100644 --- a/pkg/api/transaction_templates.go +++ b/pkg/api/transaction_templates.go @@ -16,12 +16,20 @@ import ( // TransactionTemplatesApi represents transaction template api type TransactionTemplatesApi struct { + ApiUsingConfig + ApiUsingDuplicateChecker templates *services.TransactionTemplateService } // Initialize a transaction template api singleton instance var ( TransactionTemplates = &TransactionTemplatesApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + container: duplicatechecker.Container, + }, templates: services.TransactionTemplates, } ) @@ -117,8 +125,8 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, * serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1) - if settings.Container.Current.EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" { - found, remark := duplicatechecker.Container.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId) + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" { + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId) if found { log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid) @@ -148,7 +156,7 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, * log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId) - duplicatechecker.Container.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId)) + a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId)) templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset) return templateResp, nil diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 8dff5579..f750c0fd 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -18,6 +18,8 @@ import ( // TransactionsApi represents transaction api type TransactionsApi struct { + ApiUsingConfig + ApiUsingDuplicateChecker transactions *services.TransactionService transactionCategories *services.TransactionCategoryService transactionTags *services.TransactionTagService @@ -28,6 +30,12 @@ type TransactionsApi struct { // Initialize a transaction api singleton instance var ( Transactions = &TransactionsApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + container: duplicatechecker.Container, + }, transactions: services.Transactions, transactionCategories: services.TransactionCategories, transactionTags: services.TransactionTags, @@ -663,8 +671,8 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs. return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime } - if settings.Container.Current.EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" { - found, remark := duplicatechecker.Container.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId) + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" { + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId) if found { log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] another transaction \"id:%s\" has been created for user \"uid:%d\"", remark, uid) @@ -694,7 +702,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs. log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId) - duplicatechecker.Container.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId)) + a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId)) transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable) return transactionResp, nil diff --git a/pkg/api/users.go b/pkg/api/users.go index 6f811b13..cf051389 100644 --- a/pkg/api/users.go +++ b/pkg/api/users.go @@ -1,8 +1,6 @@ package api import ( - "io" - "os" "strings" "time" @@ -15,13 +13,13 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/services" "github.com/mayswind/ezbookkeeping/pkg/settings" - "github.com/mayswind/ezbookkeeping/pkg/storage" "github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/validators" ) // UsersApi represents user api type UsersApi struct { + ApiUsingConfig users *services.UserService tokens *services.TokenService accounts *services.AccountService @@ -30,6 +28,9 @@ type UsersApi struct { // Initialize a user api singleton instance var ( Users = &UsersApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, users: services.Users, tokens: services.Tokens, accounts: services.Accounts, @@ -38,7 +39,7 @@ var ( // UserRegisterHandler saves a new user by request parameters func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) { - if !settings.Container.Current.EnableUserRegister { + if !a.CurrentConfig().EnableUserRegister { return nil, errs.ErrUserRegistrationNotAllowed } @@ -92,14 +93,14 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) { authResp := &models.RegisterResponse{ AuthResponse: models.AuthResponse{ Need2FA: false, - User: user.ToUserBasicInfo(), - NotificationContent: settings.Container.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()), + User: a.GetUserBasicInfo(user), + NotificationContent: a.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()), }, - NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail, + NeedVerifyEmail: a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableUserForceVerifyEmail, PresetCategoriesSaved: presetCategoriesSaved, } - if settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP { + if a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableSMTP { token, _, err := a.tokens.CreateEmailVerifyToken(c, user) if err != nil { @@ -115,7 +116,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) { } } - if settings.Container.Current.EnableUserForceVerifyEmail { + if a.CurrentConfig().EnableUserForceVerifyEmail { return authResp, nil } @@ -187,8 +188,8 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) { } resp.NewToken = token - resp.User = user.ToUserBasicInfo() - resp.NotificationContent = settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()) + resp.User = a.GetUserBasicInfo(user) + resp.NotificationContent = a.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()) c.SetTextualToken(token) c.SetTokenClaims(claims) @@ -212,7 +213,7 @@ func (a *UsersApi) UserProfileHandler(c *core.Context) (any, *errs.Error) { return nil, errs.ErrUserNotFound } - userResp := user.ToUserProfileResponse() + userResp := a.getUserProfileResponse(user) return userResp, nil } @@ -447,10 +448,10 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error) log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid) resp := &models.UserProfileUpdateResponse{ - User: user.ToUserBasicInfo(), + User: a.GetUserBasicInfo(user), } - if emailSetToUnverified && settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP { + if emailSetToUnverified && a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableSMTP { err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY) if err != nil { @@ -547,32 +548,15 @@ func (a *UsersApi) UserUpdateAvatarHandler(c *core.Context) (any, *errs.Error) { return nil, errs.ErrOperationFailed } - defer avatarFile.Close() - - err = storage.Container.SaveAvatar(user.Uid, avatarFile, fileExtension) + err = a.users.UpdateUserAvatar(c, user.Uid, avatarFile, fileExtension, user.CustomAvatarType) if err != nil { - log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to save avatar file for user \"uid:%d\", because %s", user.Uid, err.Error()) - return nil, errs.ErrOperationFailed - } - - err = a.users.UpdateUserAvatar(c, user.Uid, fileExtension) - - if err != nil { - log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error()) + log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to update avatar for user \"uid:%d\", because %s", user.Uid, err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } - if fileExtension != user.CustomAvatarType && user.CustomAvatarType != "" { - err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType) - - if err != nil { - log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", user.CustomAvatarType, user.Uid, err.Error()) - } - } - user.CustomAvatarType = fileExtension - userResp := user.ToUserProfileResponse() + userResp := a.getUserProfileResponse(user) return userResp, nil } @@ -593,39 +577,21 @@ func (a *UsersApi) UserRemoveAvatarHandler(c *core.Context) (any, *errs.Error) { return nil, errs.ErrNothingWillBeUpdated } - err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType) + err = a.users.RemoveUserAvatar(c, user.Uid, user.CustomAvatarType) if err != nil { - log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete avatar file for user \"uid:%d\", because %s", user.Uid, err.Error()) - - exists, err := storage.Container.ExistsAvatar(user.Uid, user.CustomAvatarType) - - if err != nil { - log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to check whether avatar file exist for user \"uid:%d\", because %s", user.Uid, err.Error()) - return nil, errs.ErrOperationFailed - } - - if exists { - log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete whether avatar file exist for user \"uid:%d\", the avatar file still exist", user.Uid) - return nil, errs.ErrOperationFailed - } - } - - err = a.users.UpdateUserAvatar(c, user.Uid, "") - - if err != nil { - log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error()) + log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to remove avatar for user \"uid:%d\", because %s", user.Uid, err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } user.CustomAvatarType = "" - userResp := user.ToUserProfileResponse() + userResp := a.getUserProfileResponse(user) return userResp, nil } // UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) { - if !settings.Container.Current.EnableUserVerifyEmail { + if !a.CurrentConfig().EnableUserVerifyEmail { return nil, errs.ErrEmailValidationNotAllowed } @@ -657,7 +623,7 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any return nil, errs.ErrEmailIsVerified } - if !settings.Container.Current.EnableSMTP { + if !a.CurrentConfig().EnableSMTP { return nil, errs.ErrSMTPServerNotEnabled } @@ -681,7 +647,7 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any // UserSendVerifyEmailByLoginedUserHandler sends logined user verify email func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any, *errs.Error) { - if !settings.Container.Current.EnableUserVerifyEmail { + if !a.CurrentConfig().EnableUserVerifyEmail { return nil, errs.ErrEmailValidationNotAllowed } @@ -701,7 +667,7 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any return nil, errs.ErrEmailIsVerified } - if !settings.Container.Current.EnableSMTP { + if !a.CurrentConfig().EnableSMTP { return nil, errs.ErrSMTPServerNotEnabled } @@ -741,46 +707,19 @@ func (a *UsersApi) UserGetAvatarHandler(c *core.Context) ([]byte, string, *errs. return nil, "", errs.ErrUserIdInvalid } - user, err := a.users.GetUserById(c, uid) + avatarData, err := a.users.GetUserAvatar(c, uid, fileExtension) if err != nil { if !errs.IsCustomError(err) { - log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user, because %s", err.Error()) + log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user avatar, because %s", err.Error()) } - return nil, "", errs.ErrUserNotFound - } - - if user.CustomAvatarType == "" { - log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user does not have avatar for user \"uid:%d\"", user.Uid) - return nil, "", errs.ErrUserAvatarNoExists - } - - if user.CustomAvatarType != fileExtension { - log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar extension is invalid \"%s\" for user \"uid:%d\"", fileExtension, user.Uid) - return nil, "", errs.ErrUserAvatarNoExists - } - - avatarFile, err := storage.Container.ReadAvatar(user.Uid, fileExtension) - - if os.IsNotExist(err) { - log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar file not exist for user \"uid:%d\", because %s", user.Uid, err.Error()) - return nil, "", errs.ErrUserAvatarNoExists - } - - if err != nil { - log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user avatar object for user \"uid:%d\", because %s", user.Uid, err.Error()) - return nil, "", errs.ErrOperationFailed - } - - defer avatarFile.Close() - - avatarData, err := io.ReadAll(avatarFile) - - if err != nil { - log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to read user avatar object data for user \"uid:%d\", because %s", user.Uid, err.Error()) - return nil, "", errs.ErrOperationFailed + return nil, "", errs.Or(err, errs.ErrOperationFailed) } return avatarData, contentType, nil } + +func (a *UsersApi) getUserProfileResponse(user *models.User) *models.UserProfileResponse { + return user.ToUserProfileResponse(a.CurrentConfig().AvatarProvider, a.CurrentConfig().RootUrl) +} diff --git a/pkg/cli/base.go b/pkg/cli/base.go new file mode 100644 index 00000000..2bfd350e --- /dev/null +++ b/pkg/cli/base.go @@ -0,0 +1,13 @@ +package cli + +import "github.com/mayswind/ezbookkeeping/pkg/settings" + +// CliUsingConfig represents an cli that need to use config +type CliUsingConfig struct { + container *settings.ConfigContainer +} + +// CurrentConfig returns the current config +func (l *CliUsingConfig) CurrentConfig() *settings.Config { + return l.container.Current +} diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index 83265875..ab58f1a2 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -19,6 +19,7 @@ const pageCountForDataExport = 1000 // UserDataCli represents user data cli type UserDataCli struct { + CliUsingConfig ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter accounts *services.AccountService @@ -34,6 +35,9 @@ type UserDataCli struct { // Initialize an user data cli singleton instance var ( UserData = &UserDataCli{ + CliUsingConfig: CliUsingConfig{ + container: settings.Container, + }, ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{}, ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{}, accounts: services.Accounts, @@ -180,7 +184,7 @@ func (l *UserDataCli) SendPasswordResetMail(c *cli.Context, username string) err return err } - if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified { + if l.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified { log.BootWarnf("[user_data.SendPasswordResetMail] user \"uid:%d\" has not verified email", user.Uid) return errs.ErrEmailIsNotVerified } @@ -238,7 +242,7 @@ func (l *UserDataCli) DisableUser(c *cli.Context, username string) error { // ResendVerifyEmail resends an email with account activation link func (l *UserDataCli) ResendVerifyEmail(c *cli.Context, username string) error { - if !settings.Container.Current.EnableUserVerifyEmail { + if !l.CurrentConfig().EnableUserVerifyEmail { return errs.ErrEmailValidationNotAllowed } diff --git a/pkg/core/user_avatar_provider_type.go b/pkg/core/user_avatar_provider_type.go new file mode 100644 index 00000000..6a039b50 --- /dev/null +++ b/pkg/core/user_avatar_provider_type.go @@ -0,0 +1,10 @@ +package core + +// UserAvatarProviderType represents type of the user avatar provider +type UserAvatarProviderType string + +// User avatar provider types +const ( + USER_AVATAR_PROVIDER_INTERNAL UserAvatarProviderType = "internal" + USER_AVATAR_PROVIDER_GRAVATAR UserAvatarProviderType = "gravatar" +) diff --git a/pkg/errs/user.go b/pkg/errs/user.go index 26ab5e41..b8298272 100644 --- a/pkg/errs/user.go +++ b/pkg/errs/user.go @@ -34,4 +34,6 @@ var ( ErrNoUserAvatar = NewNormalError(NormalSubcategoryUser, 25, http.StatusBadRequest, "no user avatar") ErrUserAvatarIsEmpty = NewNormalError(NormalSubcategoryUser, 26, http.StatusBadRequest, "user avatar is empty") ErrUserAvatarNoExists = NewNormalError(NormalSubcategoryUser, 27, http.StatusNotFound, "user avatar not exists") + ErrUserAvatarNotSet = NewNormalError(NormalSubcategoryUser, 28, http.StatusNotFound, "user avatar not set") + ErrUserAvatarExtensionInvalid = NewNormalError(NormalSubcategoryUser, 29, http.StatusNotFound, "user avatar file extension invalid") ) diff --git a/pkg/models/user.go b/pkg/models/user.go index 6371a4bb..3abbe057 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -5,7 +5,6 @@ import ( "time" "github.com/mayswind/ezbookkeeping/pkg/core" - "github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/utils" ) @@ -253,13 +252,13 @@ func (u *User) CanEditTransactionByTransactionTime(transactionTime int64, utcOff } // ToUserBasicInfo returns a user basic view-object according to database model -func (u *User) ToUserBasicInfo() *UserBasicInfo { +func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, rootUrl string) *UserBasicInfo { return &UserBasicInfo{ Username: u.Username, Email: u.Email, Nickname: u.Nickname, - AvatarUrl: u.getAvatarUrl(), - AvatarProvider: u.getAvatarProvider(), + AvatarUrl: u.getAvatarUrl(avatarProvider, rootUrl), + AvatarProvider: string(avatarProvider), DefaultAccountId: u.DefaultAccountId, TransactionEditScope: u.TransactionEditScope, Language: u.Language, @@ -280,23 +279,17 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo { } // ToUserProfileResponse returns a user profile view-object according to database model -func (u *User) ToUserProfileResponse() *UserProfileResponse { +func (u *User) ToUserProfileResponse(avatarProvider core.UserAvatarProviderType, rootUrl string) *UserProfileResponse { return &UserProfileResponse{ - UserBasicInfo: u.ToUserBasicInfo(), + UserBasicInfo: u.ToUserBasicInfo(avatarProvider, rootUrl), LastLoginAt: u.LastLoginUnixTime, } } -func (u *User) getAvatarProvider() string { - return settings.Container.Current.AvatarProvider -} - -func (u *User) getAvatarUrl() string { - avatarProvider := settings.Container.Current.AvatarProvider - - if avatarProvider == settings.InternalAvatarProvider { - return utils.GetInternalAvatarUrl(u.Uid, u.CustomAvatarType, settings.Container.Current.RootUrl) - } else if avatarProvider == settings.GravatarProvider { +func (u *User) getAvatarUrl(avatarProvider core.UserAvatarProviderType, rootUrl string) string { + if avatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL { + return utils.GetInternalAvatarUrl(u.Uid, u.CustomAvatarType, rootUrl) + } else if avatarProvider == core.USER_AVATAR_PROVIDER_GRAVATAR { return utils.GetGravatarUrl(u.Email) } diff --git a/pkg/services/base.go b/pkg/services/base.go index 240e09fc..143997b2 100644 --- a/pkg/services/base.go +++ b/pkg/services/base.go @@ -1,10 +1,13 @@ package services import ( + "fmt" + "github.com/mayswind/ezbookkeeping/pkg/datastore" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/mail" "github.com/mayswind/ezbookkeeping/pkg/settings" + "github.com/mayswind/ezbookkeeping/pkg/storage" "github.com/mayswind/ezbookkeeping/pkg/uuid" ) @@ -76,3 +79,32 @@ func (s *ServiceUsingUuid) GenerateUuid(uuidType uuid.UuidType) int64 { func (s *ServiceUsingUuid) GenerateUuids(uuidType uuid.UuidType, count uint8) []int64 { return s.container.GenerateUuids(uuidType, count) } + +// ServiceUsingStorage represents a service that need to use storage +type ServiceUsingStorage struct { + container *storage.StorageContainer +} + +// ExistsAvatar returns whether the user avatar exists from the current avatar object storage +func (s *ServiceUsingStorage) ExistsAvatar(uid int64, fileExtension string) (bool, error) { + return s.container.ExistsAvatar(s.getUserAvatarPath(uid, fileExtension)) +} + +// ReadAvatar returns the user avatar from the current avatar object storage +func (s *ServiceUsingStorage) ReadAvatar(uid int64, fileExtension string) (storage.ObjectInStorage, error) { + return s.container.ReadAvatar(s.getUserAvatarPath(uid, fileExtension)) +} + +// SaveAvatar returns whether save the user avatar into the current avatar object storage successfully +func (s *ServiceUsingStorage) SaveAvatar(uid int64, object storage.ObjectInStorage, fileExtension string) error { + return s.container.SaveAvatar(s.getUserAvatarPath(uid, fileExtension), object) +} + +// DeleteAvatar returns whether delete the user avatar from the current avatar object storage successfully +func (s *ServiceUsingStorage) DeleteAvatar(uid int64, fileExtension string) error { + return s.container.DeleteAvatar(s.getUserAvatarPath(uid, fileExtension)) +} + +func (s *ServiceUsingStorage) getUserAvatarPath(uid int64, fileExtension string) string { + return fmt.Sprintf("%d.%s", uid, fileExtension) +} diff --git a/pkg/services/users.go b/pkg/services/users.go index 8159fea2..492610c5 100644 --- a/pkg/services/users.go +++ b/pkg/services/users.go @@ -3,7 +3,10 @@ package services import ( "bytes" "fmt" + "io" + "mime/multipart" "net/url" + "os" "time" "xorm.io/xorm" @@ -12,9 +15,11 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/datastore" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/locales" + "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/mail" "github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/settings" + "github.com/mayswind/ezbookkeeping/pkg/storage" "github.com/mayswind/ezbookkeeping/pkg/templates" "github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/uuid" @@ -28,6 +33,7 @@ type UserService struct { ServiceUsingConfig ServiceUsingMailer ServiceUsingUuid + ServiceUsingStorage } // Initialize a user service singleton instance @@ -45,6 +51,9 @@ var ( ServiceUsingUuid: ServiceUsingUuid{ container: uuid.Container, }, + ServiceUsingStorage: ServiceUsingStorage{ + container: storage.Container, + }, } ) @@ -126,6 +135,50 @@ func (s *UserService) GetUserByEmail(c *core.Context, email string) (*models.Use return user, nil } +// GetUserAvatar returns the user avatar image data and image file extension according to user uid +func (s *UserService) GetUserAvatar(c *core.Context, uid int64, fileExtension string) ([]byte, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + user := &models.User{} + has, err := s.UserDB().NewSession(c).ID(uid).Cols("uid", "deleted", "custom_avatar_type").Where("deleted=?", false).Get(user) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrUserNotFound + } + + if user.CustomAvatarType == "" { + return nil, errs.ErrUserAvatarNotSet + } + + if user.CustomAvatarType != fileExtension { + return nil, errs.ErrUserAvatarExtensionInvalid + } + + avatarFile, err := s.ReadAvatar(user.Uid, user.CustomAvatarType) + + if os.IsNotExist(err) { + return nil, errs.ErrUserAvatarNoExists + } + + if err != nil { + return nil, err + } + + defer avatarFile.Close() + + avatarData, err := io.ReadAll(avatarFile) + + if err != nil { + return nil, err + } + + return avatarData, nil +} + // CreateUser saves a new user model to database func (s *UserService) CreateUser(c *core.Context, user *models.User) error { exists, err := s.ExistsUsername(c, user.Username) @@ -294,16 +347,63 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL return keyProfileUpdated, emailSetToUnverified, nil } -// UpdateUserAvatar updated the custom avatar type of specified user -func (s *UserService) UpdateUserAvatar(c *core.Context, uid int64, customAvatarType string) error { +// UpdateUserAvatar updates the custom avatar type of specified user +func (s *UserService) UpdateUserAvatar(c *core.Context, uid int64, avatarFile multipart.File, fileExtension string, oldFileExtension string) error { if uid <= 0 { return errs.ErrUserIdInvalid } + defer avatarFile.Close() + + err := s.SaveAvatar(uid, avatarFile, fileExtension) + + if err != nil { + return err + } + now := time.Now().Unix() updateModel := &models.User{ - CustomAvatarType: customAvatarType, + CustomAvatarType: fileExtension, + UpdatedUnixTime: now, + } + + err = s.UserDB().DoTransaction(c, func(sess *xorm.Session) error { + _, err := sess.ID(uid).Cols("custom_avatar_type", "updated_unix_time").Where("deleted=?", false).Update(updateModel) + return err + }) + + if err != nil { + return err + } + + if fileExtension != oldFileExtension && oldFileExtension != "" { + err = s.DeleteAvatar(uid, oldFileExtension) + + if err != nil { + log.WarnfWithRequestId(c, "[users.UpdateUserAvatar] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", oldFileExtension, uid, err.Error()) + } + } + + return nil +} + +// RemoveUserAvatar removes the custom avatar type of specified user +func (s *UserService) RemoveUserAvatar(c *core.Context, uid int64, fileExtension string) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + err := s.DeleteAvatar(uid, fileExtension) + + if err != nil && !os.IsNotExist(err) { + return err + } + + now := time.Now().Unix() + + updateModel := &models.User{ + CustomAvatarType: "", UpdatedUnixTime: now, } diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index 5b0612f6..87e4fed6 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -10,6 +10,7 @@ import ( "gopkg.in/ini.v1" + "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/locales" ) @@ -74,12 +75,6 @@ const ( InMemoryDuplicateCheckerType string = "in_memory" ) -// User avatar provider types -const ( - InternalAvatarProvider string = "internal" - GravatarProvider string = "gravatar" -) - // Map provider types const ( OpenStreetMapProvider string = "openstreetmap" @@ -276,7 +271,7 @@ type Config struct { EnableUserForceVerifyEmail bool EnableUserForgetPassword bool ForgetPasswordRequireVerifyEmail bool - AvatarProvider string + AvatarProvider core.UserAvatarProviderType // Data EnableDataExport bool @@ -744,10 +739,10 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false) config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false) - if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == InternalAvatarProvider { - config.AvatarProvider = InternalAvatarProvider - } else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider { - config.AvatarProvider = GravatarProvider + if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) { + config.AvatarProvider = core.USER_AVATAR_PROVIDER_INTERNAL + } else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_GRAVATAR) { + config.AvatarProvider = core.USER_AVATAR_PROVIDER_GRAVATAR } else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" { config.AvatarProvider = "" } else { diff --git a/pkg/settings/setting_container.go b/pkg/settings/setting_container.go index be8a0388..c9d161d6 100644 --- a/pkg/settings/setting_container.go +++ b/pkg/settings/setting_container.go @@ -16,60 +16,3 @@ var ( func SetCurrentConfig(config *Config) { Container.Current = config } - -// GetAfterRegisterNotificationContent returns the notification content displayed each time users register -func (c *ConfigContainer) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string { - language := userLanguage - - if language == "" { - language = clientLanguage - } - - if !c.Current.AfterRegisterNotification.Enabled { - return "" - } - - if multiLanguageContent, exists := c.Current.AfterRegisterNotification.MultiLanguageContent[language]; exists { - return multiLanguageContent - } - - return c.Current.AfterRegisterNotification.DefaultContent -} - -// GetAfterLoginNotificationContent returns the notification content displayed each time users log in -func (c *ConfigContainer) GetAfterLoginNotificationContent(userLanguage string, clientLanguage string) string { - language := userLanguage - - if language == "" { - language = clientLanguage - } - - if !c.Current.AfterLoginNotification.Enabled { - return "" - } - - if multiLanguageContent, exists := c.Current.AfterLoginNotification.MultiLanguageContent[language]; exists { - return multiLanguageContent - } - - return c.Current.AfterLoginNotification.DefaultContent -} - -// GetAfterOpenNotificationContent returns the notification content displayed each time users open the app -func (c *ConfigContainer) GetAfterOpenNotificationContent(userLanguage string, clientLanguage string) string { - language := userLanguage - - if language == "" { - language = clientLanguage - } - - if !c.Current.AfterOpenNotification.Enabled { - return "" - } - - if multiLanguageContent, exists := c.Current.AfterOpenNotification.MultiLanguageContent[language]; exists { - return multiLanguageContent - } - - return c.Current.AfterOpenNotification.DefaultContent -} diff --git a/pkg/storage/storage_container.go b/pkg/storage/storage_container.go index bf31bddf..4396e415 100644 --- a/pkg/storage/storage_container.go +++ b/pkg/storage/storage_container.go @@ -1,8 +1,6 @@ package storage import ( - "fmt" - "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/settings" ) @@ -36,26 +34,22 @@ func InitializeStorageContainer(config *settings.Config) error { return errs.ErrInvalidStorageType } -// ExistsAvatar returns whether the user avatar exists from the current object storage -func (s *StorageContainer) ExistsAvatar(uid int64, fileExtension string) (bool, error) { - return s.AvatarCurrentStorage.Exists(s.getUserAvatarPath(uid, fileExtension)) +// ExistsAvatar returns whether the avatar file exists from the current avatar object storage +func (s *StorageContainer) ExistsAvatar(path string) (bool, error) { + return s.AvatarCurrentStorage.Exists(path) } -// ReadAvatar returns the user avatar from the current object storage -func (s *StorageContainer) ReadAvatar(uid int64, fileExtension string) (ObjectInStorage, error) { - return s.AvatarCurrentStorage.Read(s.getUserAvatarPath(uid, fileExtension)) +// ReadAvatar returns the avatar file from the current avatar object storage +func (s *StorageContainer) ReadAvatar(path string) (ObjectInStorage, error) { + return s.AvatarCurrentStorage.Read(path) } -// SaveAvatar returns whether save the user avatar into the current object storage successfully -func (s *StorageContainer) SaveAvatar(uid int64, object ObjectInStorage, fileExtension string) error { - return s.AvatarCurrentStorage.Save(s.getUserAvatarPath(uid, fileExtension), object) +// SaveAvatar returns whether save the avatar file into the current avatar object storage successfully +func (s *StorageContainer) SaveAvatar(path string, object ObjectInStorage) error { + return s.AvatarCurrentStorage.Save(path, object) } -// DeleteAvatar returns whether delete the user avatar from the current object storage successfully -func (s *StorageContainer) DeleteAvatar(uid int64, fileExtension string) error { - return s.AvatarCurrentStorage.Delete(s.getUserAvatarPath(uid, fileExtension)) -} - -func (s *StorageContainer) getUserAvatarPath(uid int64, fileExtension string) string { - return fmt.Sprintf("%d.%s", uid, fileExtension) +// DeleteAvatar returns whether delete the avatar file from the current avatar object storage successfully +func (s *StorageContainer) DeleteAvatar(path string) error { + return s.AvatarCurrentStorage.Delete(path) } diff --git a/src/locales/en.json b/src/locales/en.json index bc403793..f14aad81 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -967,6 +967,8 @@ "no user avatar": "There is no user avatar file", "user avatar is empty": "User avatar file is empty", "user avatar not exists": "User avatar does not exist", + "user avatar not set": "User avatar is not set", + "user avatar file extension invalid": "User avatar file extension is invalid", "unauthorized access": "Unauthorized access", "current token is invalid": "Current token is invalid", "current token is expired": "Current token is expired", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index d7880eee..3309ba9d 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -967,6 +967,8 @@ "no user avatar": "没有用户头像文件", "user avatar is empty": "用户头像文件为空", "user avatar not exists": "用户头像不存在", + "user avatar not set": "用户没有设置头像", + "user avatar file extension invalid": "用户头像文件扩展名无效", "unauthorized access": "未授权的登录", "current token is invalid": "当前认证令牌无效", "current token is expired": "当前认证令牌已过期",