feat(auth): Use user info for iFlow auth identifier

This commit is contained in:
hkfires
2025-10-05 20:11:30 +08:00
parent eac8b1a27f
commit 8ca041cfcf
3 changed files with 48 additions and 45 deletions

View File

@@ -1013,13 +1013,17 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
} }
tokenStorage := authSvc.CreateTokenStorage(tokenData) tokenStorage := authSvc.CreateTokenStorage(tokenData)
tokenStorage.Email = fmt.Sprintf("iflow-%d", time.Now().UnixMilli()) identifier := strings.TrimSpace(tokenStorage.Email)
if identifier == "" {
identifier = fmt.Sprintf("iflow-%d", time.Now().UnixMilli())
tokenStorage.Email = identifier
}
record := &coreauth.Auth{ record := &coreauth.Auth{
ID: fmt.Sprintf("iflow-%s.json", tokenStorage.Email), ID: fmt.Sprintf("iflow-%s.json", identifier),
Provider: "iflow", Provider: "iflow",
FileName: fmt.Sprintf("iflow-%s.json", tokenStorage.Email), FileName: fmt.Sprintf("iflow-%s.json", identifier),
Storage: tokenStorage, Storage: tokenStorage,
Metadata: map[string]any{"email": tokenStorage.Email, "api_key": tokenStorage.APIKey}, Metadata: map[string]any{"email": identifier, "api_key": tokenStorage.APIKey},
Attributes: map[string]string{"api_key": tokenStorage.APIKey}, Attributes: map[string]string{"api_key": tokenStorage.APIKey},
} }

View File

