Files
user-system/internal/api/handler/sms_handler.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

127 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/service"
)
// SMSHandler handles SMS requests
type SMSHandler struct {
authService *service.AuthService
smsCodeService *service.SMSCodeService
}
// SMSLoginRequest 短信登录请求
type SMSLoginRequest struct {
Phone string `json:"phone" binding:"required"`
Code string `json:"code" binding:"required"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
DeviceBrowser string `json:"device_browser"`
DeviceOS string `json:"device_os"`
}
// NewSMSHandler creates a SMSHandler backed by AuthService + SMSCodeService.
// If both services are nil, the handler will return 503 for all requests.
func NewSMSHandler(authService *service.AuthService, smsCodeService *service.SMSCodeService) *SMSHandler {
return &SMSHandler{
authService: authService,
smsCodeService: smsCodeService,
}
}
// SendCode 发送短信验证码
// @Summary 发送短信验证码
// @Description 向指定手机号发送短信验证码(用于注册或登录)
// @Tags 短信验证
// @Accept json
// @Produce json
// @Param request body service.SendCodeRequest true "发送验证码请求"
// @Success 200 {object} Response "发送成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 503 {object} Response "短信服务未配置"
// @Router /api/v1/sms/send [post]
func (h *SMSHandler) SendCode(c *gin.Context) {
if h.smsCodeService == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS service not configured"})
return
}
var req service.SendCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
resp, err := h.smsCodeService.SendCode(c.Request.Context(), &req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": resp,
})
}
// LoginByCode 短信验证码登录
// @Summary 短信验证码登录
// @Description 使用手机号和短信验证码登录(带设备信息以支持设备信任链路)
// @Tags 短信验证
// @Accept json
// @Produce json
// @Param request body SMSLoginRequest true "登录请求"
// @Success 200 {object} Response "登录成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "验证码错误"
// @Failure 503 {object} Response "短信登录未配置"
// @Router /api/v1/sms/login [post]
func (h *SMSHandler) LoginByCode(c *gin.Context) {
if h.authService == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS login not configured"})
return
}
var req SMSLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
clientIP := c.ClientIP()
resp, err := h.authService.LoginByCode(c.Request.Context(), req.Phone, req.Code, clientIP)
if err != nil {
handleError(c, err)
return
}
// 自动注册/更新设备记录(不阻塞主流程)
// 注意:必须用独立的 background context不能用 c.Request.Context()gin 回收后会取消)
if req.DeviceID != "" && resp != nil && resp.User != nil {
loginReq := &service.LoginRequest{
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceBrowser: req.DeviceBrowser,
DeviceOS: req.DeviceOS,
}
userID := resp.User.ID
go func() {
devCtx, cancel := newBackgroundCtx(5)
defer cancel()
h.authService.BestEffortRegisterDevicePublic(devCtx, userID, loginReq)
}()
}
setSessionCookies(c, h.authService, resp.RefreshToken)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": resp,
})
}