mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
refactor(auth): replace TokenRecord with coreauth.Auth and migrate TokenStore to coreauth.Store
- Replaced `TokenRecord` with `coreauth.Auth` for centralized and consistent authentication data structures. - Migrated `TokenStore` interface to `coreauth.Store` for alignment with core CLIProxy authentication. - Updated related login methods, token persistence logic, and file storage handling to use the new `coreauth.Auth` model.
This commit is contained in:
@@ -160,11 +160,7 @@ func main() {
|
|||||||
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
|
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok {
|
||||||
dirSetter.SetBaseDir(cfg.AuthDir)
|
dirSetter.SetBaseDir(cfg.AuthDir)
|
||||||
}
|
}
|
||||||
store, ok := tokenStore.(coreauth.Store)
|
core := coreauth.NewManager(tokenStore, nil, nil)
|
||||||
if !ok {
|
|
||||||
panic("token store does not implement coreauth.Store")
|
|
||||||
}
|
|
||||||
core := coreauth.NewManager(store, nil, nil)
|
|
||||||
core.RegisterExecutor(MyExecutor{})
|
core.RegisterExecutor(MyExecutor{})
|
||||||
|
|
||||||
hooks := cliproxy.Hooks{
|
hooks := cliproxy.Hooks{
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ func (h *Handler) disableAuth(ctx context.Context, id string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenRecord) (string, error) {
|
func (h *Handler) saveTokenRecord(ctx context.Context, record *coreauth.Auth) (string, error) {
|
||||||
if record == nil {
|
if record == nil {
|
||||||
return "", fmt.Errorf("token record is nil")
|
return "", fmt.Errorf("token record is nil")
|
||||||
}
|
}
|
||||||
@@ -353,7 +353,12 @@ func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenReco
|
|||||||
store = sdkAuth.GetTokenStore()
|
store = sdkAuth.GetTokenStore()
|
||||||
h.tokenStore = store
|
h.tokenStore = store
|
||||||
}
|
}
|
||||||
return store.Save(ctx, h.cfg, record)
|
if h.cfg != nil {
|
||||||
|
if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok {
|
||||||
|
dirSetter.SetBaseDir(h.cfg.AuthDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return store.Save(ctx, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
||||||
@@ -496,11 +501,12 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
|
|
||||||
// Create token storage
|
// Create token storage
|
||||||
tokenStorage := anthropicAuth.CreateTokenStorage(bundle)
|
tokenStorage := anthropicAuth.CreateTokenStorage(bundle)
|
||||||
record := &sdkAuth.TokenRecord{
|
record := &coreauth.Auth{
|
||||||
|
ID: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
|
||||||
Provider: "claude",
|
Provider: "claude",
|
||||||
FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
|
FileName: fmt.Sprintf("claude-%s.json", tokenStorage.Email),
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: map[string]string{"email": tokenStorage.Email},
|
Metadata: map[string]any{"email": tokenStorage.Email},
|
||||||
}
|
}
|
||||||
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
||||||
if errSave != nil {
|
if errSave != nil {
|
||||||
@@ -659,11 +665,12 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
fmt.Println("Authentication successful.")
|
fmt.Println("Authentication successful.")
|
||||||
|
|
||||||
record := &sdkAuth.TokenRecord{
|
record := &coreauth.Auth{
|
||||||
|
ID: fmt.Sprintf("gemini-%s.json", ts.Email),
|
||||||
Provider: "gemini",
|
Provider: "gemini",
|
||||||
FileName: fmt.Sprintf("gemini-%s.json", ts.Email),
|
FileName: fmt.Sprintf("gemini-%s.json", ts.Email),
|
||||||
Storage: &ts,
|
Storage: &ts,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]any{
|
||||||
"email": ts.Email,
|
"email": ts.Email,
|
||||||
"project_id": ts.ProjectID,
|
"project_id": ts.ProjectID,
|
||||||
},
|
},
|
||||||
@@ -724,7 +731,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) {
|
|||||||
// Provide a stable label (gemini-web-<hash>) for logging and identification
|
// Provide a stable label (gemini-web-<hash>) for logging and identification
|
||||||
tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
|
tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
|
||||||
|
|
||||||
record := &sdkAuth.TokenRecord{
|
record := &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
Provider: "gemini-web",
|
Provider: "gemini-web",
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
@@ -869,11 +877,12 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
|||||||
|
|
||||||
// Create token storage and persist
|
// Create token storage and persist
|
||||||
tokenStorage := openaiAuth.CreateTokenStorage(bundle)
|
tokenStorage := openaiAuth.CreateTokenStorage(bundle)
|
||||||
record := &sdkAuth.TokenRecord{
|
record := &coreauth.Auth{
|
||||||
|
ID: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
|
||||||
Provider: "codex",
|
Provider: "codex",
|
||||||
FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
|
FileName: fmt.Sprintf("codex-%s.json", tokenStorage.Email),
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]any{
|
||||||
"email": tokenStorage.Email,
|
"email": tokenStorage.Email,
|
||||||
"account_id": tokenStorage.AccountID,
|
"account_id": tokenStorage.AccountID,
|
||||||
},
|
},
|
||||||
@@ -926,11 +935,12 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
|
|||||||
tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
|
tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
|
||||||
|
|
||||||
tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())
|
tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())
|
||||||
record := &sdkAuth.TokenRecord{
|
record := &coreauth.Auth{
|
||||||
|
ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
|
||||||
Provider: "qwen",
|
Provider: "qwen",
|
||||||
FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
|
FileName: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: map[string]string{"email": tokenStorage.Email},
|
Metadata: map[string]any{"email": tokenStorage.Email},
|
||||||
}
|
}
|
||||||
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
||||||
if errSave != nil {
|
if errSave != nil {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type Handler struct {
|
|||||||
failedAttempts map[string]*attemptInfo // keyed by client IP
|
failedAttempts map[string]*attemptInfo // keyed by client IP
|
||||||
authManager *coreauth.Manager
|
authManager *coreauth.Manager
|
||||||
usageStats *usage.RequestStatistics
|
usageStats *usage.RequestStatistics
|
||||||
tokenStore sdkAuth.TokenStore
|
tokenStore coreauth.Store
|
||||||
|
|
||||||
localPassword string
|
localPassword string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// banner prints a simple ASCII banner for clarity without ANSI colors.
|
// banner prints a simple ASCII banner for clarity without ANSI colors.
|
||||||
@@ -173,13 +174,19 @@ func DoGeminiWebAuth(cfg *config.Config) {
|
|||||||
Secure1PSIDTS: secure1psidts,
|
Secure1PSIDTS: secure1psidts,
|
||||||
Label: label,
|
Label: label,
|
||||||
}
|
}
|
||||||
record := &sdkAuth.TokenRecord{
|
record := &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
Provider: "gemini-web",
|
Provider: "gemini-web",
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
}
|
}
|
||||||
store := sdkAuth.GetTokenStore()
|
store := sdkAuth.GetTokenStore()
|
||||||
savedPath, err := store.Save(context.Background(), cfg, record)
|
if cfg != nil {
|
||||||
|
if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok {
|
||||||
|
dirSetter.SetBaseDir(cfg.AuthDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savedPath, err := store.Save(context.Background(), record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("!! Failed to save Gemini Web token to file:", err)
|
fmt.Println("!! Failed to save Gemini Web token to file:", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ func (a *ClaudeAuthenticator) RefreshLead() *time.Duration {
|
|||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||||
}
|
}
|
||||||
@@ -127,7 +128,7 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)
|
fileName := fmt.Sprintf("claude-%s.json", tokenStorage.Email)
|
||||||
metadata := map[string]string{
|
metadata := map[string]any{
|
||||||
"email": tokenStorage.Email,
|
"email": tokenStorage.Email,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +137,8 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
|||||||
fmt.Println("Claude API key obtained and stored")
|
fmt.Println("Claude API key obtained and stored")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TokenRecord{
|
return &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
Provider: a.Provider(),
|
Provider: a.Provider(),
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ func (a *CodexAuthenticator) RefreshLead() *time.Duration {
|
|||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
|
fileName := fmt.Sprintf("codex-%s.json", tokenStorage.Email)
|
||||||
metadata := map[string]string{
|
metadata := map[string]any{
|
||||||
"email": tokenStorage.Email,
|
"email": tokenStorage.Email,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +136,8 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
fmt.Println("Codex API key obtained and stored")
|
fmt.Println("Codex API key obtained and stored")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TokenRecord{
|
return &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
Provider: a.Provider(),
|
Provider: a.Provider(),
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,27 +34,71 @@ func (s *FileTokenStore) SetBaseDir(dir string) {
|
|||||||
s.dirLock.Unlock()
|
s.dirLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes the token storage to the resolved file path.
|
// Save persists token storage and metadata to the resolved auth file path.
|
||||||
func (s *FileTokenStore) Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error) {
|
func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (string, error) {
|
||||||
if record == nil || record.Storage == nil {
|
if auth == nil {
|
||||||
return "", fmt.Errorf("cliproxy auth: token record is incomplete")
|
return "", fmt.Errorf("auth filestore: auth is nil")
|
||||||
}
|
}
|
||||||
target := strings.TrimSpace(record.FileName)
|
|
||||||
if target == "" {
|
path, err := s.resolveAuthPath(auth)
|
||||||
return "", fmt.Errorf("cliproxy auth: missing file name for provider %s", record.Provider)
|
if err != nil {
|
||||||
}
|
|
||||||
if !filepath.IsAbs(target) {
|
|
||||||
baseDir := s.baseDirFromConfig(cfg)
|
|
||||||
if baseDir != "" {
|
|
||||||
target = filepath.Join(baseDir, target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if err := record.Storage.SaveTokenToFile(target); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return target, nil
|
if path == "" {
|
||||||
|
return "", fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.Disabled {
|
||||||
|
if _, statErr := os.Stat(path); os.IsNotExist(statErr) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||||
|
return "", fmt.Errorf("auth filestore: create dir failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case auth.Storage != nil:
|
||||||
|
if err = auth.Storage.SaveTokenToFile(path); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
case auth.Metadata != nil:
|
||||||
|
raw, errMarshal := json.Marshal(auth.Metadata)
|
||||||
|
if errMarshal != nil {
|
||||||
|
return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal)
|
||||||
|
}
|
||||||
|
if existing, errRead := os.ReadFile(path); errRead == nil {
|
||||||
|
if jsonEqual(existing, raw) {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
} else if errRead != nil && !os.IsNotExist(errRead) {
|
||||||
|
return "", fmt.Errorf("auth filestore: read existing failed: %w", errRead)
|
||||||
|
}
|
||||||
|
tmp := path + ".tmp"
|
||||||
|
if errWrite := os.WriteFile(tmp, raw, 0o600); errWrite != nil {
|
||||||
|
return "", fmt.Errorf("auth filestore: write temp failed: %w", errWrite)
|
||||||
|
}
|
||||||
|
if errRename := os.Rename(tmp, path); errRename != nil {
|
||||||
|
return "", fmt.Errorf("auth filestore: rename failed: %w", errRename)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("auth filestore: nothing to persist for %s", auth.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.Attributes == nil {
|
||||||
|
auth.Attributes = make(map[string]string)
|
||||||
|
}
|
||||||
|
auth.Attributes["path"] = path
|
||||||
|
|
||||||
|
if strings.TrimSpace(auth.FileName) == "" {
|
||||||
|
auth.FileName = auth.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List enumerates all auth JSON files under the configured directory.
|
// List enumerates all auth JSON files under the configured directory.
|
||||||
@@ -90,50 +133,6 @@ func (s *FileTokenStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error)
|
|||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAuth writes the auth metadata back to its source file location.
|
|
||||||
func (s *FileTokenStore) SaveAuth(ctx context.Context, auth *cliproxyauth.Auth) error {
|
|
||||||
if auth == nil {
|
|
||||||
return fmt.Errorf("auth filestore: auth is nil")
|
|
||||||
}
|
|
||||||
path, err := s.resolveAuthPath(auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if path == "" {
|
|
||||||
return fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID)
|
|
||||||
}
|
|
||||||
// If the auth has been disabled and the original file was removed, avoid recreating it on disk.
|
|
||||||
if auth.Disabled {
|
|
||||||
if _, statErr := os.Stat(path); statErr != nil {
|
|
||||||
if os.IsNotExist(statErr) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
||||||
return fmt.Errorf("auth filestore: create dir failed: %w", err)
|
|
||||||
}
|
|
||||||
raw, err := json.Marshal(auth.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("auth filestore: marshal metadata failed: %w", err)
|
|
||||||
}
|
|
||||||
if existing, errRead := os.ReadFile(path); errRead == nil {
|
|
||||||
if jsonEqual(existing, raw) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tmp := path + ".tmp"
|
|
||||||
if err = os.WriteFile(tmp, raw, 0o600); err != nil {
|
|
||||||
return fmt.Errorf("auth filestore: write temp failed: %w", err)
|
|
||||||
}
|
|
||||||
if err = os.Rename(tmp, path); err != nil {
|
|
||||||
return fmt.Errorf("auth filestore: rename failed: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the auth file.
|
// Delete removes the auth file.
|
||||||
func (s *FileTokenStore) Delete(ctx context.Context, id string) error {
|
func (s *FileTokenStore) Delete(ctx context.Context, id string) error {
|
||||||
id = strings.TrimSpace(id)
|
id = strings.TrimSpace(id)
|
||||||
@@ -185,6 +184,7 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
|
|||||||
auth := &cliproxyauth.Auth{
|
auth := &cliproxyauth.Auth{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
|
FileName: id,
|
||||||
Label: s.labelFor(metadata),
|
Label: s.labelFor(metadata),
|
||||||
Status: cliproxyauth.StatusActive,
|
Status: cliproxyauth.StatusActive,
|
||||||
Attributes: map[string]string{"path": path},
|
Attributes: map[string]string{"path": path},
|
||||||
@@ -220,6 +220,15 @@ func (s *FileTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fileName := strings.TrimSpace(auth.FileName); fileName != "" {
|
||||||
|
if filepath.IsAbs(fileName) {
|
||||||
|
return fileName, nil
|
||||||
|
}
|
||||||
|
if dir := s.baseDirSnapshot(); dir != "" {
|
||||||
|
return filepath.Join(dir, fileName), nil
|
||||||
|
}
|
||||||
|
return fileName, nil
|
||||||
|
}
|
||||||
if auth.ID == "" {
|
if auth.ID == "" {
|
||||||
return "", fmt.Errorf("auth filestore: missing id")
|
return "", fmt.Errorf("auth filestore: missing id")
|
||||||
}
|
}
|
||||||
@@ -249,13 +258,6 @@ func (s *FileTokenStore) labelFor(metadata map[string]any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileTokenStore) baseDirFromConfig(cfg *config.Config) string {
|
|
||||||
if cfg != nil && strings.TrimSpace(cfg.AuthDir) != "" {
|
|
||||||
return strings.TrimSpace(cfg.AuthDir)
|
|
||||||
}
|
|
||||||
return s.baseDirSnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FileTokenStore) baseDirSnapshot() string {
|
func (s *FileTokenStore) baseDirSnapshot() string {
|
||||||
s.dirLock.RLock()
|
s.dirLock.RLock()
|
||||||
defer s.dirLock.RUnlock()
|
defer s.dirLock.RUnlock()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GeminiWebAuthenticator provides a minimal wrapper so core components can treat
|
// GeminiWebAuthenticator provides a minimal wrapper so core components can treat
|
||||||
@@ -16,7 +17,7 @@ func NewGeminiWebAuthenticator() *GeminiWebAuthenticator { return &GeminiWebAuth
|
|||||||
|
|
||||||
func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }
|
func (a *GeminiWebAuthenticator) Provider() string { return "gemini-web" }
|
||||||
|
|
||||||
func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||||
_ = ctx
|
_ = ctx
|
||||||
_ = cfg
|
_ = cfg
|
||||||
_ = opts
|
_ = opts
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||||
// legacy client removed
|
// legacy client removed
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
|
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
|
||||||
@@ -26,7 +27,7 @@ func (a *GeminiAuthenticator) RefreshLead() *time.Duration {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||||
}
|
}
|
||||||
@@ -51,14 +52,15 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
|||||||
// Skip onboarding here; rely on upstream configuration
|
// Skip onboarding here; rely on upstream configuration
|
||||||
|
|
||||||
fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
|
fileName := fmt.Sprintf("%s-%s.json", ts.Email, ts.ProjectID)
|
||||||
metadata := map[string]string{
|
metadata := map[string]any{
|
||||||
"email": ts.Email,
|
"email": ts.Email,
|
||||||
"project_id": ts.ProjectID,
|
"project_id": ts.ProjectID,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Gemini authentication successful")
|
fmt.Println("Gemini authentication successful")
|
||||||
|
|
||||||
return &TokenRecord{
|
return &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
Provider: a.Provider(),
|
Provider: a.Provider(),
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: &ts,
|
Storage: &ts,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")
|
var ErrRefreshNotSupported = errors.New("cliproxy auth: refresh not supported")
|
||||||
@@ -20,22 +20,9 @@ type LoginOptions struct {
|
|||||||
Prompt func(prompt string) (string, error)
|
Prompt func(prompt string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenRecord represents credential material produced by an authenticator.
|
|
||||||
type TokenRecord struct {
|
|
||||||
Provider string
|
|
||||||
FileName string
|
|
||||||
Storage baseauth.TokenStorage
|
|
||||||
Metadata map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenStore persists token records.
|
|
||||||
type TokenStore interface {
|
|
||||||
Save(ctx context.Context, cfg *config.Config, record *TokenRecord) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticator manages login and optional refresh flows for a provider.
|
// Authenticator manages login and optional refresh flows for a provider.
|
||||||
type Authenticator interface {
|
type Authenticator interface {
|
||||||
Provider() string
|
Provider() string
|
||||||
Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error)
|
Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error)
|
||||||
RefreshLead() *time.Duration
|
RefreshLead() *time.Duration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager aggregates authenticators and coordinates persistence via a token store.
|
// Manager aggregates authenticators and coordinates persistence via a token store.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
authenticators map[string]Authenticator
|
authenticators map[string]Authenticator
|
||||||
store TokenStore
|
store coreauth.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager constructs a manager with the provided token store and authenticators.
|
// NewManager constructs a manager with the provided token store and authenticators.
|
||||||
// If store is nil, the caller must set it later using SetStore.
|
// If store is nil, the caller must set it later using SetStore.
|
||||||
func NewManager(store TokenStore, authenticators ...Authenticator) *Manager {
|
func NewManager(store coreauth.Store, authenticators ...Authenticator) *Manager {
|
||||||
mgr := &Manager{
|
mgr := &Manager{
|
||||||
authenticators: make(map[string]Authenticator),
|
authenticators: make(map[string]Authenticator),
|
||||||
store: store,
|
store: store,
|
||||||
@@ -38,12 +39,12 @@ func (m *Manager) Register(a Authenticator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetStore updates the token store used for persistence.
|
// SetStore updates the token store used for persistence.
|
||||||
func (m *Manager) SetStore(store TokenStore) {
|
func (m *Manager) SetStore(store coreauth.Store) {
|
||||||
m.store = store
|
m.store = store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login executes the provider login flow and persists the resulting token record.
|
// Login executes the provider login flow and persists the resulting auth record.
|
||||||
func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*TokenRecord, string, error) {
|
func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) {
|
||||||
auth, ok := m.authenticators[provider]
|
auth, ok := m.authenticators[provider]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)
|
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s not registered", provider)
|
||||||
@@ -61,7 +62,13 @@ func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config
|
|||||||
return record, "", nil
|
return record, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
savedPath, err := m.store.Save(ctx, cfg, record)
|
if cfg != nil {
|
||||||
|
if dirSetter, ok := m.store.(interface{ SetBaseDir(string) }); ok {
|
||||||
|
dirSetter.SetBaseDir(cfg.AuthDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savedPath, err := m.store.Save(ctx, record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return record, "", err
|
return record, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||||
// legacy client removed
|
// legacy client removed
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ func (a *QwenAuthenticator) RefreshLead() *time.Duration {
|
|||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*TokenRecord, error) {
|
func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||||
}
|
}
|
||||||
@@ -97,13 +98,14 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
// no legacy client construction
|
// no legacy client construction
|
||||||
|
|
||||||
fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
|
fileName := fmt.Sprintf("qwen-%s.json", tokenStorage.Email)
|
||||||
metadata := map[string]string{
|
metadata := map[string]any{
|
||||||
"email": tokenStorage.Email,
|
"email": tokenStorage.Email,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Qwen authentication successful")
|
fmt.Println("Qwen authentication successful")
|
||||||
|
|
||||||
return &TokenRecord{
|
return &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
Provider: a.Provider(),
|
Provider: a.Provider(),
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
storeMu sync.RWMutex
|
storeMu sync.RWMutex
|
||||||
registeredTokenStore TokenStore
|
registeredStore coreauth.Store
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterTokenStore sets the global token store used by the authentication helpers.
|
// RegisterTokenStore sets the global token store used by the authentication helpers.
|
||||||
func RegisterTokenStore(store TokenStore) {
|
func RegisterTokenStore(store coreauth.Store) {
|
||||||
storeMu.Lock()
|
storeMu.Lock()
|
||||||
registeredTokenStore = store
|
registeredStore = store
|
||||||
storeMu.Unlock()
|
storeMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTokenStore returns the globally registered token store.
|
// GetTokenStore returns the globally registered token store.
|
||||||
func GetTokenStore() TokenStore {
|
func GetTokenStore() coreauth.Store {
|
||||||
storeMu.RLock()
|
storeMu.RLock()
|
||||||
s := registeredTokenStore
|
s := registeredStore
|
||||||
storeMu.RUnlock()
|
storeMu.RUnlock()
|
||||||
if s != nil {
|
if s != nil {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
storeMu.Lock()
|
storeMu.Lock()
|
||||||
defer storeMu.Unlock()
|
defer storeMu.Unlock()
|
||||||
if registeredTokenStore == nil {
|
if registeredStore == nil {
|
||||||
registeredTokenStore = NewFileTokenStore()
|
registeredStore = NewFileTokenStore()
|
||||||
}
|
}
|
||||||
return registeredTokenStore
|
return registeredStore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -818,7 +818,8 @@ func (m *Manager) persist(ctx context.Context, auth *Auth) error {
|
|||||||
if auth.Metadata == nil {
|
if auth.Metadata == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return m.store.SaveAuth(ctx, auth)
|
_, err := m.store.Save(ctx, auth)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartAutoRefresh launches a background loop that evaluates auth freshness
|
// StartAutoRefresh launches a background loop that evaluates auth freshness
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import "context"
|
|||||||
type Store interface {
|
type Store interface {
|
||||||
// List returns all auth records stored in the backend.
|
// List returns all auth records stored in the backend.
|
||||||
List(ctx context.Context) ([]*Auth, error)
|
List(ctx context.Context) ([]*Auth, error)
|
||||||
// SaveAuth persists the provided auth record, replacing any existing one with same ID.
|
// Save persists the provided auth record, replacing any existing one with same ID.
|
||||||
SaveAuth(ctx context.Context, auth *Auth) error
|
Save(ctx context.Context, auth *Auth) (string, error)
|
||||||
// Delete removes the auth record identified by id.
|
// Delete removes the auth record identified by id.
|
||||||
Delete(ctx context.Context, id string) error
|
Delete(ctx context.Context, id string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
baseauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auth encapsulates the runtime state and metadata associated with a single credential.
|
// Auth encapsulates the runtime state and metadata associated with a single credential.
|
||||||
@@ -14,6 +16,10 @@ type Auth struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
// Provider is the upstream provider key (e.g. "gemini", "claude").
|
// Provider is the upstream provider key (e.g. "gemini", "claude").
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
// FileName stores the relative or absolute path of the backing auth file.
|
||||||
|
FileName string `json:"-"`
|
||||||
|
// Storage holds the token persistence implementation used during login flows.
|
||||||
|
Storage baseauth.TokenStorage `json:"-"`
|
||||||
// Label is an optional human readable label for logging.
|
// Label is an optional human readable label for logging.
|
||||||
Label string `json:"label,omitempty"`
|
Label string `json:"label,omitempty"`
|
||||||
// Status is the lifecycle status managed by the AuthManager.
|
// Status is the lifecycle status managed by the AuthManager.
|
||||||
|
|||||||
@@ -197,11 +197,7 @@ func (b *Builder) Build() (*Service, error) {
|
|||||||
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil {
|
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil {
|
||||||
dirSetter.SetBaseDir(b.cfg.AuthDir)
|
dirSetter.SetBaseDir(b.cfg.AuthDir)
|
||||||
}
|
}
|
||||||
store, ok := tokenStore.(coreauth.Store)
|
coreManager = coreauth.NewManager(tokenStore, nil, nil)
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("cliproxy: token store does not implement coreauth.Store")
|
|
||||||
}
|
|
||||||
coreManager = coreauth.NewManager(store, nil, nil)
|
|
||||||
}
|
}
|
||||||
// Attach a default RoundTripper provider so providers can opt-in per-auth transports.
|
// Attach a default RoundTripper provider so providers can opt-in per-auth transports.
|
||||||
coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())
|
coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())
|
||||||
|
|||||||
Reference in New Issue
Block a user