视频连线
This commit is contained in:
@@ -2,9 +2,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/echochat/backend/app/auth/service"
|
||||
"github.com/echochat/backend/app/constants"
|
||||
"github.com/echochat/backend/app/dto"
|
||||
"github.com/echochat/backend/config"
|
||||
"github.com/echochat/backend/pkg/logs"
|
||||
"github.com/echochat/backend/pkg/middleware"
|
||||
"github.com/echochat/backend/pkg/utils"
|
||||
@@ -16,11 +23,13 @@ import (
|
||||
// 处理用户注册、登录、Token 刷新、个人信息管理等接口
|
||||
type AuthController struct {
|
||||
authService *service.AuthService
|
||||
oauthService *service.OAuthService
|
||||
oauthCfg *config.OAuthConfig
|
||||
}
|
||||
|
||||
// NewAuthController 创建前台认证控制器实例
|
||||
func NewAuthController(authService *service.AuthService) *AuthController {
|
||||
return &AuthController{authService: authService}
|
||||
func NewAuthController(authService *service.AuthService, oauthService *service.OAuthService, oauthCfg *config.OAuthConfig) *AuthController {
|
||||
return &AuthController{authService: authService, oauthService: oauthService, oauthCfg: oauthCfg}
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
@@ -77,6 +86,53 @@ func (ctrl *AuthController) Login(c *gin.Context) {
|
||||
utils.ResponseOK(c, resp)
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) OAuthAuthorize(c *gin.Context) {
|
||||
provider := strings.ToLower(strings.TrimSpace(c.Param("provider")))
|
||||
authURL, err := ctrl.oauthService.BuildAuthorizeURL(c.Request.Context(), provider)
|
||||
if err != nil {
|
||||
handleAuthError(c, err, "第三方登录初始化失败")
|
||||
return
|
||||
}
|
||||
utils.ResponseOK(c, gin.H{"url": authURL})
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) OAuthCallback(c *gin.Context) {
|
||||
provider := strings.ToLower(strings.TrimSpace(c.Param("provider")))
|
||||
code := strings.TrimSpace(c.Query("code"))
|
||||
state := strings.TrimSpace(c.Query("state"))
|
||||
if code == "" {
|
||||
ctrl.redirectOAuthFailure(c, "第三方登录授权失败")
|
||||
return
|
||||
}
|
||||
resp, err := ctrl.oauthService.Callback(c.Request.Context(), provider, code, state, c.ClientIP())
|
||||
if err != nil {
|
||||
ctrl.redirectOAuthFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
payload, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
ctrl.redirectOAuthFailure(c, "登录结果生成失败")
|
||||
return
|
||||
}
|
||||
ctrl.redirectOAuthResult(c, "success", base64.RawURLEncoding.EncodeToString(payload))
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) redirectOAuthFailure(c *gin.Context, message string) {
|
||||
ctrl.redirectOAuthResult(c, "error", base64.RawURLEncoding.EncodeToString([]byte(message)))
|
||||
}
|
||||
|
||||
func (ctrl *AuthController) redirectOAuthResult(c *gin.Context, status, payload string) {
|
||||
callbackURL := "/echoChat-frontend/#/pages/auth/oauth-callback"
|
||||
if ctrl.oauthCfg != nil && strings.TrimSpace(ctrl.oauthCfg.FrontendCallbackURL) != "" {
|
||||
callbackURL = strings.TrimSpace(ctrl.oauthCfg.FrontendCallbackURL)
|
||||
}
|
||||
sep := "?"
|
||||
if strings.Contains(callbackURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
c.Redirect(http.StatusFound, callbackURL+sep+"status="+url.QueryEscape(status)+"&payload="+url.QueryEscape(payload))
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
// POST /api/v1/auth/logout(需认证)
|
||||
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||
@@ -226,6 +282,12 @@ func handleAuthError(c *gin.Context, err error, fallbackMsg ...string) {
|
||||
utils.ResponseForbidden(c, err.Error())
|
||||
case service.ErrRefreshTokenType:
|
||||
utils.ResponseBadRequest(c, err.Error())
|
||||
case service.ErrOAuthProviderUnsupported:
|
||||
utils.ResponseBadRequest(c, err.Error())
|
||||
case service.ErrOAuthProviderDisabled:
|
||||
utils.ResponseBadRequest(c, err.Error())
|
||||
case service.ErrOAuthStateInvalid:
|
||||
utils.ResponseBadRequest(c, err.Error())
|
||||
default:
|
||||
msg := "服务器内部错误"
|
||||
if len(fallbackMsg) > 0 && fallbackMsg[0] != "" {
|
||||
|
||||
33
backend/go-service/app/auth/dao/oauth_account_dao.go
Normal file
33
backend/go-service/app/auth/dao/oauth_account_dao.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/echochat/backend/app/auth/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OAuthAccountDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOAuthAccountDAO(db *gorm.DB) *OAuthAccountDAO {
|
||||
return &OAuthAccountDAO{db: db}
|
||||
}
|
||||
|
||||
func (d *OAuthAccountDAO) FindByProviderOpenID(ctx context.Context, provider, openID string) (*model.OAuthAccount, error) {
|
||||
var account model.OAuthAccount
|
||||
err := d.db.WithContext(ctx).Where("provider = ? AND open_id = ?", provider, openID).First(&account).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (d *OAuthAccountDAO) Create(ctx context.Context, account *model.OAuthAccount) error {
|
||||
return d.db.WithContext(ctx).Create(account).Error
|
||||
}
|
||||
|
||||
func (d *OAuthAccountDAO) Update(ctx context.Context, account *model.OAuthAccount) error {
|
||||
return d.db.WithContext(ctx).Save(account).Error
|
||||
}
|
||||
22
backend/go-service/app/auth/model/oauth_account.go
Normal file
22
backend/go-service/app/auth/model/oauth_account.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type OAuthAccount struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserID int64 `json:"user_id" gorm:"not null;index"`
|
||||
Provider string `json:"provider" gorm:"size:20;not null;uniqueIndex:idx_oauth_provider_openid"`
|
||||
OpenID string `json:"openid" gorm:"size:128;not null;uniqueIndex:idx_oauth_provider_openid"`
|
||||
UnionID string `json:"unionid" gorm:"size:128;not null;default:'';index"`
|
||||
Nickname string `json:"nickname" gorm:"size:100;not null;default:''"`
|
||||
Avatar string `json:"avatar" gorm:"size:500;not null;default:''"`
|
||||
AccessToken string `json:"-" gorm:"column:access_token;size:1024;not null;default:''"`
|
||||
RefreshToken string `json:"-" gorm:"column:refresh_token;size:1024;not null;default:''"`
|
||||
ExpiresAt time.Time `json:"expires_at" gorm:"type:timestamp(0)"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime;type:timestamp(0)"`
|
||||
}
|
||||
|
||||
func (OAuthAccount) TableName() string {
|
||||
return "auth_oauth_accounts"
|
||||
}
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
var AuthSet = wire.NewSet(
|
||||
dao.NewUserDAO,
|
||||
dao.NewRoleDAO,
|
||||
dao.NewOAuthAccountDAO,
|
||||
service.NewTokenStore,
|
||||
service.NewAuthService,
|
||||
service.NewOAuthService,
|
||||
controller.NewAuthController,
|
||||
controller.NewAdminAuthController,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@ func RegisterRoutes(
|
||||
{
|
||||
public.POST("/register", ctrl.Register)
|
||||
public.POST("/login", ctrl.Login)
|
||||
public.GET("/oauth/:provider/authorize", ctrl.OAuthAuthorize)
|
||||
public.GET("/oauth/:provider/callback", ctrl.OAuthCallback)
|
||||
public.POST("/refresh-token", ctrl.RefreshToken)
|
||||
}
|
||||
|
||||
|
||||
382
backend/go-service/app/auth/service/oauth_service.go
Normal file
382
backend/go-service/app/auth/service/oauth_service.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/echochat/backend/app/auth/dao"
|
||||
"github.com/echochat/backend/app/auth/model"
|
||||
"github.com/echochat/backend/app/constants"
|
||||
"github.com/echochat/backend/app/dto"
|
||||
"github.com/echochat/backend/config"
|
||||
"github.com/echochat/backend/pkg/utils"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
oauthStateKeyPrefix = "echo:auth:oauth_state:"
|
||||
oauthStateTTL = 10 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOAuthProviderUnsupported = errors.New("不支持的第三方登录方式")
|
||||
ErrOAuthProviderDisabled = errors.New("第三方登录未配置或未启用")
|
||||
ErrOAuthStateInvalid = errors.New("第三方登录状态已失效")
|
||||
)
|
||||
|
||||
type OAuthService struct {
|
||||
userDAO *dao.UserDAO
|
||||
oauthAccountDAO *dao.OAuthAccountDAO
|
||||
roleDAO *dao.RoleDAO
|
||||
authService *AuthService
|
||||
cfg *config.OAuthConfig
|
||||
redis *redis.Client
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type OAuthProfile struct {
|
||||
Provider string
|
||||
OpenID string
|
||||
UnionID string
|
||||
Nickname string
|
||||
Avatar string
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
ExpiresIn int64
|
||||
}
|
||||
|
||||
type oauthTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
OpenID string `json:"openid"`
|
||||
UnionID string `json:"unionid"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
type qqTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type qqOpenIDResponse struct {
|
||||
ClientID string `json:"client_id"`
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
|
||||
type wxUserInfoResponse struct {
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
HeadImg string `json:"headimgurl"`
|
||||
UnionID string `json:"unionid"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
type qqUserInfoResponse struct {
|
||||
Ret int `json:"ret"`
|
||||
Msg string `json:"msg"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"figureurl_qq_2"`
|
||||
Avatar1 string `json:"figureurl_qq_1"`
|
||||
}
|
||||
|
||||
func NewOAuthService(userDAO *dao.UserDAO, oauthAccountDAO *dao.OAuthAccountDAO, roleDAO *dao.RoleDAO, authService *AuthService, cfg *config.OAuthConfig, redisClient *redis.Client) *OAuthService {
|
||||
return &OAuthService{
|
||||
userDAO: userDAO,
|
||||
oauthAccountDAO: oauthAccountDAO,
|
||||
roleDAO: roleDAO,
|
||||
authService: authService,
|
||||
cfg: cfg,
|
||||
redis: redisClient,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OAuthService) BuildAuthorizeURL(ctx context.Context, provider string) (string, error) {
|
||||
provider = normalizeOAuthProvider(provider)
|
||||
app, err := s.providerConfig(provider)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
state, err := randomState()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = s.saveOAuthState(ctx, provider, state); err != nil {
|
||||
return "", err
|
||||
}
|
||||
redirectURI := url.QueryEscape(app.RedirectURI)
|
||||
scope := "snsapi_login"
|
||||
if provider == "qq" {
|
||||
scope = "get_user_info"
|
||||
}
|
||||
if provider == "wechat" {
|
||||
return fmt.Sprintf("https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect", url.QueryEscape(app.ClientID), redirectURI, scope, state), nil
|
||||
}
|
||||
return fmt.Sprintf("https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s", url.QueryEscape(app.ClientID), redirectURI, url.QueryEscape(scope), state), nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) Callback(ctx context.Context, provider, code, state, clientIP string) (*dto.LoginResponse, error) {
|
||||
provider = normalizeOAuthProvider(provider)
|
||||
if err := s.validateOAuthState(ctx, provider, state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile, err := s.fetchProfile(ctx, provider, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := s.oauthAccountDAO.FindByProviderOpenID(ctx, profile.Provider, profile.OpenID)
|
||||
var user *model.User
|
||||
if err == nil {
|
||||
user, err = s.userDAO.FindByID(ctx, account.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Nickname = profile.Nickname
|
||||
account.Avatar = profile.Avatar
|
||||
account.AccessToken = profile.AccessToken
|
||||
account.RefreshToken = profile.RefreshToken
|
||||
account.UnionID = profile.UnionID
|
||||
account.ExpiresAt = time.Now().Add(time.Duration(profile.ExpiresIn) * time.Second)
|
||||
_ = s.oauthAccountDAO.Update(ctx, account)
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
user, account, err = s.createOAuthUser(ctx, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return s.authService.BuildLoginResponseForUser(ctx, user, clientIP, constants.ClientTypeFrontend)
|
||||
}
|
||||
|
||||
func (s *OAuthService) createOAuthUser(ctx context.Context, profile *OAuthProfile) (*model.User, *model.OAuthAccount, error) {
|
||||
nickname := strings.TrimSpace(profile.Nickname)
|
||||
if nickname == "" {
|
||||
nickname = profile.Provider + "用户"
|
||||
}
|
||||
username := s.uniqueUsername(ctx, profile.Provider)
|
||||
passwordHash, err := utils.HashPassword(randomPassword())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Email: username + "@oauth.echochat.local",
|
||||
PasswordHash: passwordHash,
|
||||
Nickname: nickname,
|
||||
Avatar: profile.Avatar,
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
if err = s.userDAO.Create(ctx, user); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defaultRole, roleErr := s.roleDAO.FindByCode(ctx, constants.RoleUser)
|
||||
if roleErr == nil {
|
||||
_ = s.roleDAO.AssignRole(ctx, user.ID, defaultRole.ID)
|
||||
}
|
||||
account := &model.OAuthAccount{
|
||||
UserID: user.ID,
|
||||
Provider: profile.Provider,
|
||||
OpenID: profile.OpenID,
|
||||
UnionID: profile.UnionID,
|
||||
Nickname: profile.Nickname,
|
||||
Avatar: profile.Avatar,
|
||||
AccessToken: profile.AccessToken,
|
||||
RefreshToken: profile.RefreshToken,
|
||||
ExpiresAt: time.Now().Add(time.Duration(profile.ExpiresIn) * time.Second),
|
||||
}
|
||||
if err = s.oauthAccountDAO.Create(ctx, account); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return user, account, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) fetchProfile(ctx context.Context, provider, code string) (*OAuthProfile, error) {
|
||||
app, err := s.providerConfig(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if provider == "wechat" {
|
||||
return s.fetchWechatProfile(ctx, app, code)
|
||||
}
|
||||
return s.fetchQQProfile(ctx, app, code)
|
||||
}
|
||||
|
||||
func (s *OAuthService) fetchWechatProfile(ctx context.Context, app config.OAuthApp, code string) (*OAuthProfile, error) {
|
||||
tokenURL := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", url.QueryEscape(app.ClientID), url.QueryEscape(app.ClientSecret), url.QueryEscape(code))
|
||||
var token oauthTokenResponse
|
||||
if err := s.getJSON(ctx, tokenURL, &token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token.ErrCode != 0 || token.AccessToken == "" || token.OpenID == "" {
|
||||
return nil, fmt.Errorf("微信登录失败: %s", token.ErrMsg)
|
||||
}
|
||||
infoURL := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN", url.QueryEscape(token.AccessToken), url.QueryEscape(token.OpenID))
|
||||
var info wxUserInfoResponse
|
||||
if err := s.getJSON(ctx, infoURL, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.ErrCode != 0 {
|
||||
return nil, fmt.Errorf("微信用户信息获取失败: %s", info.ErrMsg)
|
||||
}
|
||||
return &OAuthProfile{Provider: "wechat", OpenID: token.OpenID, UnionID: firstNonEmpty(info.UnionID, token.UnionID), Nickname: info.Nickname, Avatar: info.HeadImg, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresIn: token.ExpiresIn}, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) fetchQQProfile(ctx context.Context, app config.OAuthApp, code string) (*OAuthProfile, error) {
|
||||
tokenURL := fmt.Sprintf("https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s&fmt=json", url.QueryEscape(app.ClientID), url.QueryEscape(app.ClientSecret), url.QueryEscape(code), url.QueryEscape(app.RedirectURI))
|
||||
var token qqTokenResponse
|
||||
if err := s.getJSON(ctx, tokenURL, &token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token.AccessToken == "" {
|
||||
return nil, errors.New("QQ登录失败")
|
||||
}
|
||||
openidURL := fmt.Sprintf("https://graph.qq.com/oauth2.0/me?access_token=%s&fmt=json", url.QueryEscape(token.AccessToken))
|
||||
var openid qqOpenIDResponse
|
||||
if err := s.getJSON(ctx, openidURL, &openid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if openid.OpenID == "" {
|
||||
return nil, errors.New("QQ openid 获取失败")
|
||||
}
|
||||
infoURL := fmt.Sprintf("https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s&fmt=json", url.QueryEscape(token.AccessToken), url.QueryEscape(app.ClientID), url.QueryEscape(openid.OpenID))
|
||||
var info qqUserInfoResponse
|
||||
if err := s.getJSON(ctx, infoURL, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Ret != 0 {
|
||||
return nil, fmt.Errorf("QQ用户信息获取失败: %s", info.Msg)
|
||||
}
|
||||
return &OAuthProfile{Provider: "qq", OpenID: openid.OpenID, Nickname: info.Nickname, Avatar: firstNonEmpty(info.Avatar, info.Avatar1), AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresIn: token.ExpiresIn}, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) providerConfig(provider string) (config.OAuthApp, error) {
|
||||
provider = normalizeOAuthProvider(provider)
|
||||
if provider != "wechat" && provider != "qq" {
|
||||
return config.OAuthApp{}, ErrOAuthProviderUnsupported
|
||||
}
|
||||
app := config.OAuthApp{}
|
||||
if s.cfg != nil && s.cfg.Providers != nil {
|
||||
app = s.cfg.Providers[provider]
|
||||
}
|
||||
prefix := "ECHOCHAT_OAUTH_" + strings.ToUpper(provider) + "_"
|
||||
if strings.TrimSpace(app.ClientID) == "" {
|
||||
app.ClientID = strings.TrimSpace(os.Getenv(prefix + "CLIENT_ID"))
|
||||
}
|
||||
if strings.TrimSpace(app.ClientSecret) == "" {
|
||||
app.ClientSecret = strings.TrimSpace(os.Getenv(prefix + "CLIENT_SECRET"))
|
||||
}
|
||||
if strings.TrimSpace(app.RedirectURI) == "" {
|
||||
app.RedirectURI = strings.TrimSpace(os.Getenv(prefix + "REDIRECT_URI"))
|
||||
}
|
||||
if !app.Enabled {
|
||||
app.Enabled = strings.EqualFold(strings.TrimSpace(os.Getenv(prefix+"ENABLED")), "true")
|
||||
}
|
||||
if !app.Enabled || app.ClientID == "" || app.ClientSecret == "" || app.RedirectURI == "" {
|
||||
return config.OAuthApp{}, ErrOAuthProviderDisabled
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) saveOAuthState(ctx context.Context, provider, state string) error {
|
||||
if s.redis == nil {
|
||||
return errors.New("OAuth state storage not initialized")
|
||||
}
|
||||
return s.redis.Set(ctx, oauthStateKey(provider, state), "1", oauthStateTTL).Err()
|
||||
}
|
||||
|
||||
func (s *OAuthService) validateOAuthState(ctx context.Context, provider, state string) error {
|
||||
provider = normalizeOAuthProvider(provider)
|
||||
state = strings.TrimSpace(state)
|
||||
if provider != "wechat" && provider != "qq" || state == "" {
|
||||
return ErrOAuthStateInvalid
|
||||
}
|
||||
if s.redis == nil {
|
||||
return ErrOAuthStateInvalid
|
||||
}
|
||||
_, err := s.redis.Get(ctx, oauthStateKey(provider, state)).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return ErrOAuthStateInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.redis.Del(ctx, oauthStateKey(provider, state)).Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
func oauthStateKey(provider, state string) string {
|
||||
return oauthStateKeyPrefix + normalizeOAuthProvider(provider) + ":" + strings.TrimSpace(state)
|
||||
}
|
||||
|
||||
func normalizeOAuthProvider(provider string) string {
|
||||
return strings.ToLower(strings.TrimSpace(provider))
|
||||
}
|
||||
|
||||
func (s *OAuthService) getJSON(ctx context.Context, rawURL string, out interface{}) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("OAuth HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
func (s *OAuthService) uniqueUsername(ctx context.Context, provider string) string {
|
||||
for i := 0; i < 5; i++ {
|
||||
name := fmt.Sprintf("%s_%s", provider, randomString(10))
|
||||
if existing, _ := s.userDAO.FindByUsername(ctx, name); existing == nil {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s_%d", provider, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func randomState() (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
buf := make([]byte, n)
|
||||
_, _ = rand.Read(buf)
|
||||
return hex.EncodeToString(buf)[:n]
|
||||
}
|
||||
|
||||
func randomPassword() string {
|
||||
return "oauth_" + randomString(32)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -191,6 +191,10 @@ func provideMeetingConfig(cfg *config.Config) *config.MeetingConfig {
|
||||
return &cfg.Meeting
|
||||
}
|
||||
|
||||
func provideOAuthConfig(cfg *config.Config) *config.OAuthConfig {
|
||||
return &cfg.OAuth
|
||||
}
|
||||
|
||||
// InfraSet 基础设施层 Provider Set
|
||||
var InfraSet = wire.NewSet(
|
||||
provideDBConfig,
|
||||
@@ -200,6 +204,7 @@ var InfraSet = wire.NewSet(
|
||||
provideServerConfig,
|
||||
provideLLMSourceConfig,
|
||||
provideMeetingConfig,
|
||||
provideOAuthConfig,
|
||||
db.NewPostgres,
|
||||
db.NewRedis,
|
||||
db.NewLLMSourceDB,
|
||||
|
||||
@@ -60,10 +60,13 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
||||
}
|
||||
userDAO := dao.NewUserDAO(gormDB)
|
||||
roleDAO := dao.NewRoleDAO(gormDB)
|
||||
oauthAccountDAO := dao.NewOAuthAccountDAO(gormDB)
|
||||
jwtConfig := provideJWTConfig(cfg)
|
||||
tokenStore := service.NewTokenStore(client, jwtConfig)
|
||||
authService := service.NewAuthService(userDAO, roleDAO, jwtConfig, tokenStore)
|
||||
authController := controller.NewAuthController(authService)
|
||||
oauthConfig := provideOAuthConfig(cfg)
|
||||
oauthService := service.NewOAuthService(userDAO, oauthAccountDAO, roleDAO, authService, oauthConfig, client)
|
||||
authController := controller.NewAuthController(authService, oauthService, oauthConfig)
|
||||
adminAuthController := controller.NewAdminAuthController(authService)
|
||||
userManageDAO := dao2.NewUserManageDAO(gormDB)
|
||||
userManageService := service2.NewUserManageService(userManageDAO, userDAO, roleDAO)
|
||||
|
||||
Reference in New Issue
Block a user