mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
refactor(util): move response helper to common/response and session logic to oauth
- Relocated generic HTTP response helpers (Response, OK, Err, etc.) from internal/util/ to a dedicated internal/common/response/ package. - Renamed ResponseAny to Any to resolve revive stuttering warnings. - Moved session building options and cookie headers logic from internal/util/ to internal/apps/oauth/. - Removed all direct imports of Gin/Sessions/HTTP frameworks from internal/util/ to keep general utilities 100% pure. - Regenerated Swagger API documentation via make swagger. - All tests and make code-check compile and pass with 0 issues.
This commit is contained in:
@@ -340,7 +340,7 @@ func setupTestRouter(dbConn *gorm.DB, mockRedis *mockRedisClient, mockClient *ht
|
||||
r.Use(mockContextMiddleware(mockClient))
|
||||
|
||||
store := cookie.NewStore([]byte(config.Config.App.SessionSecret))
|
||||
store.Options(util.GetSessionOptions(3600))
|
||||
store.Options(GetSessionOptions(3600))
|
||||
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, store))
|
||||
|
||||
db.SetDB(dbConn)
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// BasicUserInfo 用户基本信息结构体
|
||||
type BasicUserInfo struct {
|
||||
@@ -54,8 +54,8 @@ func BuildBasicUserInfo(user *model.User, needChange bool) BasicUserInfo {
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "用户信息"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "用户信息"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/oauth/user-info [get]
|
||||
// @Router /api/v1/user-info [get]
|
||||
// @Router /api/v1/user/self [get]
|
||||
@@ -66,7 +66,7 @@ func UserInfo(c *gin.Context) {
|
||||
|
||||
c.JSON(
|
||||
http.StatusOK,
|
||||
util.OK(BuildBasicUserInfo(user, needChange)),
|
||||
response.OK(BuildBasicUserInfo(user, needChange)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ func UserInfo(c *gin.Context) {
|
||||
// @Description 生成 OAuth 登录 URL,前端跳转至该地址完成授权。返回的 URL 中包含 state 参数用于 CSRF 防护。
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "OAuth 登录 URL"
|
||||
// @Failure 500 {object} util.ResponseAny "Redis 异常或内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "OAuth 登录 URL"
|
||||
// @Failure 500 {object} response.Any "Redis 异常或内部错误"
|
||||
// @Router /api/v1/oauth/login [get]
|
||||
|
||||
// Logout 退出登录
|
||||
@@ -85,8 +85,8 @@ func UserInfo(c *gin.Context) {
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "退出成功"
|
||||
// @Failure 500 {object} util.ResponseAny "Session 清除失败"
|
||||
// @Success 200 {object} response.Any{data=string} "退出成功"
|
||||
// @Failure 500 {object} response.Any "Session 清除失败"
|
||||
// @Router /api/v1/oauth/logout [get]
|
||||
func Logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
@@ -95,11 +95,11 @@ func Logout(c *gin.Context) {
|
||||
if userID != nil {
|
||||
logger.InfoF(c.Request.Context(), "[LoginAudit] user logged out: %v, ID: %v, IP: %s", username, userID, c.ClientIP())
|
||||
}
|
||||
session.Options(util.GetSessionOptions(-1))
|
||||
session.Options(GetSessionOptions(-1))
|
||||
session.Clear()
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package oauth provides authentication and OAuth integration.
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/gin-contrib/sessions"
|
||||
)
|
||||
|
||||
// GetSessionOptions 根据配置构建 Session 选项
|
||||
func GetSessionOptions(maxAge int) sessions.Options {
|
||||
return sessions.Options{
|
||||
Path: "/",
|
||||
Domain: config.Config.App.SessionDomain,
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: config.Config.App.SessionHTTPOnly,
|
||||
Secure: config.Config.App.SessionSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
// StripCookieMaxAgeAndExpires 从 Set-Cookie 响应头中移除 Max-Age 和 Expires,从而使其成为浏览器会话 Cookie
|
||||
func StripCookieMaxAgeAndExpires(header http.Header, cookieName string) {
|
||||
headers := header["Set-Cookie"]
|
||||
if len(headers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
newHeaders := make([]string, 0, len(headers))
|
||||
for _, h := range headers {
|
||||
if strings.HasPrefix(h, cookieName+"=") {
|
||||
parts := strings.Split(h, ";")
|
||||
newParts := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
trimmed := strings.TrimSpace(p)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.HasPrefix(lower, "max-age=") || strings.HasPrefix(lower, "expires=") {
|
||||
continue
|
||||
}
|
||||
newParts = append(newParts, p)
|
||||
}
|
||||
newHeaders = append(newHeaders, strings.Join(newParts, ";"))
|
||||
} else {
|
||||
newHeaders = append(newHeaders, h)
|
||||
}
|
||||
}
|
||||
header["Set-Cookie"] = newHeaders
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gin-contrib/sessions"
|
||||
@@ -27,6 +25,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
// AuthSourceView 登录源展示信息
|
||||
@@ -215,14 +214,14 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
|
||||
isSessionCookie = true
|
||||
}
|
||||
}
|
||||
session.Options(util.GetSessionOptions(maxAge))
|
||||
session.Options(GetSessionOptions(maxAge))
|
||||
|
||||
if err := session.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isSessionCookie {
|
||||
util.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
|
||||
StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -358,10 +357,10 @@ func buildCallbackResult(user *model.User, status string) OAuthCallbackResult {
|
||||
// @Description 返回当前系统已启用的所有 OAuth 登录源,前端展示登录按钮列表时调用
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny{data=[]oauth.AuthSourceView} "登录源列表"
|
||||
// @Success 200 {object} response.Any{data=[]oauth.AuthSourceView} "登录源列表"
|
||||
// @Router /api/v1/oauth/sources [get]
|
||||
func GetLoginSources(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK(activeLoginSources(c.Request.Context())))
|
||||
c.JSON(http.StatusOK, response.OK(activeLoginSources(c.Request.Context())))
|
||||
}
|
||||
|
||||
// GetLoginURL 获取登录授权地址
|
||||
@@ -370,25 +369,25 @@ func GetLoginSources(c *gin.Context) {
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Param source query string false "认证源名称,为空使用第一个启用的认证源"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} util.ResponseAny "认证源不存在或未配置"
|
||||
// @Failure 500 {object} util.ResponseAny "Redis 异常 or 构造 URL 失败"
|
||||
// @Success 200 {object} response.Any{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} response.Any "认证源不存在或未配置"
|
||||
// @Failure 500 {object} response.Any "Redis 异常 or 构造 URL 失败"
|
||||
// @Router /api/v1/oauth/login [get]
|
||||
func GetLoginURL(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if !isOIDCLoginEnabled(ctx) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := resolveAuthSource(ctx, c.Query("source"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -396,7 +395,7 @@ func GetLoginURL(c *gin.Context) {
|
||||
token, isNew := ensureSessionToken(session)
|
||||
if isNew {
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -412,20 +411,20 @@ func GetLoginURL(c *gin.Context) {
|
||||
SessionHash: sessionHash,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := db.Redis.Set(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
authorizeURL, err := buildAuthorizeURL(c.Request.Context(), source, state)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
c.JSON(http.StatusOK, response.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
}
|
||||
|
||||
func buildAuthorizeURL(ctx context.Context, source *model.AuthSource, state string) (string, error) {
|
||||
@@ -450,25 +449,25 @@ func buildAuthorizeURL(ctx context.Context, source *model.AuthSource, state stri
|
||||
// @Produce json
|
||||
// @Param source path string true "认证源名称"
|
||||
// @Param purpose query string false "授权目的:login(登录)或 bind(绑定账号),默认 login"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} util.ResponseAny "认证源不存在或未启用"
|
||||
// @Failure 500 {object} util.ResponseAny "Redis 异常或构造 URL 失败"
|
||||
// @Success 200 {object} response.Any{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} response.Any "认证源不存在或未启用"
|
||||
// @Failure 500 {object} response.Any "Redis 异常或构造 URL 失败"
|
||||
// @Router /api/v1/oauth/{source}/authorize [get]
|
||||
func Authorize(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if !isOIDCLoginEnabled(ctx) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := resolveAuthSource(ctx, c.Param("source"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
purpose := strings.ToLower(strings.TrimSpace(c.Query("purpose")))
|
||||
@@ -479,14 +478,14 @@ func Authorize(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userID := GetUserIDFromSession(session)
|
||||
if purpose == OAuthPurposeBind && userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
|
||||
token, isNew := ensureSessionToken(session)
|
||||
if isNew {
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -501,20 +500,20 @@ func Authorize(c *gin.Context) {
|
||||
SessionHash: sessionHash,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := db.Redis.Set(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
authorizeURL, err := buildAuthorizeURL(c.Request.Context(), source, state)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
c.JSON(http.StatusOK, response.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
}
|
||||
|
||||
// Callback OAuth 回调处理
|
||||
@@ -524,15 +523,15 @@ func Authorize(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body oauth.CallbackRequest true "回调请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.OAuthCallbackResult} "登录或绑定成功"
|
||||
// @Failure 400 {object} util.ResponseAny "state 无效、参数错误或认证源错误"
|
||||
// @Failure 401 {object} util.ResponseAny "绑定场景未登录"
|
||||
// @Failure 500 {object} util.ResponseAny "OAuth 认证失败或内部错误"
|
||||
// @Success 200 {object} response.Any{data=oauth.OAuthCallbackResult} "登录或绑定成功"
|
||||
// @Failure 400 {object} response.Any "state 无效、参数错误或认证源错误"
|
||||
// @Failure 401 {object} response.Any "绑定场景未登录"
|
||||
// @Failure 500 {object} response.Any "OAuth 认证失败或内部错误"
|
||||
// @Router /api/v1/oauth/callback [post]
|
||||
func Callback(c *gin.Context) {
|
||||
var req CallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -540,14 +539,14 @@ func Callback(c *gin.Context) {
|
||||
stateKey := db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, req.State))
|
||||
payloadRaw, err := db.Redis.Get(ctx, stateKey).Result()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errInvalidState))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errInvalidState))
|
||||
return
|
||||
}
|
||||
_ = db.Redis.Del(ctx, stateKey)
|
||||
|
||||
payload, err := decodeOAuthStatePayload(payloadRaw)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -555,55 +554,55 @@ func Callback(c *gin.Context) {
|
||||
currentUserID := GetUserIDFromSession(session)
|
||||
|
||||
if payload.Purpose == OAuthPurposeBind && currentUserID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
|
||||
token, ok := session.Get(SessionTokenKey).(string)
|
||||
if !ok || token == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid session context"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid session context"))
|
||||
return
|
||||
}
|
||||
|
||||
if hashSessionToken(token) != payload.SessionHash {
|
||||
c.JSON(http.StatusBadRequest, util.Err("session mismatch for oauth state"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("session mismatch for oauth state"))
|
||||
return
|
||||
}
|
||||
|
||||
if payload.Purpose == OAuthPurposeBind && currentUserID != payload.UserID {
|
||||
c.JSON(http.StatusBadRequest, util.Err("user context mismatch for oauth binding"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("user context mismatch for oauth binding"))
|
||||
return
|
||||
}
|
||||
|
||||
if !isOIDCLoginEnabled(ctx) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := resolveAuthSource(ctx, payload.SourceName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := getFrontendLoginRedirectURL(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, err := buildOAuthUserInfo(ctx, source, req.Code, req.State, redirectURL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := normalizeOAuthUserInfo(userInfo); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if userInfo.Sub == "" {
|
||||
@@ -622,12 +621,12 @@ func Callback(c *gin.Context) {
|
||||
func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthSource, userInfo *model.OAuthUserInfo) {
|
||||
userID := GetUserIDFromContext(c)
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := db.DB(ctx).First(&user, "id = ?", userID).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
@@ -637,12 +636,12 @@ func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthS
|
||||
ExternalUsername: userInfo.Username,
|
||||
Email: userInfo.Email,
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
user.LastLoginAt = time.Now()
|
||||
_ = db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(&user, "bound")))
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(&user, "bound")))
|
||||
}
|
||||
|
||||
// handleCallbackLogin 处理 OAuth 回调中的登录流程(查找已有帐号或自动注册)
|
||||
@@ -653,7 +652,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
switch {
|
||||
case err == nil:
|
||||
if err := db.DB(ctx).First(&user, "id = ?", account.UserID).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
@@ -663,14 +662,14 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
}
|
||||
user = newUser
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user.LastLoginAt = time.Now()
|
||||
_ = db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error
|
||||
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -678,7 +677,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
|
||||
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(&user, "logged_in")))
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(&user, "logged_in")))
|
||||
}
|
||||
|
||||
// handleCallbackRegister 处理 OAuth 回调中的自动注册流程
|
||||
@@ -690,20 +689,20 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
}
|
||||
|
||||
if !registrationEnabled {
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(nil, "need_bind")))
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(nil, "need_bind")))
|
||||
return model.User{}, false
|
||||
}
|
||||
|
||||
username, uniqueErr := uniqueUsername(ctx, userInfo.Username)
|
||||
if uniqueErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(uniqueErr.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(uniqueErr.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
userInfo.Username = username
|
||||
|
||||
var user model.User
|
||||
if err := user.CreateUser(ctx, db.DB(ctx), userInfo); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
if err := model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
@@ -713,7 +712,7 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
ExternalUsername: userInfo.Username,
|
||||
Email: userInfo.Email,
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
logger.InfoF(ctx, "[LoginAudit] successful OAuth registration via source: %s, external ID: %s, user: %s, ID: %d, IP: %s", source.Name, userInfo.Sub, user.Username, user.ID, c.ClientIP())
|
||||
@@ -727,18 +726,18 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.ExternalAccountView} "外部帐号列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]model.ExternalAccountView} "外部帐号列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/oauth/external-accounts [get]
|
||||
func ListExternalAccounts(c *gin.Context) {
|
||||
userID := GetUserIDFromContext(c)
|
||||
accounts, err := model.ListExternalAccountsByUserID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(accounts))
|
||||
c.JSON(http.StatusOK, response.OK(accounts))
|
||||
}
|
||||
|
||||
// DeleteExternalAccount 解除外部帐号绑定
|
||||
@@ -748,25 +747,25 @@ func ListExternalAccounts(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "外部帐号绑定记录 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "解除绑定成功"
|
||||
// @Failure 400 {object} util.ResponseAny "ID 无效或解除失败"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=string} "解除绑定成功"
|
||||
// @Failure 400 {object} response.Any "ID 无效或解除失败"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/oauth/external-accounts/{id}/delete [post]
|
||||
func DeleteExternalAccount(c *gin.Context) {
|
||||
userID := GetUserIDFromContext(c)
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
rawID := strings.TrimSpace(c.Param("id"))
|
||||
id, err := strconv.ParseUint(rawID, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errInvalidExternalAccountBindingID))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errInvalidExternalAccountBindingID))
|
||||
return
|
||||
}
|
||||
if err := model.DeleteExternalAccountForUser(c.Request.Context(), id, userID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user