package data import ( "context" "encoding/json" "kra/internal/conf" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" ) type configurationEnvelope struct { Data json.RawMessage `json:"data"` Admin json.RawMessage `json:"admin"` Email *struct { To string `json:"to"` From string `json:"from"` Host string `json:"host"` Secret string `json:"secret"` Nickname string `json:"nickname"` Port int32 `json:"port"` IsSSL bool `json:"is-ssl"` IsLoginAuth bool `json:"is-loginauth"` } `json:"email"` } func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) { dataConfig, adminConfig := r.data.runtime.Values() admin := map[string]any{"routerPrefix": ""} email := map[string]any{} if adminConfig != nil { admin["routerPrefix"] = adminConfig.RouterPrefix if adminConfig.System != nil { admin["system"] = map[string]any{"useRedis": adminConfig.System.UseRedis, "useMultipoint": adminConfig.System.UseMultipoint, "useStrictAuth": adminConfig.System.UseStrictAuth, "disableAutoMigrate": adminConfig.System.DisableAutoMigrate, "useMongo": adminConfig.System.UseMongo} } if adminConfig.Jwt != nil { admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer} } if adminConfig.Captcha != nil { admin["captcha"] = map[string]any{"keyLong": adminConfig.Captcha.KeyLong, "imgWidth": adminConfig.Captcha.ImgWidth, "imgHeight": adminConfig.Captcha.ImgHeight, "storeExpiration": durationString(adminConfig.Captcha.StoreExpiration)} } if adminConfig.Local != nil { admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix} } if adminConfig.Media != nil { admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize} } if adminConfig.Email != nil { email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": "******", "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth} } if adminConfig.Storage != nil { storage := proto.Clone(adminConfig.Storage).(*conf.AdminBackend_Storage) maskStorageSecrets(storage) admin["storage"] = storage } if adminConfig.Zap != nil { admin["zap"] = adminConfig.Zap } if adminConfig.Cors != nil { admin["cors"] = adminConfig.Cors } if adminConfig.App != nil { admin["app"] = adminConfig.App } } maskDataSecrets(dataConfig) dataMap := map[string]any{} if dataConfig != nil { raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig) if err != nil { return nil, err } if err = json.Unmarshal(raw, &dataMap); err != nil { return nil, err } } return json.Marshal(map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": dataMap}}) } func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error { currentData, currentAdmin := r.data.runtime.Values() if currentAdmin == nil { return nil } nextData := cloneDataConfig(currentData) nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend) var value configurationEnvelope if err := json.Unmarshal(raw, &value); err != nil { return err } options := protojson.UnmarshalOptions{DiscardUnknown: true} if len(value.Data) > 0 && string(value.Data) != "null" { patch := &conf.Data{} if err := options.Unmarshal(value.Data, patch); err != nil { return err } applyDataPatch(nextData, patch) } if len(value.Admin) > 0 && string(value.Admin) != "null" { patch := &conf.AdminBackend{} if err := options.Unmarshal(value.Admin, patch); err != nil { return err } applyAdminPatch(nextAdmin, patch) } preserveDataSecrets(nextData, currentData) preserveAdminSecrets(nextAdmin, currentAdmin) if value.Email != nil { if nextAdmin.Email == nil { nextAdmin.Email = &conf.AdminBackend_Email{} } nextAdmin.Email.To, nextAdmin.Email.From, nextAdmin.Email.Host = value.Email.To, value.Email.From, value.Email.Host nextAdmin.Email.Nickname, nextAdmin.Email.Port = value.Email.Nickname, value.Email.Port nextAdmin.Email.IsSsl, nextAdmin.Email.IsLoginAuth = value.Email.IsSSL, value.Email.IsLoginAuth if value.Email.Secret != "" && value.Email.Secret != "******" { nextAdmin.Email.Secret = value.Email.Secret } } if err := refreshDatabaseSources(nextData); err != nil { return err } nextAdmin.ConfigPath = currentAdmin.ConfigPath dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData) if err != nil { return err } adminRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextAdmin) if err != nil { return err } return r.PersistRuntimeConfig(ctx, dataRaw, adminRaw) } func (r *initializationRepo) DiskMountPoints() []string { config := r.data.runtime.Admin() if config == nil { return nil } points := make([]string, 0, len(config.DiskList)) for _, item := range config.DiskList { if item != nil && item.MountPoint != "" { points = append(points, item.MountPoint) } } return points } func cloneDataConfig(value *conf.Data) *conf.Data { if value == nil { return &conf.Data{} } return proto.Clone(value).(*conf.Data) } func refreshDatabaseSources(value *conf.Data) error { if value == nil { return nil } if err := refreshDatabaseSource(value.Database); err != nil { return err } for _, database := range value.DatabaseList { if database == nil || database.Disable { continue } if err := refreshDatabaseSource(database); err != nil { return err } } return nil } func refreshDatabaseSource(database *conf.Data_Database) error { if database == nil { return nil } hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" || database.Password != "" || database.Name != "" || database.Config != "" || database.Path != "" if !hasStructuredConfig { // A source-only configuration is an intentional escape hatch for custom // driver DSNs; do not reinterpret it as the structured form. return nil } previousSource := database.Source database.Source = "" source, err := databaseDSN(database, "") if err != nil { database.Source = previousSource return err } database.Source = source return nil } func durationString(value *durationpb.Duration) string { if value == nil { return "0s" } return value.AsDuration().String() } func applyDataPatch(target, patch *conf.Data) { if patch.Database != nil { target.Database = patch.Database } if patch.Redis != nil { target.Redis = patch.Redis } if patch.Mongo != nil { target.Mongo = patch.Mongo } if patch.DatabaseList != nil { target.DatabaseList = patch.DatabaseList } if patch.RedisList != nil { target.RedisList = patch.RedisList } } func applyAdminPatch(target, patch *conf.AdminBackend) { target.RouterPrefix = patch.RouterPrefix if patch.System != nil { target.System = patch.System } if patch.Jwt != nil { target.Jwt = patch.Jwt } if patch.Captcha != nil { target.Captcha = patch.Captcha } if patch.Local != nil { target.Local = patch.Local } if patch.Media != nil { target.Media = patch.Media } if patch.Storage != nil { target.Storage = patch.Storage } if patch.Zap != nil { target.Zap = patch.Zap } if patch.Cors != nil { target.Cors = patch.Cors } if patch.App != nil { target.App = patch.App } } func maskDataSecrets(value *conf.Data) { if value == nil { return } if value.Database != nil { value.Database.Password = "******" } if value.Redis != nil { value.Redis.Password = "******" } if value.Mongo != nil { value.Mongo.Password = "******" } for _, item := range value.DatabaseList { if item != nil { item.Password = "******" } } for _, item := range value.RedisList { if item != nil { item.Password = "******" } } } func preserveDataSecrets(next, current *conf.Data) { if next == nil || current == nil { return } if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) { next.Database.Password = current.Database.Password } if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) { next.Redis.Password = current.Redis.Password } if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) { next.Mongo.Password = current.Mongo.Password } for i, item := range next.DatabaseList { if item != nil && i < len(current.DatabaseList) && current.DatabaseList[i] != nil && maskedSecret(item.Password) { item.Password = current.DatabaseList[i].Password } } for i, item := range next.RedisList { if item != nil && i < len(current.RedisList) && current.RedisList[i] != nil && maskedSecret(item.Password) { item.Password = current.RedisList[i].Password } } } func preserveAdminSecrets(next, current *conf.AdminBackend) { if next.Jwt != nil && current.Jwt != nil && maskedSecret(next.Jwt.SigningKey) { next.Jwt.SigningKey = current.Jwt.SigningKey } if next.Email != nil && current.Email != nil && maskedSecret(next.Email.Secret) { next.Email.Secret = current.Email.Secret } preserveStorageSecrets(next.Storage, current.Storage) } func maskedSecret(value string) bool { return value == "" || value == "******" } func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore { if storage == nil { return nil } return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio} } func maskStorageSecrets(storage *conf.AdminBackend_Storage) { if storage.Qiniu != nil { storage.Qiniu.SecretKey = "******" } for _, item := range objectStores(storage) { if item != nil { item.SecretKey = "******" } } } func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) { if next == nil || current == nil { return } if next.Qiniu != nil && current.Qiniu != nil && maskedSecret(next.Qiniu.SecretKey) { next.Qiniu.SecretKey = current.Qiniu.SecretKey } nextItems, currentItems := objectStores(next), objectStores(current) for i := range nextItems { if nextItems[i] != nil && currentItems[i] != nil && maskedSecret(nextItems[i].SecretKey) { nextItems[i].SecretKey = currentItems[i].SecretKey } } }