Files
user-system/internal/service/auth_email.go
long-agent 3f3bb82f1d fix: v6 code review P0 auth/IDOR fixes + frontend regression patches
Backend fixes:
- auth_handler: P0 认证逻辑修复
- ratelimit: 限速中间件增强 + 新增单元测试
- auth_service: 认证服务逻辑完善 + 新增测试
- server: server 配置增强 + 新增测试
- handler_test: 新增 handler 层集成测试
- auth_bootstrap_test: bootstrap 路径测试

Frontend patches:
- LoginPage/RegisterPage: CSRF + 表单交互修复
- BootstrapAdminPage: 引导流程修复
- DevicesPage: 设备管理页修复
- auth/social-accounts/users/webhooks services: 类型修正
- csrf.ts: CSRF token 处理修正
- E2E 脚本: CDP smoke + auth e2e 增强

Docs:
- FULL_CODE_REVIEW_REPORT_2026-04-20
- report-v6 执行计划
- REAL_PROJECT_STATUS 更新
- .gitignore: 新增 .gocache-*/config.yaml 排除

验证: go build/vet 0错误, go test 42/42 PASS, 0 FAIL
2026-04-23 07:14:12 +08:00

211 lines
5.6 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/domain"
)
func (s *AuthService) SetEmailActivationService(svc *EmailActivationService) {
s.emailActivationSvc = svc
}
func (s *AuthService) SetEmailCodeService(svc *EmailCodeService) {
s.emailCodeSvc = svc
}
// HasEmailCodeService 判断邮箱验证码登录服务是否已配置
func (s *AuthService) HasEmailCodeService() bool {
return s != nil && s.emailCodeSvc != nil
}
func (s *AuthService) RegisterWithActivation(ctx context.Context, req *RegisterRequest) (*UserInfo, error) {
if err := s.validatePassword(req.Password); err != nil {
return nil, err
}
if err := s.verifyPhoneRegistration(ctx, req); err != nil {
return nil, err
}
exists, err := s.userRepo.ExistsByUsername(ctx, req.Username)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("username already exists")
}
if req.Email != "" {
exists, err = s.userRepo.ExistsByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("email already exists")
}
}
if req.Phone != "" {
exists, err = s.userRepo.ExistsByPhone(ctx, req.Phone)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("phone already exists")
}
}
hashedPassword, err := auth.HashPassword(req.Password)
if err != nil {
return nil, err
}
initialStatus := domain.UserStatusActive
if s.emailActivationSvc != nil && req.Email != "" {
initialStatus = domain.UserStatusInactive
}
nickname := req.Nickname
if nickname == "" {
nickname = req.Username
}
user := &domain.User{
Username: req.Username,
Email: domain.StrPtr(req.Email),
Phone: domain.StrPtr(req.Phone),
Password: hashedPassword,
Nickname: nickname,
Status: initialStatus,
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
s.bestEffortAssignDefaultRoles(ctx, user.ID, "register_with_activation")
if s.emailActivationSvc != nil && req.Email != "" {
// #nosec G118 - 使用独立上下文避免请求结束后被取消
go func() { // #nosec G118
bgCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.emailActivationSvc.SendActivationEmail(bgCtx, user.ID, req.Email, nickname); err != nil {
log.Printf("auth: send activation email failed, user_id=%d email=%s err=%v", user.ID, req.Email, err)
}
}()
}
userInfo := s.buildUserInfo(user)
s.publishEvent(ctx, domain.EventUserRegistered, userInfo)
return userInfo, nil
}
func (s *AuthService) ActivateEmail(ctx context.Context, token string) error {
if s.emailActivationSvc == nil {
return errors.New("email activation service is not configured")
}
userID, err := s.emailActivationSvc.ValidateActivationToken(ctx, token)
if err != nil {
return err
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
if user.Status == domain.UserStatusActive {
return errors.New("account already activated")
}
if user.Status != domain.UserStatusInactive {
return errors.New("account status does not allow activation")
}
return s.userRepo.UpdateStatus(ctx, userID, domain.UserStatusActive)
}
func (s *AuthService) ResendActivationEmail(ctx context.Context, email string) error {
if s.emailActivationSvc == nil {
return errors.New("email activation service is not configured")
}
user, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
if isUserNotFoundError(err) {
return nil
}
return err
}
if user.Status == domain.UserStatusActive {
return nil
}
if user.Status != domain.UserStatusInactive {
return errors.New("account status does not allow activation")
}
nickname := user.Nickname
if nickname == "" {
nickname = user.Username
}
return s.emailActivationSvc.SendActivationEmail(ctx, user.ID, email, nickname)
}
func (s *AuthService) SendEmailLoginCode(ctx context.Context, email string) error {
if s.emailCodeSvc == nil {
return errors.New("email code service is not configured")
}
_, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
if isUserNotFoundError(err) {
return nil
}
return err
}
return s.emailCodeSvc.SendEmailCode(ctx, email, "login")
}
func (s *AuthService) LoginByEmailCode(ctx context.Context, email, code, ip string) (*LoginResponse, error) {
if s.emailCodeSvc == nil {
return nil, errors.New("email code login is disabled")
}
if err := s.emailCodeSvc.VerifyEmailCode(ctx, email, "login", code); err != nil {
s.writeLoginLog(ctx, nil, domain.LoginTypeEmailCode, ip, false, err.Error())
return nil, err
}
user, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
if isUserNotFoundError(err) {
s.writeLoginLog(ctx, nil, domain.LoginTypeEmailCode, ip, false, "email not registered")
return nil, errors.New("email not registered")
}
s.writeLoginLog(ctx, nil, domain.LoginTypeEmailCode, ip, false, err.Error())
return nil, err
}
if err := s.ensureUserActive(user); err != nil {
s.writeLoginLog(ctx, &user.ID, domain.LoginTypeEmailCode, ip, false, err.Error())
s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", false)
return nil, err
}
s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "email_code")
s.writeLoginLog(ctx, &user.ID, domain.LoginTypeEmailCode, ip, true, "")
s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", true)
s.publishEvent(ctx, domain.EventUserLogin, map[string]interface{}{
"user_id": user.ID,
"username": user.Username,
"ip": ip,
"method": "email_code",
})
return s.generateLoginResponseWithoutRemember(ctx, user)
}