merge: feat(core): implement cordis configuration extension and migrate all plugins

This commit is contained in:
ryan
2026-08-29 10:54:28 +08:00
87 changed files with 3117 additions and 828 deletions
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package admin
import "Wavelet/plugins/domain/admin/model"
// DatabaseConfig aliases model.DatabaseConfig.
type DatabaseConfig = model.DatabaseConfig
// ClickHouseConfig aliases model.ClickHouseConfig.
type ClickHouseConfig = model.ClickHouseConfig
+1 -2
View File
@@ -4,7 +4,6 @@
package handler
import (
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"Wavelet/pkg/response"
"Wavelet/plugins/domain/admin/errs"
@@ -147,7 +146,7 @@ func GetDatabaseInfo(c *gin.Context) {
// @Failure 500 {object} response.Any "导出失败"
// @Router /api/v1/admin/db-export [get]
func ExportDatabase(c *gin.Context) {
if !config.Config.Database.Enabled {
if !service.GetDBConfig().Enabled {
exportSQLite(c)
} else {
exportPostgres(c)
@@ -0,0 +1,20 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package model
// DatabaseConfig holds database configuration needed by the admin plugin.
type DatabaseConfig struct {
Enabled bool `config:"enabled" env:"DB_ENABLED" default:"false" autoEnable:"DB_HOST"`
Host string `config:"host" env:"DB_HOST"`
Port int `config:"port" env:"DB_PORT" default:"5432"`
Database string `config:"database" env:"DB_DATABASE"`
Username string `config:"username" env:"DB_USERNAME"`
Password string `config:"password" env:"DB_PASSWORD" secret:"true"`
SQLitePath string `config:"sqlite_path" env:"DB_SQLITE_PATH" default:"./data/wavelet.db"`
}
// ClickHouseConfig holds clickhouse enablement status needed by admin log queries/switching.
type ClickHouseConfig struct {
Enabled bool `config:"enabled" env:"CLICKHOUSE_ENABLED" default:"false" autoEnable:"CLICKHOUSE_HOST"`
}
+16
View File
@@ -66,8 +66,24 @@ func (p *Plugin) Manifest() core.Manifest {
}
}
// DeclareConfig declares configuration bindings consumed by the admin plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "database", Target: &model.DatabaseConfig{}},
{Prefix: "clickhouse", Target: &model.ClickHouseConfig{}},
}
}
// Apply registers admin routes, tasks, schedules, and settings into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var dbCfg model.DatabaseConfig
_ = ctx.Config().Bind("database", &dbCfg)
service.SetDBConfig(dbCfg)
var chCfg model.ClickHouseConfig
_ = ctx.Config().Bind("clickhouse", &chCfg)
service.SetClickHouseConfig(chCfg)
// 0. Bind Services reactively
if db, err := core.Inject[contracts.DBService](ctx); err == nil && db != nil {
service.SetDBService(db)
+30 -7
View File
@@ -4,7 +4,6 @@
package repository
import (
"Wavelet/pkg/config"
"Wavelet/plugins/domain/admin/errs"
"Wavelet/plugins/domain/admin/model"
"context"
@@ -14,6 +13,7 @@ import (
"os"
"os/exec"
"strings"
"sync"
"time"
)
@@ -22,9 +22,30 @@ const (
logDBNameSQLite = "sqlite"
)
var (
dbConfigMu sync.RWMutex
dbConfig = model.DatabaseConfig{
SQLitePath: defaultSQLiteDBPath,
}
)
// SetDBConfig sets the database configuration.
func SetDBConfig(cfg model.DatabaseConfig) {
dbConfigMu.Lock()
defer dbConfigMu.Unlock()
dbConfig = cfg
}
// GetDBConfig gets the database configuration.
func GetDBConfig() model.DatabaseConfig {
dbConfigMu.RLock()
defer dbConfigMu.RUnlock()
return dbConfig
}
// sqliteDatabasePath resolves the effective SQLite file path from configuration.
func sqliteDatabasePath() string {
name := config.Config.Database.SQLitePath
name := GetDBConfig().SQLitePath
if name == "" {
name = defaultSQLiteDBPath
}
@@ -93,7 +114,7 @@ func GetPostgresOverview(ctx context.Context) (model.DBOverviewResponse, error)
return model.DBOverviewResponse{}, errs.ErrDatabaseUninitialized
}
name := config.Config.Database.Database
name := GetDBConfig().Database
var version string
var ver string
@@ -152,7 +173,7 @@ func ListDatabaseTableNames(ctx context.Context) ([]string, error) {
var tables []string
var err error
if !config.Config.Database.Enabled {
if !GetDBConfig().Enabled {
err = gormDB.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").Scan(&tables).Error
} else {
err = gormDB.Raw("SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() ORDER BY table_name").Scan(&tables).Error
@@ -288,9 +309,10 @@ func scanRowAsMap(rows *sql.Rows, cols []string) (map[string]any, error) {
// GetSQLiteInfo collects the SQLite type/name/version triple.
func GetSQLiteInfo(ctx context.Context) model.DatabaseInfoResponse {
cfg := GetDBConfig()
info := model.DatabaseInfoResponse{
Type: logDBNameSQLite,
Name: config.Config.Database.SQLitePath,
Name: cfg.SQLitePath,
Version: "SQLite",
}
if info.Name == "" {
@@ -309,9 +331,10 @@ func GetSQLiteInfo(ctx context.Context) model.DatabaseInfoResponse {
// GetPostgresInfo collects the PostgreSQL type/name/version triple.
func GetPostgresInfo(ctx context.Context) model.DatabaseInfoResponse {
cfg := GetDBConfig()
info := model.DatabaseInfoResponse{
Type: "postgres",
Name: config.Config.Database.Database,
Name: cfg.Database,
Version: "PostgreSQL",
}
gormDB := GetDB(ctx)
@@ -343,7 +366,7 @@ func OpenSQLiteExportFile() (*os.File, os.FileInfo, error) {
// NewPgDumpCommand builds the streaming pg_dump command for the active database.
func NewPgDumpCommand(ctx context.Context) (*exec.Cmd, string, error) {
dbCfg := config.Config.Database
dbCfg := GetDBConfig()
pgDumpPath, err := exec.LookPath("pg_dump")
if err != nil {
@@ -44,6 +44,13 @@ func newFlushLogTestCache(t *testing.T) (contracts.CacheService, *miniredis.Mini
p := cacheplugin.New(cacheplugin.WithRedis(rdb), cacheplugin.WithRAMCapacity(64))
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(map[string]any{
"redis": map[string]any{
"enabled": true,
"addrs": []string{mr.Addr()},
},
}))
require.NoError(t, ctx.Config().Resolve())
require.NoError(t, p.Apply(ctx))
svc, err := core.Inject[contracts.CacheService](ctx)
require.NoError(t, err)
+38 -3
View File
@@ -4,22 +4,57 @@
package service
import (
"Wavelet/pkg/config"
"Wavelet/plugins/domain/admin/model"
"Wavelet/plugins/domain/admin/repository"
"context"
"os"
"os/exec"
"strings"
"sync"
"time"
)
var (
dbConfigMu sync.RWMutex
dbConfig model.DatabaseConfig
chConfig model.ClickHouseConfig
)
// SetDBConfig sets the database configuration in service and repository.
func SetDBConfig(cfg model.DatabaseConfig) {
dbConfigMu.Lock()
defer dbConfigMu.Unlock()
dbConfig = cfg
repository.SetDBConfig(cfg)
}
// GetDBConfig returns the database configuration.
func GetDBConfig() model.DatabaseConfig {
dbConfigMu.RLock()
defer dbConfigMu.RUnlock()
return dbConfig
}
// SetClickHouseConfig sets the clickhouse configuration.
func SetClickHouseConfig(cfg model.ClickHouseConfig) {
dbConfigMu.Lock()
defer dbConfigMu.Unlock()
chConfig = cfg
}
// GetClickHouseConfig returns the clickhouse configuration.
func GetClickHouseConfig() model.ClickHouseConfig {
dbConfigMu.RLock()
defer dbConfigMu.RUnlock()
return chConfig
}
// selectSQLKeywords marks statements that return a result set instead of a row count.
var selectSQLKeywords = []string{"select", "show", "explain", "describe", "pragma"}
// DatabaseOverview collects the runtime overview of the active database.
func DatabaseOverview(ctx context.Context) (model.DBOverviewResponse, error) {
if !config.Config.Database.Enabled {
if !GetDBConfig().Enabled {
return repository.GetSQLiteOverview(ctx)
}
return repository.GetPostgresOverview(ctx)
@@ -114,7 +149,7 @@ func isSelectStatement(trimmedSQL string) bool {
// DatabaseInfo returns the active database type, name and version.
func DatabaseInfo(ctx context.Context) model.DatabaseInfoResponse {
if !config.Config.Database.Enabled {
if !GetDBConfig().Enabled {
return repository.GetSQLiteInfo(ctx)
}
return repository.GetPostgresInfo(ctx)
@@ -5,7 +5,6 @@ package service
import (
"Wavelet/core/contracts"
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"Wavelet/plugins/domain/admin/errs"
"Wavelet/plugins/domain/admin/model"
@@ -139,15 +138,15 @@ func validateSwitch(ctx context.Context, target string) error {
}
switch target {
case targetClickHouse:
if !config.Config.ClickHouse.Enabled {
if !GetClickHouseConfig().Enabled {
return errors.New(errs.ErrClickHouseNotEnabled)
}
case targetPostgres:
if !config.Config.Database.Enabled {
if !GetDBConfig().Enabled {
return errors.New(errs.ErrPostgresNotEnabled)
}
case targetSQLite:
if config.Config.Database.Enabled {
if GetDBConfig().Enabled {
return errors.New(errs.ErrSQLiteNotAllowedAsLogDB)
}
}
@@ -4,7 +4,6 @@
package service
import (
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"Wavelet/plugins/domain/admin/model"
"Wavelet/plugins/domain/admin/repository"
@@ -152,12 +151,12 @@ func retentionOr(ctx context.Context, key string) int {
func availableLogTargets(active string) []string {
if active == logDBNameClickHouse {
if config.Config.Database.Enabled {
if GetDBConfig().Enabled {
return []string{logDBNamePostgres}
}
return []string{logDBNameSQLite}
}
if config.Config.ClickHouse.Enabled {
if GetClickHouseConfig().Enabled {
return []string{logDBNameClickHouse}
}
return []string{}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package auth
// SessionConfig defines the session configuration declared by the auth plugin.
type SessionConfig struct {
SessionCookieName string `config:"session_cookie_name" env:"APP_SESSION_COOKIE_NAME" default:"wavelet_session"`
SessionSecret string `config:"session_secret" env:"APP_SESSION_SECRET" secret:"true"`
SessionDomain string `config:"session_domain" env:"APP_SESSION_DOMAIN"`
SessionAge int `config:"session_age" env:"APP_SESSION_AGE" default:"86400"`
SessionHTTPOnly bool `config:"session_http_only" env:"APP_SESSION_HTTP_ONLY" default:"true"`
SessionSecure bool `config:"session_secure" env:"APP_SESSION_SECURE"`
}
+12
View File
@@ -73,8 +73,20 @@ func (p *Plugin) Manifest() core.Manifest {
}
}
// DeclareConfig declares configuration bindings for the auth plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "app", Target: &SessionConfig{}},
}
}
// Apply registers the auth migrations, services, routes, and settings into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var cfg SessionConfig
if err := ctx.Config().Bind("app", &cfg); err == nil {
SetSessionConfig(cfg)
}
// 0. Bind DBService & CacheService from Context
if db, err := core.Inject[contracts.DBService](ctx); err == nil && db != nil {
setDBService(db)
+31 -6
View File
@@ -5,13 +5,13 @@ package auth
import (
"Wavelet/core/contracts"
"Wavelet/pkg/config"
"context"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"strings"
"sync"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -19,14 +19,38 @@ import (
gsessions "github.com/gorilla/sessions"
)
var (
sessConfigMu sync.RWMutex
sessConfig = SessionConfig{
SessionCookieName: "wavelet_session",
SessionAge: 86400,
SessionHTTPOnly: true,
}
)
// SetSessionConfig updates the active session configuration.
func SetSessionConfig(cfg SessionConfig) {
sessConfigMu.Lock()
defer sessConfigMu.Unlock()
sessConfig = cfg
}
// GetSessionConfig returns the active session configuration.
func GetSessionConfig() SessionConfig {
sessConfigMu.RLock()
defer sessConfigMu.RUnlock()
return sessConfig
}
// GetSessionOptions 根据配置构建 Session 选项
func GetSessionOptions(maxAge int) sessions.Options {
cfg := GetSessionConfig()
return sessions.Options{
Path: "/",
Domain: config.Config.App.SessionDomain,
Domain: cfg.SessionDomain,
MaxAge: maxAge,
HttpOnly: config.Config.App.SessionHTTPOnly,
Secure: config.Config.App.SessionSecure,
HttpOnly: cfg.SessionHTTPOnly,
Secure: cfg.SessionSecure,
SameSite: http.SameSiteLaxMode,
}
}
@@ -113,7 +137,8 @@ func SetLoginSession(ctx context.Context, c *gin.Context, user *contracts.UserDT
}
// 根据系统配置动态设置 Session 过期时间
maxAge := config.Config.App.SessionAge
cfg := GetSessionConfig()
maxAge := cfg.SessionAge
isSessionCookie := false
val, err := GetSystemConfigValue(ctx, "login_session_ttl_hours")
@@ -137,7 +162,7 @@ func SetLoginSession(ctx context.Context, c *gin.Context, user *contracts.UserDT
}
if isSessionCookie {
StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
StripCookieMaxAgeAndExpires(c.Writer.Header(), cfg.SessionCookieName)
}
return nil
+16
View File
@@ -41,8 +41,24 @@ func (p *Plugin) Manifest() core.Manifest {
}
}
type capAppConfig struct {
SessionSecret string `config:"session_secret" env:"APP_SESSION_SECRET" secret:"true"`
}
// DeclareConfig declares configuration bindings for the cap plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "app", Target: &capAppConfig{}},
}
}
// Apply registers the cap routes and settings into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var cfg capAppConfig
if err := ctx.Config().Bind("app", &cfg); err == nil && cfg.SessionSecret != "" {
SetSecret([]byte(cfg.SessionSecret))
}
// 0. Bind DBService from Context
if db, err := core.Inject[contracts.DBService](ctx); err == nil && db != nil {
setDBService(db)
+14 -15
View File
@@ -5,7 +5,6 @@
package cap
import (
"Wavelet/pkg/config"
"Wavelet/plugins/domain/cap/pow"
"context"
"crypto/sha256"
@@ -161,23 +160,23 @@ func sGetAndDelete(ctx context.Context, store pow.Store, key string) (string, bo
}
var (
defaultManager *Manager
once sync.Once
defaultManagerMu sync.RWMutex
defaultManager *Manager
)
// SetSecret sets the shared secret used by the default manager.
func SetSecret(secret []byte) {
defaultManagerMu.Lock()
defer defaultManagerMu.Unlock()
if len(secret) > 0 {
store := pow.NewMemoryStore(1 * time.Minute)
defaultManager = NewManager(secret, store)
}
}
// GetDefaultManager yields the global singleton CAPTCHA manager.
func GetDefaultManager() *Manager {
once.Do(func() {
var secret []byte
if config.Config != nil && strings.TrimSpace(config.Config.App.SessionSecret) != "" {
secret = []byte(config.Config.App.SessionSecret)
}
if len(secret) == 0 {
return
}
store := pow.NewMemoryStore(1 * time.Minute)
defaultManager = NewManager(secret, store)
})
defaultManagerMu.RLock()
defer defaultManagerMu.RUnlock()
return defaultManager
}
+19
View File
@@ -6,6 +6,7 @@ package domain_test
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/pkg/idgen"
"Wavelet/plugins/domain/admin"
"Wavelet/plugins/domain/auth"
"Wavelet/plugins/domain/message_gateway"
@@ -31,6 +32,7 @@ import (
func setupTestDB(t *testing.T) *gorm.DB {
t.Helper()
_ = idgen.Init(1)
dbPath := filepath.Join(t.TempDir(), "domain_test.db")
testDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
@@ -76,6 +78,8 @@ func (m *mockOAuthProvider) ExchangeCode(ctx context.Context, code string) (*con
func TestAuthPlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
require.NoError(t, db.New(db.WithDB(testDB)).Apply(ctx))
@@ -140,6 +144,8 @@ func TestAuthPlugin(t *testing.T) {
func TestUserPlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
require.NoError(t, db.New(db.WithDB(testDB)).Apply(ctx))
@@ -242,6 +248,8 @@ func TestUserPlugin(t *testing.T) {
func TestMessageGatewayPlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
require.NoError(t, db.New(db.WithDB(testDB)).Apply(ctx))
@@ -310,6 +318,8 @@ func TestMessageGatewayPlugin(t *testing.T) {
func TestRiskControlPlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
p := risk_control.New()
assert.Equal(t, "risk_control", p.Name())
assert.Equal(t, "risk_control", p.Manifest().Name)
@@ -330,6 +340,8 @@ func TestRiskControlPlugin(t *testing.T) {
func TestAdminPlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
require.NoError(t, db.New(db.WithDB(testDB)).Apply(ctx))
@@ -389,6 +401,13 @@ func TestAllDomainPluginsCombined(t *testing.T) {
defer func() { _ = rdb.Close() }()
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(map[string]any{
"redis": map[string]any{
"enabled": true,
"addrs": []string{mr.Addr()},
},
}))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
// Apply Infra plugins
@@ -72,8 +72,23 @@ func (p *Plugin) Manifest() core.Manifest {
}
}
type mgAppConfig struct {
SessionSecret string `config:"session_secret" env:"APP_SESSION_SECRET" secret:"true"`
}
// DeclareConfig declares configuration bindings for the message_gateway plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "app", Target: &mgAppConfig{}},
}
}
// Apply registers message_gateway migrations, routes, tasks, schedules, events, and settings into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var cfg mgAppConfig
if err := ctx.Config().Bind("app", &cfg); err == nil && cfg.SessionSecret != "" {
service.SetCredentialSecret(cfg.SessionSecret)
}
// 0. Bind DBService, CacheService, TaskService, UserService
if db, err := core.Inject[contracts.DBService](ctx); err == nil && db != nil {
repository.SetDBService(db)
@@ -7,7 +7,6 @@ package service
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"Wavelet/pkg/util"
"Wavelet/plugins/domain/message_gateway/errs"
@@ -101,12 +100,23 @@ func FormatCode(s string) string {
return s[:4] + "-" + s[4:]
}
var (
credentialSecretMu sync.RWMutex
credentialSecret string
)
// SetCredentialSecret sets the secret used to derive CredentialKey.
func SetCredentialSecret(secret string) {
credentialSecretMu.Lock()
defer credentialSecretMu.Unlock()
credentialSecret = secret
}
// CredentialKey is AES-256 hex derived from the session secret.
func CredentialKey() string {
secret := ""
if config.Config != nil {
secret = config.Config.App.SessionSecret
}
credentialSecretMu.RLock()
secret := credentialSecret
credentialSecretMu.RUnlock()
sum := sha256.Sum256([]byte(secret))
return hex.EncodeToString(sum[:])
}
@@ -4,6 +4,7 @@
package logstore
import (
"Wavelet/pkg/idgen"
"context"
"testing"
"time"
@@ -15,6 +16,7 @@ import (
func newTestUserAccessStore(t *testing.T) *userAccessLogGormStore {
t.Helper()
_ = idgen.Init(1)
gdb, err := gorm.Open(sqlite.Open("file:logstore-"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, gdb.AutoMigrate(&UserAccessLog{}))
@@ -4,7 +4,6 @@
package logstore
import (
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"context"
"errors"
@@ -34,6 +33,9 @@ const resolveCacheTTL = 1 * time.Second
var (
configReader ConfigReader
defaultDBMu sync.RWMutex
defaultDB = dbNameSQLite
storeMu sync.RWMutex
active *Store
activeDB string
@@ -41,6 +43,25 @@ var (
lastResolveTime time.Time
)
// SetDefaultDatabases configures the fallback database based on database and clickhouse enablement.
func SetDefaultDatabases(dbEnabled, chEnabled bool) {
defaultDBMu.Lock()
defer defaultDBMu.Unlock()
defaultDB = dbNameSQLite
if dbEnabled {
defaultDB = dbNamePostgres
}
if chEnabled {
defaultDB = dbNameClickHouse
}
}
func getDefaultDatabase() string {
defaultDBMu.RLock()
defer defaultDBMu.RUnlock()
return defaultDB
}
// SetConfigReader 注入系统配置读取函数(bootstrap 调用,测试可注入内存实现)。
func SetConfigReader(fn ConfigReader) { configReader = fn }
@@ -169,13 +190,7 @@ func resolveDatabase(ctx context.Context) (string, error) {
resolved := v
if resolved == "" {
resolved = dbNameSQLite
if config.Config.Database.Enabled {
resolved = dbNamePostgres
}
if config.Config.ClickHouse.Enabled {
resolved = dbNameClickHouse
}
resolved = getDefaultDatabase()
}
storeMu.Lock()
@@ -6,26 +6,38 @@ package risk_control
import (
"Wavelet/core/contracts"
"Wavelet/pkg/config"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/idgen"
"Wavelet/pkg/response"
"Wavelet/plugins/domain/risk_control/logstore"
"encoding/json"
"net/http"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
)
var accessLogEnabled atomic.Bool
// SetAccessLogEnabled enables or disables access log collection.
func SetAccessLogEnabled(enabled bool) {
accessLogEnabled.Store(enabled)
}
// IsAccessLogEnabled reports whether access log collection is enabled.
func IsAccessLogEnabled() bool {
return accessLogEnabled.Load()
}
// Middleware is an alias for RiskControlMiddleware.
var Middleware = RiskControlMiddleware
// RiskControlMiddleware 全局日志采集中间件
func RiskControlMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 如果未启用 ClickHouse,直接放行
if config.Config == nil || !config.Config.ClickHouse.Enabled {
// 如果未启用日志采集,直接放行
if !IsAccessLogEnabled() {
c.Next()
return
}
@@ -6,8 +6,8 @@ package risk_control_test
import (
"Wavelet/core/contracts"
"Wavelet/pkg/batchwriter"
"Wavelet/pkg/config"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/idgen"
"Wavelet/pkg/testhelper"
"Wavelet/plugins/domain/risk_control"
"Wavelet/plugins/domain/risk_control/logstore"
@@ -23,6 +23,10 @@ import (
"github.com/stretchr/testify/assert"
)
func init() {
_ = idgen.Init(1)
}
func newTestAccessLogWriter(t *testing.T, cfg batchwriter.Config) (*batchwriter.Writer[*logstore.UserAccessLog], func() []*logstore.UserAccessLog) {
t.Helper()
@@ -70,8 +74,8 @@ func TestRiskControlMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("ClickHouse disabled", func(t *testing.T) {
config.Config.ClickHouse.Enabled = false
defer func() { config.Config.ClickHouse.Enabled = false }()
risk_control.SetAccessLogEnabled(false)
defer risk_control.SetAccessLogEnabled(false)
r := testhelper.NewTestGinEngine(risk_control.RiskControlMiddleware())
r.GET("/test", func(c *gin.Context) {
@@ -87,8 +91,8 @@ func TestRiskControlMiddleware(t *testing.T) {
})
t.Run("ClickHouse enabled - Normal Authenticated Request", func(t *testing.T) {
config.Config.ClickHouse.Enabled = true
defer func() { config.Config.ClickHouse.Enabled = false }()
risk_control.SetAccessLogEnabled(true)
defer risk_control.SetAccessLogEnabled(false)
cfg := batchwriter.DefaultConfig()
cfg.MaxBatchSize = 100
@@ -133,8 +137,8 @@ func TestRiskControlMiddleware(t *testing.T) {
})
t.Run("ClickHouse enabled - Unauthenticated Request", func(t *testing.T) {
config.Config.ClickHouse.Enabled = true
defer func() { config.Config.ClickHouse.Enabled = false }()
risk_control.SetAccessLogEnabled(true)
defer risk_control.SetAccessLogEnabled(false)
cfg := batchwriter.DefaultConfig()
cfg.MaxBatchSize = 100
@@ -162,8 +166,8 @@ func TestRiskControlMiddleware(t *testing.T) {
})
t.Run("ClickHouse enabled - Buffer Full Rate Limiting", func(t *testing.T) {
config.Config.ClickHouse.Enabled = true
defer func() { config.Config.ClickHouse.Enabled = false }()
risk_control.SetAccessLogEnabled(true)
defer risk_control.SetAccessLogEnabled(false)
cfg := batchwriter.DefaultConfig()
cfg.QueueSize = 2
@@ -68,8 +68,32 @@ func (p *Plugin) Manifest() core.Manifest {
}
}
type rcClickHouseConfig struct {
Enabled bool `config:"enabled" env:"CLICKHOUSE_ENABLED" default:"false" autoEnable:"CLICKHOUSE_HOST"`
}
type rcDBConfig struct {
Enabled bool `config:"enabled" env:"DB_ENABLED" default:"false" autoEnable:"DB_HOST"`
}
// DeclareConfig declares configuration bindings for the risk_control plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "clickhouse", Target: &rcClickHouseConfig{}},
{Prefix: "database", Target: &rcDBConfig{}},
}
}
// Apply registers risk control middlewares, settings, and cleanup hooks into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var chCfg rcClickHouseConfig
_ = ctx.Config().Bind("clickhouse", &chCfg)
var dbCfg rcDBConfig
_ = ctx.Config().Bind("database", &dbCfg)
SetAccessLogEnabled(chCfg.Enabled)
logstore.SetDefaultDatabases(dbCfg.Enabled, chCfg.Enabled)
// 0. Bind DBService
if db, err := core.Inject[contracts.DBService](ctx); err == nil && db != nil {
logstore.SetDBService(db)
+3 -2
View File
@@ -7,7 +7,6 @@ package system
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/pkg/config"
"Wavelet/pkg/logger"
"Wavelet/pkg/response"
"net/http"
@@ -48,6 +47,8 @@ func (p *Plugin) Manifest() core.Manifest {
// Apply registers system routes.
func (p *Plugin) Apply(ctx *core.Context) error {
appName := ctx.Config().String("app.app_name", "Wavelet")
// 1. Health check
ctx.Router().GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
@@ -65,7 +66,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
c.JSON(http.StatusOK, response.OK(gin.H{
"configs": configs,
"app": gin.H{
"name": config.Config.App.AppName,
"name": appName,
},
}))
})
@@ -27,6 +27,7 @@ import (
"github.com/gin-gonic/gin"
"Wavelet/pkg/idgen"
uploadstats "Wavelet/plugins/domain/upload/stats"
)
@@ -36,6 +37,7 @@ type testResponse struct {
}
func setupTestRouter(authUser *contracts.UserDTO) *gin.Engine {
_ = idgen.Init(1)
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(response.ErrorHandlerMiddleware())
@@ -5,6 +5,7 @@ package ingest
import (
"Wavelet/core/contracts"
"Wavelet/pkg/idgen"
"Wavelet/plugins/domain/upload/models"
"Wavelet/plugins/domain/upload/shared"
"bytes"
@@ -17,6 +18,10 @@ import (
"testing"
)
func init() {
_ = idgen.Init(1)
}
type testStorageService struct {
mu sync.RWMutex
mockFiles map[string][]byte
@@ -6,6 +6,7 @@ package user_test
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/pkg/idgen"
"Wavelet/plugins/domain/user"
"context"
"path/filepath"
@@ -21,6 +22,7 @@ import (
func setupTestDB(t *testing.T) *gorm.DB {
t.Helper()
_ = idgen.Init(1)
dbPath := filepath.Join(t.TempDir(), "user_test.db")
testDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
@@ -36,6 +38,8 @@ func setupTestDB(t *testing.T) *gorm.DB {
func TestUserPluginUnit(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
dbPlugin := database.New(database.WithDB(testDB))
@@ -21,6 +21,8 @@ import (
// 否则调用方(如访问日志按用户补全)会按 ID 逐条打库。
func TestGetUsersByIDsUsesSingleQuery(t *testing.T) {
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(nil))
require.NoError(t, ctx.Config().Resolve())
testDB := setupTestDB(t)
require.NoError(t, database.New(database.WithDB(testDB)).Apply(ctx))
require.NoError(t, user.New().Apply(ctx))