Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
228 lines
7.3 KiB
Go
228 lines
7.3 KiB
Go
package oauth
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/i18n"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func init() {
|
|
Register("github", &GitHubProvider{})
|
|
}
|
|
|
|
// GitHubProvider implements OAuth for GitHub
|
|
type GitHubProvider struct{}
|
|
|
|
type gitHubOAuthResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
Scope string `json:"scope"`
|
|
TokenType string `json:"token_type"`
|
|
}
|
|
|
|
type gitHubUser struct {
|
|
Id int64 `json:"id"` // GitHub numeric ID (permanent, never changes)
|
|
Login string `json:"login"` // GitHub username (can be changed by user)
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
type gitHubEmail struct {
|
|
Email string `json:"email"`
|
|
Verified bool `json:"verified"`
|
|
}
|
|
|
|
func (p *GitHubProvider) GetName() string {
|
|
return "GitHub"
|
|
}
|
|
|
|
func (p *GitHubProvider) IsEnabled() bool {
|
|
return common.GitHubOAuthEnabled
|
|
}
|
|
|
|
func (p *GitHubProvider) ExchangeToken(ctx context.Context, code string, c *gin.Context) (*OAuthToken, error) {
|
|
if code == "" {
|
|
return nil, NewOAuthError(i18n.MsgOAuthInvalidCode, nil)
|
|
}
|
|
|
|
values := map[string]string{
|
|
"client_id": common.GitHubClientId,
|
|
"client_secret": common.GitHubClientSecret,
|
|
"code": code,
|
|
}
|
|
jsonData, err := json.Marshal(values)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
client := http.Client{
|
|
Timeout: 20 * time.Second,
|
|
}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] ExchangeToken error: %s", err.Error()))
|
|
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "GitHub"}, err.Error())
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
logger.LogDebug(ctx, "[OAuth-GitHub] ExchangeToken response status: %d", res.StatusCode)
|
|
|
|
var oAuthResponse gitHubOAuthResponse
|
|
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
|
|
if err != nil {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] ExchangeToken decode error: %s", err.Error()))
|
|
return nil, err
|
|
}
|
|
|
|
if oAuthResponse.AccessToken == "" {
|
|
logger.LogError(ctx, "[OAuth-GitHub] ExchangeToken failed: empty access token")
|
|
return nil, NewOAuthError(i18n.MsgOAuthTokenFailed, map[string]any{"Provider": "GitHub"})
|
|
}
|
|
|
|
logger.LogDebug(ctx, "[OAuth-GitHub] ExchangeToken success: scope=%s", oAuthResponse.Scope)
|
|
|
|
return &OAuthToken{
|
|
AccessToken: oAuthResponse.AccessToken,
|
|
TokenType: oAuthResponse.TokenType,
|
|
Scope: oAuthResponse.Scope,
|
|
}, nil
|
|
}
|
|
|
|
func (p *GitHubProvider) GetUserInfo(ctx context.Context, token *OAuthToken) (*OAuthUser, error) {
|
|
logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo: fetching user info")
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
|
|
|
client := http.Client{
|
|
Timeout: 20 * time.Second,
|
|
}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetUserInfo error: %s", err.Error()))
|
|
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "GitHub"}, err.Error())
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo response status: %d", res.StatusCode)
|
|
|
|
// Check for non-200 status codes before attempting to decode
|
|
if res.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(res.Body)
|
|
bodyStr := string(body)
|
|
if len(bodyStr) > 500 {
|
|
bodyStr = bodyStr[:500] + "..."
|
|
}
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetUserInfo failed: status=%d, body=%s", res.StatusCode, bodyStr))
|
|
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthGetUserErr, map[string]any{"Provider": "GitHub"}, fmt.Sprintf("status %d", res.StatusCode))
|
|
}
|
|
|
|
var githubUser gitHubUser
|
|
err = json.NewDecoder(res.Body).Decode(&githubUser)
|
|
if err != nil {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetUserInfo decode error: %s", err.Error()))
|
|
return nil, err
|
|
}
|
|
|
|
if githubUser.Id == 0 || githubUser.Login == "" {
|
|
logger.LogError(ctx, "[OAuth-GitHub] GetUserInfo failed: empty id or login field")
|
|
return nil, NewOAuthError(i18n.MsgOAuthUserInfoEmpty, map[string]any{"Provider": "GitHub"})
|
|
}
|
|
|
|
logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo success: id=%d, login=%s, name=%s, email=%s",
|
|
githubUser.Id, githubUser.Login, githubUser.Name, githubUser.Email)
|
|
|
|
return &OAuthUser{
|
|
ProviderUserID: strconv.FormatInt(githubUser.Id, 10), // Use numeric ID as primary identifier
|
|
Username: githubUser.Login,
|
|
DisplayName: githubUser.Name,
|
|
Email: githubUser.Email,
|
|
Extra: map[string]any{
|
|
"legacy_id": githubUser.Login, // Store login for migration from old accounts
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// GetVerifiedEmails lists the addresses GitHub has confirmed for the signed-in
|
|
// account. The email field of the user endpoint carries no confirmation status
|
|
// and is not used for this.
|
|
func (p *GitHubProvider) GetVerifiedEmails(ctx context.Context, token *OAuthToken) ([]string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/user/emails", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
|
|
client := http.Client{
|
|
Timeout: 20 * time.Second,
|
|
}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetVerifiedEmails error: %s", err.Error()))
|
|
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "GitHub"}, err.Error())
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetVerifiedEmails failed: status=%d", res.StatusCode))
|
|
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthGetUserErr, map[string]any{"Provider": "GitHub"}, fmt.Sprintf("status %d", res.StatusCode))
|
|
}
|
|
|
|
var emails []gitHubEmail
|
|
if err := common.DecodeJson(res.Body, &emails); err != nil {
|
|
logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetVerifiedEmails decode error: %s", err.Error()))
|
|
return nil, err
|
|
}
|
|
verified := make([]string, 0, len(emails))
|
|
for _, email := range emails {
|
|
if email.Verified && email.Email != "" {
|
|
verified = append(verified, email.Email)
|
|
}
|
|
}
|
|
logger.LogDebug(ctx, "[OAuth-GitHub] GetVerifiedEmails success: verified=%d", len(verified))
|
|
return verified, nil
|
|
}
|
|
|
|
func (p *GitHubProvider) IsUserIDTaken(providerUserID string) bool {
|
|
return model.IsGitHubIdAlreadyTaken(providerUserID)
|
|
}
|
|
|
|
func (p *GitHubProvider) FillUserByProviderID(user *model.User, providerUserID string) error {
|
|
user.GitHubId = providerUserID
|
|
return user.FillUserByGitHubId()
|
|
}
|
|
|
|
func (p *GitHubProvider) SetProviderUserID(user *model.User, providerUserID string) {
|
|
user.GitHubId = providerUserID
|
|
}
|
|
|
|
func (p *GitHubProvider) GetProviderPrefix() string {
|
|
return "github_"
|
|
}
|
|
|
|
// ProviderUserIDColumn returns the users-table column storing this provider's user ID.
|
|
func (p *GitHubProvider) ProviderUserIDColumn() string {
|
|
return "github_id"
|
|
}
|