github.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. package oauth
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "strconv"
  9. "time"
  10. "github.com/QuantumNous/new-api/common"
  11. "github.com/QuantumNous/new-api/i18n"
  12. "github.com/QuantumNous/new-api/logger"
  13. "github.com/QuantumNous/new-api/model"
  14. "github.com/gin-gonic/gin"
  15. )
  16. func init() {
  17. Register("github", &GitHubProvider{})
  18. }
  19. // GitHubProvider implements OAuth for GitHub
  20. type GitHubProvider struct{}
  21. type gitHubOAuthResponse struct {
  22. AccessToken string `json:"access_token"`
  23. Scope string `json:"scope"`
  24. TokenType string `json:"token_type"`
  25. }
  26. type gitHubUser struct {
  27. Id int64 `json:"id"` // GitHub numeric ID (permanent, never changes)
  28. Login string `json:"login"` // GitHub username (can be changed by user)
  29. Name string `json:"name"`
  30. Email string `json:"email"`
  31. }
  32. func (p *GitHubProvider) GetName() string {
  33. return "GitHub"
  34. }
  35. func (p *GitHubProvider) IsEnabled() bool {
  36. return common.GitHubOAuthEnabled
  37. }
  38. func (p *GitHubProvider) ExchangeToken(ctx context.Context, code string, c *gin.Context) (*OAuthToken, error) {
  39. if code == "" {
  40. return nil, NewOAuthError(i18n.MsgOAuthInvalidCode, nil)
  41. }
  42. logger.LogDebug(ctx, "[OAuth-GitHub] ExchangeToken: code=%s...", code[:min(len(code), 10)])
  43. values := map[string]string{
  44. "client_id": common.GitHubClientId,
  45. "client_secret": common.GitHubClientSecret,
  46. "code": code,
  47. }
  48. jsonData, err := json.Marshal(values)
  49. if err != nil {
  50. return nil, err
  51. }
  52. req, err := http.NewRequestWithContext(ctx, "POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData))
  53. if err != nil {
  54. return nil, err
  55. }
  56. req.Header.Set("Content-Type", "application/json")
  57. req.Header.Set("Accept", "application/json")
  58. client := http.Client{
  59. Timeout: 20 * time.Second,
  60. }
  61. res, err := client.Do(req)
  62. if err != nil {
  63. logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] ExchangeToken error: %s", err.Error()))
  64. return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "GitHub"}, err.Error())
  65. }
  66. defer res.Body.Close()
  67. logger.LogDebug(ctx, "[OAuth-GitHub] ExchangeToken response status: %d", res.StatusCode)
  68. var oAuthResponse gitHubOAuthResponse
  69. err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
  70. if err != nil {
  71. logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] ExchangeToken decode error: %s", err.Error()))
  72. return nil, err
  73. }
  74. if oAuthResponse.AccessToken == "" {
  75. logger.LogError(ctx, "[OAuth-GitHub] ExchangeToken failed: empty access token")
  76. return nil, NewOAuthError(i18n.MsgOAuthTokenFailed, map[string]any{"Provider": "GitHub"})
  77. }
  78. logger.LogDebug(ctx, "[OAuth-GitHub] ExchangeToken success: scope=%s", oAuthResponse.Scope)
  79. return &OAuthToken{
  80. AccessToken: oAuthResponse.AccessToken,
  81. TokenType: oAuthResponse.TokenType,
  82. Scope: oAuthResponse.Scope,
  83. }, nil
  84. }
  85. func (p *GitHubProvider) GetUserInfo(ctx context.Context, token *OAuthToken) (*OAuthUser, error) {
  86. logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo: fetching user info")
  87. req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil)
  88. if err != nil {
  89. return nil, err
  90. }
  91. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
  92. client := http.Client{
  93. Timeout: 20 * time.Second,
  94. }
  95. res, err := client.Do(req)
  96. if err != nil {
  97. logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetUserInfo error: %s", err.Error()))
  98. return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "GitHub"}, err.Error())
  99. }
  100. defer res.Body.Close()
  101. logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo response status: %d", res.StatusCode)
  102. var githubUser gitHubUser
  103. err = json.NewDecoder(res.Body).Decode(&githubUser)
  104. if err != nil {
  105. logger.LogError(ctx, fmt.Sprintf("[OAuth-GitHub] GetUserInfo decode error: %s", err.Error()))
  106. return nil, err
  107. }
  108. if githubUser.Id == 0 || githubUser.Login == "" {
  109. logger.LogError(ctx, "[OAuth-GitHub] GetUserInfo failed: empty id or login field")
  110. return nil, NewOAuthError(i18n.MsgOAuthUserInfoEmpty, map[string]any{"Provider": "GitHub"})
  111. }
  112. logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo success: id=%d, login=%s, name=%s, email=%s",
  113. githubUser.Id, githubUser.Login, githubUser.Name, githubUser.Email)
  114. return &OAuthUser{
  115. ProviderUserID: strconv.FormatInt(githubUser.Id, 10), // Use numeric ID as primary identifier
  116. Username: githubUser.Login,
  117. DisplayName: githubUser.Name,
  118. Email: githubUser.Email,
  119. Extra: map[string]any{
  120. "legacy_id": githubUser.Login, // Store login for migration from old accounts
  121. },
  122. }, nil
  123. }
  124. func (p *GitHubProvider) IsUserIDTaken(providerUserID string) bool {
  125. return model.IsGitHubIdAlreadyTaken(providerUserID)
  126. }
  127. func (p *GitHubProvider) FillUserByProviderID(user *model.User, providerUserID string) error {
  128. user.GitHubId = providerUserID
  129. return user.FillUserByGitHubId()
  130. }
  131. func (p *GitHubProvider) SetProviderUserID(user *model.User, providerUserID string) {
  132. user.GitHubId = providerUserID
  133. }
  134. func (p *GitHubProvider) GetProviderPrefix() string {
  135. return "github_"
  136. }