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

798 lines
26 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 (
"context"
"crypto/subtle"
"errors"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
apierrors "github.com/user-management-system/internal/pkg/errors"
"github.com/user-management-system/internal/service"
)
const (
refreshTokenCookieName = "ums_refresh_token"
sessionPresenceCookieName = "ums_session_present"
)
// newBackgroundCtx 创建用于后台 goroutine 的带超时独立 context与请求 context 无关)
func newBackgroundCtx(timeoutSec int) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
}
// ActivateEmailRequest 邮箱激活请求
type ActivateEmailRequest struct {
Token string `json:"token" binding:"required"`
}
// AuthHandler handles authentication requests
type AuthHandler struct {
authService *service.AuthService
}
// NewAuthHandler creates a new AuthHandler
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
// Register 用户注册
// @Summary 用户注册
// @Description 用户注册新账号,支持用户名+密码或手机号注册
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.RegisterRequest true "注册请求"
// @Success 201 {object} Response{data=service.UserInfo} "注册成功"
// @Failure 400 {object} Response{code=int,message=string} "请求参数错误"
// @Failure 409 {object} Response{code=int,message=string} "用户已存在"
// @Router /api/v1/auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password" binding:"required"`
Nickname string `json:"nickname"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
registerReq := &service.RegisterRequest{
Username: req.Username,
Email: req.Email,
Phone: req.Phone,
Password: req.Password,
Nickname: req.Nickname,
}
userInfo, err := h.authService.Register(c.Request.Context(), registerReq)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{
"code": 0,
"message": "success",
"data": userInfo,
})
}
// Login 用户登录
// @Summary 用户登录
// @Description 用户使用账号密码登录,支持多种认证方式(用户名/邮箱/手机号)
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.LoginRequest true "登录请求"
// @Success 200 {object} Response{data=service.LoginResponse} "登录成功"
// @Failure 400 {object} Response{code=int,message=string} "请求参数错误"
// @Failure 401 {object} Response{code=int,message=string} "认证失败"
// @Failure 429 {object} Response{code=int,message=string} "登录尝试过多"
// @Router /api/v1/auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req struct {
Account string `json:"account"`
Username string `json:"username"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
DeviceBrowser string `json:"device_browser"`
DeviceOS string `json:"device_os"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
loginReq := &service.LoginRequest{
Account: req.Account,
Username: req.Username,
Email: req.Email,
Phone: req.Phone,
Password: req.Password,
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceBrowser: req.DeviceBrowser,
DeviceOS: req.DeviceOS,
}
clientIP := c.ClientIP()
resp, err := h.authService.Login(c.Request.Context(), loginReq, clientIP)
if err != nil {
handleError(c, err)
return
}
setSessionCookies(c, h.authService, resp.RefreshToken)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": resp,
})
}
// VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证
// @Summary TOTP验证密码登录后
// @Description 当登录返回requires_totp=true时使用此接口完成TOTP验证
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body TOTPVerifyRequest true "TOTP验证请求"
// @Success 200 {object} Response{data=service.LoginResponse} "验证成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "TOTP验证失败"
// @Router /api/v1/auth/login/totp-verify [post]
func (h *AuthHandler) VerifyTOTPAfterPasswordLogin(c *gin.Context) {
var req struct {
UserID int64 `json:"user_id" binding:"required"`
Code string `json:"code" binding:"required"`
DeviceID string `json:"device_id"`
TempToken string `json:"temp_token"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
resp, err := h.authService.VerifyTOTPAfterPasswordLogin(
c.Request.Context(),
req.UserID,
req.Code,
req.DeviceID,
req.TempToken,
)
if err != nil {
handleError(c, err)
return
}
setSessionCookies(c, h.authService, resp.RefreshToken)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": resp,
})
}
// Logout 用户登出
// @Summary 用户登出
// @Description 使当前 access_token 和 refresh_token 失效
// @Tags 认证
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body service.LogoutRequest false "登出请求token可从header获取"
// @Success 200 {object} Response{code=int,message=string} "登出成功"
// @Router /api/v1/auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
var req struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// 允许 body 为空(仅凭 Authorization header 里的 access_token 注销也可以)
_ = c.ShouldBindJSON(&req)
// 如果 body 里没有 access_token则从 Authorization header 中取
if req.AccessToken == "" {
if bearer := c.GetHeader("Authorization"); len(bearer) > 7 {
req.AccessToken = bearer[7:] // 去掉 "Bearer "
}
}
if req.RefreshToken == "" {
req.RefreshToken, _ = c.Cookie(refreshTokenCookieName)
}
username, _ := c.Get("username")
usernameStr, _ := username.(string)
logoutReq := &service.LogoutRequest{
AccessToken: req.AccessToken,
RefreshToken: req.RefreshToken,
}
_ = h.authService.Logout(c.Request.Context(), usernameStr, logoutReq)
clearSessionCookies(c)
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}
// RefreshToken 刷新访问令牌
// @Summary 刷新访问令牌
// @Description 使用 refresh_token 获取新的 access_token
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body RefreshTokenRequest true "刷新令牌请求"
// @Success 200 {object} Response{data=service.LoginResponse} "刷新成功"
// @Failure 400 {object} Response{code=int,message=string} "请求参数错误"
// @Failure 401 {object} Response{code=int,message=string} "refresh_token无效或已过期"
// @Router /api/v1/auth/refresh-token [post]
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.RefreshToken == "" {
req.RefreshToken, _ = c.Cookie(refreshTokenCookieName)
}
if req.RefreshToken == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "refresh_token is required"})
return
}
resp, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken)
if err != nil {
handleError(c, err)
return
}
setSessionCookies(c, h.authService, resp.RefreshToken)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": resp,
})
}
// GetUserInfo 获取当前用户信息
// @Summary 获取当前用户信息
// @Description 获取已登录用户的详细信息
// @Tags 认证
// @Produce json
// @Security BearerAuth
// @Success 200 {object} Response{data=service.UserInfo} "用户信息"
// @Failure 401 {object} Response{code=int,message=string} "未认证"
// @Router /api/v1/auth/userinfo [get]
func (h *AuthHandler) GetUserInfo(c *gin.Context) {
userID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
userInfo, err := h.authService.GetUserInfo(c.Request.Context(), userID)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": userInfo,
})
}
// GetCSRFToken 获取CSRF令牌
// @Summary 获取CSRF令牌
// @Description 由于系统使用JWT Bearer Token认证不存在CSRF风险返回空token
// @Tags 认证
// @Produce json
// @Success 200 {object} map "CSRF token为空"
// @Router /api/v1/auth/csrf-token [get]
func (h *AuthHandler) GetCSRFToken(c *gin.Context) {
// 系统使用 JWT Bearer Token 认证Bearer Token 不会被浏览器自动携带(非 cookie
// 因此不存在传统意义上的 CSRF 风险,此端点返回空 token 作为兼容响应
c.JSON(http.StatusOK, gin.H{
"csrf_token": "",
"note": "JWT Bearer Token authentication; CSRF protection not required",
})
}
// GetAuthCapabilities 获取认证能力
// @Summary 获取系统认证能力
// @Description 返回系统支持的认证方式和配置如是否需要邮件激活、是否支持OAuth等
// @Tags 认证
// @Produce json
// @Success 200 {object} Response{data=service.AuthCapabilities} "认证能力配置"
// @Router /api/v1/auth/capabilities [get]
func (h *AuthHandler) GetAuthCapabilities(c *gin.Context) {
ctx := c.Request.Context()
caps := h.authService.GetAuthCapabilities(ctx)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": caps,
})
}
// OAuthLogin OAuth登录初始化
// @Summary OAuth登录初始化
// @Description 发起OAuth登录流程当前未配置
// @Tags OAuth
// @Produce json
// @Param provider path string true "OAuth提供商如 github, google"
// @Success 200 {object} Response "OAuth未配置"
// @Router /api/v1/auth/oauth/{provider} [get]
func (h *AuthHandler) OAuthLogin(c *gin.Context) {
provider := c.Param("provider")
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured", "data": gin.H{"provider": provider}})
}
// OAuthCallback OAuth回调
// @Summary OAuth回调处理
// @Description 处理OAuth provider回调当前未配置
// @Tags OAuth
// @Produce json
// @Param provider path string true "OAuth提供商"
// @Success 200 {object} Response "OAuth未配置"
// @Router /api/v1/auth/oauth/{provider}/callback [get]
func (h *AuthHandler) OAuthCallback(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured"})
}
// OAuthExchange OAuth令牌交换
// @Summary OAuth令牌交换
// @Description 使用OAuth code交换access_token当前未配置
// @Tags OAuth
// @Accept json
// @Produce json
// @Param provider path string true "OAuth提供商"
// @Success 200 {object} Response "OAuth未配置"
// @Router /api/v1/auth/oauth/{provider}/exchange [post]
func (h *AuthHandler) OAuthExchange(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured"})
}
// GetEnabledOAuthProviders 获取已启用的OAuth提供商
// @Summary 获取OAuth提供商列表
// @Description 返回系统已配置并启用的OAuth提供商列表
// @Tags OAuth
// @Produce json
// @Success 200 {object} Response{data=map} "提供商列表"
// @Router /api/v1/auth/oauth/providers [get]
func (h *AuthHandler) GetEnabledOAuthProviders(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"providers": []string{}}})
}
// ActivateEmail 激活邮箱
// @Summary 激活用户邮箱
// @Description 使用邮箱激活token激活用户账号
// @Tags 邮箱认证
// @Accept json
// @Produce json
// @Param request body ActivateEmailRequest true "激活请求"
// @Success 200 {object} Response "激活成功"
// @Failure 400 {object} Response "token缺失"
// @Failure 401 {object} Response "token无效或已过期"
// @Router /api/v1/auth/activate-email [post]
func (h *AuthHandler) ActivateEmail(c *gin.Context) {
var req ActivateEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "token is required"})
return
}
if err := h.authService.ActivateEmail(c.Request.Context(), req.Token); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email activated successfully"})
}
// ResendActivationEmail 重发激活邮件
// @Summary 重发激活邮件
// @Description 重新发送账号激活邮件(防枚举:无论邮箱是否注册都返回成功)
// @Tags 邮箱认证
// @Accept json
// @Produce json
// @Param request body ResendActivationRequest true "邮箱地址"
// @Success 200 {object} Response "激活邮件已发送(如果邮箱已注册)"
// @Failure 400 {object} Response "邮箱格式错误"
// @Router /api/v1/auth/resend-activation-email [post]
func (h *AuthHandler) ResendActivationEmail(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
if err := h.authService.ResendActivationEmail(c.Request.Context(), req.Email); err != nil {
handleError(c, err)
return
}
// 防枚举:无论邮箱是否存在,统一返回成功
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "activation email sent if address is registered"})
}
// SendEmailCode 发送邮箱验证码
// @Summary 发送邮箱验证码
// @Description 发送邮箱登录验证码(防枚举:无论邮箱是否注册都返回成功)
// @Tags 邮箱认证
// @Accept json
// @Produce json
// @Param request body SendEmailCodeRequest true "邮箱地址"
// @Success 200 {object} Response "验证码已发送"
// @Failure 400 {object} Response "邮箱格式错误"
// @Router /api/v1/auth/send-email-code [post]
func (h *AuthHandler) SendEmailCode(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// SendEmailLoginCode 内部会忽略未注册邮箱(防枚举),始终返回 ok
if err := h.authService.SendEmailLoginCode(c.Request.Context(), req.Email); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "验证码已发送"})
}
// LoginByEmailCode 使用邮箱验证码登录
// @Summary 邮箱验证码登录
// @Description 使用邮箱和验证码完成登录
// @Tags 邮箱认证
// @Accept json
// @Produce json
// @Param request body LoginByEmailCodeRequest true "登录请求"
// @Success 200 {object} Response{data=service.LoginResponse} "登录成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "验证码错误或已过期"
// @Router /api/v1/auth/login-by-email-code [post]
func (h *AuthHandler) LoginByEmailCode(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
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"`
}
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.LoginByEmailCode(c.Request.Context(), req.Email, req.Code, clientIP)
if err != nil {
handleError(c, err)
return
}
// 异步注册设备(不阻塞主流程)
// 注意:必须用 context.WithTimeout(context.Background()) 而非 c.Request.Context()
// gin 在 c.JSON 返回后会回收 contextgoroutine 中引用会得到已取消的 context
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,
})
}
// BootstrapAdmin 引导初始化管理员
// @Summary 引导初始化管理员账号
// @Description 在系统未配置管理员时创建第一个管理员账号需要BOOTSTRAP_SECRET
// @Tags 系统初始化
// @Accept json
// @Produce json
// @Security BootstrapSecret
// @Param X-Bootstrap-Secret header string true "引导密钥"
// @Param request body BootstrapAdminRequest true "管理员信息"
// @Success 201 {object} Response{data=service.UserInfo} "管理员创建成功"
// @Failure 401 {object} Response "引导密钥无效"
// @Failure 403 {object} Response "引导初始化未授权"
// @Router /api/v1/auth/bootstrap-admin [post]
func (h *AuthHandler) BootstrapAdmin(c *gin.Context) {
// P0 修复BootstrapAdmin 端点需要 bootstrap secret 验证
bootstrapSecret := os.Getenv("BOOTSTRAP_SECRET")
if bootstrapSecret == "" {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "引导初始化未授权"})
return
}
providedSecret := c.GetHeader("X-Bootstrap-Secret")
if providedSecret == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "缺少引导密钥"})
return
}
// 使用恒定时间比较防止时序攻击
if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(bootstrapSecret)) != 1 {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "引导密钥无效"})
return
}
var req struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
bootstrapReq := &service.BootstrapAdminRequest{
Username: req.Username,
Email: req.Email,
Password: req.Password,
}
clientIP := c.ClientIP()
resp, err := h.authService.BootstrapAdmin(c.Request.Context(), bootstrapReq, clientIP)
if err != nil {
handleError(c, err)
return
}
setSessionCookies(c, h.authService, resp.RefreshToken)
c.JSON(http.StatusCreated, gin.H{
"code": 0,
"message": "success",
"data": resp,
})
}
// SendEmailBindCode 发送邮箱绑定验证码
// @Summary 发送邮箱绑定验证码
// @Description 发送验证码到邮箱以绑定邮箱(当前未配置)
// @Tags 邮箱绑定
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/email/bind/send [post]
func (h *AuthHandler) SendEmailBindCode(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email bind not configured"})
}
// BindEmail 绑定邮箱
// @Summary 绑定邮箱
// @Description 使用邮箱验证码绑定账号(当前未配置)
// @Tags 邮箱绑定
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/email/bind [post]
func (h *AuthHandler) BindEmail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email bind not configured"})
}
// UnbindEmail 解绑邮箱
// @Summary 解绑邮箱
// @Description 解绑账号关联的邮箱(当前未配置)
// @Tags 邮箱绑定
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/email/unbind [post]
func (h *AuthHandler) UnbindEmail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email unbind not configured"})
}
// SendPhoneBindCode 发送手机绑定验证码
// @Summary 发送手机绑定验证码
// @Description 发送验证码到手机以绑定手机号(当前未配置)
// @Tags 手机绑定
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/phone/bind/send [post]
func (h *AuthHandler) SendPhoneBindCode(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone bind not configured"})
}
// BindPhone 绑定手机号
// @Summary 绑定手机号
// @Description 使用手机验证码绑定账号(当前未配置)
// @Tags 手机绑定
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/phone/bind [post]
func (h *AuthHandler) BindPhone(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone bind not configured"})
}
// UnbindPhone 解绑手机号
// @Summary 解绑手机号
// @Description 解绑账号关联的手机号(当前未配置)
// @Tags 手机绑定
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/phone/unbind [post]
func (h *AuthHandler) UnbindPhone(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone unbind not configured"})
}
// GetSocialAccounts 获取社交账号列表
// @Summary 获取已绑定的社交账号列表
// @Description 获取当前用户绑定的第三方社交账号列表
// @Tags 社交账号
// @Produce json
// @Security BearerAuth
// @Success 200 {object} Response "社交账号列表"
// @Router /api/v1/auth/social-accounts [get]
func (h *AuthHandler) GetSocialAccounts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"accounts": []interface{}{}}})
}
// BindSocialAccount 绑定社交账号
// @Summary 绑定社交账号
// @Description 绑定第三方社交账号到当前用户(当前未配置)
// @Tags 社交账号
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/social/bind [post]
func (h *AuthHandler) BindSocialAccount(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "social binding not configured"})
}
// UnbindSocialAccount 解绑社交账号
// @Summary 解绑社交账号
// @Description 解绑当前用户关联的第三方社交账号(当前未配置)
// @Tags 社交账号
// @Accept json
// @Produce json
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/social/unbind [post]
func (h *AuthHandler) UnbindSocialAccount(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "social unbinding not configured"})
}
func (h *AuthHandler) SupportsEmailCodeLogin() bool {
return h.authService.HasEmailCodeService()
}
func getUserIDFromContext(c *gin.Context) (int64, bool) {
userID, exists := c.Get("user_id")
if !exists {
return 0, false
}
id, ok := userID.(int64)
return id, ok
}
func setSessionCookies(c *gin.Context, authService *service.AuthService, refreshToken string) {
if c == nil || strings.TrimSpace(refreshToken) == "" {
return
}
maxAge := 0
if authService != nil {
if ttl := authService.RefreshTokenTTLSeconds(); ttl > 0 {
maxAge = int(ttl)
}
}
secure := requestUsesHTTPS(c)
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(refreshTokenCookieName, refreshToken, maxAge, "/", "", secure, true)
c.SetCookie(sessionPresenceCookieName, "1", maxAge, "/", "", secure, false)
}
func clearSessionCookies(c *gin.Context) {
if c == nil {
return
}
secure := requestUsesHTTPS(c)
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(refreshTokenCookieName, "", -1, "/", "", secure, true)
c.SetCookie(sessionPresenceCookieName, "", -1, "/", "", secure, false)
}
func requestUsesHTTPS(c *gin.Context) bool {
if c == nil || c.Request == nil {
return false
}
if c.Request.TLS != nil {
return true
}
return strings.EqualFold(strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")), "https")
}
// handleError 将 error 转换为对应的 HTTP 响应。
// 优先识别 ApplicationError其次通过关键词推断业务错误类型兜底返回 500。
func handleError(c *gin.Context, err error) {
if err == nil {
return
}
// 优先尝试 ApplicationError内置 HTTP 状态码)
var appErr *apierrors.ApplicationError
if errors.As(err, &appErr) {
c.JSON(int(appErr.Code), gin.H{"code": appErr.Code, "message": appErr.Message})
return
}
// 对普通 errors.New 按关键词推断语义,但只返回通用错误信息给客户端
httpCode := classifyErrorMessage(err.Error())
c.JSON(httpCode, gin.H{"code": httpCode, "message": "服务器内部错误"})
}
// classifyErrorMessage 通过错误信息关键词推断 HTTP 状态码,避免业务错误被 500 吞掉
func classifyErrorMessage(msg string) int {
lower := strings.ToLower(msg)
switch {
case contains(lower, "not found", "不存在", "找不到"):
return http.StatusNotFound
case contains(lower, "already exists", "已存在", "已注册", "duplicate"):
return http.StatusConflict
case contains(lower, "unauthorized", "invalid token", "token", "令牌", "未认证"):
return http.StatusUnauthorized
case contains(lower, "forbidden", "permission", "权限", "禁止"):
return http.StatusForbidden
case contains(lower, "invalid", "required", "must", "cannot be empty", "不能为空",
"格式", "参数", "密码不正确", "incorrect", "wrong", "too short", "too long",
"已失效", "expired", "验证码不正确", "不能与"):
return http.StatusBadRequest
case contains(lower, "locked", "too many", "账号已被锁定", "rate limit"):
return http.StatusTooManyRequests
default:
return http.StatusInternalServerError
}
}
// contains 检查 s 是否包含 keywords 中的任意一个
func contains(s string, keywords ...string) bool {
for _, kw := range keywords {
if strings.Contains(s, kw) {
return true
}
}
return false
}