package initialize import ( "context" "encoding/json" "fmt" "time" "kra/internal/config" ) type configurationEnvelope struct { Data json.RawMessage `json:"data"` Admin json.RawMessage `json:"admin"` Email json.RawMessage `json:"email"` } func (r *Repo) ConfigurationJSON() (json.RawMessage, error) { current := r.backend.Config() if current == nil { current = &config.Config{} } safe := config.Clone(current) maskConfigSecrets(safe) return json.Marshal(map[string]any{"config": managementConfig(safe)}) } func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error { current := r.backend.Config() if current == nil { current = &config.Config{} } next := config.Clone(current) var value configurationEnvelope if err := json.Unmarshal(raw, &value); err != nil { return err } if next.Data == nil { next.Data = &config.Data{} } if next.Admin == nil { next.Admin = &config.Admin{} } if err := mergeDataJSON(value.Data, &next.Data); err != nil { return err } if err := mergeAdminJSON(value.Admin, &next.Admin); err != nil { return err } if len(value.Email) > 0 && string(value.Email) != "null" { if err := mergeEmailJSON(value.Email, &next.Admin.Email); err != nil { return err } } preserveConfigSecrets(next, current) if next.Admin != nil && current.Admin != nil { next.Admin.ConfigPath = current.Admin.ConfigPath } if err := refreshDatabaseSources(next.Data); err != nil { return err } return r.PersistRuntimeConfig(ctx, next) } func (r *Repo) DiskMountPoints() []string { current := r.backend.Config() if current == nil || current.Admin == nil { return nil } points := make([]string, 0, len(current.Admin.DiskList)) for _, item := range current.Admin.DiskList { if item != nil && item.MountPoint != "" { points = append(points, item.MountPoint) } } return points } func mergeJSON(raw json.RawMessage, target any) error { if len(raw) == 0 || string(raw) == "null" { return nil } return json.Unmarshal(raw, target) } func mergeDataJSON(raw json.RawMessage, target **config.Data) error { return mergeJSONMap(raw, target, func(values map[string]any) error { if redis := jsonObject(values["redis"]); redis != nil { if err := normalizeDuration(redis, "read_timeout", "readTimeout"); err != nil { return err } if err := normalizeDuration(redis, "write_timeout", "writeTimeout"); err != nil { return err } } if items, ok := values["redis_list"].([]any); ok { for _, item := range items { redis := jsonObject(item) if err := normalizeDuration(redis, "read_timeout", "readTimeout"); err != nil { return err } if err := normalizeDuration(redis, "write_timeout", "writeTimeout"); err != nil { return err } } } return nil }) } func mergeAdminJSON(raw json.RawMessage, target **config.Admin) error { return mergeJSONMap(raw, target, func(values map[string]any) error { moveJSONKey(values, "routerPrefix", "router_prefix") if jwt := jsonObject(values["jwt"]); jwt != nil { moveJSONKey(jwt, "signingKey", "signing_key") moveJSONKey(jwt, "expiresTime", "expires_time") moveJSONKey(jwt, "bufferTime", "buffer_time") if err := normalizeDuration(jwt, "expires_time", "expiresTime"); err != nil { return err } if err := normalizeDuration(jwt, "buffer_time", "bufferTime"); err != nil { return err } } if captcha := jsonObject(values["captcha"]); captcha != nil { moveJSONKey(captcha, "keyLong", "key_long") moveJSONKey(captcha, "imgWidth", "img_width") moveJSONKey(captcha, "imgHeight", "img_height") moveJSONKey(captcha, "storeExpiration", "store_expiration") return normalizeDuration(captcha, "store_expiration", "storeExpiration") } if local := jsonObject(values["local"]); local != nil { moveJSONKey(local, "storePath", "store_path") moveJSONKey(local, "pathPrefix", "path_prefix") } if media := jsonObject(values["media"]); media != nil { moveJSONKey(media, "sessionTtl", "session_ttl") moveJSONKey(media, "maxFileSize", "max_file_size") moveJSONKey(media, "chunkDir", "chunk_dir") } if system := jsonObject(values["system"]); system != nil { moveJSONKey(system, "useRedis", "use_redis") moveJSONKey(system, "useMultipoint", "use_multipoint") moveJSONKey(system, "useStrictAuth", "use_strict_auth") moveJSONKey(system, "disableAutoMigrate", "disable_auto_migrate") moveJSONKey(system, "useMongo", "use_mongo") moveJSONKey(system, "iplimitCount", "iplimit_count") moveJSONKey(system, "iplimitTime", "iplimit_time") } return nil }) } func mergeEmailJSON(raw json.RawMessage, target **config.Email) error { return mergeJSONMap(raw, target, func(values map[string]any) error { moveJSONKey(values, "is-ssl", "is_ssl") moveJSONKey(values, "is-loginauth", "is_login_auth") return nil }) } func mergeJSONMap(raw json.RawMessage, target any, transform func(map[string]any) error) error { if len(raw) == 0 || string(raw) == "null" { return nil } var values map[string]any if err := json.Unmarshal(raw, &values); err != nil { return err } if transform != nil { if err := transform(values); err != nil { return err } } normalized, err := json.Marshal(values) if err != nil { return err } return mergeJSON(normalized, target) } func jsonObject(value any) map[string]any { result, _ := value.(map[string]any) return result } func normalizeDuration(values map[string]any, keys ...string) error { if values == nil { return nil } for _, key := range keys { raw, ok := values[key] if !ok { continue } text, ok := raw.(string) if !ok { return nil } value, err := time.ParseDuration(text) if err != nil { return fmt.Errorf("invalid duration %q: %w", text, err) } values[key] = int64(value) return nil } return nil } func moveJSONKey(values map[string]any, oldKey, newKey string) { if value, ok := values[oldKey]; ok { values[newKey] = value delete(values, oldKey) } } func managementConfig(value *config.Config) map[string]any { result := map[string]any{"data": map[string]any{}, "admin": map[string]any{}, "email": map[string]any{}} if value == nil { return result } if value.Data != nil { result["data"] = managementData(value.Data) } if value.Admin != nil { admin := value.Admin result["admin"] = map[string]any{ "routerPrefix": admin.RouterPrefix, "jwt": managementJWT(admin.JWT), "captcha": managementCaptcha(admin.Captcha), "local": managementLocal(admin.Local), "media": managementMedia(admin.Media), "system": managementSystem(admin.System), "storage": admin.Storage, "disk_list": admin.DiskList, "zap": admin.Zap, "cors": admin.CORS, "app": admin.App, } if admin.Email != nil { result["email"] = map[string]any{ "to": admin.Email.To, "from": admin.Email.From, "host": admin.Email.Host, "secret": admin.Email.Secret, "nickname": admin.Email.Nickname, "port": admin.Email.Port, "is-ssl": admin.Email.IsSSL, "is-loginauth": admin.Email.IsLoginAuth, } } } return result } func managementData(value *config.Data) any { if value == nil { return map[string]any{} } raw, err := json.Marshal(value) if err != nil { return value } var result map[string]any if json.Unmarshal(raw, &result) != nil { return value } if redis := jsonObject(result["redis"]); redis != nil { redis["read_timeout"] = value.Redis.ReadTimeout.String() redis["write_timeout"] = value.Redis.WriteTimeout.String() } if items, ok := result["redis_list"].([]any); ok { for index, item := range items { if index >= len(value.RedisList) || value.RedisList[index] == nil { continue } if redis := jsonObject(item); redis != nil { redis["read_timeout"] = value.RedisList[index].ReadTimeout.String() redis["write_timeout"] = value.RedisList[index].WriteTimeout.String() } } } return result } func managementJWT(value *config.JWT) any { if value == nil { return map[string]any{} } return map[string]any{"signingKey": value.SigningKey, "expiresTime": value.ExpiresTime.String(), "bufferTime": value.BufferTime.String(), "issuer": value.Issuer} } func managementCaptcha(value *config.Captcha) any { if value == nil { return map[string]any{} } return map[string]any{"keyLong": value.KeyLong, "imgWidth": value.ImgWidth, "imgHeight": value.ImgHeight, "storeExpiration": value.StoreExpiration.String()} } func managementLocal(value *config.Local) any { if value == nil { return map[string]any{} } return map[string]any{"storePath": value.StorePath, "pathPrefix": value.PathPrefix} } func managementMedia(value *config.Media) any { if value == nil { return map[string]any{} } chunkDir := value.ChunkDir if chunkDir == "" { chunkDir = "uploads/chunks" } return map[string]any{"sessionTtl": value.SessionTTL, "maxFileSize": value.MaxFileSize, "chunkDir": chunkDir} } func managementSystem(value *config.System) any { if value == nil { return map[string]any{} } return map[string]any{"useRedis": value.UseRedis, "useMultipoint": value.UseMultipoint, "useStrictAuth": value.UseStrictAuth, "disableAutoMigrate": value.DisableAutoMigrate, "useMongo": value.UseMongo, "addr": value.Addr, "iplimitCount": value.IplimitCount, "iplimitTime": value.IplimitTime} } func maskConfigSecrets(value *config.Config) { if value == nil { return } maskDataSecrets(value.Data) if value.Admin == nil { return } if value.Admin.JWT != nil && value.Admin.JWT.SigningKey != "" { value.Admin.JWT.SigningKey = config.MaskedSecret } if value.Admin.Email != nil && value.Admin.Email.Secret != "" { value.Admin.Email.Secret = config.MaskedSecret } config.MaskStorageSecrets(value.Admin.Storage) } func maskDataSecrets(value *config.Data) { if value == nil { return } if value.Database != nil { value.Database.Password = config.MaskedSecret value.Database.Source = "" } if value.Redis != nil { value.Redis.Password = config.MaskedSecret } if value.Mongo != nil { value.Mongo.Password = config.MaskedSecret } for _, item := range value.DatabaseList { if item != nil { item.Password = config.MaskedSecret item.Source = "" } } for _, item := range value.RedisList { if item != nil { item.Password = config.MaskedSecret } } } func preserveConfigSecrets(next, current *config.Config) { if next == nil || current == nil { return } preserveDataSecrets(next.Data, current.Data) if next.Admin == nil || current.Admin == nil { return } if next.Admin.JWT != nil && current.Admin.JWT != nil && config.IsMaskedSecret(next.Admin.JWT.SigningKey) { next.Admin.JWT.SigningKey = current.Admin.JWT.SigningKey } if next.Admin.Email != nil && current.Admin.Email != nil && config.IsMaskedSecret(next.Admin.Email.Secret) { next.Admin.Email.Secret = current.Admin.Email.Secret } preserveStorageSecrets(next.Admin.Storage, current.Admin.Storage) } func preserveDataSecrets(next, current *config.Data) { if next == nil || current == nil { return } if next.Database != nil && current.Database != nil { if config.IsMaskedSecret(next.Database.Password) { next.Database.Password = current.Database.Password } if next.Database.Source == "" { next.Database.Source = current.Database.Source } } if next.Redis != nil && current.Redis != nil && config.IsMaskedSecret(next.Redis.Password) { next.Redis.Password = current.Redis.Password } if next.Mongo != nil && current.Mongo != nil && config.IsMaskedSecret(next.Mongo.Password) { next.Mongo.Password = current.Mongo.Password } preserveDatabaseListSecrets(next.DatabaseList, current.DatabaseList) preserveRedisListSecrets(next.RedisList, current.RedisList) } func preserveDatabaseListSecrets(next, current []*config.Database) { byName := make(map[string]*config.Database, len(current)) for _, item := range current { if item != nil && item.AliasName != "" { byName[item.AliasName] = item } } for index, item := range next { if item == nil { continue } previous := byName[item.AliasName] if previous == nil && index < len(current) { previous = current[index] } if previous == nil { continue } if config.IsMaskedSecret(item.Password) { item.Password = previous.Password } if item.Source == "" { item.Source = previous.Source } } } func preserveRedisListSecrets(next, current []*config.Redis) { byName := make(map[string]*config.Redis, len(current)) for _, item := range current { if item != nil && item.Name != "" { byName[item.Name] = item } } for index, item := range next { if item == nil { continue } previous := byName[item.Name] if previous == nil && index < len(current) { previous = current[index] } if previous != nil && config.IsMaskedSecret(item.Password) { item.Password = previous.Password } } } func preserveStorageSecrets(next, current *config.Storage) { if next == nil || current == nil { return } if next.Qiniu != nil && current.Qiniu != nil && config.IsMaskedSecret(next.Qiniu.SecretKey) { next.Qiniu.SecretKey = current.Qiniu.SecretKey } nextItems, currentItems := config.ObjectStores(next), config.ObjectStores(current) for index := range nextItems { if nextItems[index] != nil && currentItems[index] != nil && config.IsMaskedSecret(nextItems[index].SecretKey) { nextItems[index].SecretKey = currentItems[index].SecretKey } } } func refreshDatabaseSources(value *config.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 *config.Database) error { if database == nil { return nil } hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" || database.Password != "" || database.Name != "" || database.Config != "" || database.Path != "" if hasStructuredConfig { // Driver-specific DSN construction remains in data. Clearing Source // makes the data backend rebuild it from structured values, while a // standalone DSN is preserved when no structured fields are supplied. database.Source = "" } return nil }