diff --git a/cmd/initializer.go b/cmd/initializer.go index 940c3d91..70bdce52 100644 --- a/cmd/initializer.go +++ b/cmd/initializer.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" + "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/datastore" "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" @@ -107,6 +108,15 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) { return nil, err } + err = avatars.InitializeAvatarProvider(config) + + if err != nil { + if !isDisableBootLog { + log.BootErrorf(c, "[initializer.initializeSystem] initializes avatar provider failed, because %s", err.Error()) + } + return nil, err + } + err = mail.InitializeMailer(config) if err != nil { diff --git a/pkg/api/authorizations.go b/pkg/api/authorizations.go index 814825fc..100def5c 100644 --- a/pkg/api/authorizations.go +++ b/pkg/api/authorizations.go @@ -3,6 +3,7 @@ package api import ( "github.com/pquerna/otp/totp" + "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/log" @@ -14,6 +15,7 @@ import ( // AuthorizationsApi represents authorization api type AuthorizationsApi struct { ApiUsingConfig + ApiWithUserInfo users *services.UserService tokens *services.TokenService twoFactorAuthorizations *services.TwoFactorAuthorizationService @@ -25,6 +27,14 @@ var ( ApiUsingConfig: ApiUsingConfig{ container: settings.Container, }, + ApiWithUserInfo: ApiWithUserInfo{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingAvatarProvider: ApiUsingAvatarProvider{ + container: avatars.Container, + }, + }, users: services.Users, tokens: services.Tokens, twoFactorAuthorizations: services.TwoFactorAuthorizations, diff --git a/pkg/api/base.go b/pkg/api/base.go index 01763103..9c0c822f 100644 --- a/pkg/api/base.go +++ b/pkg/api/base.go @@ -1,6 +1,7 @@ package api import ( + "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" "github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/settings" @@ -16,11 +17,6 @@ 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 @@ -92,3 +88,24 @@ func (a *ApiUsingDuplicateChecker) GetSubmissionRemark(checkerType duplicatechec func (a *ApiUsingDuplicateChecker) SetSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string) { a.container.SetSubmissionRemark(checkerType, uid, identification, remark) } + +// ApiUsingAvatarProvider represents an api that need to use avatar provider +type ApiUsingAvatarProvider struct { + container *avatars.AvatarProviderContainer +} + +// GetAvatarUrl returns the avatar url by the current user avatar provider +func (a *ApiUsingAvatarProvider) GetAvatarUrl(user *models.User) string { + return a.container.GetAvatarUrl(user) +} + +// ApiWithUserInfo represents an api that can returns user info +type ApiWithUserInfo struct { + ApiUsingConfig + ApiUsingAvatarProvider +} + +// GetUserBasicInfo returns the view-object of user basic info according to the user model +func (a *ApiWithUserInfo) GetUserBasicInfo(user *models.User) *models.UserBasicInfo { + return user.ToUserBasicInfo(a.CurrentConfig().AvatarProvider, a.GetAvatarUrl(user)) +} diff --git a/pkg/api/tokens.go b/pkg/api/tokens.go index 2d27ba17..491c9b3b 100644 --- a/pkg/api/tokens.go +++ b/pkg/api/tokens.go @@ -4,6 +4,7 @@ import ( "sort" "time" + "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/log" @@ -16,6 +17,7 @@ import ( // TokensApi represents token api type TokensApi struct { ApiUsingConfig + ApiWithUserInfo tokens *services.TokenService users *services.UserService } @@ -26,6 +28,14 @@ var ( ApiUsingConfig: ApiUsingConfig{ container: settings.Container, }, + ApiWithUserInfo: ApiWithUserInfo{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingAvatarProvider: ApiUsingAvatarProvider{ + container: avatars.Container, + }, + }, tokens: services.Tokens, users: services.Users, } diff --git a/pkg/api/users.go b/pkg/api/users.go index 57676cf3..ce317754 100644 --- a/pkg/api/users.go +++ b/pkg/api/users.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin/binding" + "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/locales" @@ -20,6 +21,7 @@ import ( // UsersApi represents user api type UsersApi struct { ApiUsingConfig + ApiWithUserInfo users *services.UserService tokens *services.TokenService accounts *services.AccountService @@ -31,6 +33,14 @@ var ( ApiUsingConfig: ApiUsingConfig{ container: settings.Container, }, + ApiWithUserInfo: ApiWithUserInfo{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingAvatarProvider: ApiUsingAvatarProvider{ + container: avatars.Container, + }, + }, users: services.Users, tokens: services.Tokens, accounts: services.Accounts, @@ -721,5 +731,5 @@ func (a *UsersApi) UserGetAvatarHandler(c *core.WebContext) ([]byte, string, *er } func (a *UsersApi) getUserProfileResponse(user *models.User) *models.UserProfileResponse { - return user.ToUserProfileResponse(a.CurrentConfig().AvatarProvider, a.CurrentConfig().RootUrl) + return user.ToUserProfileResponse(a.GetUserBasicInfo(user)) } diff --git a/pkg/avatars/avatar_provider.go b/pkg/avatars/avatar_provider.go new file mode 100644 index 00000000..af3b6e23 --- /dev/null +++ b/pkg/avatars/avatar_provider.go @@ -0,0 +1,8 @@ +package avatars + +import "github.com/mayswind/ezbookkeeping/pkg/models" + +// AvatarProvider is user avatar provider interface +type AvatarProvider interface { + GetAvatarUrl(user *models.User) string +} diff --git a/pkg/avatars/avatar_provider_container.go b/pkg/avatars/avatar_provider_container.go new file mode 100644 index 00000000..8e8a77c6 --- /dev/null +++ b/pkg/avatars/avatar_provider_container.go @@ -0,0 +1,39 @@ +package avatars + +import ( + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/settings" +) + +// AvatarProviderContainer contains the current user avatar provider +type AvatarProviderContainer struct { + Current AvatarProvider +} + +// Initialize a user avatar provider container singleton instance +var ( + Container = &AvatarProviderContainer{} +) + +// InitializeAvatarProvider initializes the current user avatar provider according to the config +func InitializeAvatarProvider(config *settings.Config) error { + if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL { + Container.Current = NewInternalStorageAvatarProvider(config) + return nil + } else if config.AvatarProvider == core.USER_AVATAR_PROVIDER_GRAVATAR { + Container.Current = NewGravatarAvatarProvider() + return nil + } else if config.AvatarProvider == "" { + Container.Current = NewNullAvatarProvider() + return nil + } + + return errs.ErrInvalidAvatarProvider +} + +// GetAvatarUrl returns the avatar url by the current user avatar provider +func (p *AvatarProviderContainer) GetAvatarUrl(user *models.User) string { + return p.Current.GetAvatarUrl(user) +} diff --git a/pkg/avatars/gravatar_provider.go b/pkg/avatars/gravatar_provider.go new file mode 100644 index 00000000..97377962 --- /dev/null +++ b/pkg/avatars/gravatar_provider.go @@ -0,0 +1,31 @@ +package avatars + +import ( + "fmt" + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// Reference: https://en.gravatar.com/site/implement/hash/ +const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s" + +// GravatarAvatarProvider represents the gravatar avatar provider +type GravatarAvatarProvider struct { +} + +// NewGravatarAvatarProvider returns a new gravatar avatar provider +func NewGravatarAvatarProvider() *GravatarAvatarProvider { + return &GravatarAvatarProvider{} +} + +// GetAvatarUrl returns the gravatar url +func (p *GravatarAvatarProvider) GetAvatarUrl(user *models.User) string { + email := user.Email + email = strings.TrimSpace(email) + email = strings.ToLower(email) + emailMd5 := utils.MD5EncodeToString([]byte(email)) + + return fmt.Sprintf(gravatarUrlFormat, emailMd5) +} diff --git a/pkg/avatars/gravatar_provider_test.go b/pkg/avatars/gravatar_provider_test.go new file mode 100644 index 00000000..eac4ec29 --- /dev/null +++ b/pkg/avatars/gravatar_provider_test.go @@ -0,0 +1,20 @@ +package avatars + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +func TestGravatarAvatarProvider_GetGravatarUrl(t *testing.T) { + avatarProvider := NewGravatarAvatarProvider() + + expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346" + actualValue := avatarProvider.GetAvatarUrl(&models.User{ + Email: "MyEmailAddress@example.com", + }) + + assert.Equal(t, expectedValue, actualValue) +} diff --git a/pkg/avatars/internal_storage_provider.go b/pkg/avatars/internal_storage_provider.go new file mode 100644 index 00000000..d014a518 --- /dev/null +++ b/pkg/avatars/internal_storage_provider.go @@ -0,0 +1,31 @@ +package avatars + +import ( + "fmt" + + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/settings" +) + +const internalAvatarUrlFormat = "%savatar/%d.%s" + +// InternalStorageAvatarProvider represents the internal storage avatar provider +type InternalStorageAvatarProvider struct { + webRootUrl string +} + +// NewInternalStorageAvatarProvider returns a new internal storage avatar provider +func NewInternalStorageAvatarProvider(config *settings.Config) *InternalStorageAvatarProvider { + return &InternalStorageAvatarProvider{ + webRootUrl: config.RootUrl, + } +} + +// GetAvatarUrl returns the built-in avatar url +func (p *InternalStorageAvatarProvider) GetAvatarUrl(user *models.User) string { + if user.CustomAvatarType == "" { + return "" + } + + return fmt.Sprintf(internalAvatarUrlFormat, p.webRootUrl, user.Uid, user.CustomAvatarType) +} diff --git a/pkg/avatars/internal_storage_provider_test.go b/pkg/avatars/internal_storage_provider_test.go new file mode 100644 index 00000000..c405cdc3 --- /dev/null +++ b/pkg/avatars/internal_storage_provider_test.go @@ -0,0 +1,38 @@ +package avatars + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/settings" +) + +func TestInternalStorageAvatarProvider_GetAvatarUrl(t *testing.T) { + avatarProvider := NewInternalStorageAvatarProvider(&settings.Config{ + RootUrl: "https://foo.bar/", + }) + + expectedValue := "https://foo.bar/avatar/1234567890.jpg" + actualValue := avatarProvider.GetAvatarUrl(&models.User{ + Uid: 1234567890, + CustomAvatarType: "jpg", + }) + + assert.Equal(t, expectedValue, actualValue) +} + +func TestInternalStorageAvatarProvider_GetAvatarUrl_EmptyCustomAvatarType(t *testing.T) { + avatarProvider := NewInternalStorageAvatarProvider(&settings.Config{ + RootUrl: "https://foo.bar/", + }) + + expectedValue := "" + actualValue := avatarProvider.GetAvatarUrl(&models.User{ + Uid: 1234567890, + CustomAvatarType: "", + }) + + assert.Equal(t, expectedValue, actualValue) +} diff --git a/pkg/avatars/null_avatar_provider.go b/pkg/avatars/null_avatar_provider.go new file mode 100644 index 00000000..7d1c520d --- /dev/null +++ b/pkg/avatars/null_avatar_provider.go @@ -0,0 +1,19 @@ +package avatars + +import ( + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +// NullAvatarProvider represents the null avatar provider +type NullAvatarProvider struct { +} + +// NewNullAvatarProvider returns a new null avatar provider +func NewNullAvatarProvider() *NullAvatarProvider { + return &NullAvatarProvider{} +} + +// GetAvatarUrl returns an empty url +func (p *NullAvatarProvider) GetAvatarUrl(user *models.User) string { + return "" +} diff --git a/pkg/avatars/null_avatar_provider_test.go b/pkg/avatars/null_avatar_provider_test.go new file mode 100644 index 00000000..13aa5ac1 --- /dev/null +++ b/pkg/avatars/null_avatar_provider_test.go @@ -0,0 +1,20 @@ +package avatars + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +func TestNullAvatarProvider_GetGravatarUrl(t *testing.T) { + avatarProvider := NewNullAvatarProvider() + + expectedValue := "" + actualValue := avatarProvider.GetAvatarUrl(&models.User{ + Email: "MyEmailAddress@example.com", + }) + + assert.Equal(t, expectedValue, actualValue) +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 3abbe057..423657e6 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -252,12 +252,12 @@ func (u *User) CanEditTransactionByTransactionTime(transactionTime int64, utcOff } // ToUserBasicInfo returns a user basic view-object according to database model -func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, rootUrl string) *UserBasicInfo { +func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, avatarUrl string) *UserBasicInfo { return &UserBasicInfo{ Username: u.Username, Email: u.Email, Nickname: u.Nickname, - AvatarUrl: u.getAvatarUrl(avatarProvider, rootUrl), + AvatarUrl: avatarUrl, AvatarProvider: string(avatarProvider), DefaultAccountId: u.DefaultAccountId, TransactionEditScope: u.TransactionEditScope, @@ -279,19 +279,9 @@ func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, rootU } // ToUserProfileResponse returns a user profile view-object according to database model -func (u *User) ToUserProfileResponse(avatarProvider core.UserAvatarProviderType, rootUrl string) *UserProfileResponse { +func (u *User) ToUserProfileResponse(basicInfo *UserBasicInfo) *UserProfileResponse { return &UserProfileResponse{ - UserBasicInfo: u.ToUserBasicInfo(avatarProvider, rootUrl), + UserBasicInfo: basicInfo, LastLoginAt: u.LastLoginUnixTime, } } - -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) - } - - return "" -} diff --git a/pkg/utils/avatar.go b/pkg/utils/avatar.go deleted file mode 100644 index 781ba3fe..00000000 --- a/pkg/utils/avatar.go +++ /dev/null @@ -1,25 +0,0 @@ -package utils - -import ( - "fmt" - "strings" -) - -const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s" - -// GetInternalAvatarUrl returns the internal avatar url -func GetInternalAvatarUrl(uid int64, avatarFileExtension string, webRootUrl string) string { - if avatarFileExtension == "" { - return "" - } - - return fmt.Sprintf("%savatar/%d.%s", webRootUrl, uid, avatarFileExtension) -} - -// GetGravatarUrl returns the Gravatar url according to the specified user email address -func GetGravatarUrl(email string) string { - email = strings.TrimSpace(email) - email = strings.ToLower(email) - emailMd5 := MD5EncodeToString([]byte(email)) - return fmt.Sprintf(gravatarUrlFormat, emailMd5) -} diff --git a/pkg/utils/avatar_test.go b/pkg/utils/avatar_test.go deleted file mode 100644 index e8e3747a..00000000 --- a/pkg/utils/avatar_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetInternalAvatarUrl(t *testing.T) { - expectedValue := "https://demo.ezbookkeeping.mayswind.net/avatar/1234567890.jpg" - actualValue := GetInternalAvatarUrl(1234567890, "jpg", "https://demo.ezbookkeeping.mayswind.net/") - assert.Equal(t, expectedValue, actualValue) - - expectedValue = "" - actualValue = GetInternalAvatarUrl(1234567890, "", "https://demo.ezbookkeeping.mayswind.net/") - assert.Equal(t, expectedValue, actualValue) -} - -func TestGetGravatarUrl(t *testing.T) { - // Reference: https://en.gravatar.com/site/implement/hash/ - expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346" - actualValue := GetGravatarUrl("MyEmailAddress@example.com") - assert.Equal(t, expectedValue, actualValue) -}