mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 04:03:18 +00:00
refactor: unify OAuth providers with i18n support
- Introduce Provider interface pattern for standard OAuth protocols - Create unified controller/oauth.go with common OAuth logic - Add OAuthError type for translatable error messages - Add i18n keys and translations (zh/en) for OAuth messages - Use common.ApiErrorI18n/ApiSuccessI18n for consistent responses - Preserve backward compatibility for existing routes and data
This commit is contained in:
195
oauth/linuxdo.go
Normal file
195
oauth/linuxdo.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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("linuxdo", &LinuxDOProvider{})
|
||||
}
|
||||
|
||||
// LinuxDOProvider implements OAuth for Linux DO
|
||||
type LinuxDOProvider struct{}
|
||||
|
||||
type linuxdoUser struct {
|
||||
Id int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
TrustLevel int `json:"trust_level"`
|
||||
Silenced bool `json:"silenced"`
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) GetName() string {
|
||||
return "Linux DO"
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) IsEnabled() bool {
|
||||
return common.LinuxDOOAuthEnabled
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) ExchangeToken(ctx context.Context, code string, c *gin.Context) (*OAuthToken, error) {
|
||||
if code == "" {
|
||||
return nil, NewOAuthError(i18n.MsgOAuthInvalidCode, nil)
|
||||
}
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken: code=%s...", code[:min(len(code), 10)])
|
||||
|
||||
// Get access token using Basic auth
|
||||
tokenEndpoint := common.GetEnvOrDefaultString("LINUX_DO_TOKEN_ENDPOINT", "https://connect.linux.do/oauth2/token")
|
||||
credentials := common.LinuxDOClientId + ":" + common.LinuxDOClientSecret
|
||||
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
|
||||
|
||||
// Get redirect URI from request
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
redirectURI := fmt.Sprintf("%s://%s/api/oauth/linuxdo", scheme, c.Request.Host)
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken: token_endpoint=%s, redirect_uri=%s", tokenEndpoint, redirectURI)
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "authorization_code")
|
||||
data.Set("code", code)
|
||||
data.Set("redirect_uri", redirectURI)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenEndpoint, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", basicAuth)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] ExchangeToken error: %s", err.Error()))
|
||||
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "Linux DO"}, err.Error())
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken response status: %d", res.StatusCode)
|
||||
|
||||
var tokenRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&tokenRes); err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] ExchangeToken decode error: %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenRes.AccessToken == "" {
|
||||
logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] ExchangeToken failed: %s", tokenRes.Message))
|
||||
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthTokenFailed, map[string]any{"Provider": "Linux DO"}, tokenRes.Message)
|
||||
}
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] ExchangeToken success")
|
||||
|
||||
return &OAuthToken{
|
||||
AccessToken: tokenRes.AccessToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) GetUserInfo(ctx context.Context, token *OAuthToken) (*OAuthUser, error) {
|
||||
userEndpoint := common.GetEnvOrDefaultString("LINUX_DO_USER_ENDPOINT", "https://connect.linux.do/api/user")
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo: user_endpoint=%s", userEndpoint)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", userEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] GetUserInfo error: %s", err.Error()))
|
||||
return nil, NewOAuthErrorWithRaw(i18n.MsgOAuthConnectFailed, map[string]any{"Provider": "Linux DO"}, err.Error())
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo response status: %d", res.StatusCode)
|
||||
|
||||
var linuxdoUser linuxdoUser
|
||||
if err := json.NewDecoder(res.Body).Decode(&linuxdoUser); err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("[OAuth-LinuxDO] GetUserInfo decode error: %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if linuxdoUser.Id == 0 {
|
||||
logger.LogError(ctx, "[OAuth-LinuxDO] GetUserInfo failed: invalid user id")
|
||||
return nil, NewOAuthError(i18n.MsgOAuthUserInfoEmpty, map[string]any{"Provider": "Linux DO"})
|
||||
}
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo: id=%d, username=%s, name=%s, trust_level=%d, active=%v, silenced=%v",
|
||||
linuxdoUser.Id, linuxdoUser.Username, linuxdoUser.Name, linuxdoUser.TrustLevel, linuxdoUser.Active, linuxdoUser.Silenced)
|
||||
|
||||
// Check trust level
|
||||
if linuxdoUser.TrustLevel < common.LinuxDOMinimumTrustLevel {
|
||||
logger.LogWarn(ctx, fmt.Sprintf("[OAuth-LinuxDO] GetUserInfo: trust level too low (required=%d, current=%d)",
|
||||
common.LinuxDOMinimumTrustLevel, linuxdoUser.TrustLevel))
|
||||
return nil, &TrustLevelError{
|
||||
Required: common.LinuxDOMinimumTrustLevel,
|
||||
Current: linuxdoUser.TrustLevel,
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug(ctx, "[OAuth-LinuxDO] GetUserInfo success: id=%d, username=%s", linuxdoUser.Id, linuxdoUser.Username)
|
||||
|
||||
return &OAuthUser{
|
||||
ProviderUserID: strconv.Itoa(linuxdoUser.Id),
|
||||
Username: linuxdoUser.Username,
|
||||
DisplayName: linuxdoUser.Name,
|
||||
Extra: map[string]any{
|
||||
"trust_level": linuxdoUser.TrustLevel,
|
||||
"active": linuxdoUser.Active,
|
||||
"silenced": linuxdoUser.Silenced,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) IsUserIDTaken(providerUserID string) bool {
|
||||
return model.IsLinuxDOIdAlreadyTaken(providerUserID)
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) FillUserByProviderID(user *model.User, providerUserID string) error {
|
||||
user.LinuxDOId = providerUserID
|
||||
return user.FillUserByLinuxDOId()
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) SetProviderUserID(user *model.User, providerUserID string) {
|
||||
user.LinuxDOId = providerUserID
|
||||
}
|
||||
|
||||
func (p *LinuxDOProvider) GetProviderPrefix() string {
|
||||
return "linuxdo_"
|
||||
}
|
||||
|
||||
// TrustLevelError indicates the user's trust level is too low
|
||||
type TrustLevelError struct {
|
||||
Required int
|
||||
Current int
|
||||
}
|
||||
|
||||
func (e *TrustLevelError) Error() string {
|
||||
return "trust level too low"
|
||||
}
|
||||
Reference in New Issue
Block a user