kra-new/internal/initialize/configuration.go

492 lines
14 KiB
Go

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
}