Files
olons_server/internal/config/config.go
T

293 lines
9.8 KiB
Go

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"`
FinanceLegacyMasterKeys []string `json:"finance_legacy_master_keys"`
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
}
}
configuredFinanceMasterKey := strings.TrimSpace(cfg.Auth.FinanceMasterKey)
applyEnv(cfg)
if configuredFinanceMasterKey != "" && configuredFinanceMasterKey != strings.TrimSpace(cfg.Auth.FinanceMasterKey) {
cfg.Auth.FinanceLegacyMasterKeys = append(cfg.Auth.FinanceLegacyMasterKeys, configuredFinanceMasterKey)
}
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: "default", 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")
if keys := strings.TrimSpace(os.Getenv("OLONS_SERVER_FINANCE_LEGACY_MASTER_KEYS")); keys != "" {
cfg.Auth.FinanceLegacyMasterKeys = append(cfg.Auth.FinanceLegacyMasterKeys, splitCSV(keys)...)
}
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
}