优化结构
This commit is contained in:
@@ -182,6 +182,8 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
|
||||
fileAdmin := cloneAdminConfig(adminConfig)
|
||||
fileAdmin.Storage = nil
|
||||
fileAdmin.Email = nil
|
||||
fileAdmin.Websocket = nil
|
||||
fileAdmin.Mq = nil
|
||||
adminValue, err := protoMap(fileAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -194,6 +196,8 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
|
||||
}
|
||||
deleteYAMLMapping(&document, "admin", "storage")
|
||||
deleteYAMLMapping(&document, "admin", "email")
|
||||
deleteYAMLMapping(&document, "admin", "websocket")
|
||||
deleteYAMLMapping(&document, "admin", "mq")
|
||||
if adminConfig.System != nil {
|
||||
if err = setServerHTTPPort(&document, adminConfig.System.Addr); err != nil {
|
||||
return err
|
||||
@@ -253,6 +257,8 @@ func (d *Data) persistDatabaseConfig(database *conf.Data_Database, signingKey st
|
||||
}
|
||||
deleteYAMLMapping(&document, "admin", "storage")
|
||||
deleteYAMLMapping(&document, "admin", "email")
|
||||
deleteYAMLMapping(&document, "admin", "websocket")
|
||||
deleteYAMLMapping(&document, "admin", "mq")
|
||||
return writeConfigDocument(configPath, &document)
|
||||
}
|
||||
|
||||
@@ -271,11 +277,13 @@ func (d *Data) removeIntegrationConfigFromFile() error {
|
||||
if err = yaml.Unmarshal(raw, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
if yamlMappingValue(&document, "admin", "storage") == nil && yamlMappingValue(&document, "admin", "email") == nil {
|
||||
if yamlMappingValue(&document, "admin", "storage") == nil && yamlMappingValue(&document, "admin", "email") == nil && yamlMappingValue(&document, "admin", "websocket") == nil && yamlMappingValue(&document, "admin", "mq") == nil {
|
||||
return nil
|
||||
}
|
||||
deleteYAMLMapping(&document, "admin", "storage")
|
||||
deleteYAMLMapping(&document, "admin", "email")
|
||||
deleteYAMLMapping(&document, "admin", "websocket")
|
||||
deleteYAMLMapping(&document, "admin", "mq")
|
||||
return writeConfigDocument(configPath, &document)
|
||||
}
|
||||
|
||||
@@ -357,6 +365,8 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
}
|
||||
legacyStorage := next.Admin.Storage
|
||||
legacyEmail := next.Admin.Email
|
||||
legacyWebSocket := next.Admin.Websocket
|
||||
legacyMQ := next.Admin.Mq
|
||||
currentAdmin := d.runtime.Admin()
|
||||
if legacyStorage == nil {
|
||||
if currentAdmin != nil {
|
||||
@@ -366,6 +376,12 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
if legacyEmail == nil && currentAdmin != nil {
|
||||
legacyEmail = currentAdmin.Email
|
||||
}
|
||||
if legacyWebSocket == nil && currentAdmin != nil {
|
||||
legacyWebSocket = currentAdmin.Websocket
|
||||
}
|
||||
if legacyMQ == nil && currentAdmin != nil {
|
||||
legacyMQ = currentAdmin.Mq
|
||||
}
|
||||
storageConfig, err := resolveStorageIntegrationConfig(candidateDB.WithContext(ctx), legacyStorage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload storage configuration: %w", err)
|
||||
@@ -376,6 +392,16 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
return fmt.Errorf("reload email configuration: %w", err)
|
||||
}
|
||||
next.Admin.Email = emailConfig
|
||||
websocketConfig, err := resolveWebSocketIntegrationConfig(candidateDB.WithContext(ctx), legacyWebSocket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload websocket configuration: %w", err)
|
||||
}
|
||||
next.Admin.Websocket = websocketConfig
|
||||
mqConfig, err := resolveMQIntegrationConfig(candidateDB.WithContext(ctx), legacyMQ)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload mq configuration: %w", err)
|
||||
}
|
||||
next.Admin.Mq = mqConfig
|
||||
candidateStorage, err := storage.New(next.Admin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload storage: %w", err)
|
||||
|
||||
@@ -58,6 +58,8 @@ func (d *Data) watchConfig() func() {
|
||||
if current := d.runtime.Admin(); current != nil {
|
||||
next.Admin.Storage = current.Storage
|
||||
next.Admin.Email = current.Email
|
||||
next.Admin.Mq = current.Mq
|
||||
next.Admin.Websocket = current.Websocket
|
||||
}
|
||||
next.Admin.ConfigPath = absolute
|
||||
d.runtime.Replace(next.Data, next.Admin)
|
||||
|
||||
@@ -207,6 +207,16 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger, storageManager *stor
|
||||
}
|
||||
admin.Storage = storageConfig
|
||||
admin.Email = emailConfig
|
||||
websocketConfig, websocketErr := resolveWebSocketIntegrationConfig(db, admin.Websocket)
|
||||
if websocketErr != nil {
|
||||
return nil, nil, fmt.Errorf("load websocket integration configuration: %w", websocketErr)
|
||||
}
|
||||
admin.Websocket = websocketConfig
|
||||
mqConfig, mqErr := resolveMQIntegrationConfig(db, admin.Mq)
|
||||
if mqErr != nil {
|
||||
return nil, nil, fmt.Errorf("load mq integration configuration: %w", mqErr)
|
||||
}
|
||||
admin.Mq = mqConfig
|
||||
runtime.Replace(c, admin)
|
||||
activeStorage, storageErr := storage.New(admin)
|
||||
if storageErr != nil {
|
||||
|
||||
@@ -69,6 +69,12 @@ func (d *Data) PersistAdminConfig(ctx context.Context, raw []byte) error {
|
||||
if next.Email == nil {
|
||||
next.Email = currentAdmin.Email
|
||||
}
|
||||
if next.Websocket == nil {
|
||||
next.Websocket = currentAdmin.Websocket
|
||||
}
|
||||
if next.Mq == nil {
|
||||
next.Mq = currentAdmin.Mq
|
||||
}
|
||||
next.ConfigPath = currentAdmin.ConfigPath
|
||||
candidateStorage, err := storage.New(next)
|
||||
if err != nil {
|
||||
@@ -80,6 +86,12 @@ func (d *Data) PersistAdminConfig(ctx context.Context, raw []byte) error {
|
||||
if err := d.persistEmailIntegrationConfig(ctx, next.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.PersistWebSocketConfig(ctx, next.Websocket); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.persistMQIntegrationConfig(ctx, next.Mq); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.persistConfigValues(currentData, next); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,6 +120,12 @@ func (d *Data) PersistRuntimeConfig(ctx context.Context, dataRaw, adminRaw []byt
|
||||
if nextAdmin.Email == nil {
|
||||
nextAdmin.Email = currentAdmin.Email
|
||||
}
|
||||
if nextAdmin.Websocket == nil {
|
||||
nextAdmin.Websocket = currentAdmin.Websocket
|
||||
}
|
||||
if nextAdmin.Mq == nil {
|
||||
nextAdmin.Mq = currentAdmin.Mq
|
||||
}
|
||||
nextAdmin.ConfigPath = currentAdmin.ConfigPath
|
||||
candidateStorage, err := storage.New(nextAdmin)
|
||||
if err != nil {
|
||||
@@ -119,6 +137,12 @@ func (d *Data) PersistRuntimeConfig(ctx context.Context, dataRaw, adminRaw []byt
|
||||
if err := d.persistEmailIntegrationConfig(ctx, nextAdmin.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.PersistWebSocketConfig(ctx, nextAdmin.Websocket); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.persistMQIntegrationConfig(ctx, nextAdmin.Mq); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.persistConfigValues(nextData, nextAdmin); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,6 +229,14 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *biz.DatabaseConfig
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize email integration configuration: %w", err)
|
||||
}
|
||||
var legacyWebSocket *conf.AdminBackend_WebSocket
|
||||
if currentAdmin != nil {
|
||||
legacyWebSocket = currentAdmin.Websocket
|
||||
}
|
||||
websocketConfig, err := resolveWebSocketIntegrationConfig(candidate.WithContext(ctx), legacyWebSocket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize websocket integration configuration: %w", err)
|
||||
}
|
||||
signingKey := uuid.NewString()
|
||||
if err := d.persistDatabaseConfig(config, signingKey); err != nil {
|
||||
return fmt.Errorf("persist database configuration: %w", err)
|
||||
@@ -219,7 +251,13 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *biz.DatabaseConfig
|
||||
}
|
||||
currentAdmin.Jwt.SigningKey = signingKey
|
||||
currentAdmin.Storage = storageConfig
|
||||
currentAdmin.Websocket = websocketConfig
|
||||
currentAdmin.Email = emailConfig
|
||||
mqConfig, err := resolveMQIntegrationConfig(candidate.WithContext(ctx), currentAdmin.Mq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize mq integration configuration: %w", err)
|
||||
}
|
||||
currentAdmin.Mq = mqConfig
|
||||
d.runtime.Replace(currentData, currentAdmin)
|
||||
activated = true
|
||||
return nil
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
integrationKindStorage = "storage"
|
||||
integrationKindEmail = "email"
|
||||
integrationKindPayment = "payment"
|
||||
integrationKindMQ = "mq"
|
||||
)
|
||||
|
||||
// integrationConfigPO stores credentials and provider-specific options for
|
||||
@@ -34,6 +35,88 @@ type integrationConfigPO struct {
|
||||
Config string `gorm:"type:text;not null"`
|
||||
}
|
||||
|
||||
func defaultMQIntegrationConfig() *conf.AdminBackend_MQ {
|
||||
return &conf.AdminBackend_MQ{CleanSession: true, KeepAlive: 30, ConnectTimeout: 10}
|
||||
}
|
||||
|
||||
func saveMQIntegrationConfig(db *gorm.DB, config *conf.AdminBackend_MQ) error {
|
||||
if config == nil {
|
||||
config = defaultMQIntegrationConfig()
|
||||
}
|
||||
raw, err := protojson.MarshalOptions{UseProtoNames: true, EmitDefaultValues: true}.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode emqx integration configuration: %w", err)
|
||||
}
|
||||
enabled := config.Enabled && strings.TrimSpace(config.Broker) != ""
|
||||
clean := db.Session(&gorm.Session{NewDB: true})
|
||||
var row integrationConfigPO
|
||||
err = clean.Where("kind = ? AND provider = ?", integrationKindMQ, "emqx").First(&row).Error
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
return clean.Create(&integrationConfigPO{Kind: integrationKindMQ, Provider: "emqx", Enabled: enabled, Config: string(raw)}).Error
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
return clean.Model(&row).Updates(map[string]any{"enabled": enabled, "config": string(raw)}).Error
|
||||
}
|
||||
}
|
||||
|
||||
func loadMQIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_MQ, bool, error) {
|
||||
var row integrationConfigPO
|
||||
err := db.Session(&gorm.Session{NewDB: true}).Where("kind = ? AND provider = ?", integrationKindMQ, "emqx").First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !json.Valid([]byte(row.Config)) {
|
||||
return nil, false, errors.New("invalid emqx integration configuration")
|
||||
}
|
||||
config := defaultMQIntegrationConfig()
|
||||
if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal([]byte(row.Config), config); err != nil {
|
||||
return nil, false, fmt.Errorf("decode emqx integration configuration: %w", err)
|
||||
}
|
||||
config.Enabled = row.Enabled
|
||||
return config, true, nil
|
||||
}
|
||||
|
||||
func resolveMQIntegrationConfig(db *gorm.DB, legacy *conf.AdminBackend_MQ) (*conf.AdminBackend_MQ, error) {
|
||||
clean := db.Session(&gorm.Session{NewDB: true})
|
||||
if !clean.Migrator().HasTable(&integrationConfigPO{}) {
|
||||
if legacy == nil {
|
||||
return defaultMQIntegrationConfig(), nil
|
||||
}
|
||||
return proto.Clone(legacy).(*conf.AdminBackend_MQ), nil
|
||||
}
|
||||
loaded, found, err := loadMQIntegrationConfig(clean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if found {
|
||||
return loaded, nil
|
||||
}
|
||||
if legacy == nil {
|
||||
legacy = defaultMQIntegrationConfig()
|
||||
}
|
||||
if err = saveMQIntegrationConfig(clean, legacy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loaded, _, err = loadMQIntegrationConfig(clean)
|
||||
return loaded, err
|
||||
}
|
||||
|
||||
func (d *Data) persistMQIntegrationConfig(ctx context.Context, config *conf.AdminBackend_MQ) error {
|
||||
if !d.databaseReady.Load() {
|
||||
return errors.New("database is not initialized")
|
||||
}
|
||||
db := d.gormDB.WithContext(ctx)
|
||||
if !db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||
return errors.New("integration configuration table does not exist")
|
||||
}
|
||||
return saveMQIntegrationConfig(db, config)
|
||||
}
|
||||
|
||||
func (integrationConfigPO) TableName() string { return "sys_integration_configs" }
|
||||
|
||||
var storageProviderNames = []string{
|
||||
|
||||
@@ -123,6 +123,25 @@ func TestStorageIntegrationConfigRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMQIntegrationConfigRoundTrip(t *testing.T) {
|
||||
db := openIntegrationConfigTestDB(t)
|
||||
legacy := &conf.AdminBackend_MQ{Enabled: true, Broker: "mqtt://emqx.example.com:1883", ClientId: "system", Username: "app", Password: "secret", KeepAlive: 45, CleanSession: true, ConnectTimeout: 12}
|
||||
if err := saveMQIntegrationConfig(db, legacy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, found, err := loadMQIntegrationConfig(db)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("found=%v err=%v", found, err)
|
||||
}
|
||||
if loaded.Broker != legacy.Broker || loaded.Password != legacy.Password || loaded.KeepAlive != 45 {
|
||||
t.Fatalf("loaded mq = %#v", loaded)
|
||||
}
|
||||
loaded, err = resolveMQIntegrationConfig(db, &conf.AdminBackend_MQ{Broker: "must-not-replace"})
|
||||
if err != nil || loaded.Broker != legacy.Broker {
|
||||
t.Fatalf("database mq was replaced: %#v err=%v", loaded, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveStorageIntegrationConfigMigratesLegacyOnlyOnce(t *testing.T) {
|
||||
db := openIntegrationConfigTestDB(t)
|
||||
legacy := &conf.AdminBackend_Storage{
|
||||
@@ -186,7 +205,7 @@ func TestResolveEmailIntegrationConfigMigratesLegacyOnlyOnce(t *testing.T) {
|
||||
|
||||
func TestPersistConfigValuesRemovesStorageFromYAML(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
input := []byte("data: {}\nadmin:\n router_prefix: /old\n storage:\n type: qiniu\n qiniu:\n secret_key: legacy-secret\n email:\n host: smtp.legacy.example.com\n secret: legacy-email-secret\n extension_key: retained\n")
|
||||
input := []byte("data: {}\nadmin:\n router_prefix: /old\n storage:\n type: qiniu\n qiniu:\n secret_key: legacy-secret\n email:\n host: smtp.legacy.example.com\n secret: legacy-email-secret\n mq:\n enabled: true\n broker: tcp://localhost:1883\n password: legacy-mq-secret\n extension_key: retained\n")
|
||||
if err := os.WriteFile(path, input, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -199,6 +218,7 @@ func TestPersistConfigValuesRemovesStorageFromYAML(t *testing.T) {
|
||||
Qiniu: &conf.AdminBackend_Qiniu{SecretKey: "database-only-secret"},
|
||||
},
|
||||
Email: &conf.AdminBackend_Email{Host: "smtp.database.example.com", Secret: "database-only-email-secret"},
|
||||
Mq: &conf.AdminBackend_MQ{Enabled: true, Broker: "tcp://emqx:1883", Password: "database-only-mq-secret"},
|
||||
}
|
||||
if err := d.persistConfigValues(&conf.Data{}, admin); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -222,6 +242,9 @@ func TestPersistConfigValuesRemovesStorageFromYAML(t *testing.T) {
|
||||
if _, exists := adminValue["email"]; exists {
|
||||
t.Fatalf("email remained in YAML: %s", raw)
|
||||
}
|
||||
if _, exists := adminValue["mq"]; exists {
|
||||
t.Fatalf("mq remained in YAML: %s", raw)
|
||||
}
|
||||
if adminValue["extension_key"] != "retained" {
|
||||
t.Fatalf("extension key was not retained: %#v", adminValue)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"kra/app/system/internal/biz"
|
||||
"kra/app/system/internal/conf"
|
||||
"kra/app/system/internal/security/adminauth"
|
||||
"kra/app/system/internal/security"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
@@ -95,7 +95,7 @@ func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePas
|
||||
if user.PasswordUpdatedAt != nil {
|
||||
passwordVersion = user.PasswordUpdatedAt.UnixNano()
|
||||
}
|
||||
token, claims, err := adminauth.Generate(settings.SigningKey, settings.Issuer, expires, settings.Buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, mustChangePassword, passwordVersion)
|
||||
token, claims, err := security.Generate(settings.SigningKey, settings.Issuer, expires, settings.Buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, mustChangePassword, passwordVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
|
||||
return nil, errors.New("nil JWT claims")
|
||||
}
|
||||
settings := i.settings.JWTSettings()
|
||||
claims := &adminauth.Claims{
|
||||
claims := &security.Claims{
|
||||
UUID: source.UUID, ID: source.ID, Username: source.Username, NickName: source.NickName,
|
||||
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
|
||||
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
|
||||
@@ -116,7 +116,7 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
|
||||
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
||||
},
|
||||
}
|
||||
token, err := adminauth.Sign(settings.SigningKey, claims)
|
||||
token, err := security.Sign(settings.SigningKey, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,16 +124,16 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
|
||||
}
|
||||
|
||||
func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
|
||||
claims, err := adminauth.Parse(token, i.settings.JWTSettings().SigningKey)
|
||||
claims, err := security.Parse(token, i.settings.JWTSettings().SigningKey)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, adminauth.ErrTokenExpired):
|
||||
case errors.Is(err, security.ErrTokenExpired):
|
||||
return nil, biz.ErrTokenExpired
|
||||
case errors.Is(err, adminauth.ErrTokenMalformed):
|
||||
case errors.Is(err, security.ErrTokenMalformed):
|
||||
return nil, biz.ErrTokenMalformed
|
||||
case errors.Is(err, adminauth.ErrTokenSignatureInvalid):
|
||||
case errors.Is(err, security.ErrTokenSignatureInvalid):
|
||||
return nil, biz.ErrTokenSignatureInvalid
|
||||
case errors.Is(err, adminauth.ErrTokenNotValidYet):
|
||||
case errors.Is(err, security.ErrTokenNotValidYet):
|
||||
return nil, biz.ErrTokenNotValidYet
|
||||
default:
|
||||
return nil, biz.ErrTokenInvalid
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"kra/app/system/internal/conf"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const integrationKindWebSocket = "websocket"
|
||||
|
||||
// saveWebSocketIntegrationConfig persists the Melody settings in the shared
|
||||
// integration table. It intentionally lives separately from storage/payment
|
||||
// persistence so adding another transport does not expand their API surface.
|
||||
func saveWebSocketIntegrationConfig(db *gorm.DB, config *conf.AdminBackend_WebSocket) error {
|
||||
if config == nil {
|
||||
config = &conf.AdminBackend_WebSocket{}
|
||||
}
|
||||
raw, err := protojson.MarshalOptions{UseProtoNames: true, EmitDefaultValues: true}.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode websocket integration configuration: %w", err)
|
||||
}
|
||||
clean := db.Session(&gorm.Session{NewDB: true})
|
||||
var current integrationConfigPO
|
||||
err = clean.Where("kind = ? AND provider = ?", integrationKindWebSocket, "melody").First(¤t).Error
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
return clean.Create(&integrationConfigPO{Kind: integrationKindWebSocket, Provider: "melody", Enabled: config.Enabled, Config: string(raw)}).Error
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
return clean.Model(¤t).Updates(map[string]any{"enabled": config.Enabled, "config": string(raw)}).Error
|
||||
}
|
||||
}
|
||||
|
||||
func loadWebSocketIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_WebSocket, bool, error) {
|
||||
var row integrationConfigPO
|
||||
err := db.Session(&gorm.Session{NewDB: true}).Where("kind = ? AND provider = ?", integrationKindWebSocket, "melody").First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !json.Valid([]byte(row.Config)) {
|
||||
return nil, false, errors.New("invalid websocket integration configuration")
|
||||
}
|
||||
config := &conf.AdminBackend_WebSocket{}
|
||||
if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal([]byte(row.Config), config); err != nil {
|
||||
return nil, false, fmt.Errorf("decode websocket integration configuration: %w", err)
|
||||
}
|
||||
config.Enabled = row.Enabled
|
||||
return config, true, nil
|
||||
}
|
||||
|
||||
func resolveWebSocketIntegrationConfig(db *gorm.DB, legacy *conf.AdminBackend_WebSocket) (*conf.AdminBackend_WebSocket, error) {
|
||||
clean := db.Session(&gorm.Session{NewDB: true})
|
||||
if !clean.Migrator().HasTable(&integrationConfigPO{}) {
|
||||
if legacy == nil {
|
||||
return &conf.AdminBackend_WebSocket{}, nil
|
||||
}
|
||||
return proto.Clone(legacy).(*conf.AdminBackend_WebSocket), nil
|
||||
}
|
||||
config, found, err := loadWebSocketIntegrationConfig(clean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if found {
|
||||
return config, nil
|
||||
}
|
||||
if legacy == nil {
|
||||
legacy = &conf.AdminBackend_WebSocket{}
|
||||
}
|
||||
if err = saveWebSocketIntegrationConfig(clean, legacy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, _, err = loadWebSocketIntegrationConfig(clean)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (d *Data) PersistWebSocketConfig(ctx context.Context, config *conf.AdminBackend_WebSocket) error {
|
||||
if !d.databaseReady.Load() {
|
||||
return errors.New("database is not initialized")
|
||||
}
|
||||
db := d.gormDB.WithContext(ctx)
|
||||
if !db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||
return errors.New("integration configuration table does not exist")
|
||||
}
|
||||
return saveWebSocketIntegrationConfig(db, config)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kra/app/system/internal/conf"
|
||||
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
func TestWebSocketIntegrationConfigRoundTrip(t *testing.T) {
|
||||
db := openIntegrationConfigTestDB(t)
|
||||
want := &conf.AdminBackend_WebSocket{
|
||||
Enabled: true, Path: "/events", AllowOrigins: []string{"https://admin.example.com"},
|
||||
MaxMessageSize: 4096, WriteWait: durationpb.New(3 * time.Second),
|
||||
PongWait: durationpb.New(20 * time.Second), PingPeriod: durationpb.New(15 * time.Second),
|
||||
MessageBufferSize: 32, ConcurrentMessageHandling: true,
|
||||
}
|
||||
if err := saveWebSocketIntegrationConfig(db, want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, found, err := loadWebSocketIntegrationConfig(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("websocket integration configuration was not found")
|
||||
}
|
||||
if !got.Enabled || got.Path != want.Path || got.MaxMessageSize != want.MaxMessageSize || got.MessageBufferSize != want.MessageBufferSize {
|
||||
t.Fatalf("loaded websocket config = %#v", got)
|
||||
}
|
||||
if len(got.AllowOrigins) != 1 || got.AllowOrigins[0] != want.AllowOrigins[0] {
|
||||
t.Fatalf("allow origins = %v", got.AllowOrigins)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveWebSocketIntegrationConfigPrefersDatabase(t *testing.T) {
|
||||
db := openIntegrationConfigTestDB(t)
|
||||
if err := saveWebSocketIntegrationConfig(db, &conf.AdminBackend_WebSocket{Enabled: true, Path: "/database"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := resolveWebSocketIntegrationConfig(db, &conf.AdminBackend_WebSocket{Path: "/legacy"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Path != "/database" || !got.Enabled {
|
||||
t.Fatalf("resolved websocket config = %#v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user