Files
CLIProxyAPI/internal/cmd/openai_login.go
Luis Pater 9fce13fe03 Update internal module imports to use v5 package path
- Updated all `github.com/luispater/CLIProxyAPI/internal/...` imports to point to `github.com/luispater/CLIProxyAPI/v5/internal/...`.
- Adjusted `go.mod` to specify `module github.com/luispater/CLIProxyAPI/v5`.
2025-09-13 23:34:32 +08:00

179 lines
5.5 KiB
Go

// Package cmd provides command-line interface functionality for the CLI Proxy API.
// It implements the main application commands including login/authentication
// and server startup, handling the complete user onboarding and service lifecycle.
package cmd
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/v5/internal/browser"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)
// LoginOptions contains options for the Codex login process.
type LoginOptions struct {
// NoBrowser indicates whether to skip opening the browser automatically.
NoBrowser bool
}
// DoCodexLogin handles the Codex OAuth login process for OpenAI Codex services.
// It initializes the OAuth flow, opens the user's browser for authentication,
// waits for the callback, exchanges the authorization code for tokens,
// and saves the authentication information to a file.
//
// Parameters:
// - cfg: The application configuration
// - options: The login options containing browser preferences
func DoCodexLogin(cfg *config.Config, options *LoginOptions) {
if options == nil {
options = &LoginOptions{}
}
ctx := context.Background()
log.Info("Initializing Codex authentication...")
// Generate PKCE codes
pkceCodes, err := codex.GeneratePKCECodes()
if err != nil {
log.Fatalf("Failed to generate PKCE codes: %v", err)
return
}
// Generate random state parameter
state, err := misc.GenerateRandomState()
if err != nil {
log.Fatalf("Failed to generate state parameter: %v", err)
return
}
// Initialize OAuth server
oauthServer := codex.NewOAuthServer(1455)
// Start OAuth callback server
if err = oauthServer.Start(); err != nil {
if strings.Contains(err.Error(), "already in use") {
authErr := codex.NewAuthenticationError(codex.ErrPortInUse, err)
log.Error(codex.GetUserFriendlyMessage(authErr))
os.Exit(13) // Exit code 13 for port-in-use error
}
authErr := codex.NewAuthenticationError(codex.ErrServerStartFailed, err)
log.Fatalf("Failed to start OAuth callback server: %v", authErr)
return
}
defer func() {
if err = oauthServer.Stop(ctx); err != nil {
log.Warnf("Failed to stop OAuth server: %v", err)
}
}()
// Initialize Codex auth service
openaiAuth := codex.NewCodexAuth(cfg)
// Generate authorization URL
authURL, err := openaiAuth.GenerateAuthURL(state, pkceCodes)
if err != nil {
log.Fatalf("Failed to generate authorization URL: %v", err)
return
}
// Open browser or display URL
if !options.NoBrowser {
log.Info("Opening browser for authentication...")
// Check if browser is available
if !browser.IsAvailable() {
log.Warn("No browser available on this system")
util.PrintSSHTunnelInstructions(1455)
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
} else {
if err = browser.OpenURL(authURL); err != nil {
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
log.Warn(codex.GetUserFriendlyMessage(authErr))
util.PrintSSHTunnelInstructions(1455)
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
// Log platform info for debugging
platformInfo := browser.GetPlatformInfo()
log.Debugf("Browser platform info: %+v", platformInfo)
} else {
log.Debug("Browser opened successfully")
}
}
} else {
util.PrintSSHTunnelInstructions(1455)
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
}
log.Info("Waiting for authentication callback...")
// Wait for OAuth callback
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
if strings.Contains(err.Error(), "timeout") {
authErr := codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
log.Error(codex.GetUserFriendlyMessage(authErr))
} else {
log.Errorf("Authentication failed: %v", err)
}
return
}
if result.Error != "" {
oauthErr := codex.NewOAuthError(result.Error, "", http.StatusBadRequest)
log.Error(codex.GetUserFriendlyMessage(oauthErr))
return
}
// Validate state parameter
if result.State != state {
authErr := codex.NewAuthenticationError(codex.ErrInvalidState, fmt.Errorf("expected %s, got %s", state, result.State))
log.Error(codex.GetUserFriendlyMessage(authErr))
return
}
log.Debug("Authorization code received, exchanging for tokens...")
// Exchange authorization code for tokens
authBundle, err := openaiAuth.ExchangeCodeForTokens(ctx, result.Code, pkceCodes)
if err != nil {
authErr := codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
log.Errorf("Failed to exchange authorization code for tokens: %v", authErr)
log.Debug("This may be due to network issues or invalid authorization code")
return
}
// Create token storage
tokenStorage := openaiAuth.CreateTokenStorage(authBundle)
// Initialize Codex client
openaiClient, err := client.NewCodexClient(cfg, tokenStorage)
if err != nil {
log.Fatalf("Failed to initialize Codex client: %v", err)
return
}
// Save token storage
if err = openaiClient.SaveTokenToFile(); err != nil {
log.Fatalf("Failed to save authentication tokens: %v", err)
return
}
log.Info("Authentication successful!")
if authBundle.APIKey != "" {
log.Info("API key obtained and saved")
}
log.Info("You can now use Codex services through this CLI")
}