package config import ( "encoding/json" "errors" "fmt" "os" "strconv" "strings" "time" mysqlDriver "github.com/go-sql-driver/mysql" ) type Config struct { Listen string `json:"listen"` FrontendDir string `json:"frontend_dir"` MySQL MySQLConfig `json:"mysql"` Redis RedisConfig `json:"redis"` Auth AuthConfig `json:"auth"` Upload UploadConfig `json:"upload"` ReadTimeout time.Duration `json:"-"` WriteTimeout time.Duration `json:"-"` } type UploadConfig struct { Dir string `json:"dir"` PublicPath string `json:"public_path"` } type MySQLConfig struct { DSN string `json:"dsn"` MaxOpenConns int `json:"max_open_conns"` MaxIdleConns int `json:"max_idle_conns"` } type RedisConfig struct { Addr string `json:"addr"` Username string `json:"username"` Password string `json:"password"` DB int `json:"db"` } type AuthConfig struct { APIKeys []string `json:"api_keys"` SessionKeyPattern string `json:"session_key_pattern"` SessionTTLSeconds int `json:"session_ttl_seconds"` DefaultTenantID int64 `json:"default_tenant_id"` DataEncryptionKey string `json:"data_encryption_key"` LookupHMACKey string `json:"lookup_hmac_key"` FinanceMasterKey string `json:"finance_master_key"` AllowDevHeaderAuth bool `json:"allow_dev_header_auth"` RequiredAPIKey bool `json:"required_api_key"` } func Load() (*Config, error) { cfg := defaultConfig() if path := strings.TrimSpace(os.Getenv("OLONS_SERVER_CONFIG")); path != "" { data, err := os.ReadFile(path) if err != nil { return nil, err } if err := json.Unmarshal(data, cfg); err != nil { return nil, err } } applyEnv(cfg) if err := cfg.validate(); err != nil { return nil, err } return cfg, nil } func defaultConfig() *Config { return &Config{ Listen: ":6061", FrontendDir: "web/dist", MySQL: MySQLConfig{ DSN: "root:root@tcp(127.0.0.1:3306)/olons_admin?parseTime=true&charset=utf8mb4&loc=Local", MaxOpenConns: 15, MaxIdleConns: 5, }, Redis: RedisConfig{Addr: "127.0.0.1:6379"}, Auth: AuthConfig{ SessionKeyPattern: "olons_server:session:token:%s", SessionTTLSeconds: 14 * 24 * 60 * 60, DefaultTenantID: 0, RequiredAPIKey: true, }, Upload: UploadConfig{Dir: "storage/uploads", PublicPath: "/uploads"}, ReadTimeout: 8 * time.Second, WriteTimeout: 8 * time.Second, } } func applyEnv(cfg *Config) { setString(&cfg.Listen, "OLONS_SERVER_LISTEN") setString(&cfg.FrontendDir, "OLONS_SERVER_FRONTEND_DIR") setString(&cfg.MySQL.DSN, "OLONS_SERVER_MYSQL_DSN") setInt(&cfg.MySQL.MaxOpenConns, "OLONS_SERVER_MYSQL_MAX_OPEN_CONNS") setInt(&cfg.MySQL.MaxIdleConns, "OLONS_SERVER_MYSQL_MAX_IDLE_CONNS") setString(&cfg.Redis.Addr, "OLONS_SERVER_REDIS_ADDR") setString(&cfg.Redis.Username, "OLONS_SERVER_REDIS_USERNAME") setString(&cfg.Redis.Password, "OLONS_SERVER_REDIS_PASSWORD") setInt(&cfg.Redis.DB, "OLONS_SERVER_REDIS_DB") setString(&cfg.Auth.SessionKeyPattern, "OLONS_SERVER_SESSION_KEY_PATTERN") setInt(&cfg.Auth.SessionTTLSeconds, "OLONS_SERVER_SESSION_TTL_SECONDS") setInt64(&cfg.Auth.DefaultTenantID, "OLONS_SERVER_DEFAULT_TENANT_ID") setString(&cfg.Auth.DataEncryptionKey, "OLONS_SERVER_DATA_ENCRYPTION_KEY") setString(&cfg.Auth.LookupHMACKey, "OLONS_SERVER_LOOKUP_HMAC_KEY") setString(&cfg.Auth.FinanceMasterKey, "OLONS_SERVER_FINANCE_MASTER_KEY") setBool(&cfg.Auth.AllowDevHeaderAuth, "OLONS_SERVER_ALLOW_DEV_HEADER_AUTH") setBool(&cfg.Auth.RequiredAPIKey, "OLONS_SERVER_REQUIRED_API_KEY") setString(&cfg.Upload.Dir, "OLONS_SERVER_UPLOAD_DIR") setString(&cfg.Upload.PublicPath, "OLONS_SERVER_UPLOAD_PUBLIC_PATH") if keys := strings.TrimSpace(os.Getenv("OLONS_SERVER_API_KEYS")); keys != "" { cfg.Auth.APIKeys = splitCSV(keys) } } func (c *Config) validate() error { if strings.TrimSpace(c.Listen) == "" { return errors.New("listen is required") } if strings.TrimSpace(c.MySQL.DSN) == "" { return errors.New("mysql dsn is required") } parsed, err := mysqlDriver.ParseDSN(strings.TrimPrefix(strings.TrimSpace(c.MySQL.DSN), "mysql://")) if err != nil { return fmt.Errorf("mysql dsn is invalid: %w", err) } if !strings.EqualFold(strings.TrimSpace(parsed.DBName), "olons_admin") { return errors.New("mysql database must be olons_admin") } if strings.TrimSpace(c.Redis.Addr) == "" { return errors.New("redis addr is required") } if !strings.Contains(c.Auth.SessionKeyPattern, "%s") { return errors.New("session key pattern must include %s") } if c.Auth.SessionTTLSeconds <= 0 { c.Auth.SessionTTLSeconds = 14 * 24 * 60 * 60 } if c.Auth.DefaultTenantID < 0 { return errors.New("default tenant id must not be negative") } if strings.TrimSpace(c.Upload.Dir) == "" || !strings.HasPrefix(c.Upload.PublicPath, "/") { return errors.New("upload config is invalid") } return nil } func setString(target *string, key string) { if value := strings.TrimSpace(os.Getenv(key)); value != "" { *target = value } } func setInt(target *int, key string) { if value := strings.TrimSpace(os.Getenv(key)); value != "" { if parsed, err := strconv.Atoi(value); err == nil { *target = parsed } } } func setInt64(target *int64, key string) { if value := strings.TrimSpace(os.Getenv(key)); value != "" { if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { *target = parsed } } } func setBool(target *bool, key string) { switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) { case "1", "true", "yes", "on": *target = true case "0", "false", "no", "off": *target = false } } func splitCSV(value string) []string { parts := strings.Split(value, ",") items := make([]string, 0, len(parts)) for _, part := range parts { if item := strings.TrimSpace(part); item != "" { items = append(items, item) } } return items }