273 lines
6.5 KiB
Go
273 lines
6.5 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 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: "ongtrieuhau",
|
|
Repo: "web-giao-ban",
|
|
Token: "",
|
|
CurrentVersion: "v1.0.2",
|
|
},
|
|
}
|
|
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
|
|
}
|