feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
172
internal/auth/providers/github.go
Normal file
172
internal/auth/providers/github.go
Normal file
@@ -0,0 +1,172 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user