linuxdo.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package oauth
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/QuantumNous/new-api/common"
  13. "github.com/QuantumNous/new-api/i18n"
  14. "github.com/QuantumNous/new-api/logger"
  15. "github.com/QuantumNous/new-api/model"
  16. "github.com/gin-gonic/gin"
  17. )
  18. func init() {
  19. Register("linuxdo", &LinuxDOProvider{})
  20. }
  21. // LinuxDOProvider implements OAuth for Linux DO
  22. type LinuxDOProvider struct{}
  23. type linuxdoUser struct {
  24. Id int `json:"id"`
  25. Username string `json:"username"`
  26. Name string `json:"name"`
  27. Active bool `json:"active"`
  28. TrustLevel int `json:"trust_level"`
  29. Silenced bool `json:"silenced"`
  30. }
  31. func (p *LinuxDOProvider) GetName() string {
  32. return "Linux DO"
  33. }
  34. func (p *LinuxDOProvider) IsEnabled() bool {
  35. return common.LinuxDOOAuthEnabled
  36. }
  37. func (p *LinuxDOProvider) ExchangeToken(ctx context.Context, code string, c *gin.Context) (*OAuthToken, error) {
  38. if code == "" {
  39. return nil, NewOAuthError(i18n.MsgOAuthInvalidCode, nil)
  40. }
  41. logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken: code=%s...", code[:min(len(code), 10)])
  42. // Get access token using Basic auth
  43. tokenEndpoint := common.GetEnvOrDefaultString("LINUX_DO_TOKEN_ENDPOINT", "https://connect.linux.do/oauth2/token")
  44. credentials := common.LinuxDOClientId + ":" + common.LinuxDOClientSecret
  45. basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
  46. // Get redirect URI from request
  47. scheme := "http"
  48. if c.Request.TLS != nil {
  49. scheme = "https"
  50. }
  51. redirectURI := fmt.Sprintf("%s://%s/api/oauth/linuxdo", scheme, c.Request.Host)
  52. logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken: token_endpoint=%s, redirect_uri=%s", tokenEndpoint, redirectURI)
  53. data := url.Values{}
  54. data.Set("grant_type", "authorization_code")
  55. data.Set("code", code)
  56. data.Set("redirect_uri", redirectURI)
  57. req, err := http.NewRequestWithContext(ctx, "POST", tokenEndpoint, strings.NewReader(data.Encode()))
  58. if err != nil {
  59. return nil, err
  60. }
  61. req.Header.Set("Authorization", basicAuth)
  62. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  63. req.Header.Set("Accept", "application/json")
  64. client := http.Client{Timeout: 5 * time.Second}
  65. res, err := client.Do(req)
  66. if err != nil {
  67. logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] ExchangeToken error: %s", err.Error()))
  68. return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "Linux DO"}, err.Error())
  69. }
  70. defer res.Body.Close()
  71. logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken response status: %d", res.StatusCode)
  72. var tokenRes struct {
  73. AccessToken string `json:"access_token"`
  74. Message string `json:"message"`
  75. }
  76. if err := json.NewDecoder(res.Body).Decode(&tokenRes); err != nil {
  77. logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] ExchangeToken decode error: %s", err.Error()))
  78. return nil, err
  79. }
  80. if tokenRes.AccessToken == "" {
  81. logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] ExchangeToken failed: %s", tokenRes.Message))
  82. return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthTokenFailed, map[string]any{"Provider": "Linux DO"}, tokenRes.Message)
  83. }
  84. logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken success")
  85. return &OAuthToken{
  86. AccessToken: tokenRes.AccessToken,
  87. }, nil
  88. }
  89. func (p *LinuxDOProvider) GetUserInfo(ctx context.Context, token *OAuthToken) (*OAuthUser, error) {
  90. userEndpoint := common.GetEnvOrDefaultString("LINUX_DO_USER_ENDPOINT", "https://connect.linux.do/api/user")
  91. logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo: user_endpoint=%s", userEndpoint)
  92. req, err := http.NewRequestWithContext(ctx, "GET", userEndpoint, nil)
  93. if err != nil {
  94. return nil, err
  95. }
  96. req.Header.Set("Authorization", "Bearer "+token.AccessToken)
  97. req.Header.Set("Accept", "application/json")
  98. client := http.Client{Timeout: 5 * time.Second}
  99. res, err := client.Do(req)
  100. if err != nil {
  101. logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] GetUserInfo error: %s", err.Error()))
  102. return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "Linux DO"}, err.Error())
  103. }
  104. defer res.Body.Close()
  105. logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo response status: %d", res.StatusCode)
  106. var linuxdoUser linuxdoUser
  107. if err := json.NewDecoder(res.Body).Decode(&linuxdoUser); err != nil {
  108. logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] GetUserInfo decode error: %s", err.Error()))
  109. return nil, err
  110. }
  111. if linuxdoUser.Id == 0 {
  112. logger.LogError(ctx, "[OAuth-LinuxDO] GetUserInfo failed: invalid user id")
  113. return nil, NewOAuthError(i18n.MsgOAuthUserInfoEmpty, map[string]any{"Provider": "Linux DO"})
  114. }
  115. logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo: id=%d, username=%s, name=%s, trust_level=%d, active=%v, silenced=%v",
  116. linuxdoUser.Id, linuxdoUser.Username, linuxdoUser.Name, linuxdoUser.TrustLevel, linuxdoUser.Active, linuxdoUser.Silenced)
  117. // Check trust level
  118. if linuxdoUser.TrustLevel < common.LinuxDOMinimumTrustLevel {
  119. logger.LogWarn(ctx, fmt.Sprintf("[OAuth-LinuxDO] GetUserInfo: trust level too low (required=%d, current=%d)",
  120. common.LinuxDOMinimumTrustLevel, linuxdoUser.TrustLevel))
  121. return nil, &TrustLevelError{
  122. Required: common.LinuxDOMinimumTrustLevel,
  123. Current: linuxdoUser.TrustLevel,
  124. }
  125. }
  126. logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo success: id=%d, username=%s", linuxdoUser.Id, linuxdoUser.Username)
  127. return &OAuthUser{
  128. ProviderUserID: strconv.Itoa(linuxdoUser.Id),
  129. Username: linuxdoUser.Username,
  130. DisplayName: linuxdoUser.Name,
  131. Extra: map[string]any{
  132. "trust_level": linuxdoUser.TrustLevel,
  133. "active": linuxdoUser.Active,
  134. "silenced": linuxdoUser.Silenced,
  135. },
  136. }, nil
  137. }
  138. func (p *LinuxDOProvider) IsUserIDTaken(providerUserID string) bool {
  139. return model.IsLinuxDOIdAlreadyTaken(providerUserID)
  140. }
  141. func (p *LinuxDOProvider) FillUserByProviderID(user *model.User, providerUserID string) error {
  142. user.LinuxDOId = providerUserID
  143. return user.FillUserByLinuxDOId()
  144. }
  145. func (p *LinuxDOProvider) SetProviderUserID(user *model.User, providerUserID string) {
  146. user.LinuxDOId = providerUserID
  147. }
  148. func (p *LinuxDOProvider) GetProviderPrefix() string {
  149. return "linuxdo_"
  150. }
  151. // TrustLevelError indicates the user's trust level is too low
  152. type TrustLevelError struct {
  153. Required int
  154. Current int
  155. }
  156. func (e *TrustLevelError) Error() string {
  157. return "trust level too low"
  158. }