package admin import ( "encoding/json" "strings" "time" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // KimiOAuthHandler handles Kimi OAuth admin endpoints. type KimiOAuthHandler struct { kimiOAuthService *service.KimiOAuthService proxyRepo service.ProxyRepository } // NewKimiOAuthHandler creates a new KimiOAuthHandler. func NewKimiOAuthHandler(kimiOAuthService *service.KimiOAuthService, proxyRepo service.ProxyRepository) *KimiOAuthHandler { return &KimiOAuthHandler{ kimiOAuthService: kimiOAuthService, proxyRepo: proxyRepo, } } type kimiDeviceAuthRequest struct { ProxyID *int64 `json:"proxy_id"` } // InitiateDeviceAuth starts the Kimi OAuth device authorization flow. // POST /api/v1/admin/kimi/oauth/device-auth func (h *KimiOAuthHandler) InitiateDeviceAuth(c *gin.Context) { var req kimiDeviceAuthRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } result, err := h.kimiOAuthService.InitiateDeviceAuth(c.Request.Context(), req.ProxyID, h.proxyRepo) if err != nil { response.InternalError(c, "Failed to initiate device auth: "+err.Error()) return } response.Success(c, gin.H{ "device_code": result.DeviceCode, "user_code": result.UserCode, "verification_uri": result.VerificationURI, "verification_uri_complete": result.VerificationURIComplete, "expires_in": result.ExpiresIn, "interval": result.Interval, }) } type kimiPollTokenRequest struct { DeviceCode string `json:"device_code" binding:"required"` } // PollToken polls for the OAuth token after device authorization. // POST /api/v1/admin/kimi/oauth/token func (h *KimiOAuthHandler) PollToken(c *gin.Context) { var req kimiPollTokenRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } tokenInfo, err := h.kimiOAuthService.PollToken(c.Request.Context(), strings.TrimSpace(req.DeviceCode)) if err != nil { msg := err.Error() if strings.Contains(msg, "authorization_pending") { response.Success(c, gin.H{ "status": "pending", "message": "Authorization pending, please complete authorization in the browser", }) return } if strings.Contains(msg, "expired_token") || strings.Contains(msg, "invalid_grant") { response.BadRequest(c, "Token request failed: "+msg) return } response.InternalError(c, "Failed to poll token: "+msg) return } response.Success(c, gin.H{ "status": "completed", "access_token": tokenInfo.AccessToken, "refresh_token": tokenInfo.RefreshToken, "expires_in": tokenInfo.ExpiresIn, "expires_at": tokenInfo.ExpiresAt, "scope": tokenInfo.Scope, "token_type": tokenInfo.TokenType, }) } type kimiRefreshTokenRequest struct { RefreshToken string `json:"refresh_token" binding:"required"` ProxyID *int64 `json:"proxy_id"` } // RefreshToken refreshes the Kimi access token. // POST /api/v1/admin/kimi/oauth/refresh func (h *KimiOAuthHandler) RefreshToken(c *gin.Context) { var req kimiRefreshTokenRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } var proxyURL string if req.ProxyID != nil && h.proxyRepo != nil { if proxy, err := h.proxyRepo.GetByID(c.Request.Context(), *req.ProxyID); err == nil && proxy != nil { proxyURL = proxy.URL() } } tokenInfo, err := h.kimiOAuthService.RefreshToken(c.Request.Context(), strings.TrimSpace(req.RefreshToken), proxyURL) if err != nil { response.BadRequest(c, "Failed to refresh token: "+err.Error()) return } response.Success(c, gin.H{ "access_token": tokenInfo.AccessToken, "refresh_token": tokenInfo.RefreshToken, "expires_in": tokenInfo.ExpiresIn, "expires_at": tokenInfo.ExpiresAt, "scope": tokenInfo.Scope, "token_type": tokenInfo.TokenType, }) } // ImportLocalToken imports token from local kimi-cli credentials. // POST /api/v1/admin/kimi/oauth/import-local func (h *KimiOAuthHandler) ImportLocalToken(c *gin.Context) { var req struct { CredentialsJSON string `json:"credentials_json" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } // Parse the local credentials JSON format var localCreds struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresAt float64 `json:"expires_at"` Scope string `json:"scope"` TokenType string `json:"token_type"` ExpiresIn float64 `json:"expires_in"` } if err := json.Unmarshal([]byte(req.CredentialsJSON), &localCreds); err != nil { response.BadRequest(c, "Invalid credentials JSON: "+err.Error()) return } if strings.TrimSpace(localCreds.AccessToken) == "" { response.BadRequest(c, "access_token is required in credentials") return } expiresAt := int64(localCreds.ExpiresAt) if expiresAt == 0 && localCreds.ExpiresIn > 0 { expiresAt = time.Now().Unix() + int64(localCreds.ExpiresIn) - service.KimiTokenSafetyWindow } minExpiresAt := time.Now().Unix() + service.KimiTokenMinTTL if expiresAt < minExpiresAt { expiresAt = minExpiresAt } response.Success(c, gin.H{ "access_token": localCreds.AccessToken, "refresh_token": localCreds.RefreshToken, "expires_at": expiresAt, "scope": localCreds.Scope, "token_type": localCreds.TokenType, }) }