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"` IM IMConfig `json:"im"` ReadTimeout time.Duration `json:"-"` WriteTimeout time.Duration `json:"-"` } type IMConfig struct { BaseURL string `json:"base_url"` HTTPAPIURL string `json:"http_api_url"` WebSocketURL string `json:"websocket_url"` FileURL string `json:"file_url"` TenantCode string `json:"tenant_code"` APIKey string `json:"api_key"` InternalService string `json:"internal_service"` InternalSecret string `json:"internal_secret"` Timeout time.Duration `json:"-"` } type UploadConfig struct { Driver string `json:"driver"` Dir string `json:"dir"` PublicPath string `json:"public_path"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` Endpoint string `json:"endpoint"` Secure bool `json:"secure"` Path string `json:"path"` Bucket string `json:"bucket"` Domain string `json:"domain"` Region string `json:"region"` } 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"` 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, RequiredAPIKey: true, }, Upload: UploadConfig{Driver: "local", Dir: "storage/uploads", PublicPath: "/uploads", Region: "us-east-1"}, IM: IMConfig{ BaseURL: "https://api.118im.com", HTTPAPIURL: "https://api.118im.com/v1/im", WebSocketURL: "wss://api.118im.com/v0/channels", FileURL: "https://api.118im.com/v0/file", TenantCode: "allrich", APIKey: "AQAAAAABAAChdu8_yk0OxZcId6FnwYNS", InternalService: "olons-server", Timeout: 8 * time.Second, }, 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") 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") ApplyUploadEnv(&cfg.Upload) setString(&cfg.IM.BaseURL, "OLONS_IM_BASE_URL") setString(&cfg.IM.HTTPAPIURL, "OLONS_IM_HTTP_API_URL") setString(&cfg.IM.WebSocketURL, "OLONS_IM_WS_URL") setString(&cfg.IM.FileURL, "OLONS_IM_FILE_URL") setString(&cfg.IM.TenantCode, "OLONS_IM_TENANT_CODE") setString(&cfg.IM.APIKey, "OLONS_IM_API_KEY") setString(&cfg.IM.InternalService, "OLONS_IM_INTERNAL_SERVICE") setString(&cfg.IM.InternalSecret, "OLONS_IM_INTERNAL_SECRET") setDuration(&cfg.IM.Timeout, "OLONS_IM_TIMEOUT") 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 err := ValidateUpload(c.Upload); err != nil { return err } if c.IM.Timeout <= 0 { c.IM.Timeout = 8 * time.Second } return nil } func ApplyUploadSystemConfig(cfg *UploadConfig, values map[string]string) { if cfg == nil { return } setMapString(&cfg.Driver, values, "uploadDrive") setMapString(&cfg.AccessKey, values, "uploadMinioAccessKey") setMapString(&cfg.SecretKey, values, "uploadMinioSecretKey") setMapString(&cfg.Endpoint, values, "uploadMinioEndpoint") setMapString(&cfg.Path, values, "uploadMinioPath") setMapString(&cfg.Bucket, values, "uploadMinioBucket") setMapString(&cfg.Domain, values, "uploadMinioDomain") if value := strings.TrimSpace(values["uploadMinioUseSSL"]); value != "" { cfg.Secure = value == "1" || strings.EqualFold(value, "true") } } func ApplyUploadEnv(cfg *UploadConfig) { if cfg == nil { return } setString(&cfg.Driver, "OLONS_SERVER_UPLOAD_DRIVER") setString(&cfg.Dir, "OLONS_SERVER_UPLOAD_DIR") setString(&cfg.PublicPath, "OLONS_SERVER_UPLOAD_PUBLIC_PATH") setString(&cfg.AccessKey, "OLONS_SERVER_MINIO_ACCESS_KEY") setString(&cfg.SecretKey, "OLONS_SERVER_MINIO_SECRET_KEY") setString(&cfg.Endpoint, "OLONS_SERVER_MINIO_ENDPOINT") setString(&cfg.Path, "OLONS_SERVER_MINIO_PATH") setString(&cfg.Bucket, "OLONS_SERVER_MINIO_BUCKET") setString(&cfg.Domain, "OLONS_SERVER_MINIO_DOMAIN") setString(&cfg.Region, "OLONS_SERVER_MINIO_REGION") if value := strings.TrimSpace(os.Getenv("OLONS_SERVER_MINIO_SECURE")); value != "" { cfg.Secure = value == "1" || strings.EqualFold(value, "true") } } func ValidateUpload(cfg UploadConfig) error { switch strings.ToLower(strings.TrimSpace(cfg.Driver)) { case "local": if strings.TrimSpace(cfg.Dir) == "" || !strings.HasPrefix(cfg.PublicPath, "/") { return errors.New("local upload config is invalid") } case "minio": if strings.TrimSpace(cfg.AccessKey) == "" || strings.TrimSpace(cfg.SecretKey) == "" || strings.TrimSpace(cfg.Endpoint) == "" || strings.TrimSpace(cfg.Bucket) == "" || strings.TrimSpace(cfg.Domain) == "" { return errors.New("minio upload config is incomplete") } default: return errors.New("upload driver is unsupported") } return nil } func setMapString(target *string, values map[string]string, key string) { if value := strings.TrimSpace(values[key]); value != "" { *target = value } } 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 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 setDuration(target *time.Duration, key string) { if value := strings.TrimSpace(os.Getenv(key)); value != "" { if parsed, err := time.ParseDuration(value); err == nil { *target = parsed } } } 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 }