mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
feat(auth): add iFlow cookie-based authentication support
This commit is contained in:
@@ -59,6 +59,7 @@ func main() {
|
||||
var claudeLogin bool
|
||||
var qwenLogin bool
|
||||
var iflowLogin bool
|
||||
var iflowCookie bool
|
||||
var noBrowser bool
|
||||
var projectID string
|
||||
var vertexImport string
|
||||
@@ -71,6 +72,7 @@ func main() {
|
||||
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
||||
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
||||
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
||||
flag.BoolVar(&iflowCookie, "iflow-cookie", false, "Login to iFlow using Cookie")
|
||||
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
||||
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
||||
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
|
||||
@@ -439,6 +441,8 @@ func main() {
|
||||
cmd.DoQwenLogin(cfg, options)
|
||||
} else if iflowLogin {
|
||||
cmd.DoIFlowLogin(cfg, options)
|
||||
} else if iflowCookie {
|
||||
cmd.DoIFlowCookieAuth(cfg, options)
|
||||
} else {
|
||||
// In cloud deploy mode without config file, just wait for shutdown signals
|
||||
if isCloudDeploy && !configFileExists {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package iflow
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -23,6 +24,9 @@ const (
|
||||
iFlowUserInfoEndpoint = "https://iflow.cn/api/oauth/getUserInfo"
|
||||
iFlowSuccessRedirectURL = "https://iflow.cn/oauth/success"
|
||||
|
||||
// Cookie authentication endpoints
|
||||
iFlowAPIKeyEndpoint = "https://platform.iflow.cn/api/openapi/apikey"
|
||||
|
||||
// Client credentials provided by iFlow for the Code Assist integration.
|
||||
iFlowOAuthClientID = "10009311001"
|
||||
iFlowOAuthClientSecret = "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW"
|
||||
@@ -261,6 +265,7 @@ type IFlowTokenData struct {
|
||||
Expire string
|
||||
APIKey string
|
||||
Email string
|
||||
Cookie string
|
||||
}
|
||||
|
||||
// userInfoResponse represents the structure returned by the user info endpoint.
|
||||
@@ -274,3 +279,232 @@ type userInfoData struct {
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
// iFlowAPIKeyResponse represents the response from the API key endpoint
|
||||
type iFlowAPIKeyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data iFlowKeyData `json:"data"`
|
||||
Extra interface{} `json:"extra"`
|
||||
}
|
||||
|
||||
// iFlowKeyData contains the API key information
|
||||
type iFlowKeyData struct {
|
||||
HasExpired bool `json:"hasExpired"`
|
||||
ExpireTime string `json:"expireTime"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
APIKeyMask string `json:"apiKeyMask"`
|
||||
}
|
||||
|
||||
// iFlowRefreshRequest represents the request body for refreshing API key
|
||||
type iFlowRefreshRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// AuthenticateWithCookie performs authentication using browser cookies
|
||||
func (ia *IFlowAuth) AuthenticateWithCookie(ctx context.Context, cookie string) (*IFlowTokenData, error) {
|
||||
if strings.TrimSpace(cookie) == "" {
|
||||
return nil, fmt.Errorf("iflow cookie authentication: cookie is empty")
|
||||
}
|
||||
|
||||
// First, get initial API key information using GET request
|
||||
keyInfo, err := ia.fetchAPIKeyInfo(ctx, cookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie authentication: fetch initial API key info failed: %w", err)
|
||||
}
|
||||
|
||||
// Convert to token data format
|
||||
data := &IFlowTokenData{
|
||||
APIKey: keyInfo.APIKey,
|
||||
Expire: keyInfo.ExpireTime,
|
||||
Email: keyInfo.Name,
|
||||
Cookie: cookie,
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// fetchAPIKeyInfo retrieves API key information using GET request with cookie
|
||||
func (ia *IFlowAuth) fetchAPIKeyInfo(ctx context.Context, cookie string) (*iFlowKeyData, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, iFlowAPIKeyEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie: create GET request failed: %w", err)
|
||||
}
|
||||
|
||||
// Set cookie and other headers to mimic browser
|
||||
req.Header.Set("Cookie", cookie)
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
||||
|
||||
resp, err := ia.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie: GET request failed: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// Handle gzip compression
|
||||
var reader io.Reader = resp.Body
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gzipReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie: create gzip reader failed: %w", err)
|
||||
}
|
||||
defer func() { _ = gzipReader.Close() }()
|
||||
reader = gzipReader
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie: read GET response failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Debugf("iflow cookie GET request failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||
return nil, fmt.Errorf("iflow cookie: GET request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var keyResp iFlowAPIKeyResponse
|
||||
if err = json.Unmarshal(body, &keyResp); err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie: decode GET response failed: %w", err)
|
||||
}
|
||||
|
||||
if !keyResp.Success {
|
||||
return nil, fmt.Errorf("iflow cookie: GET request not successful: %s", keyResp.Message)
|
||||
}
|
||||
|
||||
// Handle initial response where apiKey field might be apiKeyMask
|
||||
if keyResp.Data.APIKey == "" && keyResp.Data.APIKeyMask != "" {
|
||||
keyResp.Data.APIKey = keyResp.Data.APIKeyMask
|
||||
}
|
||||
|
||||
return &keyResp.Data, nil
|
||||
}
|
||||
|
||||
// RefreshAPIKey refreshes the API key using POST request
|
||||
func (ia *IFlowAuth) RefreshAPIKey(ctx context.Context, cookie, name string) (*iFlowKeyData, error) {
|
||||
if strings.TrimSpace(cookie) == "" {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: cookie is empty")
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: name is empty")
|
||||
}
|
||||
|
||||
// Prepare request body
|
||||
refreshReq := iFlowRefreshRequest{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(refreshReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: marshal request failed: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, iFlowAPIKeyEndpoint, strings.NewReader(string(bodyBytes)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: create POST request failed: %w", err)
|
||||
}
|
||||
|
||||
// Set cookie and other headers to mimic browser
|
||||
req.Header.Set("Cookie", cookie)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Origin", "https://platform.iflow.cn")
|
||||
req.Header.Set("Referer", "https://platform.iflow.cn/")
|
||||
|
||||
resp, err := ia.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: POST request failed: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// Handle gzip compression
|
||||
var reader io.Reader = resp.Body
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gzipReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: create gzip reader failed: %w", err)
|
||||
}
|
||||
defer func() { _ = gzipReader.Close() }()
|
||||
reader = gzipReader
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: read POST response failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Debugf("iflow cookie POST request failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||
return nil, fmt.Errorf("iflow cookie refresh: POST request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var keyResp iFlowAPIKeyResponse
|
||||
if err = json.Unmarshal(body, &keyResp); err != nil {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: decode POST response failed: %w", err)
|
||||
}
|
||||
|
||||
if !keyResp.Success {
|
||||
return nil, fmt.Errorf("iflow cookie refresh: POST request not successful: %s", keyResp.Message)
|
||||
}
|
||||
|
||||
return &keyResp.Data, nil
|
||||
}
|
||||
|
||||
// ShouldRefreshAPIKey checks if the API key needs to be refreshed (within 2 days of expiry)
|
||||
func ShouldRefreshAPIKey(expireTime string) (bool, time.Duration, error) {
|
||||
if strings.TrimSpace(expireTime) == "" {
|
||||
return false, 0, fmt.Errorf("iflow cookie: expire time is empty")
|
||||
}
|
||||
|
||||
expire, err := time.Parse("2006-01-02 15:04", expireTime)
|
||||
if err != nil {
|
||||
return false, 0, fmt.Errorf("iflow cookie: parse expire time failed: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
twoDaysFromNow := now.Add(48 * time.Hour)
|
||||
|
||||
needsRefresh := expire.Before(twoDaysFromNow)
|
||||
timeUntilExpiry := expire.Sub(now)
|
||||
|
||||
return needsRefresh, timeUntilExpiry, nil
|
||||
}
|
||||
|
||||
// CreateCookieTokenStorage converts cookie-based token data into persistence storage
|
||||
func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenStorage {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &IFlowTokenStorage{
|
||||
APIKey: data.APIKey,
|
||||
Email: data.Email,
|
||||
Expire: data.Expire,
|
||||
Cookie: data.Cookie,
|
||||
LastRefresh: time.Now().Format(time.RFC3339),
|
||||
Type: "iflow",
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCookieTokenStorage updates the persisted token storage with refreshed API key data
|
||||
func (ia *IFlowAuth) UpdateCookieTokenStorage(storage *IFlowTokenStorage, keyData *iFlowKeyData) {
|
||||
if storage == nil || keyData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
storage.APIKey = keyData.APIKey
|
||||
storage.Expire = keyData.ExpireTime
|
||||
storage.LastRefresh = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type IFlowTokenStorage struct {
|
||||
Email string `json:"email"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
Cookie string `json:"cookie"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
|
||||
111
internal/cmd/iflow_cookie.go
Normal file
111
internal/cmd/iflow_cookie.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
)
|
||||
|
||||
// DoIFlowCookieAuth performs the iFlow cookie-based authentication.
|
||||
func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) {
|
||||
if options == nil {
|
||||
options = &LoginOptions{}
|
||||
}
|
||||
|
||||
promptFn := options.Prompt
|
||||
if promptFn == nil {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
promptFn = func(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
value, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt user for cookie
|
||||
cookie, err := promptForCookie(promptFn)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get cookie: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate with cookie
|
||||
auth := iflow.NewIFlowAuth(cfg)
|
||||
ctx := context.Background()
|
||||
|
||||
tokenData, err := auth.AuthenticateWithCookie(ctx, cookie)
|
||||
if err != nil {
|
||||
fmt.Printf("iFlow cookie authentication failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create token storage
|
||||
tokenStorage := auth.CreateCookieTokenStorage(tokenData)
|
||||
|
||||
// Get auth file path using email in filename
|
||||
authFilePath := getAuthFilePath(cfg, "iflow", tokenData.Email)
|
||||
|
||||
// Save token to file
|
||||
if err := tokenStorage.SaveTokenToFile(authFilePath); err != nil {
|
||||
fmt.Printf("Failed to save authentication: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Authentication successful! API key: %s\n", tokenData.APIKey)
|
||||
fmt.Printf("Expires at: %s\n", tokenData.Expire)
|
||||
fmt.Printf("Authentication saved to: %s\n", authFilePath)
|
||||
}
|
||||
|
||||
// promptForCookie prompts the user to enter their iFlow cookie
|
||||
func promptForCookie(promptFn func(string) (string, error)) (string, error) {
|
||||
line, err := promptFn("Enter iFlow Cookie (from browser cookies): ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read cookie: %w", err)
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return "", fmt.Errorf("cookie cannot be empty")
|
||||
}
|
||||
|
||||
// Clean up any extra whitespace and join multiple spaces
|
||||
cookie := strings.Join(strings.Fields(line), " ")
|
||||
|
||||
// Ensure it ends properly
|
||||
if !strings.HasSuffix(cookie, ";") {
|
||||
cookie = cookie + ";"
|
||||
}
|
||||
|
||||
// Ensure BXAuth is present in the cookie
|
||||
if !strings.Contains(cookie, "BXAuth=") {
|
||||
return "", fmt.Errorf("BXAuth field not found in cookie")
|
||||
}
|
||||
|
||||
return cookie, nil
|
||||
}
|
||||
|
||||
// getAuthFilePath returns the auth file path for the given provider and email
|
||||
func getAuthFilePath(cfg *config.Config, provider, email string) string {
|
||||
// Clean email to make it filename-safe
|
||||
cleanEmail := strings.ReplaceAll(email, "@", "_at_")
|
||||
cleanEmail = strings.ReplaceAll(cleanEmail, ".", "_")
|
||||
cleanEmail = strings.ReplaceAll(cleanEmail, "-", "_")
|
||||
|
||||
// Remove any remaining special characters
|
||||
var result strings.Builder
|
||||
for _, r := range cleanEmail {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s-%s.json", cfg.AuthDir, provider, result.String())
|
||||
}
|
||||
@@ -242,13 +242,87 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
||||
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
||||
}
|
||||
|
||||
// Refresh refreshes OAuth tokens and updates the stored API key.
|
||||
// Refresh refreshes OAuth tokens or cookie-based API keys and updates the stored API key.
|
||||
func (e *IFlowExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||
log.Debugf("iflow executor: refresh called")
|
||||
if auth == nil {
|
||||
return nil, fmt.Errorf("iflow executor: auth is nil")
|
||||
}
|
||||
|
||||
// Check if this is cookie-based authentication
|
||||
var cookie string
|
||||
var email string
|
||||
if auth.Metadata != nil {
|
||||
if v, ok := auth.Metadata["cookie"].(string); ok {
|
||||
cookie = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := auth.Metadata["email"].(string); ok {
|
||||
email = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
|
||||
// If cookie is present, use cookie-based refresh
|
||||
if cookie != "" && email != "" {
|
||||
return e.refreshCookieBased(ctx, auth, cookie, email)
|
||||
}
|
||||
|
||||
// Otherwise, use OAuth-based refresh
|
||||
return e.refreshOAuthBased(ctx, auth)
|
||||
}
|
||||
|
||||
// refreshCookieBased refreshes API key using browser cookie
|
||||
func (e *IFlowExecutor) refreshCookieBased(ctx context.Context, auth *cliproxyauth.Auth, cookie, email string) (*cliproxyauth.Auth, error) {
|
||||
log.Debugf("iflow executor: checking refresh need for cookie-based API key for user: %s", email)
|
||||
|
||||
// Get current expiry time from metadata
|
||||
var currentExpire string
|
||||
if auth.Metadata != nil {
|
||||
if v, ok := auth.Metadata["expired"].(string); ok {
|
||||
currentExpire = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if refresh is needed
|
||||
needsRefresh, _, err := iflowauth.ShouldRefreshAPIKey(currentExpire)
|
||||
if err != nil {
|
||||
log.Warnf("iflow executor: failed to check refresh need: %v", err)
|
||||
// If we can't check, continue with refresh anyway as a safety measure
|
||||
} else if !needsRefresh {
|
||||
log.Debugf("iflow executor: no refresh needed for user: %s", email)
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
log.Infof("iflow executor: refreshing cookie-based API key for user: %s", email)
|
||||
|
||||
svc := iflowauth.NewIFlowAuth(e.cfg)
|
||||
keyData, err := svc.RefreshAPIKey(ctx, cookie, email)
|
||||
if err != nil {
|
||||
log.Errorf("iflow executor: cookie-based API key refresh failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if auth.Metadata == nil {
|
||||
auth.Metadata = make(map[string]any)
|
||||
}
|
||||
auth.Metadata["api_key"] = keyData.APIKey
|
||||
auth.Metadata["expired"] = keyData.ExpireTime
|
||||
auth.Metadata["type"] = "iflow"
|
||||
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
|
||||
auth.Metadata["cookie"] = cookie
|
||||
auth.Metadata["email"] = email
|
||||
|
||||
log.Infof("iflow executor: cookie-based API key refreshed successfully, new expiry: %s", keyData.ExpireTime)
|
||||
|
||||
if auth.Attributes == nil {
|
||||
auth.Attributes = make(map[string]string)
|
||||
}
|
||||
auth.Attributes["api_key"] = keyData.APIKey
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
// refreshOAuthBased refreshes tokens using OAuth refresh token
|
||||
func (e *IFlowExecutor) refreshOAuthBased(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||
refreshToken := ""
|
||||
oldAccessToken := ""
|
||||
if auth.Metadata != nil {
|
||||
|
||||
@@ -152,11 +152,29 @@ func (a *Auth) AccountInfo() (string, string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if a.Metadata != nil {
|
||||
if v, ok := a.Metadata["email"].(string); ok {
|
||||
return "oauth", v
|
||||
|
||||
// For iFlow provider, prioritize OAuth type if email is present
|
||||
if strings.ToLower(a.Provider) == "iflow" {
|
||||
if a.Metadata != nil {
|
||||
if email, ok := a.Metadata["email"].(string); ok {
|
||||
email = strings.TrimSpace(email)
|
||||
if email != "" {
|
||||
return "oauth", email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check metadata for email first (OAuth-style auth)
|
||||
if a.Metadata != nil {
|
||||
if v, ok := a.Metadata["email"].(string); ok {
|
||||
email := strings.TrimSpace(v)
|
||||
if email != "" {
|
||||
return "oauth", email
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to API key (API-key auth)
|
||||
if a.Attributes != nil {
|
||||
if v := a.Attributes["api_key"]; v != "" {
|
||||
return "api_key", v
|
||||
@@ -259,6 +277,7 @@ func parseTimeValue(v any) (time.Time, bool) {
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02T15:04:05Z07:00",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
|
||||
Reference in New Issue
Block a user