Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user