supports local file system object storage and use it as the default avatar provider
This commit is contained in:
+2
-1
@@ -27,7 +27,8 @@ COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
|
|||||||
RUN chmod +x /docker-entrypoint.sh
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
|
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
|
||||||
&& mkdir -p /ezbookkeeping/data && chown 1000:1000 /ezbookkeeping/data \
|
&& 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
|
WORKDIR /ezbookkeeping
|
||||||
COPY --from=be-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/ezbookkeeping /ezbookkeeping/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
|
COPY --from=fe-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/dist /ezbookkeeping/public
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||||
)
|
)
|
||||||
@@ -80,6 +81,15 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
return nil, err
|
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)
|
err = uuid.InitializeUuidGenerator(config)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ func startWebServer(c *cli.Context) error {
|
|||||||
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
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))
|
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
||||||
|
|
||||||
if config.Mode == settings.MODE_DEVELOPMENT {
|
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.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
|
||||||
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
|
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 {
|
if config.EnableUserVerifyEmail {
|
||||||
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
|
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 {
|
func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
|
||||||
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
|
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapContext(ginCtx)
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ level = info
|
|||||||
# For "file" mode only, log file path (relative or absolute path)
|
# For "file" mode only, log file path (relative or absolute path)
|
||||||
log_path = log/ezbookkeeping.log
|
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]
|
||||||
# Uuid generator type, supports "internal" currently
|
# Uuid generator type, supports "internal" currently
|
||||||
generator_type = internal
|
generator_type = internal
|
||||||
@@ -153,9 +160,10 @@ enable_forget_password = true
|
|||||||
forget_password_require_email_verify = false
|
forget_password_require_email_verify = false
|
||||||
|
|
||||||
# User avatar provider, supports the following types:
|
# 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
|
# "gravatar": https://gravatar.com
|
||||||
# Leave blank if you want to disable user avatar
|
# Leave blank if you want to disable user avatar
|
||||||
avatar_provider =
|
avatar_provider = internal
|
||||||
|
|
||||||
[data]
|
[data]
|
||||||
# Set to true to allow users to export their data
|
# Set to true to allow users to export their data
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -494,6 +498,128 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
return resp, nil
|
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
|
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
|
||||||
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
|
||||||
if !settings.Container.Current.EnableUserVerifyEmail {
|
if !settings.Container.Current.EnableUserVerifyEmail {
|
||||||
@@ -593,3 +719,60 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
|
|||||||
|
|
||||||
return true, nil
|
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
@@ -12,8 +12,11 @@ type MiddlewareHandlerFunc func(*Context)
|
|||||||
// ApiHandlerFunc represents the api handler function
|
// ApiHandlerFunc represents the api handler function
|
||||||
type ApiHandlerFunc func(*Context) (any, *errs.Error)
|
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)
|
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
|
// ProxyHandlerFunc represents the reverse proxy handler function
|
||||||
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
|
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
|
||||||
|
|||||||
+14
-12
@@ -9,16 +9,18 @@ var (
|
|||||||
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
|
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
|
||||||
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
|
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
|
||||||
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "failed to get local address")
|
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "failed to get local address")
|
||||||
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid uuid mode")
|
ErrInvalidStorageType = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid storage type")
|
||||||
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid duplicate checker type")
|
ErrInvalidLocalFileSystemStoragePath = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid local file system storage path")
|
||||||
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
|
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid uuid mode")
|
||||||
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid token expired time")
|
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid duplicate checker type")
|
||||||
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid token min refresh interval")
|
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
|
||||||
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid temporary token expired time")
|
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid token expired time")
|
||||||
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid email verify token expired time")
|
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid token min refresh interval")
|
||||||
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid avatar provider")
|
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid temporary token expired time")
|
||||||
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid map provider")
|
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid email verify token expired time")
|
||||||
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid amap security verification method")
|
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid avatar provider")
|
||||||
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid password reset token expired time")
|
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid map provider")
|
||||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid exchange rates data source")
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ var (
|
|||||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
||||||
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
||||||
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
||||||
|
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,4 +31,7 @@ var (
|
|||||||
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
|
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")
|
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")
|
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
@@ -88,6 +88,7 @@ type User struct {
|
|||||||
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
|
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
|
||||||
Password string `xorm:"VARCHAR(64) NOT NULL"`
|
Password string `xorm:"VARCHAR(64) NOT NULL"`
|
||||||
Salt string `xorm:"VARCHAR(10) NOT NULL"`
|
Salt string `xorm:"VARCHAR(10) NOT NULL"`
|
||||||
|
CustomAvatarType string `xorm:"VARCHAR(10)"`
|
||||||
DefaultAccountId int64
|
DefaultAccountId int64
|
||||||
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
|
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
|
||||||
Language string `xorm:"VARCHAR(10)"`
|
Language string `xorm:"VARCHAR(10)"`
|
||||||
@@ -313,7 +314,9 @@ func (u *User) getAvatarProvider() string {
|
|||||||
func (u *User) getAvatarUrl() string {
|
func (u *User) getAvatarUrl() string {
|
||||||
avatarProvider := settings.Container.Current.AvatarProvider
|
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)
|
return utils.GetGravatarUrl(u.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,25 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
|
|||||||
return keyProfileUpdated, emailSetToUnverified, nil
|
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
|
// UpdateUserLastLoginTime updates the last login time field
|
||||||
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
|
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
|
|||||||
+38
-2
@@ -57,6 +57,11 @@ const (
|
|||||||
Sqlite3DbType string = "sqlite3"
|
Sqlite3DbType string = "sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Object Storage types
|
||||||
|
const (
|
||||||
|
LocalFileSystemObjectStorageType string = "local_filesystem"
|
||||||
|
)
|
||||||
|
|
||||||
// Uuid generator types
|
// Uuid generator types
|
||||||
const (
|
const (
|
||||||
InternalUuidGeneratorType string = "internal"
|
InternalUuidGeneratorType string = "internal"
|
||||||
@@ -69,6 +74,7 @@ const (
|
|||||||
|
|
||||||
// User avatar provider types
|
// User avatar provider types
|
||||||
const (
|
const (
|
||||||
|
InternalAvatarProvider string = "internal"
|
||||||
GravatarProvider string = "gravatar"
|
GravatarProvider string = "gravatar"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -201,6 +207,10 @@ type Config struct {
|
|||||||
LogLevel Level
|
LogLevel Level
|
||||||
FileLogPath string
|
FileLogPath string
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
StorageType string
|
||||||
|
LocalFileSystemPath string
|
||||||
|
|
||||||
// Uuid
|
// Uuid
|
||||||
UuidGeneratorType string
|
UuidGeneratorType string
|
||||||
UuidServerId uint8
|
UuidServerId uint8
|
||||||
@@ -311,6 +321,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = loadStorageConfiguration(config, cfgFile, "storage")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
err = loadUuidConfiguration(config, cfgFile, "uuid")
|
err = loadUuidConfiguration(config, cfgFile, "uuid")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -523,6 +539,24 @@ func loadLogConfiguration(config *Config, configFile *ini.File, sectionName stri
|
|||||||
return nil
|
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 {
|
func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||||
if getConfigItemStringValue(configFile, sectionName, "generator_type") == InternalUuidGeneratorType {
|
if getConfigItemStringValue(configFile, sectionName, "generator_type") == InternalUuidGeneratorType {
|
||||||
config.UuidGeneratorType = 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.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
||||||
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
|
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
|
||||||
|
|
||||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
|
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == InternalAvatarProvider {
|
||||||
config.AvatarProvider = ""
|
config.AvatarProvider = InternalAvatarProvider
|
||||||
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
|
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
|
||||||
config.AvatarProvider = GravatarProvider
|
config.AvatarProvider = GravatarProvider
|
||||||
|
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
|
||||||
|
config.AvatarProvider = ""
|
||||||
} else {
|
} else {
|
||||||
return errs.ErrInvalidAvatarProvider
|
return errs.ErrInvalidAvatarProvider
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectInStorage represents the object instance in the storage
|
||||||
|
type ObjectInStorage interface {
|
||||||
|
io.ReadCloser
|
||||||
|
io.Seeker
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -7,6 +7,15 @@ import (
|
|||||||
|
|
||||||
const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s"
|
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
|
// GetGravatarUrl returns the Gravatar url according to the specified user email address
|
||||||
func GetGravatarUrl(email string) string {
|
func GetGravatarUrl(email string) string {
|
||||||
email = strings.TrimSpace(email)
|
email = strings.TrimSpace(email)
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestGetGravatarUrl(t *testing.T) {
|
||||||
// Reference: https://en.gravatar.com/site/implement/hash/
|
// Reference: https://en.gravatar.com/site/implement/hash/
|
||||||
expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346"
|
expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346"
|
||||||
|
|||||||
@@ -3,9 +3,29 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"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
|
// ListFileNamesWithPrefixAndSuffix returns file name list which has specified prefix and suffix
|
||||||
func ListFileNamesWithPrefixAndSuffix(path string, prefix string, suffix string) []string {
|
func ListFileNamesWithPrefixAndSuffix(path string, prefix string, suffix string) []string {
|
||||||
dir, err := os.Open(path)
|
dir, err := os.Open(path)
|
||||||
@@ -69,6 +89,29 @@ func WriteFile(path string, data []byte) error {
|
|||||||
return err
|
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
|
// IdentReader returns the original io reader
|
||||||
func IdentReader(encoding string, input io.Reader) (io.Reader, error) {
|
func IdentReader(encoding string, input io.Reader) (io.Reader, error) {
|
||||||
return input, nil
|
return input, nil
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const supportedImageExtensions = '.jpg,.jpeg,.png,.gif,.webp';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
supportedImageExtensions: supportedImageExtensions
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ import axios from 'axios';
|
|||||||
|
|
||||||
import apiConstants from '@/consts/api.js';
|
import apiConstants from '@/consts/api.js';
|
||||||
import userState from './userstate.js';
|
import userState from './userstate.js';
|
||||||
|
import { isBoolean } from './common.js';
|
||||||
import {
|
import {
|
||||||
getGoogleMapAPIKey,
|
getGoogleMapAPIKey,
|
||||||
getBaiduMapAK,
|
getBaiduMapAK,
|
||||||
getAmapApplicationKey
|
getAmapApplicationKey
|
||||||
} from './server_settings.js';
|
} from './server_settings.js';
|
||||||
import { getTimezoneOffsetMinutes } from './datetime.js';
|
import { getTimezoneOffsetMinutes } from './datetime.js';
|
||||||
|
import { generateRandomUUID } from './misc.js';
|
||||||
|
|
||||||
let needBlockRequest = false;
|
let needBlockRequest = false;
|
||||||
let blockedRequests = [];
|
let blockedRequests = [];
|
||||||
@@ -192,6 +194,14 @@ export default {
|
|||||||
incomeAmountColor
|
incomeAmountColor
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateAvatar: ({ avatarFile }) => {
|
||||||
|
return axios.postForm('v1/users/avatar/update.json', {
|
||||||
|
avatar: avatarFile
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeAvatar: () => {
|
||||||
|
return axios.post('v1/users/avatar/remove.json');
|
||||||
|
},
|
||||||
resendVerifyEmailByLoginedUser: () => {
|
resendVerifyEmailByLoginedUser: () => {
|
||||||
return axios.post('v1/users/verify_email/resend.json');
|
return axios.post('v1/users/verify_email/resend.json');
|
||||||
},
|
},
|
||||||
@@ -552,5 +562,27 @@ export default {
|
|||||||
},
|
},
|
||||||
generateAmapApiInternalProxyUrl: () => {
|
generateAmapApiInternalProxyUrl: () => {
|
||||||
return `${window.location.origin}${apiConstants.baseAmapApiProxyUrlPath}`;
|
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('&');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -592,6 +592,7 @@ export default {
|
|||||||
'not implemented': 'Not implemented',
|
'not implemented': 'Not implemented',
|
||||||
'system is busy': 'System is busy',
|
'system is busy': 'System is busy',
|
||||||
'not supported': 'Not supported',
|
'not supported': 'Not supported',
|
||||||
|
'image type not supported': 'Image type is not supported',
|
||||||
'database operation failed': 'Database operation failed',
|
'database operation failed': 'Database operation failed',
|
||||||
'SMTP server is not enabled': 'SMTP server is not enabled',
|
'SMTP server is not enabled': 'SMTP server is not enabled',
|
||||||
'incomplete or incorrect submission': 'Incomplete or incorrect submission',
|
'incomplete or incorrect submission': 'Incomplete or incorrect submission',
|
||||||
@@ -624,6 +625,9 @@ export default {
|
|||||||
'email validation not allowed': 'Email validation is not allowed',
|
'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',
|
'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',
|
'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',
|
'unauthorized access': 'Unauthorized access',
|
||||||
'current token is invalid': 'Current token is invalid',
|
'current token is invalid': 'Current token is invalid',
|
||||||
'current token is expired': 'Current token is expired',
|
'current token is expired': 'Current token is expired',
|
||||||
@@ -1159,6 +1163,8 @@ export default {
|
|||||||
'Basic Settings': 'Basic Settings',
|
'Basic Settings': 'Basic Settings',
|
||||||
'Security Settings': 'Security Settings',
|
'Security Settings': 'Security Settings',
|
||||||
'Two-Factor Authentication Settings': 'Two-Factor Authentication Settings',
|
'Two-Factor Authentication Settings': 'Two-Factor Authentication Settings',
|
||||||
|
'Update Avatar': 'Update Avatar',
|
||||||
|
'Remove Avatar': 'Remove Avatar',
|
||||||
'(Verified)': '(Verified)',
|
'(Verified)': '(Verified)',
|
||||||
'(Not Verified)': '(Not Verified)',
|
'(Not Verified)': '(Not Verified)',
|
||||||
'Email address is verified': 'Email address is 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',
|
'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',
|
'Nothing has been modified': 'Nothing has been modified',
|
||||||
'Your profile has been successfully updated': 'Your profile has been successfully updated',
|
'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',
|
'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.',
|
'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',
|
'Data Management': 'Data Management',
|
||||||
|
|||||||
@@ -592,6 +592,7 @@ export default {
|
|||||||
'not implemented': '未实现',
|
'not implemented': '未实现',
|
||||||
'system is busy': '系统繁忙',
|
'system is busy': '系统繁忙',
|
||||||
'not supported': '不支持',
|
'not supported': '不支持',
|
||||||
|
'image type not supported': '图片类型不支持',
|
||||||
'database operation failed': '数据库操作失败',
|
'database operation failed': '数据库操作失败',
|
||||||
'SMTP server is not enabled': 'SMTP 服务器没有启用',
|
'SMTP server is not enabled': 'SMTP 服务器没有启用',
|
||||||
'incomplete or incorrect submission': '提交不完整或不正确',
|
'incomplete or incorrect submission': '提交不完整或不正确',
|
||||||
@@ -624,6 +625,9 @@ export default {
|
|||||||
'email validation not allowed': '不允许邮箱验证',
|
'email validation 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': '不能把隐藏账户设置为默认账户',
|
'user default account is hidden': '不能把隐藏账户设置为默认账户',
|
||||||
|
'no user avatar': '没有用户头像文件',
|
||||||
|
'user avatar is empty': '用户头像文件为空',
|
||||||
|
'user avatar not exists': '用户头像不存在',
|
||||||
'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': '更新头像',
|
||||||
|
'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': '无法更新用户头像',
|
||||||
|
'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': '无法更新用户信息',
|
'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': '数据管理',
|
||||||
|
|||||||
+56
-1
@@ -18,7 +18,7 @@ export const useUserStore = defineStore('user', {
|
|||||||
},
|
},
|
||||||
currentUserAvatar(state) {
|
currentUserAvatar(state) {
|
||||||
const userInfo = state.currentUserBasicInfo || {};
|
const userInfo = state.currentUserBasicInfo || {};
|
||||||
return userInfo.avatar || null;
|
return state.getUserAvatarUrl(userInfo, false);
|
||||||
},
|
},
|
||||||
currentUserDefaultAccountId(state) {
|
currentUserDefaultAccountId(state) {
|
||||||
const userInfo = state.currentUserBasicInfo || {};
|
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() {
|
getUserDataStatistics() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
services.getUserDataStatistics().then(response => {
|
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>
|
</template>
|
||||||
|
|
||||||
<v-card-text class="d-flex">
|
<v-card-text class="d-flex">
|
||||||
<v-avatar rounded="lg" color="primary" variant="tonal" size="100" class="me-4">
|
<v-avatar rounded="lg" color="primary" variant="tonal" size="100" class="me-4" :class="{ 'cursor-pointer': oldProfile.avatarProvider === 'internal' }">
|
||||||
<v-img :src="oldProfile.avatar" v-if="oldProfile.avatar">
|
<v-img :src="currentUserAvatar" v-if="currentUserAvatar">
|
||||||
<template #placeholder>
|
<template #placeholder>
|
||||||
<div class="d-flex align-center justify-center fill-height">
|
<div class="d-flex align-center justify-center fill-height">
|
||||||
<v-icon size="48" :icon="icons.user"/>
|
<v-icon size="48" :icon="icons.user"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-img>
|
</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>
|
</v-avatar>
|
||||||
<div class="d-flex flex-column justify-center gap-3">
|
<div class="d-flex flex-column justify-center gap-3">
|
||||||
<div class="d-flex text-body-1">
|
<div class="d-flex text-body-1">
|
||||||
@@ -301,6 +307,7 @@
|
|||||||
|
|
||||||
<confirm-dialog ref="confirmDialog"/>
|
<confirm-dialog ref="confirmDialog"/>
|
||||||
<snack-bar ref="snackbar" />
|
<snack-bar ref="snackbar" />
|
||||||
|
<input ref="avatarInput" type="file" style="display: none" :accept="supportedImageExtensions" @change="updateAvatar($event)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -312,7 +319,9 @@ import { useAccountsStore } from '@/stores/account.js';
|
|||||||
import { useOverviewStore } from '@/stores/overview.js';
|
import { useOverviewStore } from '@/stores/overview.js';
|
||||||
|
|
||||||
import datetimeConstants from '@/consts/datetime.js';
|
import datetimeConstants from '@/consts/datetime.js';
|
||||||
|
import fileConstants from '@/consts/file.js';
|
||||||
import { getNameByKeyValue } from '@/lib/common.js';
|
import { getNameByKeyValue } from '@/lib/common.js';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.js';
|
||||||
import { getCategorizedAccounts } from '@/lib/account.js';
|
import { getCategorizedAccounts } from '@/lib/account.js';
|
||||||
import { isUserVerifyEmailEnabled } from '@/lib/server_settings.js';
|
import { isUserVerifyEmailEnabled } from '@/lib/server_settings.js';
|
||||||
import { setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
|
import { setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
|
||||||
@@ -366,6 +375,7 @@ export default {
|
|||||||
expenseAmountColor: 0,
|
expenseAmountColor: 0,
|
||||||
incomeAmountColor: 0
|
incomeAmountColor: 0
|
||||||
},
|
},
|
||||||
|
avatarNoCacheId: '',
|
||||||
emailVerified: false,
|
emailVerified: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
resending: false,
|
resending: false,
|
||||||
@@ -428,6 +438,12 @@ export default {
|
|||||||
allTransactionEditScopeTypes() {
|
allTransactionEditScopeTypes() {
|
||||||
return this.$locale.getAllTransactionEditScopeTypes();
|
return this.$locale.getAllTransactionEditScopeTypes();
|
||||||
},
|
},
|
||||||
|
supportedImageExtensions() {
|
||||||
|
return fileConstants.supportedImageExtensions;
|
||||||
|
},
|
||||||
|
currentUserAvatar() {
|
||||||
|
return this.userStore.getUserAvatarUrl(this.oldProfile, this.avatarNoCacheId);
|
||||||
|
},
|
||||||
isUserVerifyEmailEnabled() {
|
isUserVerifyEmailEnabled() {
|
||||||
return 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() {
|
reset() {
|
||||||
this.setCurrentUserProfile(this.oldProfile);
|
this.setCurrentUserProfile(this.oldProfile);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user