Unified · Split
| @@ -1745,6 +1745,17 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { · 1744 unmodified lines | ||
| 1745 | 1745 | return |
| 1746 | 1746 | } |
| 1747 | 1747 | |
| 1748 | + // Check for duplicate BXAuth before authentication | |
| 1749 | + bxAuth := iflowauth.ExtractBXAuth(cookieValue) | |
| 1750 | + if existingFile, err := iflowauth.CheckDuplicateBXAuth(h.cfg.AuthDir, bxAuth); err != nil { | |
| 1751 | + c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to check duplicate"}) | |
| 1752 | + return | |
| 1753 | + } else if existingFile != "" { | |
| 1754 | + existingFileName := filepath.Base(existingFile) | |
| 1755 | + c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "duplicate BXAuth found", "existing_file": existingFileName}) | |
| 1756 | + return | |
| 1757 | + } | |
| 1758 | + | |
| 1748 | 1759 | authSvc := iflowauth.NewIFlowAuth(h.cfg) |
| 1749 | 1760 | tokenData, errAuth := authSvc.AuthenticateWithCookie(ctx, cookieValue) |
| 1750 | 1761 | if errAuth != nil { |
| @@ -1767,11 +1778,12 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { · 16 unmodified lines | ||
| 1767 | 1778 | } |
| 1768 | 1779 | |
| 1769 | 1780 | tokenStorage.Email = email |
| 1781 | + timestamp := time.Now().Unix() | |
| 1770 | 1782 | |
| 1771 | 1783 | record := &coreauth.Auth{ |
| 1772 | − ID: fmt.Sprintf("iflow-%s.json", fileName), | |
| 1784 | + ID: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp), | |
| 1773 | 1785 | Provider: "iflow", |
| 1774 | − FileName: fmt.Sprintf("iflow-%s.json", fileName), | |
| 1786 | + FileName: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp), | |
| 1775 | 1787 | Storage: tokenStorage, |
| 1776 | 1788 | Metadata: map[string]any{ |
| 1777 | 1789 | "email": email, |
| @@ -1,7 +1,10 @@ | ||
| 1 | 1 | package iflow |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | + "encoding/json" | |
| 4 | 5 | "fmt" |
| 6 | + "os" | |
| 7 | + "path/filepath" | |
| 5 | 8 | "strings" |
| 6 | 9 | ) |
| 7 | 10 | |
| @@ -36,3 +39,61 @@ func SanitizeIFlowFileName(raw string) string { · 28 unmodified lines | ||
| 36 | 39 | } |
| 37 | 40 | return strings.TrimSpace(result.String()) |
| 38 | 41 | } |
| 42 | + | |
| 43 | +// ExtractBXAuth extracts the BXAuth value from a cookie string. | |
| 44 | +func ExtractBXAuth(cookie string) string { | |
| 45 | + parts := strings.Split(cookie, ";") | |
| 46 | + for _, part := range parts { | |
| 47 | + part = strings.TrimSpace(part) | |
| 48 | + if strings.HasPrefix(part, "BXAuth=") { | |
| 49 | + return strings.TrimPrefix(part, "BXAuth=") | |
| 50 | + } | |
| 51 | + } | |
| 52 | + return "" | |
| 53 | +} | |
| 54 | + | |
| 55 | +// CheckDuplicateBXAuth checks if the given BXAuth value already exists in any iflow auth file. | |
| 56 | +// Returns the path of the existing file if found, empty string otherwise. | |
| 57 | +func CheckDuplicateBXAuth(authDir, bxAuth string) (string, error) { | |
| 58 | + if bxAuth == "" { | |
| 59 | + return "", nil | |
| 60 | + } | |
| 61 | + | |
| 62 | + entries, err := os.ReadDir(authDir) | |
| 63 | + if err != nil { | |
| 64 | + if os.IsNotExist(err) { | |
| 65 | + return "", nil | |
| 66 | + } | |
| 67 | + return "", fmt.Errorf("read auth dir failed: %w", err) | |
| 68 | + } | |
| 69 | + | |
| 70 | + for _, entry := range entries { | |
| 71 | + if entry.IsDir() { | |
| 72 | + continue | |
| 73 | + } | |
| 74 | + name := entry.Name() | |
| 75 | + if !strings.HasPrefix(name, "iflow-") || !strings.HasSuffix(name, ".json") { | |
| 76 | + continue | |
| 77 | + } | |
| 78 | + | |
| 79 | + filePath := filepath.Join(authDir, name) | |
| 80 | + data, err := os.ReadFile(filePath) | |
| 81 | + if err != nil { | |
| 82 | + continue | |
| 83 | + } | |
| 84 | + | |
| 85 | + var tokenData struct { | |
| 86 | + Cookie string `json:"cookie"` | |
| 87 | + } | |
| 88 | + if err := json.Unmarshal(data, &tokenData); err != nil { | |
| 89 | + continue | |
| 90 | + } | |
| 91 | + | |
| 92 | + existingBXAuth := ExtractBXAuth(tokenData.Cookie) | |
| 93 | + if existingBXAuth != "" && existingBXAuth == bxAuth { | |
| 94 | + return filePath, nil | |
| 95 | + } | |
| 96 | + } | |
| 97 | + | |
| 98 | + return "", nil | |
| 99 | +} | |
| @@ -506,11 +506,18 @@ func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenS · 505 unmodified lines | ||
| 506 | 506 | return nil |
| 507 | 507 | } |
| 508 | 508 | |
| 509 | + // Only save the BXAuth field from the cookie | |
| 510 | + bxAuth := ExtractBXAuth(data.Cookie) | |
| 511 | + cookieToSave := "" | |
| 512 | + if bxAuth != "" { | |
| 513 | + cookieToSave = "BXAuth=" + bxAuth + ";" | |
| 514 | + } | |
| 515 | + | |
| 509 | 516 | return &IFlowTokenStorage{ |
| 510 | 517 | APIKey: data.APIKey, |
| 511 | 518 | Email: data.Email, |
| 512 | 519 | Expire: data.Expire, |
| 513 | − Cookie: data.Cookie, | |
| 520 | + Cookie: cookieToSave, | |
| 514 | 521 | LastRefresh: time.Now().Format(time.RFC3339), |
| 515 | 522 | Type: "iflow", |
| 516 | 523 | } |
| @@ -5,7 +5,9 @@ import ( · 4 unmodified lines | ||
| 5 | 5 | "context" |
| 6 | 6 | "fmt" |
| 7 | 7 | "os" |
| 8 | + "path/filepath" | |
| 8 | 9 | "strings" |
| 10 | + "time" | |
| 9 | 11 | |
| 10 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" |
| 11 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| @@ -37,6 +39,16 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) { · 25 unmodified lines | ||
| 37 | 39 | return |
| 38 | 40 | } |
| 39 | 41 | |
| 42 | + // Check for duplicate BXAuth before authentication | |
| 43 | + bxAuth := iflow.ExtractBXAuth(cookie) | |
| 44 | + if existingFile, err := iflow.CheckDuplicateBXAuth(cfg.AuthDir, bxAuth); err != nil { | |
| 45 | + fmt.Printf("Failed to check duplicate: %v\n", err) | |
| 46 | + return | |
| 47 | + } else if existingFile != "" { | |
| 48 | + fmt.Printf("Duplicate BXAuth found, authentication already exists: %s\n", filepath.Base(existingFile)) | |
| 49 | + return | |
| 50 | + } | |
| 51 | + | |
| 40 | 52 | // Authenticate with cookie |
| 41 | 53 | auth := iflow.NewIFlowAuth(cfg) |
| 42 | 54 | ctx := context.Background() |
| @@ -82,5 +94,5 @@ func promptForCookie(promptFn func(string) (string, error)) (string, error) { · 39 unmodified lines | ||
| 82 | 94 | // getAuthFilePath returns the auth file path for the given provider and email |
| 83 | 95 | func getAuthFilePath(cfg *config.Config, provider, email string) string { |
| 84 | 96 | fileName := iflow.SanitizeIFlowFileName(email) |
| 85 | − return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, fileName) | |
| 97 | + return fmt.Sprintf("%s/%s-%s-%d.json", cfg.AuthDir, provider, fileName, time.Now().Unix()) | |
| 86 | 98 | } |
| @@ -107,7 +107,7 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts · 106 unmodified lines | ||
| 107 | 107 | return nil, fmt.Errorf("iflow authentication failed: missing account identifier") |
| 108 | 108 | } |
| 109 | 109 | |
| 110 | − fileName := fmt.Sprintf("iflow-%s.json", email) | |
| 110 | + fileName := fmt.Sprintf("iflow-%s-%d.json", email, time.Now().Unix()) | |
| 111 | 111 | metadata := map[string]any{ |
| 112 | 112 | "email": email, |
| 113 | 113 | "api_key": tokenStorage.APIKey, |