package providers import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) // GitHubProvider GitHub OAuth提供者 type GitHubProvider struct { ClientID string ClientSecret string RedirectURI string } // GitHubTokenResponse GitHub Token响应 type GitHubTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` Scope string `json:"scope"` } // GitHubUserInfo GitHub用户信息 type GitHubUserInfo struct { ID int64 `json:"id"` Login string `json:"login"` Name string `json:"name"` Email string `json:"email"` AvatarURL string `json:"avatar_url"` Bio string `json:"bio"` Location string `json:"location"` } // NewGitHubProvider 创建GitHub OAuth提供者 func NewGitHubProvider(clientID, clientSecret, redirectURI string) *GitHubProvider { return &GitHubProvider{ ClientID: clientID, ClientSecret: clientSecret, RedirectURI: redirectURI, } } // GetAuthURL 获取GitHub授权URL func (g *GitHubProvider) GetAuthURL(state string) (string, error) { authURL := fmt.Sprintf( "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=read:user,user:email&state=%s", g.ClientID, url.QueryEscape(g.RedirectURI), url.QueryEscape(state), ) return authURL, nil } // ExchangeCode 用授权码换取访问令牌 func (g *GitHubProvider) ExchangeCode(ctx context.Context, code string) (*GitHubTokenResponse, error) { tokenURL := "https://github.com/login/oauth/access_token" data := url.Values{} data.Set("client_id", g.ClientID) data.Set("client_secret", g.ClientSecret) data.Set("code", code) data.Set("redirect_uri", g.RedirectURI) req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(data.Encode())) if err != nil { return nil, fmt.Errorf("create request failed: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() body, err := readOAuthResponseBody(resp) if err != nil { return nil, fmt.Errorf("read response failed: %w", err) } var tokenResp GitHubTokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("parse token response failed: %w", err) } if tokenResp.AccessToken == "" { return nil, fmt.Errorf("GitHub OAuth: empty access token in response") } return &tokenResp, nil } // GetUserInfo 获取GitHub用户信息 func (g *GitHubProvider) GetUserInfo(ctx context.Context, accessToken string) (*GitHubUserInfo, error) { req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil) if err != nil { return nil, fmt.Errorf("create request failed: %w", err) } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("X-GitHub-Api-Version", "2022-11-28") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() body, err := readOAuthResponseBody(resp) if err != nil { return nil, fmt.Errorf("read response failed: %w", err) } var userInfo GitHubUserInfo if err := json.Unmarshal(body, &userInfo); err != nil { return nil, fmt.Errorf("parse user info failed: %w", err) } // 如果用户信息中的邮箱为空,尝试通过邮箱 API 获取主要邮箱 if userInfo.Email == "" { email, _ := g.getPrimaryEmail(ctx, accessToken) userInfo.Email = email } return &userInfo, nil } // getPrimaryEmail 获取用户的主要邮箱 func (g *GitHubProvider) getPrimaryEmail(ctx context.Context, accessToken string) (string, error) { req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user/emails", nil) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Accept", "application/vnd.github+json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, err := readOAuthResponseBody(resp) if err != nil { return "", err } var emails []struct { Email string `json:"email"` Primary bool `json:"primary"` Verified bool `json:"verified"` } if err := json.Unmarshal(body, &emails); err != nil { return "", err } for _, e := range emails { if e.Primary && e.Verified { return e.Email, nil } } return "", nil }