Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
67
service/user/info.go
Normal file
67
service/user/info.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetUser(c *gin.Context) (*ent.User, error) {
|
||||
uid := hashid.FromContext(c)
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
return userClient.GetByID(ctx, uid)
|
||||
}
|
||||
|
||||
func GetUserCapacity(c *gin.Context) (*fs.Capacity, error) {
|
||||
user := inventory.UserFromContext(c)
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
return m.Capacity(c)
|
||||
}
|
||||
|
||||
type (
|
||||
SearchUserService struct {
|
||||
Keyword string `form:"keyword" binding:"required,min=2"`
|
||||
}
|
||||
SearchUserParamCtx struct{}
|
||||
)
|
||||
|
||||
const resultLimit = 10
|
||||
|
||||
func (s *SearchUserService) Search(c *gin.Context) ([]*ent.User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
res, err := userClient.SearchActive(c, resultLimit, s.Keyword)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to search user", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListAllGroups lists all groups.
|
||||
func ListAllGroups(c *gin.Context) ([]*ent.Group, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
res, err := groupClient.ListAll(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list all groups", err)
|
||||
}
|
||||
|
||||
res = lo.Filter(res, func(g *ent.Group, index int) bool {
|
||||
return g.ID != inventory.AnonymousGroupID
|
||||
})
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,203 +1,245 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// LoginParameterCtx define key fore UserLoginService
|
||||
type LoginParameterCtx struct{}
|
||||
|
||||
// UserLoginService 管理用户登录的服务
|
||||
type UserLoginService struct {
|
||||
//TODO 细致调整验证规则
|
||||
UserName string `form:"userName" json:"userName" binding:"required,email"`
|
||||
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
|
||||
UserName string `form:"email" json:"email" binding:"required,email"`
|
||||
Password string `form:"password" json:"password" binding:"required,min=4,max=64"`
|
||||
}
|
||||
|
||||
// UserResetEmailService 发送密码重设邮件服务
|
||||
type UserResetEmailService struct {
|
||||
UserName string `form:"userName" json:"userName" binding:"required,email"`
|
||||
}
|
||||
|
||||
// UserResetService 密码重设服务
|
||||
type UserResetService struct {
|
||||
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
|
||||
ID string `json:"id" binding:"required"`
|
||||
Secret string `json:"secret" binding:"required"`
|
||||
}
|
||||
type (
|
||||
// UserResetService 密码重设服务
|
||||
UserResetService struct {
|
||||
Password string `form:"password" json:"password" binding:"required,min=6,max=64"`
|
||||
Secret string `json:"secret" binding:"required"`
|
||||
}
|
||||
UserResetParameterCtx struct{}
|
||||
)
|
||||
|
||||
// Reset 重设密码
|
||||
func (service *UserResetService) Reset(c *gin.Context) serializer.Response {
|
||||
// 取得原始用户ID
|
||||
uid, err := hashid.DecodeHashID(service.ID, hashid.UserID)
|
||||
func (service *UserResetService) Reset(c *gin.Context) (*User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
kv := dep.KV()
|
||||
uid := hashid.FromContext(c)
|
||||
|
||||
resetSession, ok := kv.Get(fmt.Sprintf("user_reset_%d", uid))
|
||||
if !ok || resetSession.(string) != service.Secret {
|
||||
return nil, serializer.NewError(serializer.CodeTempLinkExpired, "Link is expired", nil)
|
||||
}
|
||||
|
||||
if err := kv.Delete(fmt.Sprintf("user_reset_%d", uid)); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to delete reset session", err)
|
||||
}
|
||||
|
||||
u, err := userClient.GetActiveByID(c, uid)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInvalidTempLink, "Invalid link", err)
|
||||
return nil, serializer.NewError(serializer.CodeUserNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
// 检查重设会话
|
||||
resetSession, exist := cache.Get(fmt.Sprintf("user_reset_%d", uid))
|
||||
if !exist || resetSession.(string) != service.Secret {
|
||||
return serializer.Err(serializer.CodeTempLinkExpired, "Link is expired", err)
|
||||
}
|
||||
|
||||
// 重设用户密码
|
||||
user, err := model.GetActiveUserByID(uid)
|
||||
u, err = userClient.UpdatePassword(c, u, service.Password)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "User not found", nil)
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to update password", err)
|
||||
}
|
||||
|
||||
user.SetPassword(service.Password)
|
||||
if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil {
|
||||
return serializer.DBErr("Failed to reset password", err)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{fmt.Sprintf("%d", uid)}, "user_reset_")
|
||||
return serializer.Response{}
|
||||
userRes := BuildUser(u, dep.HashIDEncoder())
|
||||
return &userRes, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// UserResetEmailService 发送密码重设邮件服务
|
||||
UserResetEmailService struct {
|
||||
UserName string `form:"email" json:"email" binding:"required,email"`
|
||||
}
|
||||
UserResetEmailParameterCtx struct{}
|
||||
)
|
||||
|
||||
const userResetPrefix = "user_reset_"
|
||||
|
||||
// Reset 发送密码重设邮件
|
||||
func (service *UserResetEmailService) Reset(c *gin.Context) serializer.Response {
|
||||
// 查找用户
|
||||
if user, err := model.GetUserByEmail(service.UserName); err == nil {
|
||||
|
||||
if user.Status == model.Baned || user.Status == model.OveruseBaned {
|
||||
return serializer.Err(serializer.CodeUserBaned, "This user is banned", nil)
|
||||
}
|
||||
if user.Status == model.NotActivicated {
|
||||
return serializer.Err(serializer.CodeUserNotActivated, "This user is not activated", nil)
|
||||
}
|
||||
// 创建密码重设会话
|
||||
secret := util.RandStringRunes(32)
|
||||
cache.Set(fmt.Sprintf("user_reset_%d", user.ID), secret, 3600)
|
||||
|
||||
// 生成用户访问的重设链接
|
||||
controller, _ := url.Parse("/reset")
|
||||
finalURL := model.GetSiteURL().ResolveReference(controller)
|
||||
queries := finalURL.Query()
|
||||
queries.Add("id", hashid.HashID(user.ID, hashid.UserID))
|
||||
queries.Add("sign", secret)
|
||||
finalURL.RawQuery = queries.Encode()
|
||||
|
||||
// 发送密码重设邮件
|
||||
title, body := email.NewResetEmail(user.Nick, finalURL.String())
|
||||
if err := email.Send(user.Email, title, body); err != nil {
|
||||
return serializer.Err(serializer.CodeFailedSendEmail, "Failed to send email", err)
|
||||
}
|
||||
func (service *UserResetEmailService) Reset(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
u, err := userClient.GetByEmail(c, service.UserName)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeUserNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Login 二步验证继续登录
|
||||
func (service *Enable2FA) Login(c *gin.Context) serializer.Response {
|
||||
if uid, ok := util.GetSession(c, "2fa_user_id").(uint); ok {
|
||||
// 查找用户
|
||||
expectedUser, err := model.GetActiveUserByID(uid)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "User not found", nil)
|
||||
}
|
||||
|
||||
// 验证二步验证代码
|
||||
if !totp.Validate(service.Code, expectedUser.TwoFactor) {
|
||||
return serializer.Err(serializer.Code2FACodeErr, "2FA code not correct", nil)
|
||||
}
|
||||
|
||||
//登陆成功,清空并设置session
|
||||
util.DeleteSession(c, "2fa_user_id")
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": expectedUser.ID,
|
||||
})
|
||||
|
||||
return serializer.BuildUserResponse(expectedUser)
|
||||
if u.Status == user.StatusManualBanned || u.Status == user.StatusSysBanned {
|
||||
return serializer.NewError(serializer.CodeUserBaned, "This user is banned", nil)
|
||||
}
|
||||
|
||||
return serializer.Err(serializer.CodeLoginSessionNotExist, "Login session not exist", nil)
|
||||
if u.Status == user.StatusInactive {
|
||||
return serializer.NewError(serializer.CodeUserNotActivated, "This user is not activated", nil)
|
||||
}
|
||||
|
||||
secret := util.RandStringRunes(32)
|
||||
if err := dep.KV().Set(fmt.Sprintf("%s%d", userResetPrefix, u.ID), secret, 3600); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create reset session", err)
|
||||
}
|
||||
|
||||
base := dep.SettingProvider().SiteURL(c)
|
||||
resetUrl := routes.MasterUserResetUrl(base)
|
||||
queries := resetUrl.Query()
|
||||
queries.Add("id", hashid.EncodeUserID(dep.HashIDEncoder(), u.ID))
|
||||
queries.Add("secret", secret)
|
||||
resetUrl.RawQuery = queries.Encode()
|
||||
|
||||
title, body, err := email.NewResetEmail(c, dep.SettingProvider(), u, resetUrl.String())
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
if err := dep.EmailClient(c).Send(c, u.Email, title, body); err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login 用户登录函数
|
||||
func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
|
||||
expectedUser, err := model.GetUserByEmail(service.UserName)
|
||||
func (service *UserLoginService) Login(c *gin.Context) (*ent.User, string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
expectedUser, err := userClient.GetByEmail(ctx, service.UserName)
|
||||
|
||||
// 一系列校验
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCredentialInvalid, "Wrong password or email address", err)
|
||||
}
|
||||
if authOK, _ := expectedUser.CheckPassword(service.Password); !authOK {
|
||||
return serializer.Err(serializer.CodeCredentialInvalid, "Wrong password or email address", nil)
|
||||
}
|
||||
if expectedUser.Status == model.Baned || expectedUser.Status == model.OveruseBaned {
|
||||
return serializer.Err(serializer.CodeUserBaned, "This account has been blocked", nil)
|
||||
}
|
||||
if expectedUser.Status == model.NotActivicated {
|
||||
return serializer.Err(serializer.CodeUserNotActivated, "This account is not activated", nil)
|
||||
err = serializer.NewError(serializer.CodeInvalidPassword, "Incorrect password or email address", err)
|
||||
} else if checkErr := inventory.CheckPassword(expectedUser, service.Password); checkErr != nil {
|
||||
err = serializer.NewError(serializer.CodeInvalidPassword, "Incorrect password or email address", err)
|
||||
} else if expectedUser.Status == user.StatusManualBanned || expectedUser.Status == user.StatusSysBanned {
|
||||
err = serializer.NewError(serializer.CodeUserBaned, "This account has been blocked", nil)
|
||||
} else if expectedUser.Status == user.StatusInactive {
|
||||
err = serializer.NewError(serializer.CodeUserNotActivated, "This account is not activated", nil)
|
||||
}
|
||||
|
||||
if expectedUser.TwoFactor != "" {
|
||||
// 需要二步验证
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"2fa_user_id": expectedUser.ID,
|
||||
})
|
||||
return serializer.Response{Code: 203}
|
||||
}
|
||||
|
||||
//登陆成功,清空并设置session
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": expectedUser.ID,
|
||||
})
|
||||
|
||||
return serializer.BuildUserResponse(expectedUser)
|
||||
|
||||
}
|
||||
|
||||
// CopySessionService service for copy user session
|
||||
type CopySessionService struct {
|
||||
ID string `uri:"id" binding:"required,uuid4"`
|
||||
}
|
||||
|
||||
const CopySessionTTL = 60
|
||||
|
||||
// Prepare generates the URL with short expiration duration
|
||||
func (s *CopySessionService) Prepare(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 用户组有效期
|
||||
urlID := uuid.Must(uuid.NewV4())
|
||||
if err := cache.Set(fmt.Sprintf("copy_session_%s", urlID.String()), user.ID, CopySessionTTL); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to create copy session", err)
|
||||
}
|
||||
|
||||
base := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/user/session/copy/" + urlID.String())
|
||||
apiURL := base.ResolveReference(apiBaseURI)
|
||||
res, err := auth.SignURI(auth.General, apiURL.String(), CopySessionTTL)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to sign temp URL", err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: res.String(),
|
||||
if expectedUser.TwoFactorSecret != "" {
|
||||
twoFaSessionID := uuid.Must(uuid.NewV4())
|
||||
dep.KV().Set(fmt.Sprintf("user_2fa_%s", twoFaSessionID), expectedUser.ID, 600)
|
||||
return expectedUser, twoFaSessionID.String(), nil
|
||||
}
|
||||
|
||||
return expectedUser, "", nil
|
||||
}
|
||||
|
||||
// Copy a new session from active session, refresh max-age
|
||||
func (s *CopySessionService) Copy(c *gin.Context) serializer.Response {
|
||||
// 用户组有效期
|
||||
cacheKey := fmt.Sprintf("copy_session_%s", s.ID)
|
||||
uid, ok := cache.Get(cacheKey)
|
||||
type (
|
||||
LoginLogCtx struct{}
|
||||
)
|
||||
|
||||
func IssueToken(c *gin.Context) (*BuiltinLoginResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
token, err := dep.TokenAuth().Issue(c, u)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeEncryptError, "Failed to issue token pair", err)
|
||||
}
|
||||
|
||||
return &BuiltinLoginResponse{
|
||||
User: BuildUser(u, dep.HashIDEncoder()),
|
||||
Token: *token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshTokenParameterCtx define key fore RefreshTokenService
|
||||
type RefreshTokenParameterCtx struct{}
|
||||
|
||||
// RefreshTokenService refresh token service
|
||||
type RefreshTokenService struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *RefreshTokenService) Refresh(c *gin.Context) (*auth.Token, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
token, err := dep.TokenAuth().Refresh(c, s.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCredentialInvalid, "Failed to issue token pair", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
type (
|
||||
OtpValidationParameterCtx struct{}
|
||||
OtpValidationService struct {
|
||||
OTP string `json:"otp" binding:"required"`
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Login 用户登录函数
|
||||
func (service *OtpValidationService) Verify2FA(c *gin.Context) (*ent.User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
|
||||
sessionRaw, ok := kv.Get(fmt.Sprintf("user_2fa_%s", service.SessionID))
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{cacheKey}, "")
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": uid.(uint),
|
||||
})
|
||||
uid := sessionRaw.(int)
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
expectedUser, err := dep.UserClient().GetByID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
if expectedUser.TwoFactorSecret != "" {
|
||||
if !totp.Validate(service.OTP, expectedUser.TwoFactorSecret) {
|
||||
err := serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
kv.Delete("user_2fa_", service.SessionID)
|
||||
return expectedUser, nil
|
||||
}
|
||||
|
||||
type (
|
||||
PrepareLoginParameterCtx struct{}
|
||||
PrepareLoginService struct {
|
||||
Email string `form:"email" binding:"required,email"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *PrepareLoginService) Prepare(c *gin.Context) (*PrepareLoginResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
ctx := context.WithValue(c, inventory.LoadUserPasskey{}, true)
|
||||
expectedUser, err := dep.UserClient().GetByEmail(ctx, service.Email)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
return &PrepareLoginResponse{
|
||||
WebAuthnEnabled: len(expectedUser.Edges.Passkey) > 0,
|
||||
PasswordEnabled: expectedUser.Password != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
288
service/user/passkey.go
Normal file
288
service/user/passkey.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(webauthn.SessionData{})
|
||||
}
|
||||
|
||||
type authnUser struct {
|
||||
hasher hashid.Encoder
|
||||
u *ent.User
|
||||
credentials []*ent.Passkey
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnID() []byte {
|
||||
return []byte(hashid.EncodeUserID(a.hasher, a.u.ID))
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnName() string {
|
||||
return a.u.Email
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnDisplayName() string {
|
||||
return a.u.Nick
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnCredentials() []webauthn.Credential {
|
||||
if a.credentials == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return lo.Map(a.credentials, func(item *ent.Passkey, index int) webauthn.Credential {
|
||||
return *item.Credential
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
authnSessionKey = "authn_session_"
|
||||
)
|
||||
|
||||
func PreparePasskeyLogin(c *gin.Context) (*PreparePasskeyLoginResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
options, sessionData, err := webAuthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
||||
}
|
||||
|
||||
sessionID := uuid.Must(uuid.NewV4()).String()
|
||||
if err := dep.KV().Set(fmt.Sprint("%s%s", authnSessionKey, sessionID), *sessionData, 300); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
||||
}
|
||||
|
||||
return &PreparePasskeyLoginResponse{
|
||||
Options: options,
|
||||
SessionID: sessionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FinishPasskeyLoginParameterCtx struct{}
|
||||
FinishPasskeyLoginService struct {
|
||||
Response string `json:"response" binding:"required"`
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *FinishPasskeyLoginService) FinishPasskeyLogin(c *gin.Context) (*ent.User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
userClient := dep.UserClient()
|
||||
|
||||
sessionDataRaw, ok := kv.Get(fmt.Sprint("%s%s", authnSessionKey, s.SessionID))
|
||||
if !ok {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||
}
|
||||
|
||||
_ = kv.Delete(authnSessionKey, s.Response)
|
||||
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
sessionData := sessionDataRaw.(webauthn.SessionData)
|
||||
pcc, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(s.Response))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
|
||||
}
|
||||
|
||||
var loginedUser *ent.User
|
||||
discoverUserHandle := func(rawID, userHandle []byte) (user webauthn.User, err error) {
|
||||
uid, err := dep.HashIDEncoder().Decode(string(userHandle), hashid.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserPasskey{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadUserGroup{}, true)
|
||||
u, err := userClient.GetLoginUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
|
||||
}
|
||||
|
||||
if inventory.IsAnonymousUser(u) {
|
||||
return nil, errors.New("anonymous user")
|
||||
}
|
||||
|
||||
loginedUser = u
|
||||
return &authnUser{u: u, hasher: dep.HashIDEncoder(), credentials: u.Edges.Passkey}, nil
|
||||
}
|
||||
|
||||
credential, err := webAuthn.ValidateDiscoverableLogin(discoverUserHandle, sessionData, pcc)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to validate login", err)
|
||||
}
|
||||
|
||||
// Find the credential just used
|
||||
usedCredentialId := base64.StdEncoding.EncodeToString(credential.ID)
|
||||
usedCredential, found := lo.Find(loginedUser.Edges.Passkey, func(item *ent.Passkey) bool {
|
||||
return item.CredentialID == usedCredentialId
|
||||
})
|
||||
|
||||
if !found {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Passkey login passed but credential used is unknown", nil)
|
||||
}
|
||||
|
||||
// Update used at
|
||||
if err := userClient.MarkPasskeyUsed(c, loginedUser.ID, usedCredential.CredentialID); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update passkey", err)
|
||||
}
|
||||
|
||||
return loginedUser, nil
|
||||
}
|
||||
|
||||
func PreparePasskeyRegister(c *gin.Context) (*protocol.CredentialCreation, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
u := inventory.UserFromContext(c)
|
||||
|
||||
existingKeys, err := userClient.ListPasskeys(c, u.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
|
||||
}
|
||||
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
authSelect := protocol.AuthenticatorSelection{
|
||||
RequireResidentKey: protocol.ResidentKeyRequired(),
|
||||
UserVerification: protocol.VerificationPreferred,
|
||||
}
|
||||
|
||||
options, sessionData, err := webAuthn.BeginRegistration(
|
||||
&authnUser{u: u, hasher: dep.HashIDEncoder()},
|
||||
webauthn.WithAuthenticatorSelection(authSelect),
|
||||
webauthn.WithExclusions(lo.Map(existingKeys, func(item *ent.Passkey, index int) protocol.CredentialDescriptor {
|
||||
return protocol.CredentialDescriptor{
|
||||
Type: protocol.PublicKeyCredentialType,
|
||||
CredentialID: item.Credential.ID,
|
||||
Transport: item.Credential.Transport,
|
||||
AttestationType: item.Credential.AttestationType,
|
||||
}
|
||||
})),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
||||
}
|
||||
|
||||
if err := dep.KV().Set(fmt.Sprint("%s%d", authnSessionKey, u.ID), *sessionData, 300); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FinishPasskeyRegisterParameterCtx struct{}
|
||||
FinishPasskeyRegisterService struct {
|
||||
Response string `json:"response" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
UA string `json:"ua" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *FinishPasskeyRegisterService) FinishPasskeyRegister(c *gin.Context) (*Passkey, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
u := inventory.UserFromContext(c)
|
||||
|
||||
sessionDataRaw, ok := kv.Get(fmt.Sprint("%s%d", authnSessionKey, u.ID))
|
||||
if !ok {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||
}
|
||||
|
||||
_ = kv.Delete(authnSessionKey, strconv.Itoa(u.ID))
|
||||
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
sessionData := sessionDataRaw.(webauthn.SessionData)
|
||||
pcc, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(s.Response))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
|
||||
}
|
||||
|
||||
credential, err := webAuthn.CreateCredential(&authnUser{u: u, hasher: dep.HashIDEncoder()}, sessionData, pcc)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to finish registration", err)
|
||||
}
|
||||
|
||||
client := dep.UAParser().Parse(s.UA)
|
||||
name := util.Replace(map[string]string{
|
||||
"{os}": client.Os.Family,
|
||||
"{browser}": client.UserAgent.Family,
|
||||
}, s.Name)
|
||||
|
||||
passkey, err := dep.UserClient().AddPasskey(c, u.ID, name, credential)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to add passkey", err)
|
||||
}
|
||||
|
||||
res := BuildPasskey(passkey)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
DeletePasskeyService struct {
|
||||
ID string `form:"id" binding:"required"`
|
||||
}
|
||||
DeletePasskeyParameterCtx struct{}
|
||||
)
|
||||
|
||||
func (s *DeletePasskeyService) DeletePasskey(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
existingKeys, err := userClient.ListPasskeys(c, u.ID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
|
||||
}
|
||||
|
||||
var existing *ent.Passkey
|
||||
for _, key := range existingKeys {
|
||||
if key.CredentialID == s.ID {
|
||||
existing = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Passkey not found", nil)
|
||||
}
|
||||
|
||||
if err := userClient.RemovePasskey(c, u.ID, s.ID); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete passkey", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,113 +1,148 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterParameterCtx define key fore UserRegisterService
|
||||
type RegisterParameterCtx struct{}
|
||||
|
||||
// UserRegisterService 管理用户注册的服务
|
||||
type UserRegisterService struct {
|
||||
//TODO 细致调整验证规则
|
||||
UserName string `form:"userName" json:"userName" binding:"required,email"`
|
||||
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
|
||||
UserName string `form:"email" json:"email" binding:"required,email"`
|
||||
Password string `form:"password" json:"password" binding:"required,min=6,max=64"`
|
||||
Language string `form:"language" json:"language"`
|
||||
}
|
||||
|
||||
// Register 新用户注册
|
||||
func (service *UserRegisterService) Register(c *gin.Context) serializer.Response {
|
||||
// 相关设定
|
||||
options := model.GetSettingByNames("email_active")
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
// 相关设定
|
||||
isEmailRequired := model.IsTrueVal(options["email_active"])
|
||||
defaultGroup := model.GetIntSetting("default_group", 2)
|
||||
|
||||
// 创建新的用户对象
|
||||
user := model.NewUser()
|
||||
user.Email = service.UserName
|
||||
user.Nick = strings.Split(service.UserName, "@")[0]
|
||||
user.SetPassword(service.Password)
|
||||
user.Status = model.Active
|
||||
isEmailRequired := settings.EmailActivationEnabled(c)
|
||||
args := &inventory.NewUserArgs{
|
||||
Email: strings.ToLower(service.UserName),
|
||||
PlainPassword: service.Password,
|
||||
Status: user.StatusActive,
|
||||
GroupID: settings.DefaultGroup(c),
|
||||
Language: service.Language,
|
||||
}
|
||||
if isEmailRequired {
|
||||
user.Status = model.NotActivicated
|
||||
}
|
||||
user.GroupID = uint(defaultGroup)
|
||||
userNotActivated := false
|
||||
// 创建用户
|
||||
if err := model.DB.Create(&user).Error; err != nil {
|
||||
//检查已存在使用者是否尚未激活
|
||||
expectedUser, err := model.GetUserByEmail(service.UserName)
|
||||
if expectedUser.Status == model.NotActivicated {
|
||||
userNotActivated = true
|
||||
user = expectedUser
|
||||
} else {
|
||||
return serializer.Err(serializer.CodeEmailExisted, "Email already in use", err)
|
||||
}
|
||||
args.Status = user.StatusInactive
|
||||
}
|
||||
|
||||
userClient := dep.UserClient()
|
||||
uc, tx, _, err := inventory.WithTx(c, userClient)
|
||||
if err != nil {
|
||||
return serializer.DBErr(c, "Failed to start transaction", err)
|
||||
}
|
||||
|
||||
expectedUser, err := uc.Create(c, args)
|
||||
if expectedUser != nil {
|
||||
util.WithValue(c, inventory.UserCtx{}, expectedUser)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
if errors.Is(err, inventory.ErrUserEmailExisted) {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeEmailExisted, "Email already in use", err)
|
||||
}
|
||||
|
||||
if errors.Is(err, inventory.ErrInactiveUserExisted) {
|
||||
if err := sendActivationEmail(c, dep, expectedUser); err != nil {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeNotSet, "", err)
|
||||
}
|
||||
|
||||
return serializer.ErrWithDetails(c, serializer.CodeEmailSent, "User is not activated, activation email has been resent", nil)
|
||||
}
|
||||
|
||||
return serializer.DBErr(c, "Failed to insert user row", err)
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
return serializer.DBErr(c, "Failed to commit user row", err)
|
||||
}
|
||||
|
||||
// 发送激活邮件
|
||||
if isEmailRequired {
|
||||
|
||||
// 签名激活请求API
|
||||
base := model.GetSiteURL()
|
||||
userID := hashid.HashID(user.ID, hashid.UserID)
|
||||
controller, _ := url.Parse("/api/v3/user/activate/" + userID)
|
||||
activateURL, err := auth.SignURI(auth.General, base.ResolveReference(controller).String(), 86400)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeEncryptError, "Failed to sign the activation link", err)
|
||||
}
|
||||
|
||||
// 取得签名
|
||||
credential := activateURL.Query().Get("sign")
|
||||
|
||||
// 生成对用户访问的激活地址
|
||||
controller, _ = url.Parse("/activate")
|
||||
finalURL := base.ResolveReference(controller)
|
||||
queries := finalURL.Query()
|
||||
queries.Add("id", userID)
|
||||
queries.Add("sign", credential)
|
||||
finalURL.RawQuery = queries.Encode()
|
||||
|
||||
// 返送激活邮件
|
||||
title, body := email.NewActivationEmail(user.Email,
|
||||
finalURL.String(),
|
||||
)
|
||||
if err := email.Send(user.Email, title, body); err != nil {
|
||||
return serializer.Err(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
if userNotActivated == true {
|
||||
//原本在上面要抛出的DBErr,放来这边抛出
|
||||
return serializer.Err(serializer.CodeEmailSent, "User is not activated, activation email has been resent", nil)
|
||||
} else {
|
||||
return serializer.Response{Code: 203}
|
||||
if err := sendActivationEmail(c, dep, expectedUser); err != nil {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeNotSet, "", err)
|
||||
}
|
||||
return serializer.Response{Code: serializer.CodeNotFullySuccess}
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return serializer.Response{Data: BuildUser(expectedUser, dep.HashIDEncoder())}
|
||||
}
|
||||
|
||||
// Activate 激活用户
|
||||
func (service *SettingService) Activate(c *gin.Context) serializer.Response {
|
||||
// 查找待激活用户
|
||||
uid, _ := c.Get("object_id")
|
||||
user, err := model.GetUserByID(uid.(uint))
|
||||
func sendActivationEmail(ctx context.Context, dep dependency.Dep, newUser *ent.User) error {
|
||||
base := dep.SettingProvider().SiteURL(ctx)
|
||||
userID := hashid.EncodeUserID(dep.HashIDEncoder(), newUser.ID)
|
||||
ttl := time.Now().Add(time.Duration(24) * time.Hour)
|
||||
activateURL, err := auth.SignURI(ctx, dep.GeneralAuth(), routes.MasterUserActivateAPIUrl(base, userID).String(), &ttl)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "User not fount", err)
|
||||
return serializer.NewError(serializer.CodeEncryptError, "Failed to sign the activation link", err)
|
||||
}
|
||||
|
||||
// 取得签名
|
||||
credential := activateURL.Query().Get("sign")
|
||||
|
||||
// 生成对用户访问的激活地址
|
||||
finalURL := routes.MasterUserActivateUrl(base)
|
||||
queries := finalURL.Query()
|
||||
queries.Add("id", userID)
|
||||
queries.Add("sign", credential)
|
||||
finalURL.RawQuery = queries.Encode()
|
||||
|
||||
// 返送激活邮件
|
||||
title, body, err := email.NewActivationEmail(ctx, dep.SettingProvider(), newUser, finalURL.String())
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
if err := dep.EmailClient(ctx).Send(ctx, newUser.Email, title, body); err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActivateUser 激活用户
|
||||
func ActivateUser(c *gin.Context) serializer.Response {
|
||||
uid := hashid.FromContext(c)
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
// 查找待激活用户
|
||||
inactiveUser, err := userClient.GetByID(c, uid)
|
||||
if err != nil {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeUserNotFound, "User not fount", err)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if user.Status != model.NotActivicated {
|
||||
return serializer.Err(serializer.CodeUserCannotActivate, "This user cannot be activated", nil)
|
||||
if inactiveUser.Status != user.StatusInactive {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeUserCannotActivate, "This user cannot be activated", nil)
|
||||
}
|
||||
|
||||
// 激活用户
|
||||
user.SetStatus(model.Active)
|
||||
activeUser, err := userClient.SetStatus(c, inactiveUser, user.StatusActive)
|
||||
if err != nil {
|
||||
return serializer.DBErr(c, "Failed to update user", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: user.Email}
|
||||
util.WithValue(c, inventory.UserCtx{}, activeUser)
|
||||
return serializer.Response{Data: BuildUser(activeUser, dep.HashIDEncoder())}
|
||||
}
|
||||
|
||||
219
service/user/response.go
Normal file
219
service/user/response.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/samber/lo"
|
||||
"github.com/ua-parser/uap-go/uaparser"
|
||||
)
|
||||
|
||||
type PreparePasskeyLoginResponse struct {
|
||||
Options *protocol.CredentialAssertion `json:"options"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
VersionRetentionEnabled bool `json:"version_retention_enabled"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Paswordless bool `json:"passwordless"`
|
||||
TwoFAEnabled bool `json:"two_fa_enabled"`
|
||||
Passkeys []Passkey `json:"passkeys,omitempty"`
|
||||
}
|
||||
|
||||
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
|
||||
return &UserSettings{
|
||||
VersionRetentionEnabled: u.Settings.VersionRetention,
|
||||
VersionRetentionExt: u.Settings.VersionRetentionExt,
|
||||
VersionRetentionMax: u.Settings.VersionRetentionMax,
|
||||
TwoFAEnabled: u.TwoFactorSecret != "",
|
||||
Paswordless: u.Password == "",
|
||||
Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey {
|
||||
return BuildPasskey(item)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type Passkey struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func BuildPasskey(passkey *ent.Passkey) Passkey {
|
||||
return Passkey{
|
||||
ID: passkey.CredentialID,
|
||||
Name: passkey.Name,
|
||||
UsedAt: passkey.UsedAt,
|
||||
CreatedAt: passkey.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Node option for handling workflows.
|
||||
type Node struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Capabilities *boolset.BooleanSet `json:"capabilities"`
|
||||
}
|
||||
|
||||
// BuildNodes serialize a list of nodes.
|
||||
func BuildNodes(nodes []*ent.Node, idEncoder hashid.Encoder) []*Node {
|
||||
res := make([]*Node, 0, len(nodes))
|
||||
for _, v := range nodes {
|
||||
res = append(res, BuildNode(v, idEncoder))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// BuildNode serialize a node.
|
||||
func BuildNode(node *ent.Node, idEncoder hashid.Encoder) *Node {
|
||||
return &Node{
|
||||
ID: hashid.EncodeNodeID(idEncoder, node.ID),
|
||||
Name: node.Name,
|
||||
Type: string(node.Type),
|
||||
Capabilities: node.Capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
// BuiltinLoginResponse response for a successful login for builtin auth provider.
|
||||
type BuiltinLoginResponse struct {
|
||||
User User `json:"user"`
|
||||
Token auth.Token `json:"token"`
|
||||
}
|
||||
|
||||
// User 用户序列化器
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status user.Status `json:"status,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
Anonymous bool `json:"anonymous,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Pined []types.PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permission *boolset.BooleanSet `json:"permission,omitempty"`
|
||||
DirectLinkBatchSize int `json:"direct_link_batch_size,omitempty"`
|
||||
TrashRetention int `json:"trash_retention,omitempty"`
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
Used uint64 `json:"used"`
|
||||
Free uint64 `json:"free"`
|
||||
Total uint64 `json:"total"`
|
||||
}
|
||||
|
||||
// WebAuthnCredentials 外部验证器凭证
|
||||
type WebAuthnCredentials struct {
|
||||
ID []byte `json:"id"`
|
||||
FingerPrint string `json:"fingerprint"`
|
||||
}
|
||||
|
||||
type PrepareLoginResponse struct {
|
||||
WebAuthnEnabled bool `json:"webauthn_enabled"`
|
||||
PasswordEnabled bool `json:"password_enabled"`
|
||||
}
|
||||
|
||||
// BuildWebAuthnList 构建设置页面凭证列表
|
||||
func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials {
|
||||
res := make([]WebAuthnCredentials, 0, len(credentials))
|
||||
for _, v := range credentials {
|
||||
credential := WebAuthnCredentials{
|
||||
ID: v.ID,
|
||||
FingerPrint: fmt.Sprintf("% X", v.Authenticator.AAGUID),
|
||||
}
|
||||
res = append(res, credential)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// BuildUser 序列化用户
|
||||
func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
|
||||
return User{
|
||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.Settings.PreferredTheme,
|
||||
Anonymous: user.ID == 0,
|
||||
Group: BuildGroup(user.Edges.Group, idEncoder),
|
||||
Pined: user.Settings.Pined,
|
||||
Language: user.Settings.Language,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildGroup(group *ent.Group, idEncoder hashid.Encoder) *Group {
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
return &Group{
|
||||
ID: hashid.EncodeGroupID(idEncoder, group.ID),
|
||||
Name: group.Name,
|
||||
Permission: group.Permissions,
|
||||
DirectLinkBatchSize: group.Settings.SourceBatchSize,
|
||||
TrashRetention: group.Settings.TrashRetention,
|
||||
}
|
||||
}
|
||||
|
||||
const sensitiveTag = "redacted"
|
||||
|
||||
const (
|
||||
RedactLevelAnonymous = iota
|
||||
RedactLevelUser
|
||||
)
|
||||
|
||||
// BuildUserRedacted Serialize a user without sensitive information.
|
||||
func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User {
|
||||
userRaw := BuildUser(u, idEncoder)
|
||||
|
||||
user := User{
|
||||
ID: userRaw.ID,
|
||||
Nickname: userRaw.Nickname,
|
||||
Avatar: userRaw.Avatar,
|
||||
CreatedAt: userRaw.CreatedAt,
|
||||
}
|
||||
|
||||
if userRaw.Group != nil {
|
||||
user.Group = RedactedGroup(userRaw.Group)
|
||||
}
|
||||
|
||||
if level == RedactLevelUser {
|
||||
user.Email = userRaw.Email
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// BuildGroupRedacted Serialize a group without sensitive information.
|
||||
func RedactedGroup(g *Group) *Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
}
|
||||
}
|
||||
@@ -1,256 +1,308 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"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"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
// SettingService 通用设置服务
|
||||
type SettingService struct {
|
||||
}
|
||||
|
||||
// SettingListService 通用设置列表服务
|
||||
type SettingListService struct {
|
||||
Page int `form:"page" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// AvatarService 头像服务
|
||||
type AvatarService struct {
|
||||
Size string `uri:"size" binding:"required,eq=l|eq=m|eq=s"`
|
||||
}
|
||||
|
||||
// SettingUpdateService 设定更改服务
|
||||
type SettingUpdateService struct {
|
||||
Option string `uri:"option" binding:"required,eq=nick|eq=theme|eq=homepage|eq=vip|eq=qq|eq=policy|eq=password|eq=2fa|eq=authn"`
|
||||
}
|
||||
|
||||
// OptionsChangeHandler 属性更改接口
|
||||
type OptionsChangeHandler interface {
|
||||
Update(*gin.Context, *model.User) serializer.Response
|
||||
}
|
||||
|
||||
// ChangerNick 昵称更改服务
|
||||
type ChangerNick struct {
|
||||
Nick string `json:"nick" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// PolicyChange 更改存储策略
|
||||
type PolicyChange struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// HomePage 更改个人主页开关
|
||||
type HomePage struct {
|
||||
Enabled bool `json:"status"`
|
||||
}
|
||||
|
||||
// PasswordChange 更改密码
|
||||
type PasswordChange struct {
|
||||
Old string `json:"old" binding:"required,min=4,max=64"`
|
||||
New string `json:"new" binding:"required,min=4,max=64"`
|
||||
}
|
||||
|
||||
// Enable2FA 开启二步验证
|
||||
type Enable2FA struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
// DeleteWebAuthn 删除WebAuthn凭证
|
||||
type DeleteWebAuthn struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// ThemeChose 主题选择
|
||||
type ThemeChose struct {
|
||||
Theme string `json:"theme" binding:"required,hexcolor|rgb|rgba|hsl"`
|
||||
}
|
||||
|
||||
// Update 更新主题设定
|
||||
func (service *ThemeChose) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.OptionsSerialized.PreferredTheme = service.Theme
|
||||
if err := user.UpdateOptions(); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 删除凭证
|
||||
func (service *DeleteWebAuthn) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.RemoveAuthn(service.ID)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 更改二步验证设定
|
||||
func (service *Enable2FA) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
if user.TwoFactor == "" {
|
||||
// 开启2FA
|
||||
secret, ok := util.GetSession(c, "2fa_init").(string)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "You have not initiated 2FA session", nil)
|
||||
}
|
||||
|
||||
if !totp.Validate(service.Code, secret) {
|
||||
return serializer.ParamErr("Incorrect 2FA code", nil)
|
||||
}
|
||||
|
||||
if err := user.Update(map[string]interface{}{"two_factor": secret}); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// 关闭2FA
|
||||
if !totp.Validate(service.Code, user.TwoFactor) {
|
||||
return serializer.ParamErr("Incorrect 2FA code", nil)
|
||||
}
|
||||
|
||||
if err := user.Update(map[string]interface{}{"two_factor": ""}); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
const (
|
||||
twoFaEnableSessionKey = "2fa_init_"
|
||||
)
|
||||
|
||||
// Init2FA 初始化二步验证
|
||||
func (service *SettingService) Init2FA(c *gin.Context, user *model.User) serializer.Response {
|
||||
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.Err(serializer.CodeInternalSetting, "Failed to generate TOTP secret", err)
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to generate TOTP secret", err)
|
||||
}
|
||||
|
||||
util.SetSession(c, map[string]interface{}{"2fa_init": key.Secret()})
|
||||
return serializer.Response{Data: key.Secret()}
|
||||
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
|
||||
}
|
||||
|
||||
// Update 更改密码
|
||||
func (service *PasswordChange) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 验证老密码
|
||||
if ok, _ := user.CheckPassword(service.Old); !ok {
|
||||
return serializer.Err(serializer.CodeIncorrectPassword, "", nil)
|
||||
type (
|
||||
// AvatarService Service to get avatar
|
||||
GetAvatarService struct {
|
||||
NoCache bool `form:"nocache"`
|
||||
}
|
||||
GetAvatarServiceParamsCtx struct{}
|
||||
)
|
||||
|
||||
// 更改为新密码
|
||||
user.SetPassword(service.New)
|
||||
if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil {
|
||||
return serializer.DBErr("Failed to update password", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 切换个人主页开关
|
||||
func (service *HomePage) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.OptionsSerialized.ProfileOff = !service.Enabled
|
||||
if err := user.UpdateOptions(); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 更改昵称
|
||||
func (service *ChangerNick) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
if err := user.Update(map[string]interface{}{"nick": service.Nick}); err != nil {
|
||||
return serializer.DBErr("Failed to update user", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
const (
|
||||
GravatarAvatar = "gravatar"
|
||||
FileAvatar = "file"
|
||||
)
|
||||
|
||||
// Get 获取用户头像
|
||||
func (service *AvatarService) Get(c *gin.Context) serializer.Response {
|
||||
func (service *GetAvatarService) Get(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
// 查找目标用户
|
||||
uid, _ := c.Get("object_id")
|
||||
user, err := model.GetActiveUserByID(uid.(uint))
|
||||
uid := hashid.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
user, err := userClient.GetByID(c, uid)
|
||||
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
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 serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取头像设置
|
||||
sizes := map[string]string{
|
||||
"s": model.GetSettingByName("avatar_size_s"),
|
||||
"m": model.GetSettingByName("avatar_size_m"),
|
||||
"l": model.GetSettingByName("avatar_size_l"),
|
||||
}
|
||||
avatarSettings := settings.Avatar(c)
|
||||
|
||||
// Gravatar 头像重定向
|
||||
if user.Avatar == "gravatar" {
|
||||
server := model.GetSettingByName("gravatar_server")
|
||||
gravatarRoot, err := url.Parse(server)
|
||||
if user.Avatar == GravatarAvatar {
|
||||
gravatarRoot, err := url.Parse(avatarSettings.Gravatar)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to parse Gravatar server", err)
|
||||
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=%s", has, sizes[service.Size]))
|
||||
avatar, _ := url.Parse(fmt.Sprintf("/avatar/%x?d=mm&s=200", has))
|
||||
|
||||
return serializer.Response{
|
||||
Code: -301,
|
||||
Data: gravatarRoot.ResolveReference(avatar).String(),
|
||||
}
|
||||
c.Redirect(http.StatusFound, gravatarRoot.ResolveReference(avatar).String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 本地文件头像
|
||||
if user.Avatar == "file" {
|
||||
avatarRoot := util.RelativePath(model.GetSettingByName("avatar_path"))
|
||||
sizeToInt := map[string]string{
|
||||
"s": "0",
|
||||
"m": "1",
|
||||
"l": "2",
|
||||
}
|
||||
if user.Avatar == FileAvatar {
|
||||
avatarRoot := util.DataPath(avatarSettings.Path)
|
||||
|
||||
avatar, err := os.Open(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d_%s.png", user.ID, sizeToInt[service.Size])))
|
||||
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)
|
||||
return serializer.Response{}
|
||||
}
|
||||
defer avatar.Close()
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, "avatar.png", user.UpdatedAt, avatar)
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// ListTasks 列出任务
|
||||
func (service *SettingListService) ListTasks(c *gin.Context, user *model.User) serializer.Response {
|
||||
tasks, total := model.ListTasks(user.ID, service.Page, 10, "updated_at desc")
|
||||
return serializer.BuildTaskList(tasks, total)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Settings 获取用户设定
|
||||
func (service *SettingService) Settings(c *gin.Context, user *model.User) serializer.Response {
|
||||
return serializer.Response{
|
||||
Data: map[string]interface{}{
|
||||
"uid": user.ID,
|
||||
"homepage": !user.OptionsSerialized.ProfileOff,
|
||||
"two_factor": user.TwoFactor != "",
|
||||
"prefer_theme": user.OptionsSerialized.PreferredTheme,
|
||||
"themes": model.GetSettingByName("themes"),
|
||||
"authn": serializer.BuildWebAuthnList(user.WebAuthnCredentials()),
|
||||
},
|
||||
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=64"`
|
||||
NewPassword *string `json:"new_password" binding:"omitempty,min=6,max=64"`
|
||||
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
|
||||
TwoFACode *string `json:"two_fa_code" 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.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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user