diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 010ed084..1b238768 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -24,6 +24,7 @@ import ( "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" + "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot" geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro" @@ -1843,6 +1844,89 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "url": authURL, "state": state}) } +func (h *Handler) RequestGitHubToken(c *gin.Context) { + ctx := context.Background() + + fmt.Println("Initializing GitHub Copilot authentication...") + + state := fmt.Sprintf("gh-%d", time.Now().UnixNano()) + + // Initialize Copilot auth service + // We need to import "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot" first if not present + // Assuming copilot package is imported as "copilot" + deviceClient := copilot.NewDeviceFlowClient(h.cfg) + + // Initiate device flow + deviceCode, err := deviceClient.RequestDeviceCode(ctx) + if err != nil { + log.Errorf("Failed to initiate device flow: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initiate device flow"}) + return + } + + authURL := deviceCode.VerificationURI + userCode := deviceCode.UserCode + + RegisterOAuthSession(state, "github") + + go func() { + fmt.Printf("Please visit %s and enter code: %s\n", authURL, userCode) + + tokenData, errPoll := deviceClient.PollForToken(ctx, deviceCode) + if errPoll != nil { + SetOAuthSessionError(state, "Authentication failed") + fmt.Printf("Authentication failed: %v\n", errPoll) + return + } + + username, errUser := deviceClient.FetchUserInfo(ctx, tokenData.AccessToken) + if errUser != nil { + log.Warnf("Failed to fetch user info: %v", errUser) + username = "github-user" + } + + tokenStorage := &copilot.CopilotTokenStorage{ + AccessToken: tokenData.AccessToken, + TokenType: tokenData.TokenType, + Scope: tokenData.Scope, + Username: username, + Type: "github-copilot", + } + + fileName := fmt.Sprintf("github-%s.json", username) + record := &coreauth.Auth{ + ID: fileName, + Provider: "github", + FileName: fileName, + Storage: tokenStorage, + Metadata: map[string]any{ + "email": username, + "username": username, + }, + } + + savedPath, errSave := h.saveTokenRecord(ctx, record) + if errSave != nil { + log.Errorf("Failed to save authentication tokens: %v", errSave) + SetOAuthSessionError(state, "Failed to save authentication tokens") + return + } + + fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) + fmt.Println("You can now use GitHub Copilot services through this CLI") + CompleteOAuthSession(state) + CompleteOAuthSessionsByProvider("github") + }() + + c.JSON(200, gin.H{ + "status": "ok", + "url": authURL, + "state": state, + "user_code": userCode, + "verification_uri": authURL, + }) +} + func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { ctx := context.Background() diff --git a/internal/api/handlers/management/oauth_sessions.go b/internal/api/handlers/management/oauth_sessions.go index 08e047f5..bc882e99 100644 --- a/internal/api/handlers/management/oauth_sessions.go +++ b/internal/api/handlers/management/oauth_sessions.go @@ -238,6 +238,8 @@ func NormalizeOAuthProvider(provider string) (string, error) { return "qwen", nil case "kiro": return "kiro", nil + case "github": + return "github", nil default: return "", errUnsupportedOAuthFlow } diff --git a/internal/api/server.go b/internal/api/server.go index 4df42ec8..2beb1d94 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -643,6 +643,7 @@ func (s *Server) registerManagementRoutes() { mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken) mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken) mgmt.GET("/kiro-auth-url", s.mgmt.RequestKiroToken) + mgmt.GET("/github-auth-url", s.mgmt.RequestGitHubToken) mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback) mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) } diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index 4c69ae90..44825951 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -227,6 +227,18 @@ func (a *Auth) AccountInfo() (string, string) { } } + // For GitHub provider, return username + if strings.ToLower(a.Provider) == "github" { + if a.Metadata != nil { + if username, ok := a.Metadata["username"].(string); ok { + username = strings.TrimSpace(username) + if username != "" { + return "oauth", username + } + } + } + } + // Check metadata for email first (OAuth-style auth) if a.Metadata != nil { if v, ok := a.Metadata["email"].(string); ok {