diff --git a/api/router.go b/api/router.go index ae02db7..f5c4263 100644 --- a/api/router.go +++ b/api/router.go @@ -43,6 +43,7 @@ func SetupRouter(engine *gin.RouterGroup) { userRouter := v1.Group("/user") { userRouter.POST("/sms", userEndpoint.SendVerificationCode) + userRouter.POST("/email/code", userEndpoint.SendEmailVerificationCode) userRouter.POST("/register", userEndpoint.RegisterByEmail) userRouter.POST("/login", userEndpoint.LoginByEmail) userRouter.GET("/oauth/google/url", userEndpoint.GetGoogleOAuthUrl) diff --git a/api/v1/user/handler.go b/api/v1/user/handler.go index 1c18b1b..2a690df 100644 --- a/api/v1/user/handler.go +++ b/api/v1/user/handler.go @@ -106,6 +106,26 @@ func (h *UserEndpoint) GetUserInfo(ctx *gin.Context) { })) } +func (h *UserEndpoint) SendEmailVerificationCode(ctx *gin.Context) { + req := model.EmailCodeSendRequest{} + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeParamError, common.CodeParamErrorMsg)) + return + } + + if err := h.userService.SendEmailCode(ctx, req.Email); err != nil { + if bizErr, ok := err.(*common.BusinessError); ok { + ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, int(bizErr.Code), bizErr.Message)) + return + } + log.Error(ctx, "func", "SendEmailVerificationCode", "msg", "发送邮箱验证码失败", "error", err) + ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeSystemError, common.CodeSystemErrorMsg)) + return + } + + ctx.JSON(http.StatusOK, common.SuccessResponse(ctx, model.EmailCodeSendResponse{})) +} + func (h *UserEndpoint) RegisterByEmail(ctx *gin.Context) { req := model.EmailRegisterRequest{} if err := ctx.ShouldBindJSON(&req); err != nil { @@ -113,7 +133,7 @@ func (h *UserEndpoint) RegisterByEmail(ctx *gin.Context) { return } - uid, err := h.userService.RegisterByEmail(ctx, req.Email, req.Password) + uid, err := h.userService.RegisterByEmail(ctx, req.Email, req.Password, req.Code) if err != nil { if bizErr, ok := err.(*common.BusinessError); ok { ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, int(bizErr.Code), bizErr.Message)) diff --git a/deploy_dev.sh b/deploy_dev.sh index 5097974..66a903e 100755 --- a/deploy_dev.sh +++ b/deploy_dev.sh @@ -1,13 +1,15 @@ #!/bin/bash -git push origin test +BRANCH=$(git rev-parse --abbrev-ref HEAD) -ssh ubuntu << 'ENDSSH' -cd /home/yoge/Dev/doc_ai_backed -git checkout test -git pull origin test -docker compose -f docker-compose.infra.yml up -d -docker compose down -docker image rm doc_ai_backed-doc_ai:latest -docker compose up -d -ENDSSH \ No newline at end of file +git push origin "$BRANCH" + +ssh ubuntu " + cd /home/yoge/Dev/doc_ai_backed && + git checkout $BRANCH && + git pull origin $BRANCH && + docker compose -f docker-compose.infra.yml up -d && + docker compose down && + docker image rm doc_ai_backed-doc_ai:latest && + docker compose up -d +" \ No newline at end of file diff --git a/internal/model/user/user.go b/internal/model/user/user.go index f558210..8f12f2d 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -23,9 +23,16 @@ type UserInfoResponse struct { Email string `json:"email"` } +type EmailCodeSendRequest struct { + Email string `json:"email" binding:"required,email"` +} + +type EmailCodeSendResponse struct{} + type EmailRegisterRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` + Code string `json:"code" binding:"required"` } type EmailRegisterResponse struct { diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 97929c9..f9fa43e 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -14,6 +14,7 @@ import ( "gitea.com/texpixel/document_ai/internal/storage/cache" "gitea.com/texpixel/document_ai/internal/storage/dao" "gitea.com/texpixel/document_ai/pkg/common" + "gitea.com/texpixel/document_ai/pkg/email" "gitea.com/texpixel/document_ai/pkg/log" "gitea.com/texpixel/document_ai/pkg/sms" "golang.org/x/crypto/bcrypt" @@ -115,14 +116,52 @@ func (svc *UserService) GetUserInfo(ctx context.Context, uid int64) (*dao.User, return user, nil } -func (svc *UserService) RegisterByEmail(ctx context.Context, email, password string) (uid int64, err error) { - existingUser, err := svc.userDao.GetByEmail(dao.DB.WithContext(ctx), email) +func (svc *UserService) SendEmailCode(ctx context.Context, emailAddr string) error { + limit, err := cache.GetUserSendEmailLimit(ctx, emailAddr) + if err != nil { + log.Error(ctx, "func", "SendEmailCode", "msg", "get send email limit error", "error", err) + return err + } + if limit >= cache.UserSendEmailLimitCount { + return common.ErrEmailSendLimit + } + + code := fmt.Sprintf("%06d", rand.Intn(1000000)) + + subject := "TexPixel 邮箱验证码" + body := fmt.Sprintf(`
您的验证码为:%s,10分钟内有效,请勿泄露。
`, code) + if err = email.Send(ctx, emailAddr, subject, body); err != nil { + log.Error(ctx, "func", "SendEmailCode", "msg", "send email error", "error", err) + return err + } + + if cacheErr := cache.SetUserEmailCode(ctx, emailAddr, code); cacheErr != nil { + log.Error(ctx, "func", "SendEmailCode", "msg", "set email code error", "error", cacheErr) + } + if cacheErr := cache.SetUserSendEmailLimit(ctx, emailAddr); cacheErr != nil { + log.Error(ctx, "func", "SendEmailCode", "msg", "set send email limit error", "error", cacheErr) + } + return nil +} + +func (svc *UserService) RegisterByEmail(ctx context.Context, emailAddr, password, code string) (uid int64, err error) { + storedCode, err := cache.GetUserEmailCode(ctx, emailAddr) + if err != nil { + log.Error(ctx, "func", "RegisterByEmail", "msg", "get email code error", "error", err) + return 0, err + } + if storedCode == "" || storedCode != code { + log.Warn(ctx, "func", "RegisterByEmail", "msg", "invalid email code", "email", emailAddr) + return 0, common.ErrEmailCodeError + } + + existingUser, err := svc.userDao.GetByEmail(dao.DB.WithContext(ctx), emailAddr) if err != nil { log.Error(ctx, "func", "RegisterByEmail", "msg", "get user by email error", "error", err) return 0, err } if existingUser != nil { - log.Warn(ctx, "func", "RegisterByEmail", "msg", "email already registered", "email", email) + log.Warn(ctx, "func", "RegisterByEmail", "msg", "email already registered", "email", emailAddr) return 0, common.ErrEmailExists } @@ -133,15 +172,18 @@ func (svc *UserService) RegisterByEmail(ctx context.Context, email, password str } user := &dao.User{ - Email: email, + Email: emailAddr, Password: string(hashedPassword), } - err = svc.userDao.Create(dao.DB.WithContext(ctx), user) - if err != nil { + if err = svc.userDao.Create(dao.DB.WithContext(ctx), user); err != nil { log.Error(ctx, "func", "RegisterByEmail", "msg", "create user error", "error", err) return 0, err } + if cacheErr := cache.DeleteUserEmailCode(ctx, emailAddr); cacheErr != nil { + log.Error(ctx, "func", "RegisterByEmail", "msg", "delete email code error", "error", cacheErr) + } + return user.ID, nil } diff --git a/internal/storage/cache/user.go b/internal/storage/cache/user.go index c4b5802..2cc807e 100644 --- a/internal/storage/cache/user.go +++ b/internal/storage/cache/user.go @@ -61,3 +61,56 @@ func SetUserSendSmsLimit(ctx context.Context, phone string) error { func DeleteUserSmsCode(ctx context.Context, phone string) error { return RedisClient.Del(ctx, fmt.Sprintf(UserSmsCodePrefix, phone)).Err() } + +const ( + UserEmailCodeTTL = 10 * time.Minute + UserSendEmailLimitTTL = 24 * time.Hour + UserSendEmailLimitCount = 5 +) + +const ( + UserEmailCodePrefix = "user:email_code:%s" + UserSendEmailLimit = "user:send_email_limit:%s" +) + +func GetUserEmailCode(ctx context.Context, email string) (string, error) { + code, err := RedisClient.Get(ctx, fmt.Sprintf(UserEmailCodePrefix, email)).Result() + if err != nil { + if err == redis.Nil { + return "", nil + } + return "", err + } + return code, nil +} + +func SetUserEmailCode(ctx context.Context, email, code string) error { + return RedisClient.Set(ctx, fmt.Sprintf(UserEmailCodePrefix, email), code, UserEmailCodeTTL).Err() +} + +func DeleteUserEmailCode(ctx context.Context, email string) error { + return RedisClient.Del(ctx, fmt.Sprintf(UserEmailCodePrefix, email)).Err() +} + +func GetUserSendEmailLimit(ctx context.Context, email string) (int, error) { + limit, err := RedisClient.Get(ctx, fmt.Sprintf(UserSendEmailLimit, email)).Result() + if err != nil { + if err == redis.Nil { + return 0, nil + } + return 0, err + } + return strconv.Atoi(limit) +} + +func SetUserSendEmailLimit(ctx context.Context, email string) error { + key := fmt.Sprintf(UserSendEmailLimit, email) + count, err := RedisClient.Incr(ctx, key).Result() + if err != nil { + return err + } + if count > UserSendEmailLimitCount { + return errors.New("send email limit") + } + return RedisClient.Expire(ctx, key, UserSendEmailLimitTTL).Err() +} diff --git a/pkg/common/errors.go b/pkg/common/errors.go index 9b4540a..6f823fc 100644 --- a/pkg/common/errors.go +++ b/pkg/common/errors.go @@ -18,6 +18,8 @@ const ( CodeEmailExists = 1004 CodeEmailNotFound = 1005 CodePasswordMismatch = 1006 + CodeEmailCodeError = 1007 + CodeEmailSendLimit = 1008 ) const ( @@ -36,6 +38,8 @@ const ( CodeEmailExistsMsg = "email already registered" CodeEmailNotFoundMsg = "email not found" CodePasswordMismatchMsg = "password mismatch" + CodeEmailCodeErrorMsg = "email code error" + CodeEmailSendLimitMsg = "email send limit reached" ) type BusinessError struct { @@ -61,4 +65,6 @@ var ( ErrEmailExists = NewError(CodeEmailExists, CodeEmailExistsMsg, nil) ErrEmailNotFound = NewError(CodeEmailNotFound, CodeEmailNotFoundMsg, nil) ErrPasswordMismatch = NewError(CodePasswordMismatch, CodePasswordMismatchMsg, nil) + ErrEmailCodeError = NewError(CodeEmailCodeError, CodeEmailCodeErrorMsg, nil) + ErrEmailSendLimit = NewError(CodeEmailSendLimit, CodeEmailSendLimitMsg, nil) )