mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
Merge branch 'dev' into think
This commit is contained in:
@@ -1722,6 +1722,17 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for duplicate BXAuth before authentication
|
||||||
|
bxAuth := iflowauth.ExtractBXAuth(cookieValue)
|
||||||
|
if existingFile, err := iflowauth.CheckDuplicateBXAuth(h.cfg.AuthDir, bxAuth); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to check duplicate"})
|
||||||
|
return
|
||||||
|
} else if existingFile != "" {
|
||||||
|
existingFileName := filepath.Base(existingFile)
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "duplicate BXAuth found", "existing_file": existingFileName})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
authSvc := iflowauth.NewIFlowAuth(h.cfg)
|
authSvc := iflowauth.NewIFlowAuth(h.cfg)
|
||||||
tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue)
|
tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue)
|
||||||
if errAuth != nil {
|
if errAuth != nil {
|
||||||
@@ -1744,11 +1755,12 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenStorage.Email = email
|
tokenStorage.Email = email
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
|
||||||
record := &coreauth.Auth{
|
record := &coreauth.Auth{
|
||||||
ID: fmt.Sprintf("iflow-%s.json", fileName),
|
ID: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
|
||||||
Provider: "iflow",
|
Provider: "iflow",
|
||||||
FileName: fmt.Sprintf("iflow-%s.json", fileName),
|
FileName: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: map[string]any{
|
Metadata: map[string]any{
|
||||||
"email": email,
|
"email": email,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package iflow
|
package iflow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,3 +39,61 @@ func SanitizeIFlowFileName(raw string) string {
|
|||||||
}
|
}
|
||||||
return strings.TrimSpace(result.String())
|
return strings.TrimSpace(result.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractBXAuth extracts the BXAuth value from a cookie string.
|
||||||
|
func ExtractBXAuth(cookie string) string {
|
||||||
|
parts := strings.Split(cookie, ";")
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if strings.HasPrefix(part, "BXAuth=") {
|
||||||
|
return strings.TrimPrefix(part, "BXAuth=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDuplicateBXAuth checks if the given BXAuth value already exists in any iflow auth file.
|
||||||
|
// Returns the path of the existing file if found, empty string otherwise.
|
||||||
|
func CheckDuplicateBXAuth(authDir, bxAuth string) (string, error) {
|
||||||
|
if bxAuth == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(authDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("read auth dir failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasPrefix(name, "iflow-") || !strings.HasSuffix(name, ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(authDir, name)
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenData struct {
|
||||||
|
Cookie string `json:"cookie"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &tokenData); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBXAuth := ExtractBXAuth(tokenData.Cookie)
|
||||||
|
if existingBXAuth != "" && existingBXAuth == bxAuth {
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -494,11 +494,18 @@ func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenS
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only save the BXAuth field from the cookie
|
||||||
|
bxAuth := ExtractBXAuth(data.Cookie)
|
||||||
|
cookieToSave := ""
|
||||||
|
if bxAuth != "" {
|
||||||
|
cookieToSave = "BXAuth=" + bxAuth + ";"
|
||||||
|
}
|
||||||
|
|
||||||
return &IFlowTokenStorage{
|
return &IFlowTokenStorage{
|
||||||
APIKey: data.APIKey,
|
APIKey: data.APIKey,
|
||||||
Email: data.Email,
|
Email: data.Email,
|
||||||
Expire: data.Expire,
|
Expire: data.Expire,
|
||||||
Cookie: data.Cookie,
|
Cookie: cookieToSave,
|
||||||
LastRefresh: time.Now().Format(time.RFC3339),
|
LastRefresh: time.Now().Format(time.RFC3339),
|
||||||
Type: "iflow",
|
Type: "iflow",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
@@ -37,6 +39,16 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for duplicate BXAuth before authentication
|
||||||
|
bxAuth := iflow.ExtractBXAuth(cookie)
|
||||||
|
if existingFile, err := iflow.CheckDuplicateBXAuth(cfg.AuthDir, bxAuth); err != nil {
|
||||||
|
fmt.Printf("Failed to check duplicate: %v\n", err)
|
||||||
|
return
|
||||||
|
} else if existingFile != "" {
|
||||||
|
fmt.Printf("Duplicate BXAuth found, authentication already exists: %s\n", filepath.Base(existingFile))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate with cookie
|
// Authenticate with cookie
|
||||||
auth := iflow.NewIFlowAuth(cfg)
|
auth := iflow.NewIFlowAuth(cfg)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -82,5 +94,5 @@ func promptForCookie(promptFn func(string) (string, error)) (string, error) {
|
|||||||
// getAuthFilePath returns the auth file path for the given provider and email
|
// getAuthFilePath returns the auth file path for the given provider and email
|
||||||
func getAuthFilePath(cfg *config.Config, provider, email string) string {
|
func getAuthFilePath(cfg *config.Config, provider, email string) string {
|
||||||
fileName := iflow.SanitizeIFlowFileName(email)
|
fileName := iflow.SanitizeIFlowFileName(email)
|
||||||
return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, fileName)
|
return fmt.Sprintf("%s/%s-%s-%d.json", cfg.AuthDir, provider, fileName, time.Now().Unix())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,13 +54,14 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), opts.Stream)
|
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), opts.Stream)
|
||||||
if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
|
modelOverride := e.resolveUpstreamModel(req.Model, auth)
|
||||||
|
if modelOverride != "" {
|
||||||
translated = e.overrideModel(translated, modelOverride)
|
translated = e.overrideModel(translated, modelOverride)
|
||||||
}
|
}
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
||||||
translated = applyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort")
|
translated = applyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort")
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" {
|
if upstreamModel != "" && modelOverride == "" {
|
||||||
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
translated = normalizeThinkingConfig(translated, upstreamModel)
|
translated = normalizeThinkingConfig(translated, upstreamModel)
|
||||||
@@ -148,13 +149,14 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
if modelOverride := e.resolveUpstreamModel(req.Model, auth); modelOverride != "" {
|
modelOverride := e.resolveUpstreamModel(req.Model, auth)
|
||||||
|
if modelOverride != "" {
|
||||||
translated = e.overrideModel(translated, modelOverride)
|
translated = e.overrideModel(translated, modelOverride)
|
||||||
}
|
}
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
translated = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", translated)
|
||||||
translated = applyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort")
|
translated = applyReasoningEffortMetadata(translated, req.Metadata, req.Model, "reasoning_effort")
|
||||||
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
upstreamModel := util.ResolveOriginalModel(req.Model, req.Metadata)
|
||||||
if upstreamModel != "" {
|
if upstreamModel != "" && modelOverride == "" {
|
||||||
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
translated, _ = sjson.SetBytes(translated, "model", upstreamModel)
|
||||||
}
|
}
|
||||||
translated = normalizeThinkingConfig(translated, upstreamModel)
|
translated = normalizeThinkingConfig(translated, upstreamModel)
|
||||||
|
|||||||
@@ -52,10 +52,14 @@ func applyReasoningEffortMetadata(payload []byte, metadata map[string]any, model
|
|||||||
if len(metadata) == 0 {
|
if len(metadata) == 0 {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
if !util.ModelSupportsThinking(model) {
|
if field == "" {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
if field == "" {
|
baseModel := util.ResolveOriginalModel(model, metadata)
|
||||||
|
if baseModel == "" {
|
||||||
|
baseModel = model
|
||||||
|
}
|
||||||
|
if !util.ModelSupportsThinking(baseModel) && !util.IsOpenAICompatibilityModel(baseModel) {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" {
|
if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" {
|
||||||
@@ -239,6 +243,9 @@ func normalizeThinkingConfig(payload []byte, model string) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !util.ModelSupportsThinking(model) {
|
if !util.ModelSupportsThinking(model) {
|
||||||
|
if util.IsOpenAICompatibilityModel(model) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
return stripThinkingFields(payload, false)
|
return stripThinkingFields(payload, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,33 +25,33 @@ func ModelSupportsThinking(model string) bool {
|
|||||||
// or min (0 if zero is allowed and mid <= 0).
|
// or min (0 if zero is allowed and mid <= 0).
|
||||||
func NormalizeThinkingBudget(model string, budget int) int {
|
func NormalizeThinkingBudget(model string, budget int) int {
|
||||||
if budget == -1 { // dynamic
|
if budget == -1 { // dynamic
|
||||||
if found, min, max, zeroAllowed, dynamicAllowed := thinkingRangeFromRegistry(model); found {
|
if found, minBudget, maxBudget, zeroAllowed, dynamicAllowed := thinkingRangeFromRegistry(model); found {
|
||||||
if dynamicAllowed {
|
if dynamicAllowed {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
mid := (min + max) / 2
|
mid := (minBudget + maxBudget) / 2
|
||||||
if mid <= 0 && zeroAllowed {
|
if mid <= 0 && zeroAllowed {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if mid <= 0 {
|
if mid <= 0 {
|
||||||
return min
|
return minBudget
|
||||||
}
|
}
|
||||||
return mid
|
return mid
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
if found, min, max, zeroAllowed, _ := thinkingRangeFromRegistry(model); found {
|
if found, minBudget, maxBudget, zeroAllowed, _ := thinkingRangeFromRegistry(model); found {
|
||||||
if budget == 0 {
|
if budget == 0 {
|
||||||
if zeroAllowed {
|
if zeroAllowed {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return min
|
return minBudget
|
||||||
}
|
}
|
||||||
if budget < min {
|
if budget < minBudget {
|
||||||
return min
|
return minBudget
|
||||||
}
|
}
|
||||||
if budget > max {
|
if budget > maxBudget {
|
||||||
return max
|
return maxBudget
|
||||||
}
|
}
|
||||||
return budget
|
return budget
|
||||||
}
|
}
|
||||||
@@ -105,3 +105,16 @@ func NormalizeReasoningEffortLevel(model, effort string) (string, bool) {
|
|||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsOpenAICompatibilityModel reports whether the model is registered as an OpenAI-compatibility model.
|
||||||
|
// These models may not advertise Thinking metadata in the registry.
|
||||||
|
func IsOpenAICompatibilityModel(model string) bool {
|
||||||
|
if model == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
info := registry.GetGlobalRegistry().GetModelInfo(model)
|
||||||
|
if info == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(strings.TrimSpace(info.Type), "openai-compatibility")
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -61,6 +62,7 @@ type Watcher struct {
|
|||||||
reloadCallback func(*config.Config)
|
reloadCallback func(*config.Config)
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
lastAuthHashes map[string]string
|
lastAuthHashes map[string]string
|
||||||
|
lastRemoveTimes map[string]time.Time
|
||||||
lastConfigHash string
|
lastConfigHash string
|
||||||
authQueue chan<- AuthUpdate
|
authQueue chan<- AuthUpdate
|
||||||
currentAuths map[string]*coreauth.Auth
|
currentAuths map[string]*coreauth.Auth
|
||||||
@@ -129,6 +131,7 @@ const (
|
|||||||
// before deciding whether a Remove event indicates a real deletion.
|
// before deciding whether a Remove event indicates a real deletion.
|
||||||
replaceCheckDelay = 50 * time.Millisecond
|
replaceCheckDelay = 50 * time.Millisecond
|
||||||
configReloadDebounce = 150 * time.Millisecond
|
configReloadDebounce = 150 * time.Millisecond
|
||||||
|
authRemoveDebounceWindow = 1 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWatcher creates a new file watcher instance
|
// NewWatcher creates a new file watcher instance
|
||||||
@@ -721,8 +724,9 @@ func (w *Watcher) authFileUnchanged(path string) (bool, error) {
|
|||||||
sum := sha256.Sum256(data)
|
sum := sha256.Sum256(data)
|
||||||
curHash := hex.EncodeToString(sum[:])
|
curHash := hex.EncodeToString(sum[:])
|
||||||
|
|
||||||
|
normalized := w.normalizeAuthPath(path)
|
||||||
w.clientsMutex.RLock()
|
w.clientsMutex.RLock()
|
||||||
prevHash, ok := w.lastAuthHashes[path]
|
prevHash, ok := w.lastAuthHashes[normalized]
|
||||||
w.clientsMutex.RUnlock()
|
w.clientsMutex.RUnlock()
|
||||||
if ok && prevHash == curHash {
|
if ok && prevHash == curHash {
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -731,19 +735,63 @@ func (w *Watcher) authFileUnchanged(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) isKnownAuthFile(path string) bool {
|
func (w *Watcher) isKnownAuthFile(path string) bool {
|
||||||
|
normalized := w.normalizeAuthPath(path)
|
||||||
w.clientsMutex.RLock()
|
w.clientsMutex.RLock()
|
||||||
defer w.clientsMutex.RUnlock()
|
defer w.clientsMutex.RUnlock()
|
||||||
_, ok := w.lastAuthHashes[path]
|
_, ok := w.lastAuthHashes[normalized]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) normalizeAuthPath(path string) string {
|
||||||
|
trimmed := strings.TrimSpace(path)
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cleaned := filepath.Clean(trimmed)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cleaned = strings.TrimPrefix(cleaned, `\\?\`)
|
||||||
|
cleaned = strings.ToLower(cleaned)
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) shouldDebounceRemove(normalizedPath string, now time.Time) bool {
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.clientsMutex.Lock()
|
||||||
|
if w.lastRemoveTimes == nil {
|
||||||
|
w.lastRemoveTimes = make(map[string]time.Time)
|
||||||
|
}
|
||||||
|
if last, ok := w.lastRemoveTimes[normalizedPath]; ok {
|
||||||
|
if now.Sub(last) < authRemoveDebounceWindow {
|
||||||
|
w.clientsMutex.Unlock()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.lastRemoveTimes[normalizedPath] = now
|
||||||
|
if len(w.lastRemoveTimes) > 128 {
|
||||||
|
cutoff := now.Add(-2 * authRemoveDebounceWindow)
|
||||||
|
for p, t := range w.lastRemoveTimes {
|
||||||
|
if t.Before(cutoff) {
|
||||||
|
delete(w.lastRemoveTimes, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.clientsMutex.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// handleEvent processes individual file system events
|
// handleEvent processes individual file system events
|
||||||
func (w *Watcher) handleEvent(event fsnotify.Event) {
|
func (w *Watcher) handleEvent(event fsnotify.Event) {
|
||||||
// Filter only relevant events: config file or auth-dir JSON files.
|
// Filter only relevant events: config file or auth-dir JSON files.
|
||||||
configOps := fsnotify.Write | fsnotify.Create | fsnotify.Rename
|
configOps := fsnotify.Write | fsnotify.Create | fsnotify.Rename
|
||||||
isConfigEvent := event.Name == w.configPath && event.Op&configOps != 0
|
normalizedName := w.normalizeAuthPath(event.Name)
|
||||||
|
normalizedConfigPath := w.normalizeAuthPath(w.configPath)
|
||||||
|
normalizedAuthDir := w.normalizeAuthPath(w.authDir)
|
||||||
|
isConfigEvent := normalizedName == normalizedConfigPath && event.Op&configOps != 0
|
||||||
authOps := fsnotify.Create | fsnotify.Write | fsnotify.Remove | fsnotify.Rename
|
authOps := fsnotify.Create | fsnotify.Write | fsnotify.Remove | fsnotify.Rename
|
||||||
isAuthJSON := strings.HasPrefix(event.Name, w.authDir) && strings.HasSuffix(event.Name, ".json") && event.Op&authOps != 0
|
isAuthJSON := strings.HasPrefix(normalizedName, normalizedAuthDir) && strings.HasSuffix(normalizedName, ".json") && event.Op&authOps != 0
|
||||||
if !isConfigEvent && !isAuthJSON {
|
if !isConfigEvent && !isAuthJSON {
|
||||||
// Ignore unrelated files (e.g., cookie snapshots *.cookie) and other noise.
|
// Ignore unrelated files (e.g., cookie snapshots *.cookie) and other noise.
|
||||||
return
|
return
|
||||||
@@ -761,6 +809,10 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|||||||
|
|
||||||
// Handle auth directory changes incrementally (.json only)
|
// Handle auth directory changes incrementally (.json only)
|
||||||
if event.Op&(fsnotify.Remove|fsnotify.Rename) != 0 {
|
if event.Op&(fsnotify.Remove|fsnotify.Rename) != 0 {
|
||||||
|
if w.shouldDebounceRemove(normalizedName, now) {
|
||||||
|
log.Debugf("debouncing remove event for %s", filepath.Base(event.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
// Atomic replace on some platforms may surface as Rename (or Remove) before the new file is ready.
|
// Atomic replace on some platforms may surface as Rename (or Remove) before the new file is ready.
|
||||||
// Wait briefly; if the path exists again, treat as an update instead of removal.
|
// Wait briefly; if the path exists again, treat as an update instead of removal.
|
||||||
time.Sleep(replaceCheckDelay)
|
time.Sleep(replaceCheckDelay)
|
||||||
@@ -978,7 +1030,8 @@ func (w *Watcher) reloadClients(rescanAuth bool, affectedOAuthProviders []string
|
|||||||
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
|
||||||
if data, errReadFile := os.ReadFile(path); errReadFile == nil && len(data) > 0 {
|
if data, errReadFile := os.ReadFile(path); errReadFile == nil && len(data) > 0 {
|
||||||
sum := sha256.Sum256(data)
|
sum := sha256.Sum256(data)
|
||||||
w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
|
normalizedPath := w.normalizeAuthPath(path)
|
||||||
|
w.lastAuthHashes[normalizedPath] = hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1025,6 +1078,7 @@ func (w *Watcher) addOrUpdateClient(path string) {
|
|||||||
|
|
||||||
sum := sha256.Sum256(data)
|
sum := sha256.Sum256(data)
|
||||||
curHash := hex.EncodeToString(sum[:])
|
curHash := hex.EncodeToString(sum[:])
|
||||||
|
normalized := w.normalizeAuthPath(path)
|
||||||
|
|
||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
|
|
||||||
@@ -1034,14 +1088,14 @@ func (w *Watcher) addOrUpdateClient(path string) {
|
|||||||
w.clientsMutex.Unlock()
|
w.clientsMutex.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if prev, ok := w.lastAuthHashes[path]; ok && prev == curHash {
|
if prev, ok := w.lastAuthHashes[normalized]; ok && prev == curHash {
|
||||||
log.Debugf("auth file unchanged (hash match), skipping reload: %s", filepath.Base(path))
|
log.Debugf("auth file unchanged (hash match), skipping reload: %s", filepath.Base(path))
|
||||||
w.clientsMutex.Unlock()
|
w.clientsMutex.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update hash cache
|
// Update hash cache
|
||||||
w.lastAuthHashes[path] = curHash
|
w.lastAuthHashes[normalized] = curHash
|
||||||
|
|
||||||
w.clientsMutex.Unlock() // Unlock before the callback
|
w.clientsMutex.Unlock() // Unlock before the callback
|
||||||
|
|
||||||
@@ -1056,10 +1110,11 @@ func (w *Watcher) addOrUpdateClient(path string) {
|
|||||||
|
|
||||||
// removeClient handles the removal of a single client.
|
// removeClient handles the removal of a single client.
|
||||||
func (w *Watcher) removeClient(path string) {
|
func (w *Watcher) removeClient(path string) {
|
||||||
|
normalized := w.normalizeAuthPath(path)
|
||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
|
|
||||||
cfg := w.config
|
cfg := w.config
|
||||||
delete(w.lastAuthHashes, path)
|
delete(w.lastAuthHashes, normalized)
|
||||||
|
|
||||||
w.clientsMutex.Unlock() // Release the lock before the callback
|
w.clientsMutex.Unlock() // Release the lock before the callback
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
return nil, fmt.Errorf("iflow authentication failed: missing account identifier")
|
return nil, fmt.Errorf("iflow authentication failed: missing account identifier")
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := fmt.Sprintf("iflow-%s.json", email)
|
fileName := fmt.Sprintf("iflow-%s-%d.json", email, time.Now().Unix())
|
||||||
metadata := map[string]any{
|
metadata := map[string]any{
|
||||||
"email": email,
|
"email": email,
|
||||||
"api_key": tokenStorage.APIKey,
|
"api_key": tokenStorage.APIKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user