Cloudreve/service/user/setting.go

323 lines
9.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package user
import (
"context"
"crypto/md5"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/request"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
)
const (
twoFaEnableSessionKey = "2fa_init_"
)
// Init2FA 初始化二步验证
func Init2FA(c *gin.Context) (string, error) {
dep := dependency.FromContext(c)
user := inventory.UserFromContext(c)
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "Cloudreve",
AccountName: user.Email,
})
if err != nil {
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to generate TOTP secret", err)
}
if err := dep.KV().Set(fmt.Sprintf("%s%d", twoFaEnableSessionKey, user.ID), key.Secret(), 600); err != nil {
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to store TOTP session", err)
}
return key.Secret(), nil
}
type (
// AvatarService Service to get avatar
GetAvatarService struct {
NoCache bool `form:"nocache"`
}
GetAvatarServiceParamsCtx struct{}
)
const (
GravatarAvatar = "gravatar"
FileAvatar = "file"
)
// Get 获取用户头像
func (service *GetAvatarService) Get(c *gin.Context) error {
dep := dependency.FromContext(c)
settings := dep.SettingProvider()
// 查找目标用户
uid := hashid.FromContext(c)
userClient := dep.UserClient()
user, err := userClient.GetByID(c, uid)
if err != nil {
return serializer.NewError(serializer.CodeUserNotFound, "", err)
}
if !service.NoCache {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", settings.PublicResourceMaxAge(c)))
}
// 未设定头像时返回404错误
if user.Avatar == "" {
c.Status(404)
return nil
}
avatarSettings := settings.Avatar(c)
// Gravatar 头像重定向
if user.Avatar == GravatarAvatar {
gravatarRoot, err := url.Parse(avatarSettings.Gravatar)
if err != nil {
return serializer.NewError(serializer.CodeInternalSetting, "Failed to parse Gravatar server", err)
}
email_lowered := strings.ToLower(user.Email)
has := md5.Sum([]byte(email_lowered))
avatar, _ := url.Parse(fmt.Sprintf("/avatar/%x?d=mm&s=200", has))
c.Redirect(http.StatusFound, gravatarRoot.ResolveReference(avatar).String())
return nil
}
// 本地文件头像
if user.Avatar == FileAvatar {
avatarRoot := util.DataPath(avatarSettings.Path)
avatar, err := os.Open(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d.png", user.ID)))
if err != nil {
dep.Logger().Warning("Failed to open avatar file", err)
c.Status(404)
}
defer avatar.Close()
http.ServeContent(c.Writer, c.Request, "avatar.png", user.UpdatedAt, avatar)
return nil
}
c.Status(404)
return nil
}
// Settings 获取用户设定
func GetUserSettings(c *gin.Context) (*UserSettings, error) {
dep := dependency.FromContext(c)
u := inventory.UserFromContext(c)
userClient := dep.UserClient()
passkeys, err := userClient.ListPasskeys(c, u.ID)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user passkey", err)
}
return BuildUserSettings(u, passkeys, dep.UAParser()), nil
// 用户组有效期
//return serializer.Response{
// Data: map[string]interface{}{
// "uid": user.ID,
// "qq": user.OpenID != "",
// "homepage": !user.OptionsSerialized.ProfileOff,
// "two_factor": user.TwoFactor != "",
// "prefer_theme": user.OptionsSerialized.PreferredTheme,
// "themes": model.GetSettingByName("themes"),
// "group_expires": groupExpires,
// "authn": serializer.BuildWebAuthnList(user.WebAuthnCredentials()),
// },
//}
}
func UpdateUserAvatar(c *gin.Context) error {
dep := dependency.FromContext(c)
u := inventory.UserFromContext(c)
settings := dep.SettingProvider()
avatarSettings := settings.AvatarProcess(c)
if c.Request.ContentLength == -1 || c.Request.ContentLength > avatarSettings.MaxFileSize {
request.BlackHole(c.Request.Body)
return serializer.NewError(serializer.CodeFileTooLarge, "", nil)
}
if c.Request.ContentLength == 0 {
// Use Gravatar for empty body
if _, err := dep.UserClient().UpdateAvatar(c, u, GravatarAvatar); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
}
return nil
}
return updateAvatarFile(c, u, c.GetHeader("Content-Type"), c.Request.Body, avatarSettings)
}
func updateAvatarFile(ctx context.Context, u *ent.User, contentType string, file io.Reader, avatarSettings *setting.AvatarProcess) error {
dep := dependency.FromContext(ctx)
// Detect ext from content type
ext := "png"
switch contentType {
case "image/jpeg", "image/jpg":
ext = "jpg"
case "image/gif":
ext = "gif"
}
avatar, err := thumb.NewThumbFromFile(file, ext)
if err != nil {
return serializer.NewError(serializer.CodeParamErr, "Invalid image", err)
}
// Resize and save avatar
avatar.CreateAvatar(avatarSettings.MaxWidth)
avatarRoot := util.DataPath(avatarSettings.Path)
f, err := util.CreatNestedFile(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d.png", u.ID)))
if err != nil {
return serializer.NewError(serializer.CodeIOFailed, "Failed to create avatar file", err)
}
defer f.Close()
if err := avatar.Save(f, &setting.ThumbEncode{
Quality: 100,
Format: "png",
}); err != nil {
return serializer.NewError(serializer.CodeIOFailed, "Failed to save avatar file", err)
}
if _, err := dep.UserClient().UpdateAvatar(ctx, u, FileAvatar); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
}
return nil
}
type (
PatchUserSetting struct {
Nick *string `json:"nick" binding:"omitempty,min=1,max=255"`
Language *string `json:"language" binding:"omitempty,min=1,max=255"`
PreferredTheme *string `json:"preferred_theme" binding:"omitempty,hexcolor|rgb|rgba|hsl"`
VersionRetentionEnabled *bool `json:"version_retention_enabled" binding:"omitempty"`
VersionRetentionExt *[]string `json:"version_retention_ext" binding:"omitempty"`
VersionRetentionMax *int `json:"version_retention_max" binding:"omitempty,min=0"`
CurrentPassword *string `json:"current_password" binding:"omitempty,min=4,max=128"`
NewPassword *string `json:"new_password" binding:"omitempty,min=6,max=128"`
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"`
ShareLinksInProfile *string `json:"share_links_in_profile" binding:"omitempty"`
}
PatchUserSettingParamsCtx struct{}
)
func (s *PatchUserSetting) Patch(c *gin.Context) error {
dep := dependency.FromContext(c)
u := inventory.UserFromContext(c)
userClient := dep.UserClient()
saveSetting := false
if s.Nick != nil {
if _, err := userClient.UpdateNickname(c, u, *s.Nick); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user nick", err)
}
}
if s.Language != nil {
u.Settings.Language = *s.Language
saveSetting = true
}
if s.PreferredTheme != nil {
u.Settings.PreferredTheme = *s.PreferredTheme
saveSetting = true
}
if s.VersionRetentionEnabled != nil {
u.Settings.VersionRetention = *s.VersionRetentionEnabled
saveSetting = true
}
if s.VersionRetentionExt != nil {
u.Settings.VersionRetentionExt = *s.VersionRetentionExt
saveSetting = true
}
if s.VersionRetentionMax != nil {
u.Settings.VersionRetentionMax = *s.VersionRetentionMax
saveSetting = true
}
if s.DisableViewSync != nil {
u.Settings.DisableViewSync = *s.DisableViewSync
saveSetting = true
}
if s.ShareLinksInProfile != nil {
u.Settings.ShareLinksInProfile = types.ShareLinksInProfileLevel(*s.ShareLinksInProfile)
saveSetting = true
}
if s.CurrentPassword != nil && s.NewPassword != nil {
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)
}
if _, err := userClient.UpdatePassword(c, u, *s.NewPassword); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user password", err)
}
}
if s.TwoFAEnabled != nil {
if *s.TwoFAEnabled {
kv := dep.KV()
secret, ok := kv.Get(fmt.Sprintf("%s%d", twoFaEnableSessionKey, u.ID))
if !ok {
return serializer.NewError(serializer.CodeInternalSetting, "You have not initiated 2FA session", nil)
}
if !totp.Validate(*s.TwoFACode, secret.(string)) {
return serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
}
if _, err := userClient.UpdateTwoFASecret(c, u, secret.(string)); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
}
} else {
if !totp.Validate(*s.TwoFACode, u.TwoFactorSecret) {
return serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
}
if _, err := userClient.UpdateTwoFASecret(c, u, ""); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
}
}
}
if saveSetting {
if err := userClient.SaveSettings(c, u); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user settings", err)
}
}
return nil
}