Compare commits

..

2 Commits

Author SHA1 Message Date
Luis Pater
57ead9a4bc Refactor user onboarding and token management
- Enhanced the `Client` initialization to include `TokenStorage` and configuration parameters.
- Replaced `SaveTokenToFile` with a `Client` method for better encapsulation.
- Improved onboarding flow with project ID verification and API enablement checks.
- Refactored token saving logic to ensure proper handling of directory creation and JSON encoding.
- Removed unused file-related code in `auth.go` for improved maintainability.
2025-07-04 07:53:07 +08:00
Luis Pater
79acea5976 Refactor authentication and service initialization code
- Moved login and service management logic to `internal/cmd` package (`login.go` and `run.go`).
- Introduced `DoLogin` and `StartService` functions for modularity.
- Enhanced error handling by using structured `ErrorMessage` in `Client`.
- Improved token file saving process and added project-specific token identification.
- Updated API handlers to handle more detailed error responses, including status codes.
2025-07-04 00:43:15 +08:00
6 changed files with 369 additions and 216 deletions

View File

@@ -2,23 +2,14 @@ package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"github.com/luispater/CLIProxyAPI/internal/api"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/cmd"
"github.com/luispater/CLIProxyAPI/internal/config"
log "github.com/sirupsen/logrus"
"io/fs"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
)
type LogFormatter struct {
@@ -95,147 +86,8 @@ func main() {
}
if login {
var ts auth.TokenStorage
if projectID != "" {
ts.ProjectID = projectID
}
// 2. Initialize authenticated HTTP Client
clientCtx := context.Background()
log.Info("Initializing authentication...")
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg)
if errGetClient != nil {
log.Fatalf("failed to get authenticated client: %v", errGetClient)
return
}
log.Info("Authentication successful.")
// 3. Initialize CLI Client
cliClient := client.NewClient(httpClient)
if err = cliClient.SetupUser(clientCtx, ts.Email, projectID); err != nil {
if err.Error() == "failed to start user onboarding, need define a project id" {
log.Error("failed to start user onboarding")
project, errGetProjectList := cliClient.GetProjectList(clientCtx)
if errGetProjectList != nil {
log.Fatalf("failed to complete user setup: %v", err)
} else {
log.Infof("Your account %s needs specify a project id.", ts.Email)
log.Info("========================================================================")
for i := 0; i < len(project.Projects); i++ {
log.Infof("Project ID: %s", project.Projects[i].ProjectID)
log.Infof("Project Name: %s", project.Projects[i].Name)
log.Info("========================================================================")
}
log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
}
} else {
// Log as a warning because in some cases, the CLI might still be usable
// or the user might want to retry setup later.
log.Fatalf("failed to complete user setup: %v", err)
}
}
cmd.DoLogin(cfg, projectID)
} else {
// Create API server configuration
apiConfig := &api.ServerConfig{
Port: fmt.Sprintf("%d", cfg.Port),
Debug: cfg.Debug,
ApiKeys: cfg.ApiKeys,
}
cliClients := make([]*client.Client, 0)
err = filepath.Walk(cfg.AuthDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") {
log.Debugf(path)
f, errOpen := os.Open(path)
if errOpen != nil {
return errOpen
}
defer func() {
_ = f.Close()
}()
var ts auth.TokenStorage
if err = json.NewDecoder(f).Decode(&ts); err == nil {
// 2. Initialize authenticated HTTP Client
clientCtx := context.Background()
log.Info("Initializing authentication...")
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg)
if errGetClient != nil {
log.Fatalf("failed to get authenticated client: %v", errGetClient)
return errGetClient
}
log.Info("Authentication successful.")
// 3. Initialize CLI Client
cliClient := client.NewClient(httpClient)
if err = cliClient.SetupUser(clientCtx, ts.Email, ts.ProjectID); err != nil {
if err.Error() == "failed to start user onboarding, need define a project id" {
log.Error("failed to start user onboarding")
project, errGetProjectList := cliClient.GetProjectList(clientCtx)
if errGetProjectList != nil {
log.Fatalf("failed to complete user setup: %v", err)
} else {
log.Infof("Your account %s needs specify a project id.", ts.Email)
log.Info("========================================================================")
for i := 0; i < len(project.Projects); i++ {
log.Infof("Project ID: %s", project.Projects[i].ProjectID)
log.Infof("Project Name: %s", project.Projects[i].Name)
log.Info("========================================================================")
}
log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
}
} else {
// Log as a warning because in some cases, the CLI might still be usable
// or the user might want to retry setup later.
log.Fatalf("failed to complete user setup: %v", err)
}
} else {
cliClients = append(cliClients, cliClient)
}
}
}
return nil
})
// Create API server
apiServer := api.NewServer(apiConfig, cliClients)
log.Infof("Starting API server on port %s", apiConfig.Port)
if err = apiServer.Start(); err != nil {
log.Fatalf("API server failed to start: %v", err)
return
}
// Set up graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <-sigChan:
log.Debugf("Received shutdown signal. Cleaning up...")
// Create shutdown context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
_ = ctx // Mark ctx as used to avoid error, as apiServer.Stop(ctx) is commented out
// Stop API server
if err = apiServer.Stop(ctx); err != nil {
log.Debugf("Error stopping API server: %v", err)
}
cancel()
log.Debugf("Cleanup completed. Exiting...")
os.Exit(0)
case <-time.After(5 * time.Second):
}
}
cmd.StartService(cfg)
}
}