@@ -141,61 +141,69 @@ func (ia *IFlowAuth) doTokenRequest(ctx context.Context, req *http.Request) (*IF
return nil, fmt.Errorf("iflow token: missing access token in response") return nil, fmt.Errorf("iflow token: missing access token in response")
} }
apiKey, errAPI := ia.FetchAPIKey(ctx, tokenResp.AccessToken) info, errAPI := ia.FetchUserInfo(ctx, tokenResp.AccessToken)
if errAPI != nil { if errAPI != nil {
return nil, fmt.Errorf("iflow token: fetch api key failed: %w", errAPI) return nil, fmt.Errorf("iflow token: fetch user info failed: %w", errAPI)
} }
if strings.TrimSpace(apiKey) == "" { if strings.TrimSpace(info.APIKey) == "" {
return nil, fmt.Errorf("iflow token: empty api key returned") return nil, fmt.Errorf("iflow token: empty api key returned")
} }
data.APIKey = apiKey email := strings.TrimSpace(info.Email)
if email == "" {
email = strings.TrimSpace(info.Phone)
}
if email == "" {
return nil, fmt.Errorf("iflow token: missing account email/phone in user info")
}
data.APIKey = info.APIKey
data.Email = email
return data, nil return data, nil
} }
// FetchAPIKey retrieves the account API key associated with the provided access token. // FetchUserInfo retrieves account metadata (including API key) for the provided access token.
func (ia *IFlowAuth) FetchAPIKey(ctx context.Context, accessToken string) (string, error) { func (ia *IFlowAuth) FetchUserInfo(ctx context.Context, accessToken string) (*userInfoData, error) {
if strings.TrimSpace(accessToken) == "" { if strings.TrimSpace(accessToken) == "" {
return "", fmt.Errorf("iflow api key: access token is empty") return nil, fmt.Errorf("iflow api key: access token is empty")
} }
endpoint := fmt.Sprintf("%s?accessToken=%s", iFlowUserInfoEndpoint, url.QueryEscape(accessToken)) endpoint := fmt.Sprintf("%s?accessToken=%s", iFlowUserInfoEndpoint, url.QueryEscape(accessToken))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("iflow api key: create request failed: %w", err) return nil, fmt.Errorf("iflow api key: create request failed: %w", err)
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
resp, err := ia.httpClient.Do(req) resp, err := ia.httpClient.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("iflow api key: request failed: %w", err) return nil, fmt.Errorf("iflow api key: request failed: %w", err)
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("iflow api key: read response failed: %w", err) return nil, fmt.Errorf("iflow api key: read response failed: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
log.Debugf("iflow api key failed: status=%d body=%s", resp.StatusCode, string(body)) log.Debugf("iflow api key failed: status=%d body=%s", resp.StatusCode, string(body))
return "", fmt.Errorf("iflow api key: %d %s", resp.StatusCode, strings.TrimSpace(string(body))) return nil, fmt.Errorf("iflow api key: %d %s", resp.StatusCode, strings.TrimSpace(string(body)))
} }
var result userInfoResponse var result userInfoResponse
if err = json.Unmarshal(body, &result); err != nil { if err = json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("iflow api key: decode body failed: %w", err) return nil, fmt.Errorf("iflow api key: decode body failed: %w", err)
} }
if !result.Success { if !result.Success {
return "", fmt.Errorf("iflow api key: request not successful") return nil, fmt.Errorf("iflow api key: request not successful")
} }
if result.Data.APIKey == "" { if result.Data.APIKey == "" {
return "", fmt.Errorf("iflow api key: missing api key in response") return nil, fmt.Errorf("iflow api key: missing api key in response")
} }
return result.Data.APIKey, nil return &result.Data, nil
} }
// CreateTokenStorage converts token data into persistence storage. // CreateTokenStorage converts token data into persistence storage.
@@ -209,6 +217,7 @@ func (ia *IFlowAuth) CreateTokenStorage(data *IFlowTokenData) *IFlowTokenStorage
LastRefresh: time.Now().Format(time.RFC3339), LastRefresh: time.Now().Format(time.RFC3339),
Expire: data.Expire, Expire: data.Expire,
APIKey: data.APIKey, APIKey: data.APIKey,
Email: data.Email,
TokenType: data.TokenType, TokenType: data.TokenType,
Scope: data.Scope, Scope: data.Scope,
} }
@@ -226,6 +235,9 @@ func (ia *IFlowAuth) UpdateTokenStorage(storage *IFlowTokenStorage, data *IFlowT
if data.APIKey != "" { if data.APIKey != "" {
storage.APIKey = data.APIKey storage.APIKey = data.APIKey
} }
if data.Email != "" {
storage.Email = data.Email
}
storage.TokenType = data.TokenType storage.TokenType = data.TokenType
storage.Scope = data.Scope storage.Scope = data.Scope
} }
@@ -247,13 +259,17 @@ type IFlowTokenData struct {
Scope string Scope string
Expire string Expire string
APIKey string APIKey string
Email string
} }
// userInfoResponse represents the structure returned by the user info endpoint. // userInfoResponse represents the structure returned by the user info endpoint.
type userInfoResponse struct { type userInfoResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Data struct { Data userInfoData `json:"data"`
APIKey string `json:"apiKey"` }
Email string `json:"email"`
} `json:"data"` type userInfoData struct {
APIKey string `json:"apiKey"`
Email string `json:"email"`
Phone string `json:"phone"`
} }

View File

@@ -102,31 +102,14 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
tokenStorage := authSvc.CreateTokenStorage(tokenData) tokenStorage := authSvc.CreateTokenStorage(tokenData)
email := "" email := strings.TrimSpace(tokenStorage.Email)
if opts.Metadata != nil {
email = opts.Metadata["email"]
if email == "" {
email = opts.Metadata["alias"]
}
}
if email == "" && opts.Prompt != nil {
email, err = opts.Prompt("Please input your email address or alias for iFlow:")
if err != nil {
return nil, err
}
}
email = strings.TrimSpace(email)
if email == "" { if email == "" {
return nil, &EmailRequiredError{Prompt: "Please provide an email address or alias for iFlow."} return nil, fmt.Errorf("iflow authentication failed: missing account identifier")
} }
tokenStorage.Email = email fileName := fmt.Sprintf("iflow-%s.json", email)
fileName := fmt.Sprintf("iflow-%s.json", tokenStorage.Email)
metadata := map[string]any{ metadata := map[string]any{
"email": tokenStorage.Email, "email": email,
"api_key": tokenStorage.APIKey, "api_key": tokenStorage.APIKey,
"access_token": tokenStorage.AccessToken, "access_token": tokenStorage.AccessToken,
"refresh_token": tokenStorage.RefreshToken, "refresh_token": tokenStorage.RefreshToken,