supports local file system object storage and use it as the default avatar provider

This commit is contained in:
MaysWind
2024-07-27 23:29:18 +08:00
parent 731b6e8bad
commit 2e04affb00
26 changed files with 858 additions and 29 deletions
+2 -1
View File
@@ -27,7 +27,8 @@ COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
&& mkdir -p /ezbookkeeping/data && chown 1000:1000 /ezbookkeeping/data \
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log \
&& mkdir -p /ezbookkeeping/storage && chown 1000:1000 /ezbookkeeping/storage
WORKDIR /ezbookkeeping
COPY --from=be-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/ezbookkeeping /ezbookkeeping/ezbookkeeping
COPY --from=fe-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/dist /ezbookkeeping/public
+10
View File
@@ -12,6 +12,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/storage"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
@@ -80,6 +81,15 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
err = storage.InitializeStorageContainer(config)
if err != nil {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes object storage failed, because %s", err.Error())
}
return nil, err
}
err = uuid.InitializeUuidGenerator(config)
if err != nil {
+26
View File
@@ -145,6 +145,14 @@ func startWebServer(c *cli.Context) error {
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
if config.AvatarProvider == settings.InternalAvatarProvider {
avatarRoute := router.Group("/avatar")
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
}
}
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
if config.Mode == settings.MODE_DEVELOPMENT {
@@ -245,6 +253,11 @@ 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 {
apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler))
apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler))
}
if config.EnableUserVerifyEmail {
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
}
@@ -397,6 +410,19 @@ func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}
func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, contentType, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, contentType, "", result)
}
}
}
func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
+9 -1
View File
@@ -92,6 +92,13 @@ level = info
# For "file" mode only, log file path (relative or absolute path)
log_path = log/ezbookkeeping.log
[storage]
# Object storage type, supports "local_filesystem" currently
storage_type = local_filesystem
# For "local_filesystem" storage only, the storage root path (relative or absolute path)
local_filesystem_path = storage/
[uuid]
# Uuid generator type, supports "internal" currently
generator_type = internal
@@ -153,9 +160,10 @@ enable_forget_password = true
forget_password_require_email_verify = false
# User avatar provider, supports the following types:
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
# "gravatar": https://gravatar.com
# Leave blank if you want to disable user avatar
avatar_provider =
avatar_provider = internal
[data]
# Set to true to allow users to export their data
+183
View File
@@ -1,6 +1,8 @@
package api
import (
"io"
"os"
"strings"
"time"
@@ -13,6 +15,8 @@ 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"
)
@@ -494,6 +498,128 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
return resp, nil
}
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
func (a *UsersApi) UserUpdateAvatarHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
form, err := c.MultipartForm()
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get multi-part form data for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrParameterInvalid
}
avatars := form.File["avatar"]
if len(avatars) < 1 {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
return nil, errs.ErrNoUserAvatar
}
if avatars[0].Size < 1 {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
return nil, errs.ErrUserAvatarIsEmpty
}
fileExtension := utils.GetFileNameExtension(avatars[0].Filename)
if utils.GetImageContentType(fileExtension) == "" {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the file extension \"%s\" of user avatar in request is not supported for user \"uid:%d\"", fileExtension, user.Uid)
return nil, errs.ErrImageTypeNotSupported
}
avatarFile, err := avatars[0].Open()
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get avatar file from request for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
defer avatarFile.Close()
err = storage.Container.SaveAvatar(user.Uid, avatarFile, fileExtension)
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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if fileExtension != 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()
return userResp, nil
}
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
func (a *UsersApi) UserRemoveAvatarHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.CustomAvatarType == "" {
return nil, errs.ErrNothingWillBeUpdated
}
err = storage.Container.DeleteAvatar(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())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
user.CustomAvatarType = ""
userResp := user.ToUserProfileResponse()
return userResp, nil
}
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserVerifyEmail {
@@ -593,3 +719,60 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
return true, nil
}
// UserGetAvatarHandler returns user avatar data for current user
func (a *UsersApi) UserGetAvatarHandler(c *core.Context) ([]byte, string, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user, 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
}
fileName := c.Param("fileName")
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
if utils.Int64ToString(user.Uid) != fileBaseName {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, user.Uid)
return nil, "", errs.ErrUserIdInvalid
}
fileExtension := utils.GetFileNameExtension(fileName)
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 avatarData, utils.GetImageContentType(fileExtension), nil
}
+4 -1
View File
@@ -12,8 +12,11 @@ type MiddlewareHandlerFunc func(*Context)
// ApiHandlerFunc represents the api handler function
type ApiHandlerFunc func(*Context) (any, *errs.Error)
// DataHandlerFunc represents the handler function that returns byte array
// DataHandlerFunc represents the handler function that returns file data byte array and file name
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ImageHandlerFunc represents the handler function that returns image byte array and content type
type ImageHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ProxyHandlerFunc represents the reverse proxy handler function
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
+14 -12
View File
@@ -9,16 +9,18 @@ var (
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "failed to get local address")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid duplicate checker type")
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid token expired time")
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid token min refresh interval")
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid temporary token expired time")
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid email verify token expired time")
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid avatar provider")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid map provider")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid amap security verification method")
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid password reset token expired time")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid exchange rates data source")
ErrInvalidStorageType = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid storage type")
ErrInvalidLocalFileSystemStoragePath = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid local file system storage path")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid duplicate checker type")
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid token expired time")
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid token min refresh interval")
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid temporary token expired time")
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid email verify token expired time")
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid avatar provider")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid map provider")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid amap security verification method")
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 17, http.StatusInternalServerError, "invalid password reset token expired time")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 18, http.StatusInternalServerError, "invalid exchange rates data source")
)
+7 -6
View File
@@ -4,10 +4,11 @@ import "net/http"
// Error codes related to transaction categories
var (
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
)
+3
View File
@@ -31,4 +31,7 @@ var (
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual = NewNormalError(NormalSubcategoryUser, 23, http.StatusBadRequest, "decimal separator and digit grouping symbol cannot be equal")
ErrUserDefaultAccountIsHidden = NewNormalError(NormalSubcategoryUser, 24, http.StatusBadRequest, "user default account is hidden")
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")
)
+4 -1
View File
@@ -88,6 +88,7 @@ type User struct {
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
Password string `xorm:"VARCHAR(64) NOT NULL"`
Salt string `xorm:"VARCHAR(10) NOT NULL"`
CustomAvatarType string `xorm:"VARCHAR(10)"`
DefaultAccountId int64
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
@@ -313,7 +314,9 @@ func (u *User) getAvatarProvider() string {
func (u *User) getAvatarUrl() string {
avatarProvider := settings.Container.Current.AvatarProvider
if avatarProvider == settings.GravatarProvider {
if avatarProvider == settings.InternalAvatarProvider {
return utils.GetInternalAvatarUrl(u.Uid, u.CustomAvatarType, settings.Container.Current.RootUrl)
} else if avatarProvider == settings.GravatarProvider {
return utils.GetGravatarUrl(u.Email)
}
+19
View File
@@ -294,6 +294,25 @@ 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 {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.User{
CustomAvatarType: customAvatarType,
UpdatedUnixTime: now,
}
return 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
})
}
// UpdateUserLastLoginTime updates the last login time field
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
if uid <= 0 {
+39 -3
View File
@@ -57,6 +57,11 @@ const (
Sqlite3DbType string = "sqlite3"
)
// Object Storage types
const (
LocalFileSystemObjectStorageType string = "local_filesystem"
)
// Uuid generator types
const (
InternalUuidGeneratorType string = "internal"
@@ -69,7 +74,8 @@ const (
// User avatar provider types
const (
GravatarProvider string = "gravatar"
InternalAvatarProvider string = "internal"
GravatarProvider string = "gravatar"
)
// Map provider types
@@ -201,6 +207,10 @@ type Config struct {
LogLevel Level
FileLogPath string
// Storage
StorageType string
LocalFileSystemPath string
// Uuid
UuidGeneratorType string
UuidServerId uint8
@@ -311,6 +321,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadStorageConfiguration(config, cfgFile, "storage")
if err != nil {
return nil, err
}
err = loadUuidConfiguration(config, cfgFile, "uuid")
if err != nil {
@@ -523,6 +539,24 @@ func loadLogConfiguration(config *Config, configFile *ini.File, sectionName stri
return nil
}
func loadStorageConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "storage_type") == LocalFileSystemObjectStorageType {
config.StorageType = LocalFileSystemObjectStorageType
} else {
return errs.ErrInvalidStorageType
}
localFileSystemRootPath := getConfigItemStringValue(configFile, sectionName, "local_filesystem_path")
finalLocalFileSystemRootPath, err := getFinalPath(config.WorkingPath, localFileSystemRootPath)
config.LocalFileSystemPath = finalLocalFileSystemRootPath
if config.StorageType == LocalFileSystemObjectStorageType && err != nil {
return errs.ErrInvalidLocalFileSystemStoragePath
}
return nil
}
func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "generator_type") == InternalUuidGeneratorType {
config.UuidGeneratorType = InternalUuidGeneratorType
@@ -619,10 +653,12 @@ 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") == "" {
config.AvatarProvider = ""
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == InternalAvatarProvider {
config.AvatarProvider = InternalAvatarProvider
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
config.AvatarProvider = GravatarProvider
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
config.AvatarProvider = ""
} else {
return errs.ErrInvalidAvatarProvider
}
+68
View File
@@ -0,0 +1,68 @@
package storage
import (
"io"
"os"
"path/filepath"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// LocalFileSystemObjectStorage represents local file system object storage
type LocalFileSystemObjectStorage struct {
rootPath string
}
// NewLocalFileSystemObjectStorage returns a local file system object storage
func NewLocalFileSystemObjectStorage(config *settings.Config, pathPrefix string) (*LocalFileSystemObjectStorage, error) {
storage := &LocalFileSystemObjectStorage{
rootPath: filepath.Join(config.LocalFileSystemPath, pathPrefix),
}
if err := os.MkdirAll(storage.rootPath, os.ModePerm); err != nil {
return nil, err
}
return storage, nil
}
// Exists returns whether the file exists
func (s *LocalFileSystemObjectStorage) Exists(path string) (bool, error) {
return utils.IsExists(s.getFinalPath(path))
}
// Read returns the object instance according to specified the file path
func (s *LocalFileSystemObjectStorage) Read(path string) (ObjectInStorage, error) {
return os.Open(s.getFinalPath(path))
}
// Save returns whether save the object instance successfully
func (s *LocalFileSystemObjectStorage) Save(path string, object ObjectInStorage) error {
finalPath := s.getFinalPath(path)
if err := os.MkdirAll(filepath.Dir(finalPath), os.ModePerm); err != nil {
return err
}
targetFile, err := os.Create(finalPath)
if err != nil {
return err
}
defer targetFile.Close()
_, err = io.Copy(targetFile, object)
return err
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *LocalFileSystemObjectStorage) Delete(path string) error {
return os.Remove(s.getFinalPath(path))
}
func (s *LocalFileSystemObjectStorage) getFinalPath(path string) string {
return filepath.Join(s.rootPath, path)
}
+11
View File
@@ -0,0 +1,11 @@
package storage
import (
"io"
)
// ObjectInStorage represents the object instance in the storage
type ObjectInStorage interface {
io.ReadCloser
io.Seeker
}
+9
View File
@@ -0,0 +1,9 @@
package storage
// ObjectStorage represents an object storage to store file object
type ObjectStorage interface {
Exists(path string) (bool, error)
Read(path string) (ObjectInStorage, error)
Save(path string, object ObjectInStorage) error
Delete(path string) error
}
+56
View File
@@ -0,0 +1,56 @@
package storage
import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const avatarPathPrefix = "avatar"
// StorageContainer contains the current object storage
type StorageContainer struct {
AvatarCurrentStorage ObjectStorage
}
// Initialize a object storage container singleton instance
var (
Container = &StorageContainer{}
)
// InitializeStorageContainer initializes the current object storage according to the config
func InitializeStorageContainer(config *settings.Config) error {
if config.StorageType == settings.LocalFileSystemObjectStorageType {
storage, err := NewLocalFileSystemObjectStorage(config, avatarPathPrefix)
Container.AvatarCurrentStorage = storage
return err
}
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))
}
// 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))
}
// 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)
}
// 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)
}
+9
View File
@@ -7,6 +7,15 @@ import (
const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s"
// GetInternalAvatarUrl returns the internal avatar url
func GetInternalAvatarUrl(uid int64, avatarFileExtesion string, webRootUrl string) string {
if avatarFileExtesion == "" {
return ""
}
return fmt.Sprintf("%savatar/%d.%s", webRootUrl, uid, avatarFileExtesion)
}
// GetGravatarUrl returns the Gravatar url according to the specified user email address
func GetGravatarUrl(email string) string {
email = strings.TrimSpace(email)
+10
View File
@@ -6,6 +6,16 @@ import (
"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"
+43
View File
@@ -3,9 +3,29 @@ package utils
import (
"io"
"os"
"path/filepath"
"strings"
)
var imageFileExtensionContentTypeMap = map[string]string{
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
}
// GetImageContentType returns the content type of specified image file extension or returns empty when the file extension is not image or not supported
func GetImageContentType(fileExtension string) string {
contentType, exists := imageFileExtensionContentTypeMap[fileExtension]
if !exists {
return ""
}
return contentType
}
// ListFileNamesWithPrefixAndSuffix returns file name list which has specified prefix and suffix
func ListFileNamesWithPrefixAndSuffix(path string, prefix string, suffix string) []string {
dir, err := os.Open(path)
@@ -69,6 +89,29 @@ func WriteFile(path string, data []byte) error {
return err
}
// GetFileNameWithoutExtension returns the file name without extension
func GetFileNameWithoutExtension(path string) string {
fileName := filepath.Base(path)
extension := filepath.Ext(fileName)
if len(extension) < 1 {
return fileName
}
return fileName[0 : len(fileName)-len(extension)]
}
// GetFileNameExtension returns the file extension without dot
func GetFileNameExtension(path string) string {
extension := filepath.Ext(path)
if len(extension) < 1 || extension[0] != '.' {
return extension
}
return extension[1:]
}
// IdentReader returns the original io reader
func IdentReader(encoding string, input io.Reader) (io.Reader, error) {
return input, nil
+143
View File
@@ -0,0 +1,143 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetImageContentType(t *testing.T) {
fileName := "gif"
expectedContentType := "image/gif"
actualContentType := GetImageContentType(fileName)
assert.Equal(t, expectedContentType, actualContentType)
fileName = "bmp"
expectedContentType = ""
actualContentType = GetImageContentType(fileName)
assert.Equal(t, expectedContentType, actualContentType)
}
func TestGetFileNameWithoutExtension(t *testing.T) {
fileName := "name.ext"
expectedName := "name"
actualName := GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "C:\\name.ext"
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "/root/name.ext"
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "name"
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "C:\\name"
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "/root/name"
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "name."
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "C:\\name."
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "/root/name."
expectedName = "name"
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = ".ext"
expectedName = ""
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "C:\\.ext"
expectedName = ""
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
fileName = "/root/.ext"
expectedName = ""
actualName = GetFileNameWithoutExtension(fileName)
assert.Equal(t, expectedName, actualName)
}
func TestGetFileNameExtension(t *testing.T) {
fileName := "name.ext"
expectedExt := "ext"
actualExt := GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "C:\\name.ext"
expectedExt = "ext"
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "/root/name.ext"
expectedExt = "ext"
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "name"
expectedExt = ""
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "C:\\name"
expectedExt = ""
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "/root/name"
expectedExt = ""
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "name."
expectedExt = ""
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "C:\\name."
expectedExt = ""
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "/root/name."
expectedExt = ""
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = ".ext"
expectedExt = "ext"
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "C:\\.ext"
expectedExt = "ext"
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
fileName = "/root/.ext"
expectedExt = "ext"
actualExt = GetFileNameExtension(fileName)
assert.Equal(t, expectedExt, actualExt)
}
+5
View File
@@ -0,0 +1,5 @@
const supportedImageExtensions = '.jpg,.jpeg,.png,.gif,.webp';
export default {
supportedImageExtensions: supportedImageExtensions
}
+32
View File
@@ -2,12 +2,14 @@ import axios from 'axios';
import apiConstants from '@/consts/api.js';
import userState from './userstate.js';
import { isBoolean } from './common.js';
import {
getGoogleMapAPIKey,
getBaiduMapAK,
getAmapApplicationKey
} from './server_settings.js';
import { getTimezoneOffsetMinutes } from './datetime.js';
import { generateRandomUUID } from './misc.js';
let needBlockRequest = false;
let blockedRequests = [];
@@ -192,6 +194,14 @@ export default {
incomeAmountColor
});
},
updateAvatar: ({ avatarFile }) => {
return axios.postForm('v1/users/avatar/update.json', {
avatar: avatarFile
});
},
removeAvatar: () => {
return axios.post('v1/users/avatar/remove.json');
},
resendVerifyEmailByLoginedUser: () => {
return axios.post('v1/users/verify_email/resend.json');
},
@@ -552,5 +562,27 @@ export default {
},
generateAmapApiInternalProxyUrl: () => {
return `${window.location.origin}${apiConstants.baseAmapApiProxyUrlPath}`;
},
getInternalAvatarUrlWithToken(avatarUrl, disableBrowserCache) {
if (!avatarUrl) {
return avatarUrl;
}
const params = [];
params.push('token=' + userState.getToken());
if (disableBrowserCache) {
if (isBoolean(disableBrowserCache)) {
params.push('_nocache=' + generateRandomUUID());
} else {
params.push('_nocache=' + disableBrowserCache);
}
}
if (avatarUrl.indexOf('?') >= 0) {
return avatarUrl + '&' + params.join('&');
} else {
return avatarUrl + '?' + params.join('&');
}
}
};
+11
View File
@@ -592,6 +592,7 @@ export default {
'not implemented': 'Not implemented',
'system is busy': 'System is busy',
'not supported': 'Not supported',
'image type not supported': 'Image type is not supported',
'database operation failed': 'Database operation failed',
'SMTP server is not enabled': 'SMTP server is not enabled',
'incomplete or incorrect submission': 'Incomplete or incorrect submission',
@@ -624,6 +625,9 @@ export default {
'email validation not allowed': 'Email validation is not allowed',
'decimal separator and digit grouping symbol cannot be equal': 'Decimal separator and digit grouping symbol cannot be equal',
'user default account is hidden': 'Cannot set hidden account as default account',
'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',
'unauthorized access': 'Unauthorized access',
'current token is invalid': 'Current token is invalid',
'current token is expired': 'Current token is expired',
@@ -1159,6 +1163,8 @@ export default {
'Basic Settings': 'Basic Settings',
'Security Settings': 'Security Settings',
'Two-Factor Authentication Settings': 'Two-Factor Authentication Settings',
'Update Avatar': 'Update Avatar',
'Remove Avatar': 'Remove Avatar',
'(Verified)': '(Verified)',
'(Not Verified)': '(Not Verified)',
'Email address is verified': 'Email address is verified',
@@ -1170,6 +1176,11 @@ export default {
'Please enter your current password when modifying your password': 'Please enter your current password when modifying your password',
'Nothing has been modified': 'Nothing has been modified',
'Your profile has been successfully updated': 'Your profile has been successfully updated',
'Unable to update user avatar': 'Unable to update user avatar',
'Your avatar has been successfully updated': 'Your avatar has been successfully updated',
'Are you sure you want to remove avatar?': 'Are you sure you want to remove avatar?',
'Unable to remove user avatar': 'Unable to remove user avatar',
'Your avatar has been successfully removed': 'Your avatar has been successfully removed',
'Unable to update user profile': 'Unable to update user profile',
'After changing the password, other devices will be logged out. Please use the new password to log in on other devices.': 'After changing the password, other devices will be logged out. Please use the new password to log in on other devices.',
'Data Management': 'Data Management',
+11
View File
@@ -592,6 +592,7 @@ export default {
'not implemented': '未实现',
'system is busy': '系统繁忙',
'not supported': '不支持',
'image type not supported': '图片类型不支持',
'database operation failed': '数据库操作失败',
'SMTP server is not enabled': 'SMTP 服务器没有启用',
'incomplete or incorrect submission': '提交不完整或不正确',
@@ -624,6 +625,9 @@ export default {
'email validation not allowed': '不允许邮箱验证',
'decimal separator and digit grouping symbol cannot be equal': '小数点和数字分组符号不能相同',
'user default account is hidden': '不能把隐藏账户设置为默认账户',
'no user avatar': '没有用户头像文件',
'user avatar is empty': '用户头像文件为空',
'user avatar not exists': '用户头像不存在',
'unauthorized access': '未授权的登录',
'current token is invalid': '当前认证令牌无效',
'current token is expired': '当前认证令牌已过期',
@@ -1159,6 +1163,8 @@ export default {
'Basic Settings': '基本设置',
'Security Settings': '安全设置',
'Two-Factor Authentication Settings': '两步验证设置',
'Update Avatar': '更新头像',
'Remove Avatar': '删除头像',
'(Verified)': '(已验证)',
'(Not Verified)': '(未验证)',
'Email address is verified': '邮箱地址已验证',
@@ -1170,6 +1176,11 @@ export default {
'Please enter your current password when modifying your password': '修改密码时请输入您的当前密码',
'Nothing has been modified': '没有修改的项目',
'Your profile has been successfully updated': '您的用户信息更新成功',
'Unable to update user avatar': '无法更新用户头像',
'Your avatar has been successfully updated': '您的头像更新成功',
'Are you sure you want to remove avatar?': '您确定要删除头像?',
'Unable to remove user avatar': '无法删除用户头像',
'Your avatar has been successfully removed': '您的用户头像删除成功',
'Unable to update user profile': '无法更新用户信息',
'After changing the password, other devices will be logged out. Please use the new password to log in on other devices.': '密码修改后,其他设备将会退出登录,请使用新密码在其他设备上重新登录。',
'Data Management': '数据管理',
+56 -1
View File
@@ -18,7 +18,7 @@ export const useUserStore = defineStore('user', {
},
currentUserAvatar(state) {
const userInfo = state.currentUserBasicInfo || {};
return userInfo.avatar || null;
return state.getUserAvatarUrl(userInfo, false);
},
currentUserDefaultAccountId(state) {
const userInfo = state.currentUserBasicInfo || {};
@@ -126,6 +126,54 @@ export const useUserStore = defineStore('user', {
});
});
},
updateUserAvatar({ avatarFile }) {
return new Promise((resolve, reject) => {
services.updateAvatar({ avatarFile }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to update user avatar' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('failed to update user avatar', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to update user avatar' });
} else {
reject(error);
}
});
});
},
removeUserAvatar() {
return new Promise((resolve, reject) => {
services.removeAvatar().then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to remove user avatar' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('failed to remove user avatar', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to remove user avatar' });
} else {
reject(error);
}
});
});
},
getUserDataStatistics() {
return new Promise((resolve, reject) => {
services.getUserDataStatistics().then(response => {
@@ -178,5 +226,12 @@ export const useUserStore = defineStore('user', {
});
});
},
getUserAvatarUrl(userInfo, disableBrowserCache) {
if (!userInfo || !userInfo.avatar) {
return null;
}
return services.getInternalAvatarUrlWithToken(userInfo.avatar, disableBrowserCache);
}
}
});
@@ -8,15 +8,21 @@
</template>
<v-card-text class="d-flex">
<v-avatar rounded="lg" color="primary" variant="tonal" size="100" class="me-4">
<v-img :src="oldProfile.avatar" v-if="oldProfile.avatar">
<v-avatar rounded="lg" color="primary" variant="tonal" size="100" class="me-4" :class="{ 'cursor-pointer': oldProfile.avatarProvider === 'internal' }">
<v-img :src="currentUserAvatar" v-if="currentUserAvatar">
<template #placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-icon size="48" :icon="icons.user"/>
</div>
</template>
</v-img>
<v-icon size="48" :icon="icons.user" v-else-if="!oldProfile.avatar"/>
<v-icon size="48" :icon="icons.user" v-else-if="!currentUserAvatar"/>
<v-menu activator="parent" width="200" location="bottom" offset="14px" v-if="oldProfile.avatarProvider === 'internal'">
<v-list>
<v-list-item :disabled="saving" :title="$t('Update Avatar')" @click="showOpenAvatarDialog"></v-list-item>
<v-list-item :disabled="!currentUserAvatar || saving" :title="$t('Remove Avatar')" @click="removeAvatar"></v-list-item>
</v-list>
</v-menu>
</v-avatar>
<div class="d-flex flex-column justify-center gap-3">
<div class="d-flex text-body-1">
@@ -301,6 +307,7 @@
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
<input ref="avatarInput" type="file" style="display: none" :accept="supportedImageExtensions" @change="updateAvatar($event)" />
</template>
<script>
@@ -312,7 +319,9 @@ import { useAccountsStore } from '@/stores/account.js';
import { useOverviewStore } from '@/stores/overview.js';
import datetimeConstants from '@/consts/datetime.js';
import fileConstants from '@/consts/file.js';
import { getNameByKeyValue } from '@/lib/common.js';
import { generateRandomUUID } from '@/lib/misc.js';
import { getCategorizedAccounts } from '@/lib/account.js';
import { isUserVerifyEmailEnabled } from '@/lib/server_settings.js';
import { setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
@@ -366,6 +375,7 @@ export default {
expenseAmountColor: 0,
incomeAmountColor: 0
},
avatarNoCacheId: '',
emailVerified: false,
loading: true,
resending: false,
@@ -428,6 +438,12 @@ export default {
allTransactionEditScopeTypes() {
return this.$locale.getAllTransactionEditScopeTypes();
},
supportedImageExtensions() {
return fileConstants.supportedImageExtensions;
},
currentUserAvatar() {
return this.userStore.getUserAvatarUrl(this.oldProfile, this.avatarNoCacheId);
},
isUserVerifyEmailEnabled() {
return isUserVerifyEmailEnabled();
},
@@ -558,6 +574,61 @@ export default {
}
});
},
showOpenAvatarDialog() {
this.$refs.avatarInput.click();
},
updateAvatar(event) {
if (!event || !event.target || !event.target.files || !event.target.files.length) {
return;
}
const self = this;
const avatarFile = event.target.files[0];
event.target.value = null;
self.saving = true;
self.userStore.updateUserAvatar({ avatarFile }).then(response => {
self.saving = false;
if (response) {
self.avatarNoCacheId = generateRandomUUID();
self.setCurrentUserProfile(response);
}
self.$refs.snackbar.showMessage('Your avatar has been successfully updated');
}).catch(error => {
self.saving = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
removeAvatar() {
const self = this;
self.$refs.confirmDialog.open('Are you sure you want to remove avatar?').then(() => {
self.saving = true;
self.userStore.removeUserAvatar().then(response => {
self.saving = false;
if (response) {
self.setCurrentUserProfile(response);
}
self.$refs.snackbar.showMessage('Your profile has been successfully updated');
}).catch(error => {
self.saving = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
});
},
reset() {
this.setCurrentUserProfile(this.oldProfile);
},