View File

@@ -407,7 +407,7 @@ func (h *APIHandlers) handleNonStreamingResponse(c *gin.Context, rawJson []byte)
cliClient.RequestMutex.Lock()
}
log.Debugf("Request use account: %s", cliClient.Email)
log.Debugf("Request use account: %s, project id: %s", cliClient.Email, cliClient.ProjectID)
jsonTemplate := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
respChan, errChan := cliClient.SendMessageStream(cliCtx, rawJson, modelName, contents, tools)
for {
@@ -429,8 +429,8 @@ func (h *APIHandlers) handleNonStreamingResponse(c *gin.Context, rawJson []byte)
}
case err, okError := <-errChan:
if okError {
c.Status(http.StatusInternalServerError)
_, _ = fmt.Fprint(c.Writer, err.Error())
c.Status(err.StatusCode)
_, _ = fmt.Fprint(c.Writer, err.Error.Error())
flusher.Flush()
// c.JSON(http.StatusInternalServerError, ErrorResponse{
// Error: ErrorDetail{
@@ -501,7 +501,7 @@ func (h *APIHandlers) handleStreamingResponse(c *gin.Context, rawJson []byte) {
cliClient.RequestMutex.Lock()
}
log.Debugf("Request use account: %s", cliClient.Email)
log.Debugf("Request use account: %s, project id: %s", cliClient.Email, cliClient.ProjectID)
respChan, errChan := cliClient.SendMessageStream(cliCtx, rawJson, modelName, contents, tools)
for {
select {
@@ -526,8 +526,8 @@ func (h *APIHandlers) handleStreamingResponse(c *gin.Context, rawJson []byte) {
}
case err, okError := <-errChan:
if okError {
c.Status(http.StatusInternalServerError)
_, _ = fmt.Fprint(c.Writer, err.Error())
c.Status(err.StatusCode)
_, _ = fmt.Fprint(c.Writer, err.Error.Error())
flusher.Flush()
// c.JSON(http.StatusInternalServerError, ErrorResponse{
// Error: ErrorDetail{

View File

@@ -13,8 +13,6 @@ import (
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"github.com/skratchdot/open-golang/open"
@@ -39,6 +37,8 @@ type TokenStorage struct {
Token any `json:"token"`
ProjectID string `json:"project_id"`
Email string `json:"email"`
Auto bool `json:"auto"`
Checked bool `json:"checked"`
}
// GetAuthenticatedClient configures and returns an HTTP client with OAuth2 tokens.
@@ -95,11 +95,12 @@ func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, cfg *config.C
if err != nil {
return nil, fmt.Errorf("failed to get token from web: %w", err)
}
ts, err = saveTokenToFile(ctx, conf, token, ts.ProjectID, cfg.AuthDir)
if err != nil {
// Log the error but proceed, as we have a valid token for the session.
newTs, errSaveTokenToFile := createTokenStorage(ctx, conf, token, ts.ProjectID)
if errSaveTokenToFile != nil {
log.Errorf("Warning: failed to save token to file: %v", err)
return nil, errSaveTokenToFile
}
*ts = *newTs
}
tsToken, _ := json.Marshal(ts.Token)
if err = json.Unmarshal(tsToken, &token); err != nil {
@@ -109,8 +110,8 @@ func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, cfg *config.C
return conf.Client(ctx, token), nil
}
// saveTokenToFile saves a token to the local credentials file.
func saveTokenToFile(ctx context.Context, config *oauth2.Config, token *oauth2.Token, projectID, authDir string) (*TokenStorage, error) {
// createTokenStorage creates a token storage.
func createTokenStorage(ctx context.Context, config *oauth2.Config, token *oauth2.Token, projectID string) (*TokenStorage, error) {
httpClient := config.Client(ctx, token)
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", nil)
if err != nil {
@@ -139,19 +140,6 @@ func saveTokenToFile(ctx context.Context, config *oauth2.Config, token *oauth2.T
log.Info("Failed to get user email from token")
}
log.Infof("Saving credentials to %s", filepath.Join(authDir, fmt.Sprintf("%s.json", emailResult.String())))
if err = os.MkdirAll(authDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
f, err := os.Create(filepath.Join(authDir, fmt.Sprintf("%s.json", emailResult.String())))
if err != nil {
return nil, fmt.Errorf("failed to create token file: %w", err)
}
defer func() {
_ = f.Close()
}()
var ifToken map[string]any
jsonData, _ := json.Marshal(token)
err = json.Unmarshal(jsonData, &ifToken)
@@ -171,9 +159,6 @@ func saveTokenToFile(ctx context.Context, config *oauth2.Config, token *oauth2.T
Email: emailResult.String(),
}
if err = json.NewEncoder(f).Encode(ts); err != nil {
return nil, fmt.Errorf("failed to write token to file: %w", err)
}
return &ts, nil
}
@@ -212,7 +197,7 @@ func getTokenFromWeb(ctx context.Context, config *oauth2.Config) (*oauth2.Token,
// Open the authorization URL in the user's browser.
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
log.Debugf("CLI login required.\nAttempting to open authentication page in your browser.\nIf it does not open, please navigate to this URL:\n\n%s\n\n", authURL)
log.Debugf("CLI login required.\nAttempting to open authentication page in your browser.\nIf it does not open, please navigate to this URL:\n\n%s\n", authURL)
err := open.Run(authURL)
if err != nil {

View File

@@ -6,12 +6,16 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/config"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"golang.org/x/oauth2"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
@@ -25,6 +29,11 @@ const (
pluginVersion = "1.0.0"
)
type ErrorMessage struct {
StatusCode int
Error error
}
type GCPProject struct {
Projects []GCPProjectProjects `json:"projects"`
}
@@ -100,20 +109,44 @@ type ToolDeclaration struct {
// Client is the main client for interacting with the CLI API.
type Client struct {
httpClient *http.Client
projectID string
ProjectID string
RequestMutex sync.Mutex
Email string
tokenStorage *auth.TokenStorage
cfg *config.Config
}
// NewClient creates a new CLI API client.
func NewClient(httpClient *http.Client) *Client {
func NewClient(httpClient *http.Client, ts *auth.TokenStorage, cfg *config.Config) *Client {
return &Client{
httpClient: httpClient,
httpClient: httpClient,
tokenStorage: ts,
cfg: cfg,
}
}
func (c *Client) SetProjectID(projectID string) {
c.tokenStorage.ProjectID = projectID
}
func (c *Client) SetIsAuto(auto bool) {
c.tokenStorage.Auto = auto
}
func (c *Client) SetIsChecked(checked bool) {
c.tokenStorage.Checked = checked
}
func (c *Client) IsChecked() bool {
return c.tokenStorage.Checked
}
func (c *Client) IsAuto() bool {
return c.tokenStorage.Auto
}
// SetupUser performs the initial user onboarding and setup.
func (c *Client) SetupUser(ctx context.Context, email, projectID string) error {
func (c *Client) SetupUser(ctx context.Context, email, projectID string) (string, error) {
c.Email = email
log.Info("Performing user onboarding...")
@@ -128,11 +161,14 @@ func (c *Client) SetupUser(ctx context.Context, email, projectID string) error {
var loadAssistResp map[string]interface{}
err := c.makeAPIRequest(ctx, "loadCodeAssist", "POST", loadAssistReqBody, &loadAssistResp)
if err != nil {
return fmt.Errorf("failed to load code assist: %w", err)
return projectID, fmt.Errorf("failed to load code assist: %w", err)
}
// a, _ := json.Marshal(&loadAssistResp)
// log.Debug(string(a))
//
// a, _ = json.Marshal(loadAssistReqBody)
// log.Debug(string(a))
// 2. OnboardUser
var onboardTierID = "legacy-tier"
@@ -161,27 +197,35 @@ func (c *Client) SetupUser(ctx context.Context, email, projectID string) error {
if onboardProjectID != "" {
onboardReqBody["cloudaicompanionProject"] = onboardProjectID
} else {
return fmt.Errorf("failed to start user onboarding, need define a project id")
return projectID, fmt.Errorf("failed to start user onboarding, need define a project id")
}
var lroResp map[string]interface{}
err = c.makeAPIRequest(ctx, "onboardUser", "POST", onboardReqBody, &lroResp)
if err != nil {
return fmt.Errorf("failed to start user onboarding: %w", err)
}
for {
var lroResp map[string]interface{}
err = c.makeAPIRequest(ctx, "onboardUser", "POST", onboardReqBody, &lroResp)
if err != nil {
return projectID, fmt.Errorf("failed to start user onboarding: %w", err)
}
// a, _ := json.Marshal(&lroResp)
// log.Debug(string(a))
// a, _ = json.Marshal(&lroResp)
// log.Debug(string(a))
// 3. Poll Long-Running Operation (LRO)
if done, doneOk := lroResp["done"].(bool); doneOk && done {
if project, projectOk := lroResp["response"].(map[string]interface{})["cloudaicompanionProject"].(map[string]interface{}); projectOk {
c.projectID = project["id"].(string)
log.Infof("Onboarding complete. Using Project ID: %s", c.projectID)
return nil
// 3. Poll Long-Running Operation (LRO)
done, doneOk := lroResp["done"].(bool)
if doneOk && done {
if project, projectOk := lroResp["response"].(map[string]interface{})["cloudaicompanionProject"].(map[string]interface{}); projectOk {
if projectID != "" {
c.ProjectID = projectID
} else {
c.ProjectID = project["id"].(string)
}
log.Infof("Onboarding complete. Using Project ID: %s", c.ProjectID)
return c.ProjectID, nil
}
} else {
log.Println("Onboarding in progress, waiting 5 seconds...")
time.Sleep(5 * time.Second)
}
}
return fmt.Errorf("failed to get operation name from onboarding response: %v", lroResp)
}
// makeAPIRequest handles making requests to the CLI API endpoints.
@@ -240,7 +284,7 @@ func (c *Client) makeAPIRequest(ctx context.Context, endpoint, method string, bo
}
// StreamAPIRequest handles making streaming requests to the CLI API endpoints.
func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body interface{}) (io.ReadCloser, error) {
func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body interface{}) (io.ReadCloser, *ErrorMessage) {
var jsonBody []byte
var err error
if byteBody, ok := body.([]byte); ok {
@@ -248,7 +292,7 @@ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body int
} else {
jsonBody, err = json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
return nil, &ErrorMessage{500, fmt.Errorf("failed to marshal request body: %w", err)}
}
}
// log.Debug(string(jsonBody))
@@ -259,12 +303,12 @@ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body int
req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
return nil, &ErrorMessage{500, fmt.Errorf("failed to create request: %w", err)}
}
token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
if err != nil {
return nil, fmt.Errorf("failed to get token: %w", err)
return nil, &ErrorMessage{500, fmt.Errorf("failed to get token: %w", err)}
}
// Set headers
@@ -276,7 +320,7 @@ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body int
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
return nil, &ErrorMessage{500, fmt.Errorf("failed to execute request: %w", err)}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@@ -285,7 +329,7 @@ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body int
}()
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf(string(bodyBytes))
return nil, &ErrorMessage{resp.StatusCode, fmt.Errorf(string(bodyBytes))}
// return nil, fmt.Errorf("api streaming request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
@@ -293,9 +337,9 @@ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body int
}
// SendMessageStream handles a single conversational turn, including tool calls.
func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model string, contents []Content, tools []ToolDeclaration) (<-chan []byte, <-chan error) {
func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model string, contents []Content, tools []ToolDeclaration) (<-chan []byte, <-chan *ErrorMessage) {
dataTag := []byte("data: ")
errChan := make(chan error)
errChan := make(chan *ErrorMessage)
dataChan := make(chan []byte)
go func() {
defer close(errChan)
@@ -312,14 +356,14 @@ func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model st
request.Tools = tools
requestBody := map[string]interface{}{
"project": c.projectID, // Assuming ProjectID is available
"project": c.ProjectID, // Assuming ProjectID is available
"request": request,
"model": model,
}
byteRequestBody, _ := json.Marshal(requestBody)
// log.Debug(string(rawJson))
// log.Debug(string(byteRequestBody))
reasoningEffortResult := gjson.GetBytes(rawJson, "reasoning_effort")
if reasoningEffortResult.String() == "none" {
@@ -370,9 +414,9 @@ func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model st
}
}
if err = scanner.Err(); err != nil {
if errScanner := scanner.Err(); errScanner != nil {
// log.Println(err)
errChan <- err
errChan <- &ErrorMessage{500, errScanner}
_ = stream.Close()
return
}
@@ -383,6 +427,57 @@ func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model st
return dataChan, errChan
}
func (c *Client) CheckCloudAPIIsEnabled() (bool, error) {
ctx, cancel := context.WithCancel(context.Background())
defer func() {
c.RequestMutex.Unlock()
cancel()
}()
c.RequestMutex.Lock()
requestBody := `{"project":"%s","request":{"contents":[{"role":"user","parts":[{"text":"Be concise. What is the capital of France?"}]}],"generationConfig":{"thinkingConfig":{"include_thoughts":false,"thinkingBudget":0}}},"model":"gemini-2.5-flash"}`
requestBody = fmt.Sprintf(requestBody, c.tokenStorage.ProjectID)
// log.Debug(requestBody)
stream, err := c.StreamAPIRequest(ctx, "streamGenerateContent", []byte(requestBody))
if err != nil {
if err.StatusCode == 403 {
errJson := err.Error.Error()
codeResult := gjson.Get(errJson, "error.code")
if codeResult.Exists() && codeResult.Type == gjson.Number {
if codeResult.Int() == 403 {
activationUrlResult := gjson.Get(errJson, "error.details.0.metadata.activationUrl")
if activationUrlResult.Exists() {
log.Warnf(
"\n\nPlease activate your account with this url:\n\n%s\n And execute this command again:\n%s --login --project_id %s",
activationUrlResult.String(),
os.Args[0],
c.tokenStorage.ProjectID,
)
}
}
}
return false, nil
}
return false, err.Error
}
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
}
if scannerErr := scanner.Err(); scannerErr != nil {
_ = stream.Close()
} else {
_ = stream.Close()
}
return true, nil
}
func (c *Client) GetProjectList(ctx context.Context) (*GCPProject, error) {
token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
req, err := http.NewRequestWithContext(ctx, "GET", "https://cloudresourcemanager.googleapis.com/v1/projects", nil)
@@ -413,6 +508,27 @@ func (c *Client) GetProjectList(ctx context.Context) (*GCPProject, error) {
return &project, nil
}
func (c *Client) SaveTokenToFile() error {
if err := os.MkdirAll(c.cfg.AuthDir, 0700); err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
fileName := filepath.Join(c.cfg.AuthDir, fmt.Sprintf("%s-%s.json", c.tokenStorage.Email, c.tokenStorage.ProjectID))
log.Infof("Saving credentials to %s", fileName)
f, err := os.Create(fileName)
if err != nil {
return fmt.Errorf("failed to create token file: %w", err)
}
defer func() {
_ = f.Close()
}()
if err = json.NewEncoder(f).Encode(c.tokenStorage); err != nil {
return fmt.Errorf("failed to write token to file: %w", err)
}
return nil
}
// getClientMetadata returns metadata about the client environment.
func getClientMetadata() map[string]string {
return map[string]string{
@@ -439,9 +555,9 @@ func getUserAgent() string {
// getPlatform returns the OS and architecture in the format expected by the API.
func getPlatform() string {
os := runtime.GOOS
goOS := runtime.GOOS
arch := runtime.GOARCH
switch os {
switch goOS {
case "darwin":
return fmt.Sprintf("DARWIN_%s", strings.ToUpper(arch))
case "linux":

78
internal/cmd/login.go Normal file
View File

@@ -0,0 +1,78 @@
package cmd
import (
"context"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
log "github.com/sirupsen/logrus"
"os"
)
func DoLogin(cfg *config.Config, projectID string) {
var err error
var ts auth.TokenStorage
if projectID != "" {
ts.ProjectID = projectID
}
// 2. Initialize authenticated HTTP Client
clientCtx := context.Background()
log.Info("Initializing authentication...")
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg)
if errGetClient != nil {
log.Fatalf("failed to get authenticated client: %v", errGetClient)
return
}
log.Info("Authentication successful.")
// 3. Initialize CLI Client
cliClient := client.NewClient(httpClient, &ts, cfg)
projectID, err = cliClient.SetupUser(clientCtx, ts.Email, projectID)
if err != nil {
if err.Error() == "failed to start user onboarding, need define a project id" {
log.Error("failed to start user onboarding")
project, errGetProjectList := cliClient.GetProjectList(clientCtx)
if errGetProjectList != nil {
log.Fatalf("failed to complete user setup: %v", err)
} else {
log.Infof("Your account %s needs specify a project id.", ts.Email)
log.Info("========================================================================")
for i := 0; i < len(project.Projects); i++ {
log.Infof("Project ID: %s", project.Projects[i].ProjectID)
log.Infof("Project Name: %s", project.Projects[i].Name)
log.Info("========================================================================")
}
log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
}
} else {
// Log as a warning because in some cases, the CLI might still be usable
// or the user might want to retry setup later.
log.Fatalf("failed to complete user setup: %v", err)
}
} else {
auto := ts.ProjectID == ""
cliClient.SetProjectID(projectID)
cliClient.SetIsAuto(auto)
if !cliClient.IsChecked() && !cliClient.IsAuto() {
isChecked, checkErr := cliClient.CheckCloudAPIIsEnabled()
if checkErr != nil {
log.Fatalf("failed to check cloud api is enabled: %v", checkErr)
return
}
cliClient.SetIsChecked(isChecked)
}
if !cliClient.IsChecked() && !cliClient.IsAuto() {
return
}
err = cliClient.SaveTokenToFile()
if err != nil {
log.Fatal(err)
return
}
}
}

122
internal/cmd/run.go Normal file
View File

@@ -0,0 +1,122 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"github.com/luispater/CLIProxyAPI/internal/api"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
log "github.com/sirupsen/logrus"
"io/fs"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
func StartService(cfg *config.Config) {
// Create API server configuration
apiConfig := &api.ServerConfig{
Port: fmt.Sprintf("%d", cfg.Port),
Debug: cfg.Debug,
ApiKeys: cfg.ApiKeys,
}
cliClients := make([]*client.Client, 0)
err := filepath.Walk(cfg.AuthDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") {
log.Debugf("Loading token from: %s", path)
f, errOpen := os.Open(path)
if errOpen != nil {
return errOpen
}
defer func() {
_ = f.Close()
}()
var ts auth.TokenStorage
if err = json.NewDecoder(f).Decode(&ts); err == nil {
// 2. Initialize authenticated HTTP Client
clientCtx := context.Background()
log.Info("Initializing authentication...")
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg)
if errGetClient != nil {
log.Fatalf("failed to get authenticated client: %v", errGetClient)
return errGetClient
}
log.Info("Authentication successful.")
// 3. Initialize CLI Client
cliClient := client.NewClient(httpClient, &ts, cfg)
if _, err = cliClient.SetupUser(clientCtx, ts.Email, ts.ProjectID); err != nil {
if err.Error() == "failed to start user onboarding, need define a project id" {
log.Error("failed to start user onboarding")
project, errGetProjectList := cliClient.GetProjectList(clientCtx)
if errGetProjectList != nil {
log.Fatalf("failed to complete user setup: %v", err)
} else {
log.Infof("Your account %s needs specify a project id.", ts.Email)
log.Info("========================================================================")
for i := 0; i < len(project.Projects); i++ {
log.Infof("Project ID: %s", project.Projects[i].ProjectID)
log.Infof("Project Name: %s", project.Projects[i].Name)
log.Info("========================================================================")
}
log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
}
} else {
// Log as a warning because in some cases, the CLI might still be usable
// or the user might want to retry setup later.
log.Fatalf("failed to complete user setup: %v", err)
}
} else {
cliClients = append(cliClients, cliClient)
}
}
}
return nil
})
// Create API server
apiServer := api.NewServer(apiConfig, cliClients)
log.Infof("Starting API server on port %s", apiConfig.Port)
if err = apiServer.Start(); err != nil {
log.Fatalf("API server failed to start: %v", err)
return
}
// Set up graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <-sigChan:
log.Debugf("Received shutdown signal. Cleaning up...")
// Create shutdown context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
_ = ctx // Mark ctx as used to avoid error, as apiServer.Stop(ctx) is commented out
// Stop API server
if err = apiServer.Stop(ctx); err != nil {
log.Debugf("Error stopping API server: %v", err)
}
cancel()
log.Debugf("Cleanup completed. Exiting...")
os.Exit(0)
case <-time.After(5 * time.Second):
}
}
}