| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767 |
- package handler
- import (
- "context"
- "log/slog"
- "strings"
- "sync"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/handler/dto"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // AuthHandler handles authentication-related requests
- type AuthHandler struct {
- cfg *config.Config
- authService *service.AuthService
- userService *service.UserService
- settingSvc *service.SettingService
- promoService *service.PromoService
- redeemService *service.RedeemService
- totpService *service.TotpService
- userAttributeService *service.UserAttributeService
- dingTalkClientInstance *DingTalkClient
- dingTalkClientMu sync.Mutex
- }
- // NewAuthHandler creates a new AuthHandler
- func NewAuthHandler(cfg *config.Config, authService *service.AuthService, userService *service.UserService, settingService *service.SettingService, promoService *service.PromoService, redeemService *service.RedeemService, totpService *service.TotpService, userAttributeService *service.UserAttributeService) *AuthHandler {
- return &AuthHandler{
- cfg: cfg,
- authService: authService,
- userService: userService,
- settingSvc: settingService,
- promoService: promoService,
- redeemService: redeemService,
- totpService: totpService,
- userAttributeService: userAttributeService,
- }
- }
- // RegisterRequest represents the registration request payload
- type RegisterRequest struct {
- Email string `json:"email" binding:"required,email"`
- Password string `json:"password" binding:"required,min=6"`
- VerifyCode string `json:"verify_code"`
- TurnstileToken string `json:"turnstile_token"`
- TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
- TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
- PromoCode string `json:"promo_code"` // 注册优惠码
- InvitationCode string `json:"invitation_code"` // 邀请码
- AffCode string `json:"aff_code"` // 邀请返利码
- }
- // SendVerifyCodeRequest 发送验证码请求
- type SendVerifyCodeRequest struct {
- Email string `json:"email" binding:"required,email"`
- TurnstileToken string `json:"turnstile_token"`
- TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
- TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
- }
- // SendVerifyCodeResponse 发送验证码响应
- type SendVerifyCodeResponse struct {
- Message string `json:"message"`
- Countdown int `json:"countdown"` // 倒计时秒数
- }
- // LoginRequest represents the login request payload
- type LoginRequest struct {
- Email string `json:"email" binding:"required,email"`
- Password string `json:"password" binding:"required"`
- TurnstileToken string `json:"turnstile_token"`
- TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
- TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
- }
- func captchaProof(turnstileToken, tencentTicket, tencentRandstr string) service.CaptchaProof {
- return service.CaptchaProof{
- TurnstileToken: turnstileToken,
- TencentTicket: tencentTicket,
- TencentRandstr: tencentRandstr,
- }
- }
- // AuthResponse 认证响应格式(匹配前端期望)
- type AuthResponse struct {
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token,omitempty"` // 新增:Refresh Token
- ExpiresIn int `json:"expires_in,omitempty"` // 新增:Access Token有效期(秒)
- TokenType string `json:"token_type"`
- User *dto.User `json:"user"`
- }
- func ensureLoginUserActive(user *service.User) error {
- if user == nil {
- return infraerrors.Unauthorized("INVALID_USER", "user not found")
- }
- if !user.IsActive() {
- return service.ErrUserNotActive
- }
- return nil
- }
- // respondWithTokenPair 生成 Token 对并返回认证响应
- // 如果 Token 对生成失败,回退到只返回 Access Token(向后兼容)
- func (h *AuthHandler) respondWithTokenPair(c *gin.Context, user *service.User) {
- respondWithTokenPair(c, h.authService, user)
- }
- func respondWithTokenPair(c *gin.Context, authService *service.AuthService, user *service.User) {
- if err := ensureLoginUserActive(user); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- tokenPair, err := authService.GenerateTokenPair(c.Request.Context(), user, "")
- if err != nil {
- slog.Error("failed to generate token pair", "error", err, "user_id", user.ID)
- // 回退到只返回Access Token
- token, tokenErr := authService.GenerateToken(c.Request.Context(), user)
- if tokenErr != nil {
- response.InternalError(c, "Failed to generate token")
- return
- }
- response.Success(c, AuthResponse{
- AccessToken: token,
- TokenType: "Bearer",
- User: dto.UserFromService(user),
- })
- return
- }
- response.Success(c, AuthResponse{
- AccessToken: tokenPair.AccessToken,
- RefreshToken: tokenPair.RefreshToken,
- ExpiresIn: tokenPair.ExpiresIn,
- TokenType: "Bearer",
- User: dto.UserFromService(user),
- })
- }
- func (h *AuthHandler) ensureBackendModeAllowsUser(ctx context.Context, user *service.User) error {
- if user == nil {
- return infraerrors.Unauthorized("INVALID_USER", "user not found")
- }
- if h == nil || !h.isBackendModeEnabled(ctx) || user.IsAdmin() {
- return nil
- }
- return infraerrors.Forbidden("BACKEND_MODE_ADMIN_ONLY", "Backend mode is active. Only admin login is allowed.")
- }
- func (h *AuthHandler) ensureBackendModeAllowsNewUserLogin(ctx context.Context) error {
- if h == nil || !h.isBackendModeEnabled(ctx) {
- return nil
- }
- return infraerrors.Forbidden("BACKEND_MODE_ADMIN_ONLY", "Backend mode is active. Only admin login is allowed.")
- }
- func (h *AuthHandler) isBackendModeEnabled(ctx context.Context) bool {
- if h == nil || h.settingSvc == nil {
- return false
- }
- settings, err := h.settingSvc.GetPublicSettings(ctx)
- if err == nil && settings != nil {
- return settings.BackendModeEnabled
- }
- return h.settingSvc.IsBackendModeEnabled(ctx)
- }
- // Register handles user registration
- // POST /api/v1/auth/register
- func (h *AuthHandler) Register(c *gin.Context) {
- var req RegisterRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- // 验证当前启用的验证码(邮箱验证码注册场景避免重复校验一次性票据)
- proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
- if err := h.authService.VerifyCaptchaForRegister(c.Request.Context(), proof, ip.GetClientIP(c), req.VerifyCode); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- _, user, err := h.authService.RegisterWithVerification(
- c.Request.Context(),
- req.Email,
- req.Password,
- req.VerifyCode,
- req.PromoCode,
- req.InvitationCode,
- req.AffCode,
- )
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- h.respondWithTokenPair(c, user)
- }
- // SendVerifyCode 发送邮箱验证码
- // POST /api/v1/auth/send-verify-code
- func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
- var req SendVerifyCodeRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
- if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- result, err := h.authService.SendVerifyCodeAsync(c.Request.Context(), req.Email, c.GetHeader("Accept-Language"))
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, SendVerifyCodeResponse{
- Message: "Verification code sent successfully",
- Countdown: result.Countdown,
- })
- }
- // Login handles user login
- // POST /api/v1/auth/login
- func (h *AuthHandler) Login(c *gin.Context) {
- var req LoginRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
- if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- token, user, err := h.authService.Login(c.Request.Context(), req.Email, req.Password)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- _ = token // token 由 authService.Login 返回但此处由 respondWithTokenPair 重新生成
- if err := h.ensureBackendModeAllowsUser(c.Request.Context(), user); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // Check if TOTP 2FA is enabled for this user
- if h.totpService != nil && h.settingSvc.IsTotpEnabled(c.Request.Context()) && user.TotpEnabled {
- // Create a temporary login session for 2FA
- tempToken, err := h.totpService.CreateLoginSession(c.Request.Context(), user.ID, user.Email)
- if err != nil {
- response.InternalError(c, "Failed to create 2FA session")
- return
- }
- response.Success(c, TotpLoginResponse{
- Requires2FA: true,
- TempToken: tempToken,
- UserEmailMasked: service.MaskEmail(user.Email),
- })
- return
- }
- h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
- h.respondWithTokenPair(c, user)
- }
- // TotpLoginResponse represents the response when 2FA is required
- type TotpLoginResponse struct {
- Requires2FA bool `json:"requires_2fa"`
- TempToken string `json:"temp_token,omitempty"`
- UserEmailMasked string `json:"user_email_masked,omitempty"`
- }
- // Login2FARequest represents the 2FA login request
- type Login2FARequest struct {
- TempToken string `json:"temp_token" binding:"required"`
- TotpCode string `json:"totp_code" binding:"required,len=6"`
- }
- // Login2FA completes the login with 2FA verification
- // POST /api/v1/auth/login/2fa
- func (h *AuthHandler) Login2FA(c *gin.Context) {
- var req Login2FARequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- slog.Debug("login_2fa_request",
- "temp_token_len", len(req.TempToken),
- "totp_code_len", len(req.TotpCode))
- // Get the login session
- session, err := h.totpService.GetLoginSession(c.Request.Context(), req.TempToken)
- if err != nil || session == nil {
- tokenPrefix := ""
- if len(req.TempToken) >= 8 {
- tokenPrefix = req.TempToken[:8]
- }
- slog.Debug("login_2fa_session_invalid",
- "temp_token_prefix", tokenPrefix,
- "error", err)
- response.BadRequest(c, "Invalid or expired 2FA session")
- return
- }
- slog.Debug("login_2fa_session_found",
- "user_id", session.UserID,
- "email", session.Email)
- // Verify the TOTP code
- if err := h.totpService.VerifyCode(c.Request.Context(), session.UserID, req.TotpCode); err != nil {
- slog.Debug("login_2fa_verify_failed",
- "user_id", session.UserID,
- "error", err)
- response.ErrorFrom(c, err)
- return
- }
- // Get the user (before session deletion so we can check backend mode)
- user, err := h.userService.GetByID(c.Request.Context(), session.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if err := ensureLoginUserActive(user); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if err := h.ensureBackendModeAllowsUser(c.Request.Context(), user); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if session.PendingOAuthBind != nil {
- pendingSvc, err := h.pendingIdentityService()
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- pendingSession, err := pendingSvc.GetBrowserSession(
- c.Request.Context(),
- session.PendingOAuthBind.PendingSessionToken,
- session.PendingOAuthBind.BrowserSessionKey,
- )
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- decision, err := h.ensurePendingOAuthAdoptionDecision(c, pendingSession.ID, oauthAdoptionDecisionRequest{})
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if err := applyPendingOAuthBinding(
- c.Request.Context(),
- h.entClient(),
- h.authService,
- h.userService,
- pendingSession,
- decision,
- &user.ID,
- true,
- true,
- ); err != nil {
- response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_BIND_APPLY_FAILED", "failed to bind pending oauth identity").WithCause(err))
- return
- }
- if _, err := pendingSvc.ConsumeBrowserSession(
- c.Request.Context(),
- pendingSession.SessionToken,
- pendingSession.BrowserSessionKey,
- ); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- secureCookie := isRequestHTTPS(c)
- clearOAuthPendingSessionCookie(c, secureCookie)
- clearOAuthPendingBrowserCookie(c, secureCookie)
- h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
- user, err = h.userService.GetByID(c.Request.Context(), session.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- }
- // Delete the login session (only after all checks pass)
- _ = h.totpService.DeleteLoginSession(c.Request.Context(), req.TempToken)
- if session.PendingOAuthBind == nil {
- h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
- }
- h.respondWithTokenPair(c, user)
- }
- // GetCurrentUser handles getting current authenticated user
- // GET /api/v1/auth/me
- func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- user, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- identities, err := h.userService.GetProfileIdentitySummaries(c.Request.Context(), subject.UserID, user)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- type UserResponse struct {
- userProfileResponse
- RunMode string `json:"run_mode"`
- }
- runMode := config.RunModeStandard
- if h.cfg != nil {
- runMode = h.cfg.RunMode
- }
- response.Success(c, UserResponse{
- userProfileResponse: userProfileResponseFromService(user, identities),
- RunMode: runMode,
- })
- }
- // ValidatePromoCodeRequest 验证优惠码请求
- type ValidatePromoCodeRequest struct {
- Code string `json:"code" binding:"required"`
- }
- // ValidatePromoCodeResponse 验证优惠码响应
- type ValidatePromoCodeResponse struct {
- Valid bool `json:"valid"`
- BonusAmount float64 `json:"bonus_amount,omitempty"`
- ErrorCode string `json:"error_code,omitempty"`
- Message string `json:"message,omitempty"`
- }
- // ValidatePromoCode 验证优惠码(公开接口,注册前调用)
- // POST /api/v1/auth/validate-promo-code
- func (h *AuthHandler) ValidatePromoCode(c *gin.Context) {
- // 检查优惠码功能是否启用
- if h.settingSvc != nil && !h.settingSvc.IsPromoCodeEnabled(c.Request.Context()) {
- response.Success(c, ValidatePromoCodeResponse{
- Valid: false,
- ErrorCode: "PROMO_CODE_DISABLED",
- })
- return
- }
- var req ValidatePromoCodeRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- promoCode, err := h.promoService.ValidatePromoCode(c.Request.Context(), req.Code)
- if err != nil {
- // 根据错误类型返回对应的错误码
- errorCode := "PROMO_CODE_INVALID"
- switch err {
- case service.ErrPromoCodeNotFound:
- errorCode = "PROMO_CODE_NOT_FOUND"
- case service.ErrPromoCodeExpired:
- errorCode = "PROMO_CODE_EXPIRED"
- case service.ErrPromoCodeDisabled:
- errorCode = "PROMO_CODE_DISABLED"
- case service.ErrPromoCodeMaxUsed:
- errorCode = "PROMO_CODE_MAX_USED"
- case service.ErrPromoCodeAlreadyUsed:
- errorCode = "PROMO_CODE_ALREADY_USED"
- }
- response.Success(c, ValidatePromoCodeResponse{
- Valid: false,
- ErrorCode: errorCode,
- })
- return
- }
- if promoCode == nil {
- response.Success(c, ValidatePromoCodeResponse{
- Valid: false,
- ErrorCode: "PROMO_CODE_INVALID",
- })
- return
- }
- response.Success(c, ValidatePromoCodeResponse{
- Valid: true,
- BonusAmount: promoCode.BonusAmount,
- })
- }
- // ValidateInvitationCodeRequest 验证邀请码请求
- type ValidateInvitationCodeRequest struct {
- Code string `json:"code" binding:"required"`
- }
- // ValidateInvitationCodeResponse 验证邀请码响应
- type ValidateInvitationCodeResponse struct {
- Valid bool `json:"valid"`
- ErrorCode string `json:"error_code,omitempty"`
- }
- // ValidateInvitationCode 验证邀请码(公开接口,注册前调用)
- // POST /api/v1/auth/validate-invitation-code
- func (h *AuthHandler) ValidateInvitationCode(c *gin.Context) {
- // 检查邀请码功能是否启用
- if h.settingSvc == nil || !h.settingSvc.IsInvitationCodeEnabled(c.Request.Context()) {
- response.Success(c, ValidateInvitationCodeResponse{
- Valid: false,
- ErrorCode: "INVITATION_CODE_DISABLED",
- })
- return
- }
- var req ValidateInvitationCodeRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- // 验证邀请码
- redeemCode, err := h.redeemService.GetByCode(c.Request.Context(), req.Code)
- if err != nil {
- response.Success(c, ValidateInvitationCodeResponse{
- Valid: false,
- ErrorCode: "INVITATION_CODE_NOT_FOUND",
- })
- return
- }
- // 检查类型和状态
- if redeemCode.Type != service.RedeemTypeInvitation {
- response.Success(c, ValidateInvitationCodeResponse{
- Valid: false,
- ErrorCode: "INVITATION_CODE_INVALID",
- })
- return
- }
- if redeemCode.Status != service.StatusUnused {
- response.Success(c, ValidateInvitationCodeResponse{
- Valid: false,
- ErrorCode: "INVITATION_CODE_USED",
- })
- return
- }
- response.Success(c, ValidateInvitationCodeResponse{
- Valid: true,
- })
- }
- // ForgotPasswordRequest 忘记密码请求
- type ForgotPasswordRequest struct {
- Email string `json:"email" binding:"required,email"`
- TurnstileToken string `json:"turnstile_token"`
- TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
- TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
- }
- // ForgotPasswordResponse 忘记密码响应
- type ForgotPasswordResponse struct {
- Message string `json:"message"`
- }
- // ForgotPassword 请求密码重置
- // POST /api/v1/auth/forgot-password
- func (h *AuthHandler) ForgotPassword(c *gin.Context) {
- var req ForgotPasswordRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
- if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- frontendBaseURL := strings.TrimSpace(h.settingSvc.GetFrontendURL(c.Request.Context()))
- if frontendBaseURL == "" {
- slog.Error("frontend_url not configured in settings or config; cannot build password reset link")
- response.InternalError(c, "Password reset is not configured")
- return
- }
- // Request password reset (async)
- // Note: This returns success even if email doesn't exist (to prevent enumeration)
- if err := h.authService.RequestPasswordResetAsync(c.Request.Context(), req.Email, frontendBaseURL, c.GetHeader("Accept-Language")); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, ForgotPasswordResponse{
- Message: "If your email is registered, you will receive a password reset link shortly.",
- })
- }
- // ResetPasswordRequest 重置密码请求
- type ResetPasswordRequest struct {
- Email string `json:"email" binding:"required,email"`
- Token string `json:"token" binding:"required"`
- NewPassword string `json:"new_password" binding:"required,min=6"`
- }
- // ResetPasswordResponse 重置密码响应
- type ResetPasswordResponse struct {
- Message string `json:"message"`
- }
- // ResetPassword 重置密码
- // POST /api/v1/auth/reset-password
- func (h *AuthHandler) ResetPassword(c *gin.Context) {
- var req ResetPasswordRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- // Reset password
- if err := h.authService.ResetPassword(c.Request.Context(), req.Email, req.Token, req.NewPassword); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, ResetPasswordResponse{
- Message: "Your password has been reset successfully. You can now log in with your new password.",
- })
- }
- // ==================== Token Refresh Endpoints ====================
- // RefreshTokenRequest 刷新Token请求
- type RefreshTokenRequest struct {
- RefreshToken string `json:"refresh_token" binding:"required"`
- }
- // RefreshTokenResponse 刷新Token响应
- type RefreshTokenResponse struct {
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
- ExpiresIn int `json:"expires_in"` // Access Token有效期(秒)
- TokenType string `json:"token_type"`
- }
- // RefreshToken 刷新Token
- // POST /api/v1/auth/refresh
- func (h *AuthHandler) RefreshToken(c *gin.Context) {
- var req RefreshTokenRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- result, err := h.authService.RefreshTokenPair(c.Request.Context(), req.RefreshToken)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // Backend mode: block non-admin token refresh
- if h.settingSvc.IsBackendModeEnabled(c.Request.Context()) && !service.IsAdminRole(result.UserRole) {
- response.Forbidden(c, "Backend mode is active. Only admin login is allowed.")
- return
- }
- response.Success(c, RefreshTokenResponse{
- AccessToken: result.AccessToken,
- RefreshToken: result.RefreshToken,
- ExpiresIn: result.ExpiresIn,
- TokenType: "Bearer",
- })
- }
- // LogoutRequest 登出请求
- type LogoutRequest struct {
- RefreshToken string `json:"refresh_token,omitempty"` // 可选:撤销指定的Refresh Token
- }
- // LogoutResponse 登出响应
- type LogoutResponse struct {
- Message string `json:"message"`
- }
- // Logout 用户登出
- // POST /api/v1/auth/logout
- func (h *AuthHandler) Logout(c *gin.Context) {
- var req LogoutRequest
- // 允许空请求体(向后兼容)
- _ = c.ShouldBindJSON(&req)
- // 如果提供了Refresh Token,撤销它
- if req.RefreshToken != "" {
- if err := h.authService.RevokeRefreshToken(c.Request.Context(), req.RefreshToken); err != nil {
- slog.Debug("failed to revoke refresh token", "error", err)
- // 不影响登出流程
- }
- }
- h.consumePendingOAuthSessionOnLogout(c)
- clearOAuthLogoutCookies(c)
- response.Success(c, LogoutResponse{
- Message: "Logged out successfully",
- })
- }
- // RevokeAllSessionsResponse 撤销所有会话响应
- type RevokeAllSessionsResponse struct {
- Message string `json:"message"`
- }
- // RevokeAllSessions 撤销当前用户的所有会话
- // POST /api/v1/auth/revoke-all-sessions
- func (h *AuthHandler) RevokeAllSessions(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- if err := h.authService.RevokeAllUserTokens(c.Request.Context(), subject.UserID); err != nil {
- slog.Error("failed to revoke all sessions", "user_id", subject.UserID, "error", err)
- response.InternalError(c, "Failed to revoke sessions")
- return
- }
- response.Success(c, RevokeAllSessionsResponse{
- Message: "All sessions have been revoked. Please log in again.",
- })
- }
|