gemini_oauth_handler.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package admin
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. "github.com/gin-gonic/gin"
  8. )
  9. type GeminiOAuthHandler struct {
  10. geminiOAuthService *service.GeminiOAuthService
  11. }
  12. func NewGeminiOAuthHandler(geminiOAuthService *service.GeminiOAuthService) *GeminiOAuthHandler {
  13. return &GeminiOAuthHandler{geminiOAuthService: geminiOAuthService}
  14. }
  15. // GetCapabilities returns the Gemini OAuth configuration capabilities.
  16. // GET /api/v1/admin/gemini/oauth/capabilities
  17. func (h *GeminiOAuthHandler) GetCapabilities(c *gin.Context) {
  18. cfg := h.geminiOAuthService.GetOAuthConfig()
  19. response.Success(c, cfg)
  20. }
  21. type GeminiGenerateAuthURLRequest struct {
  22. ProxyID *int64 `json:"proxy_id"`
  23. ProjectID string `json:"project_id"`
  24. // OAuth 类型: "code_assist" (需要 project_id) 或 "ai_studio" (不需要 project_id)
  25. // 默认为 "code_assist" 以保持向后兼容
  26. OAuthType string `json:"oauth_type"`
  27. // TierID is a user-selected tier to be used when auto detection is unavailable or fails.
  28. TierID string `json:"tier_id"`
  29. }
  30. // GenerateAuthURL generates Google OAuth authorization URL for Gemini.
  31. // POST /api/v1/admin/gemini/oauth/auth-url
  32. func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) {
  33. var req GeminiGenerateAuthURLRequest
  34. if err := c.ShouldBindJSON(&req); err != nil {
  35. response.BadRequest(c, "Invalid request: "+err.Error())
  36. return
  37. }
  38. // 默认使用 code_assist 以保持向后兼容
  39. oauthType := strings.TrimSpace(req.OAuthType)
  40. if oauthType == "" {
  41. oauthType = "code_assist"
  42. }
  43. if oauthType != "code_assist" && oauthType != "google_one" && oauthType != "ai_studio" {
  44. response.BadRequest(c, "Invalid oauth_type: must be 'code_assist', 'google_one', or 'ai_studio'")
  45. return
  46. }
  47. // Always pass the "hosted" callback URI; the OAuth service may override it depending on
  48. // oauth_type and whether the built-in Gemini CLI OAuth client is used.
  49. redirectURI := deriveGeminiRedirectURI(c)
  50. result, err := h.geminiOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, redirectURI, req.ProjectID, oauthType, req.TierID)
  51. if err != nil {
  52. msg := err.Error()
  53. // Treat missing/invalid OAuth client configuration as a user/config error.
  54. if strings.Contains(msg, "OAuth client not configured") ||
  55. strings.Contains(msg, "requires your own OAuth Client") ||
  56. strings.Contains(msg, "requires a custom OAuth Client") ||
  57. strings.Contains(msg, "GEMINI_CLI_OAUTH_CLIENT_SECRET_MISSING") ||
  58. strings.Contains(msg, "built-in Gemini CLI OAuth client_secret is not configured") {
  59. response.BadRequest(c, "Failed to generate auth URL: "+msg)
  60. return
  61. }
  62. response.InternalError(c, "Failed to generate auth URL: "+msg)
  63. return
  64. }
  65. response.Success(c, result)
  66. }
  67. type GeminiExchangeCodeRequest struct {
  68. SessionID string `json:"session_id" binding:"required"`
  69. State string `json:"state" binding:"required"`
  70. Code string `json:"code" binding:"required"`
  71. ProxyID *int64 `json:"proxy_id"`
  72. // OAuth 类型: "code_assist" 或 "ai_studio",需要与 GenerateAuthURL 时的类型一致
  73. OAuthType string `json:"oauth_type"`
  74. // TierID is a user-selected tier to be used when auto detection is unavailable or fails.
  75. // This field is optional; when omitted, the server uses the tier stored in the OAuth session.
  76. TierID string `json:"tier_id"`
  77. }
  78. // ExchangeCode exchanges authorization code for tokens.
  79. // POST /api/v1/admin/gemini/oauth/exchange-code
  80. func (h *GeminiOAuthHandler) ExchangeCode(c *gin.Context) {
  81. var req GeminiExchangeCodeRequest
  82. if err := c.ShouldBindJSON(&req); err != nil {
  83. response.BadRequest(c, "Invalid request: "+err.Error())
  84. return
  85. }
  86. // 默认使用 code_assist 以保持向后兼容
  87. oauthType := strings.TrimSpace(req.OAuthType)
  88. if oauthType == "" {
  89. oauthType = "code_assist"
  90. }
  91. if oauthType != "code_assist" && oauthType != "google_one" && oauthType != "ai_studio" {
  92. response.BadRequest(c, "Invalid oauth_type: must be 'code_assist', 'google_one', or 'ai_studio'")
  93. return
  94. }
  95. tokenInfo, err := h.geminiOAuthService.ExchangeCode(c.Request.Context(), &service.GeminiExchangeCodeInput{
  96. SessionID: req.SessionID,
  97. State: req.State,
  98. Code: req.Code,
  99. ProxyID: req.ProxyID,
  100. OAuthType: oauthType,
  101. TierID: req.TierID,
  102. })
  103. if err != nil {
  104. response.BadRequest(c, "Failed to exchange code: "+err.Error())
  105. return
  106. }
  107. response.Success(c, tokenInfo)
  108. }
  109. func deriveGeminiRedirectURI(c *gin.Context) string {
  110. origin := strings.TrimSpace(c.GetHeader("Origin"))
  111. if origin != "" {
  112. return strings.TrimRight(origin, "/") + "/auth/callback"
  113. }
  114. scheme := "http"
  115. if c.Request.TLS != nil {
  116. scheme = "https"
  117. }
  118. if xfProto := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); xfProto != "" {
  119. scheme = strings.TrimSpace(strings.Split(xfProto, ",")[0])
  120. }
  121. host := strings.TrimSpace(c.Request.Host)
  122. if xfHost := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); xfHost != "" {
  123. host = strings.TrimSpace(strings.Split(xfHost, ",")[0])
  124. }
  125. return fmt.Sprintf("%s://%s/auth/callback", scheme, host)
  126. }