mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
feat(push): implement system notification and push framework
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -28,6 +29,19 @@ type loginRequiredAuditLog struct {
|
||||
Referer string `json:"referer"`
|
||||
}
|
||||
|
||||
func getUserByToken(ctx context.Context, tokenStr string) (*model.User, *model.AccessToken, error) {
|
||||
tokenHash := model.HashToken(tokenStr)
|
||||
var tokenRecord model.AccessToken
|
||||
if err := db.DB(ctx).Where("token_hash = ?", tokenHash).First(&tokenRecord).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Where("id = ? AND is_active = ?", tokenRecord.UserID, true).First(&user).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &user, &tokenRecord, nil
|
||||
}
|
||||
|
||||
// GetUserFromRequest 校验 Access Token 或 Session 并返回用户对象,如果未登录或用户失效则返回 error
|
||||
func GetUserFromRequest(c *gin.Context) (*model.User, error) {
|
||||
ctx := c.Request.Context()
|
||||
@@ -41,18 +55,16 @@ func GetUserFromRequest(c *gin.Context) (*model.User, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
|
||||
// 优先使用 Access Token 鉴权
|
||||
if tokenStr != "" {
|
||||
tokenHash := model.HashToken(tokenStr)
|
||||
var tokenRecord model.AccessToken
|
||||
if err := db.DB(ctx).Where("token_hash = ?", tokenHash).First(&tokenRecord).Error; err == nil {
|
||||
if err := db.DB(ctx).Where("id = ? AND is_active = ?", tokenRecord.UserID, true).First(&user).Error; err == nil {
|
||||
util.SetToContext(c, TokenAuthKey, true)
|
||||
util.SetToContext(c, TokenAdminKey, tokenRecord.IsAdmin)
|
||||
return &user, nil
|
||||
if user, tokenRecord, err := getUserByToken(ctx, tokenStr); err == nil {
|
||||
// 强行阻止 system 用户任何会话/Token 鉴权通过
|
||||
if user.Username == "system" {
|
||||
return nil, errors.New("system user is not allowed to login")
|
||||
}
|
||||
util.SetToContext(c, TokenAuthKey, true)
|
||||
util.SetToContext(c, TokenAdminKey, tokenRecord.IsAdmin)
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +74,7 @@ func GetUserFromRequest(c *gin.Context) (*model.User, error) {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
var user model.User
|
||||
// load user from db to make sure is active
|
||||
tx := db.DB(ctx).Where("id = ? AND is_active = ?", userID, true).First(&user)
|
||||
if tx.Error != nil {
|
||||
@@ -81,6 +94,11 @@ func GetUserFromRequest(c *gin.Context) (*model.User, error) {
|
||||
util.SetToContext(c, TokenAuthKey, false)
|
||||
util.SetToContext(c, TokenAdminKey, false)
|
||||
|
||||
// 强行阻止 system 用户任何会话/Token 鉴权通过
|
||||
if user.Username == "system" {
|
||||
return nil, errors.New("system user is not allowed to login")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
@@ -1181,3 +1183,75 @@ func TestOIDCPolicyEnforcement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemUserBlockedByMiddleware(t *testing.T) {
|
||||
initializeTestConfig()
|
||||
dbConn := setupTestDB(t)
|
||||
|
||||
// 1. 创建正常管理员
|
||||
adminUser := &model.User{ID: 1001, Username: "normal_admin", IsAdmin: true, IsActive: true}
|
||||
err := dbConn.Create(adminUser).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
// 2. 创建系统用户 (根据架构设计,系统用户 id = 999)
|
||||
systemUser := &model.User{ID: 999, Username: "system", Nickname: "系统", Password: "*", IsActive: true}
|
||||
err = dbConn.Create(systemUser).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
// 3. 设置全局测试数据库连接并构建测试路由组
|
||||
db.SetDB(dbConn)
|
||||
rProtected := gin.New()
|
||||
store := cookie.NewStore([]byte("secret"))
|
||||
rProtected.Use(sessions.Sessions("mysession", store))
|
||||
rProtected.Use(LoginRequired())
|
||||
rProtected.GET("/test-auth", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// 4. 测试未登录用户 (401)
|
||||
w1 := httptest.NewRecorder()
|
||||
req1, _ := http.NewRequest("GET", "/test-auth", nil)
|
||||
rProtected.ServeHTTP(w1, req1)
|
||||
assert.Equal(t, http.StatusUnauthorized, w1.Code)
|
||||
|
||||
// 5. 测试正常用户登录并访问 (200)
|
||||
rLogin := gin.New()
|
||||
rLogin.Use(sessions.Sessions("mysession", store))
|
||||
rLogin.GET("/login-mock", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Set("user_id", uint64(1001))
|
||||
_ = session.Save()
|
||||
c.Status(200)
|
||||
})
|
||||
|
||||
wLogin := httptest.NewRecorder()
|
||||
reqLogin, _ := http.NewRequest("GET", "/login-mock", nil)
|
||||
rLogin.ServeHTTP(wLogin, reqLogin)
|
||||
cookieStr := wLogin.Header().Get("Set-Cookie")
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("GET", "/test-auth", nil)
|
||||
req2.Header.Set("Cookie", cookieStr)
|
||||
rProtected.ServeHTTP(w2, req2)
|
||||
assert.Equal(t, http.StatusOK, w2.Code)
|
||||
|
||||
// 6. 测试 system 用户(ID: 999)登录并访问 (被中间件阻断返回 401)
|
||||
rLoginSystem := gin.New()
|
||||
rLoginSystem.Use(sessions.Sessions("mysession", store))
|
||||
rLoginSystem.GET("/login-system-mock", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Set("user_id", uint64(999))
|
||||
_ = session.Save()
|
||||
c.Status(200)
|
||||
})
|
||||
|
||||
wLoginSystem := httptest.NewRecorder()
|
||||
reqLoginSystem, _ := http.NewRequest("GET", "/login-system-mock", nil)
|
||||
rLoginSystem.ServeHTTP(wLoginSystem, reqLoginSystem)
|
||||
cookieSystemStr := wLoginSystem.Header().Get("Set-Cookie")
|
||||
|
||||
w3 := httptest.NewRecorder()
|
||||
req3, _ := http.NewRequest("GET", "/test-auth", nil)
|
||||
req3.Header.Set("Cookie", cookieSystemStr)
|
||||
rProtected.ServeHTTP(w3, req3)
|
||||
assert.Equal(t, http.StatusUnauthorized, w3.Code)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
||||
"github.com/Rain-kl/Wavelet/internal/common"
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
@@ -675,6 +676,8 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
|
||||
logger.InfoF(ctx, "[LoginAudit] successful OAuth login via source: %s, external ID: %s, user: %s, ID: %d, IP: %s", source.Name, userInfo.Sub, user.Username, user.ID, c.ClientIP())
|
||||
|
||||
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(&user, "logged_in")))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user