Files

281 lines
6.7 KiB
Go

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 LicenseConfig struct {
Key string `json:"key"` // stored ciphertext U2FsdGVk...
}
type AppConfig struct {
Server ServerConfig `json:"server"`
Database DBConfig `json:"database"`
Updater UpdaterConfig `json:"updater"`
License LicenseConfig `json:"license"`
// 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: "ongtrieuhau",
Repo: "web-giao-ban",
Token: "",
CurrentVersion: "v1.0.2",
},
License: LicenseConfig{
Key: "",
},
}
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
// Auto-migrate legacy owner & repo
if cfg.Updater.Owner == "" || cfg.Updater.Owner == "hdhiswork" {
cfg.Updater.Owner = "ongtrieuhau"
}
if cfg.Updater.Repo == "" || cfg.Updater.Repo == "DUAN" {
cfg.Updater.Repo = "web-giao-ban"
}
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
}