package config import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "io" "os" "path/filepath" "strings" "sync" ) var secretKey = sha256.Sum256([]byte("dh-his-giaoban-secret-key-salt-2026-v1")) type DatabaseInfo struct { ID string `json:"id"` Name string `json:"name"` } type ServerConfig struct { Port int `json:"port"` AdminPath string `json:"admin_path"` AdminPassword string `json:"admin_password"` // stored as ENC(...) } type DBConfig struct { Host string `json:"host"` Port int `json:"port"` User string `json:"user"` Password string `json:"password"` // stored as ENC(...) DefaultDB string `json:"default_db"` Databases []DatabaseInfo `json:"databases"` } type UpdaterConfig struct { Enabled bool `json:"enabled"` GiteaURL string `json:"gitea_url"` Owner string `json:"owner"` Repo string `json:"repo"` Token string `json:"token"` // stored as ENC(...) CurrentVersion string `json:"current_version"` } type AppConfig struct { Server ServerConfig `json:"server"` Database DBConfig `json:"database"` Updater UpdaterConfig `json:"updater"` // runtime plaintext fields (not exported to JSON if needed) DecryptedDBPassword string `json:"-"` DecryptedUpdaterToken string `json:"-"` DecryptedAdminPassword string `json:"-"` } var ( currentConfig *AppConfig configMutex sync.RWMutex configPath = "config.json" ) func Encrypt(plaintext string) (string, error) { if plaintext == "" { return "", nil } block, err := aes.NewCipher(secretKey[:]) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return "ENC(" + base64.StdEncoding.EncodeToString(ciphertext) + ")", nil } func Decrypt(encText string) (string, error) { if encText == "" { return "", nil } if !strings.HasPrefix(encText, "ENC(") || !strings.HasSuffix(encText, ")") { // Already plaintext return encText, nil } rawB64 := encText[4 : len(encText)-1] data, err := base64.StdEncoding.DecodeString(rawB64) if err != nil { return "", err } block, err := aes.NewCipher(secretKey[:]) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := gcm.NonceSize() if len(data) < nonceSize { return "", errors.New("ciphertext quá ngắn") } nonce, ciphertext := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", err } return string(plaintext), nil } func DefaultConfig() *AppConfig { cfg := &AppConfig{ Server: ServerConfig{ Port: 3000, AdminPath: "/admin", AdminPassword: "admin", }, Database: DBConfig{ Host: "192.168.50.79", Port: 5437, User: "postgres", Password: "123", DefaultDB: "an_omon_full_04052026", Databases: []DatabaseInfo{ {ID: "an_omon_full_04052026", Name: "BVĐK Ô Môn - Cần Thơ (33 GB - Chuẩn khoa phòng)"}, {ID: "tuan_dbgovap_full", Name: "BVĐK Gò Vấp - TP.HCM (65 GB - Dữ liệu lớn)"}, {ID: "binh_bvq12_062026052026042026032026022026", Name: "BV Quận 12 - TP.HCM (21 GB)"}, {ID: "bvq12_697", Name: "BV Quận 12 - Bản 697 (17 GB)"}, {ID: "an_dalieu_full_05082026", Name: "BV Da Liễu (31 GB)"}, }, }, Updater: UpdaterConfig{ Enabled: true, GiteaURL: "https://i.dh-his.com", Owner: "hdhiswork", Repo: "DUAN", Token: "", CurrentVersion: "v1.0.0", }, } return cfg } func LoadConfig(path string) (*AppConfig, error) { configMutex.Lock() defer configMutex.Unlock() if path != "" { configPath = path } if _, err := os.Stat(configPath); os.IsNotExist(err) { cfg := DefaultConfig() cfg.DecryptedAdminPassword = cfg.Server.AdminPassword cfg.DecryptedDBPassword = cfg.Database.Password cfg.DecryptedUpdaterToken = cfg.Updater.Token // Encrypt for disk save encAdmin, _ := Encrypt(cfg.Server.AdminPassword) encDB, _ := Encrypt(cfg.Database.Password) encTok, _ := Encrypt(cfg.Updater.Token) cfg.Server.AdminPassword = encAdmin cfg.Database.Password = encDB cfg.Updater.Token = encTok data, _ := json.MarshalIndent(cfg, "", " ") _ = os.MkdirAll(filepath.Dir(configPath), 0755) _ = os.WriteFile(configPath, data, 0644) currentConfig = cfg return cfg, nil } data, err := os.ReadFile(configPath) if err != nil { return nil, err } cfg := &AppConfig{} if err := json.Unmarshal(data, cfg); err != nil { return nil, err } // Decrypt sensitive fields for runtime use decAdmin, _ := Decrypt(cfg.Server.AdminPassword) decDB, _ := Decrypt(cfg.Database.Password) decTok, _ := Decrypt(cfg.Updater.Token) cfg.DecryptedAdminPassword = decAdmin cfg.DecryptedDBPassword = decDB cfg.DecryptedUpdaterToken = decTok currentConfig = cfg return cfg, nil } func SaveConfig(cfg *AppConfig) error { configMutex.Lock() defer configMutex.Unlock() clone := *cfg // Ensure passwords/tokens are stored encrypted if !strings.HasPrefix(clone.Server.AdminPassword, "ENC(") { clone.DecryptedAdminPassword = clone.Server.AdminPassword enc, _ := Encrypt(clone.Server.AdminPassword) clone.Server.AdminPassword = enc } if !strings.HasPrefix(clone.Database.Password, "ENC(") { clone.DecryptedDBPassword = clone.Database.Password enc, _ := Encrypt(clone.Database.Password) clone.Database.Password = enc } if !strings.HasPrefix(clone.Updater.Token, "ENC(") { clone.DecryptedUpdaterToken = clone.Updater.Token enc, _ := Encrypt(clone.Updater.Token) clone.Updater.Token = enc } data, err := json.MarshalIndent(&clone, "", " ") if err != nil { return err } tmpPath := configPath + ".tmp" if err := os.WriteFile(tmpPath, data, 0644); err != nil { return err } if err := os.Rename(tmpPath, configPath); err != nil { return err } currentConfig = &clone return nil } func Get() *AppConfig { configMutex.RLock() defer configMutex.RUnlock() if currentConfig == nil { configMutex.RUnlock() cfg, _ := LoadConfig("config.json") return cfg } return currentConfig }