package config import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "log" "os" "strconv" "strings" "time" "github.com/go-playground/validator/v10" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) type Config struct { Server ServerConfig GRPC GRPCConfig Databases map[string]DatabaseConfig ReadReplicas map[string][]DatabaseConfig Auth AuthConfig Keycloak KeycloakConfig Bpjs BpjsConfig SatuSehat SatuSehatConfig Swagger SwaggerConfig Security SecurityConfig Validator *validator.Validate } type GRPCConfig struct { Port int Enabled bool } type SwaggerConfig struct { Title string Description string Version string TermsOfService string ContactName string ContactURL string ContactEmail string LicenseName string LicenseURL string Host string BasePath string Schemes []string } type ServerConfig struct { Port int Mode string } type DatabaseConfig struct { Name string Type string // postgres, mysql, sqlserver, sqlite, mongodb Host string Port int Username string Password string Database string Schema string SSLMode string Path string // For SQLite Options string // Additional connection options MaxOpenConns int // Max open connections MaxIdleConns int // Max idle connections ConnMaxLifetime time.Duration // Connection max lifetime // Security settings RequireSSL bool // Require SSL connection SSLRootCert string // Path to SSL root certificate SSLCert string // Path to SSL client certificate SSLKey string // Path to SSL client key Timeout time.Duration // Connection timeout ConnectTimeout time.Duration // Connect timeout ReadTimeout time.Duration // Read timeout WriteTimeout time.Duration // Write timeout StatementTimeout time.Duration // Statement timeout for PostgreSQL // Connection pool settings MaxLifetime time.Duration // Maximum amount of time a connection may be reused MaxIdleTime time.Duration // Maximum amount of time a connection may be idle HealthCheckPeriod time.Duration // Health check period } type AuthConfig struct { Type string `yaml:"type" env:"AUTH_TYPE"` // "keycloak", "jwt", "static", "hybrid" StaticTokens []string `yaml:"static_tokens" env:"AUTH_STATIC_TOKENS"` // Support multiple static tokens FallbackTo string `yaml:"fallback_to" env:"AUTH_FALLBACK_TO"` // fallback auth type if primary fails } // AuthYAMLConfig represents the auth section in config.yaml type AuthYAMLConfig struct { Type string `yaml:"type"` StaticTokens []string `yaml:"static_tokens"` FallbackTo string `yaml:"fallback_to"` } type KeycloakYAMLConfig struct { Issuer string `yaml:"issuer"` Audience string `yaml:"audience"` JwksURL string `yaml:"jwks_url"` Enabled bool `yaml:"enabled"` } type KeycloakConfig struct { Issuer string Audience string JwksURL string Enabled bool } type BpjsConfig struct { BaseURL string `json:"base_url"` ConsID string `json:"cons_id"` UserKey string `json:"user_key"` SecretKey string `json:"secret_key"` Timeout time.Duration `json:"timeout"` } type SatuSehatConfig struct { OrgID string `json:"org_id"` FasyakesID string `json:"fasyakes_id"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` AuthURL string `json:"auth_url"` BaseURL string `json:"base_url"` ConsentURL string `json:"consent_url"` KFAURL string `json:"kfa_url"` Timeout time.Duration `json:"timeout"` } // SecurityConfig berisi semua pengaturan untuk middleware keamanan type SecurityConfig struct { // CORS TrustedOrigins []string `mapstructure:"trusted_origins"` // Rate Limiting RateLimit RateLimitConfig `mapstructure:"rate_limit"` // Input Validation MaxInputLength int `mapstructure:"max_input_length"` // SQL Injection Protection SanitizeQueries bool `mapstructure:"sanitize_queries"` // Connection Security RequireSecureConnections bool `mapstructure:"require_secure_connections"` } // RateLimitConfig berisi pengaturan untuk rate limiter type RateLimitConfig struct { RequestsPerMinute int `mapstructure:"requests_per_minute"` Redis RedisConfig `mapstructure:"redis"` } // RedisConfig berisi detail koneksi ke Redis type RedisConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` Password string `mapstructure:"password"` DB int `mapstructure:"db"` } func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { timenow := time.Now().UTC() t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z") if err != nil { log.Fatal(err) } tstamp := timenow.Unix() - t.Unix() secret := []byte(cfg.SecretKey) message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp)) hash := hmac.New(sha256.New, secret) hash.Write(message) // to lowercase hexits hex.EncodeToString(hash.Sum(nil)) // to base64 xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil)) return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature } type ConfigBpjs struct { Cons_id string Secret_key string User_key string } // SetHeader for backward compatibility func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) { bpjsConfig := BpjsConfig{ ConsID: cfg.Cons_id, SecretKey: cfg.Secret_key, UserKey: cfg.User_key, } return bpjsConfig.SetHeader() } func LoadConfig() *Config { log.Printf("DEBUG: Raw ENV for SECURITY_MAX_INPUT_LENGTH is: '%s'", os.Getenv("SECURITY_MAX_INPUT_LENGTH")) config := &Config{ Server: ServerConfig{ Port: getEnvAsInt("PORT", 8080), Mode: getEnv("GIN_MODE", "debug"), }, GRPC: GRPCConfig{ Port: getEnvAsInt("GRPC_PORT", 50051), Enabled: getEnvAsBool("GRPC_ENABLED", true), }, Databases: make(map[string]DatabaseConfig), ReadReplicas: make(map[string][]DatabaseConfig), Auth: loadAuthConfig(), Keycloak: loadKeycloakConfig(), Bpjs: BpjsConfig{ BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"), ConsID: getEnv("BPJS_CONSID", ""), UserKey: getEnv("BPJS_USERKEY", ""), SecretKey: getEnv("BPJS_SECRETKEY", ""), Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")), }, SatuSehat: SatuSehatConfig{ OrgID: getEnv("BRIDGING_SATUSEHAT_ORG_ID", ""), FasyakesID: getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", ""), ClientID: getEnv("BRIDGING_SATUSEHAT_CLIENT_ID", ""), ClientSecret: getEnv("BRIDGING_SATUSEHAT_CLIENT_SECRET", ""), AuthURL: getEnv("BRIDGING_SATUSEHAT_AUTH_URL", "https://api-satusehat.kemkes.go.id/oauth2/v1"), BaseURL: getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1"), ConsentURL: getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1"), KFAURL: getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2"), Timeout: parseDuration(getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s")), }, Swagger: SwaggerConfig{ Title: getEnv("SWAGGER_TITLE", "SERVICE API"), Description: getEnv("SWAGGER_DESCRIPTION", "CUSTUM SERVICE API"), Version: getEnv("SWAGGER_VERSION", "1.0.0"), TermsOfService: getEnv("SWAGGER_TERMS_OF_SERVICE", "http://swagger.io/terms/"), ContactName: getEnv("SWAGGER_CONTACT_NAME", "API Support"), ContactURL: getEnv("SWAGGER_CONTACT_URL", "http://rssa.example.com/support"), ContactEmail: getEnv("SWAGGER_CONTACT_EMAIL", "support@swagger.io"), LicenseName: getEnv("SWAGGER_LICENSE_NAME", "Apache 2.0"), LicenseURL: getEnv("SWAGGER_LICENSE_URL", "http://www.apache.org/licenses/LICENSE-2.0.html"), Host: getEnv("SWAGGER_HOST", "localhost:8080"), BasePath: getEnv("SWAGGER_BASE_PATH", "/api/v1"), Schemes: parseSchemes(getEnv("SWAGGER_SCHEMES", "http,https")), }, Security: SecurityConfig{ TrustedOrigins: parseOrigins(getEnv("SECURITY_TRUSTED_ORIGINS", "http://localhost:3000,http://localhost,http://localhost:8080,http://10.10.150.207:3001,http://10.10.150.114:3000,http://10.10.150.175:3000,http://192.168.18.7:3001,http://localhost:3001")), MaxInputLength: getEnvAsInt("SECURITY_MAX_INPUT_LENGTH", 500), RateLimit: RateLimitConfig{ RequestsPerMinute: getEnvAsInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 230), Redis: RedisConfig{ Host: getEnv("REDIS_HOST", "localhost"), Port: getEnvAsInt("REDIS_PORT", 6379), Password: getEnv("REDIS_PASSWORD", ""), DB: getEnvAsInt("REDIS_DB", 0), }, }, SanitizeQueries: getEnvAsBool("SECURITY_SANITIZE_QUERIES", true), RequireSecureConnections: getEnvAsBool("SECURITY_REQUIRE_SECURE_CONNECTIONS", false), }, } log.Printf("DEBUG: Final Config Object. MaxInputLength is: %d", config.Security.MaxInputLength) // Initialize validator config.Validator = validator.New() // Load database configurations config.loadDatabaseConfigs() // Load read replica configurations config.loadReadReplicaConfigs() log.Printf("DEBUG [LoadConfig]: Config object created at address: %p", config) log.Printf("DEBUG [LoadConfig]: Security.MaxInputLength is: %d", config.Security.MaxInputLength) return config } func loadAuthConfig() AuthConfig { // --- AWAL TAMBAHAN DEBUG --- // Cetak direktori kerja saat ini untuk debugging wd, err := os.Getwd() if err != nil { log.Printf("Error getting working directory: %v", err) } else { log.Printf("DEBUG: Current working directory is: %s", wd) } // --- AKHIR TAMBAHAN DEBUG --- authConfig := AuthConfig{ Type: "jwt", // default to jwt for backward compatibility FallbackTo: "", StaticTokens: []string{}, } // Path file yang akan dibaca configPath := "internal/config/config.yaml" log.Printf("DEBUG: Attempting to read auth config from: %s", configPath) // Load auth configuration from config.yaml first if data, err := os.ReadFile(configPath); err == nil { log.Printf("DEBUG: Successfully read config.yaml file. Parsing...") // Tambahkan log sukses var yamlConfig struct { Auth AuthYAMLConfig `yaml:"auth"` } if err := yaml.Unmarshal(data, &yamlConfig); err == nil { // Log nilai yang berhasil dibaca log.Printf("DEBUG: Parsed YAML. Type: '%s', Tokens: %d", yamlConfig.Auth.Type, len(yamlConfig.Auth.StaticTokens)) authConfig.Type = yamlConfig.Auth.Type authConfig.FallbackTo = yamlConfig.Auth.FallbackTo authConfig.StaticTokens = yamlConfig.Auth.StaticTokens } else { log.Printf("ERROR: Failed to unmarshal YAML: %v", err) } } else { // --- AWAL TAMBAHAN DEBUG --- // Cetak error spesifik jika file tidak ditemukan log.Printf("ERROR: Could not read config file at '%s': %v", configPath, err) // --- AKHIR TAMBAHAN DEBUG --- } // Then override with environment variables if set if envType := getEnv("AUTH_TYPE", ""); envType != "" { log.Printf("DEBUG: Overriding auth type with environment variable: %s", envType) authConfig.Type = envType } if envFallback := getEnv("AUTH_FALLBACK_TO", ""); envFallback != "" { authConfig.FallbackTo = envFallback } envTokens := parseStaticTokens(getEnv("AUTH_STATIC_TOKENS", "")) if len(envTokens) > 0 { authConfig.StaticTokens = envTokens } // Log hasil akhir sebelum dikembalikan log.Printf("DEBUG: Final AuthConfig before returning: Type='%s', TokenCount=%d", authConfig.Type, len(authConfig.StaticTokens)) return authConfig } // Lakukan hal yang sama untuk loadKeycloakConfig func loadKeycloakConfig() KeycloakConfig { // --- AWAL TAMBAHAN DEBUG --- // Cetak direktori kerja saat ini untuk debugging wd, err := os.Getwd() if err != nil { log.Printf("Error getting working directory for keycloak config: %v", err) } else { log.Printf("DEBUG (Keycloak): Current working directory is: %s", wd) } // --- AKHIR TAMBAHAN DEBUG --- v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") v.AddConfigPath("./config") v.AddConfigPath("./internal/config") // --- AWAL TAMBAHAN DEBUG --- log.Printf("DEBUG (Keycloak): Viper is set to search for config in: '.', './config', './internal/config'") // --- AKHIR TAMBAHAN DEBUG --- if err := v.ReadInConfig(); err == nil { // Log jika file berhasil ditemukan dan dibaca log.Printf("DEBUG (Keycloak): Successfully read config file: %s", v.ConfigFileUsed()) keycloakConfig := KeycloakConfig{ Issuer: v.GetString("keycloak.issuer"), Audience: v.GetString("keycloak.audience"), JwksURL: v.GetString("keycloak.jwks_url"), Enabled: v.GetBool("keycloak.enabled"), } // Log nilai yang berhasil dibaca dari file log.Printf("DEBUG (Keycloak): Parsed values from file. Issuer: '%s', Enabled: %t", keycloakConfig.Issuer, keycloakConfig.Enabled) log.Printf("Loaded keycloak config from file: enabled=%t", keycloakConfig.Enabled) return keycloakConfig } else { // --- AWAL TAMBAHAN DEBUG --- // Cetak error spesifik jika file tidak ditemukan log.Printf("ERROR (Keycloak): Could not read config file: %v", err) // --- AKHIR TAMBAHAN DEBUG --- } // Fallback ke environment variable log.Printf("DEBUG (Keycloak): Falling back to environment variables.") fallbackConfig := KeycloakConfig{ Issuer: getEnv("KEYCLOAK_ISSUER", ""), Audience: getEnv("KEYCLOAK_AUDIENCE", ""), JwksURL: getEnv("KEYCLOAK_JWKS_URL", ""), Enabled: getEnvAsBool("KEYCLOAK_ENABLED", false), } // Log hasil akhir dari fallback log.Printf("DEBUG (Keycloak): Final fallback config. Issuer: '%s', Enabled: %t", fallbackConfig.Issuer, fallbackConfig.Enabled) return fallbackConfig } func (c *Config) loadDatabaseConfigs() { // Load PostgreSQL configurations // c.addPostgreSQLConfigs() // // Load MySQL configurations // c.addMySQLConfigs() // // Load MongoDB configurations // c.addMongoDBConfigs() // // Load SQLite configurations // c.addSQLiteConfigs() // Load custom database configurations from environment variables c.loadCustomDatabaseConfigs() // Remove duplicate database configurations // c.removeDuplicateDatabases() } func (c *Config) removeDuplicateDatabases() { // Create a map to track unique database connections uniqueDBs := make(map[string]DatabaseConfig) duplicates := make(map[string][]string) // First pass: identify duplicates for name, config := range c.Databases { // Create a unique key based on connection parameters key := fmt.Sprintf("%s:%s:%d:%s", config.Type, config.Host, config.Port, config.Database) if existing, exists := uniqueDBs[key]; exists { // Found a duplicate if duplicates[key] == nil { duplicates[key] = []string{existing.Name} } duplicates[key] = append(duplicates[key], name) log.Printf("⚠️ Database %s is a duplicate of %s (same connection parameters)", name, existing.Name) } else { uniqueDBs[key] = config } } // Second pass: remove duplicates, keeping the first one for _, dupNames := range duplicates { // Keep the first database name, remove the rest keepName := dupNames[0] for i := 1; i < len(dupNames); i++ { removeName := dupNames[i] delete(c.Databases, removeName) log.Printf("🗑️ Removed duplicate database configuration: %s (kept: %s)", removeName, keepName) } } } func (c *Config) loadCustomDatabaseConfigs() { envVars := os.Environ() dbConfigs := make(map[string]map[string]string) // Parse database configurations from environment variables for _, envVar := range envVars { parts := strings.SplitN(envVar, "=", 2) if len(parts) != 2 { continue } key := parts[0] value := parts[1] // Parse specific database configurations if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") || strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") || strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") || strings.HasSuffix(key, "_NAME") { segments := strings.Split(key, "_") if len(segments) >= 2 { dbName := strings.ToLower(strings.Join(segments[:len(segments)-1], "_")) property := strings.ToLower(segments[len(segments)-1]) if dbConfigs[dbName] == nil { dbConfigs[dbName] = make(map[string]string) } dbConfigs[dbName][property] = value } } } // Create DatabaseConfig from parsed configurations for additional databases for name, config := range dbConfigs { // Skip empty configurations or system configurations if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" { continue } dbType := getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")) // Skip if username is empty and it's not a system config username := getEnvFromMap(config, "username", "") if username == "" && !strings.HasPrefix(name, "chrome") { continue } dbConfig := DatabaseConfig{ Name: name, Type: dbType, Host: getEnvFromMap(config, "host", "localhost"), Port: getEnvAsIntFromMap(config, "port", getDefaultPort(dbType)), Username: username, Password: getEnvFromMap(config, "password", ""), Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)), Schema: getEnvFromMap(config, "schema", getDefaultSchema(dbType)), SSLMode: getEnvFromMap(config, "sslmode", getDefaultSSLMode(dbType)), Path: getEnvFromMap(config, "path", ""), Options: getEnvFromMap(config, "options", ""), MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", getDefaultMaxOpenConns(dbType)), MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", getDefaultMaxIdleConns(dbType)), ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", getDefaultConnMaxLifetime(dbType))), // Security settings RequireSSL: getEnvAsBoolFromMap(config, "require_ssl", false), SSLRootCert: getEnvFromMap(config, "ssl_root_cert", ""), SSLCert: getEnvFromMap(config, "ssl_cert", ""), SSLKey: getEnvFromMap(config, "ssl_key", ""), Timeout: parseDuration(getEnvFromMap(config, "timeout", "30s")), ConnectTimeout: parseDuration(getEnvFromMap(config, "connect_timeout", "10s")), ReadTimeout: parseDuration(getEnvFromMap(config, "read_timeout", "30s")), WriteTimeout: parseDuration(getEnvFromMap(config, "write_timeout", "30s")), StatementTimeout: parseDuration(getEnvFromMap(config, "statement_timeout", "120s")), // Connection pool settings MaxLifetime: parseDuration(getEnvFromMap(config, "max_lifetime", "1h")), MaxIdleTime: parseDuration(getEnvFromMap(config, "max_idle_time", "5m")), HealthCheckPeriod: parseDuration(getEnvFromMap(config, "health_check_period", "1m")), } c.Databases[name] = dbConfig } } func (c *Config) loadReadReplicaConfigs() { envVars := os.Environ() for _, envVar := range envVars { parts := strings.SplitN(envVar, "=", 2) if len(parts) != 2 { continue } key := parts[0] value := parts[1] // Parse read replica configurations (format: [DBNAME]_REPLICA_[INDEX]_[PROPERTY]) if strings.Contains(key, "_REPLICA_") { segments := strings.Split(key, "_") if len(segments) >= 5 && strings.ToUpper(segments[2]) == "REPLICA" { dbName := strings.ToLower(segments[1]) replicaIndex := segments[3] property := strings.ToLower(strings.Join(segments[4:], "_")) replicaKey := dbName + "_replica_" + replicaIndex if c.ReadReplicas[dbName] == nil { c.ReadReplicas[dbName] = []DatabaseConfig{} } // Find or create replica config var replicaConfig *DatabaseConfig for i := range c.ReadReplicas[dbName] { if c.ReadReplicas[dbName][i].Name == replicaKey { replicaConfig = &c.ReadReplicas[dbName][i] break } } if replicaConfig == nil { // Get primary DB config as base primaryDB, exists := c.Databases[dbName] if !exists { log.Printf("Warning: Primary database %s not found for replica configuration", dbName) continue } // Create new replica config based on primary newConfig := DatabaseConfig{ Name: replicaKey, Type: primaryDB.Type, Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", primaryDB.Host), Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", primaryDB.Port), Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", primaryDB.Username), Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", primaryDB.Password), Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", primaryDB.Database), Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", primaryDB.Schema), SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", primaryDB.SSLMode), MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", primaryDB.MaxOpenConns), MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", primaryDB.MaxIdleConns), ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", primaryDB.ConnMaxLifetime.String())), // Security settings RequireSSL: getEnvAsBool("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_REQUIRE_SSL", primaryDB.RequireSSL), SSLRootCert: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSL_ROOT_CERT", primaryDB.SSLRootCert), SSLCert: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSL_CERT", primaryDB.SSLCert), SSLKey: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSL_KEY", primaryDB.SSLKey), Timeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_TIMEOUT", primaryDB.Timeout.String())), ConnectTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONNECT_TIMEOUT", primaryDB.ConnectTimeout.String())), ReadTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_READ_TIMEOUT", primaryDB.ReadTimeout.String())), WriteTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_WRITE_TIMEOUT", primaryDB.WriteTimeout.String())), StatementTimeout: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_STATEMENT_TIMEOUT", primaryDB.StatementTimeout.String())), // Connection pool settings MaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_LIFETIME", primaryDB.MaxLifetime.String())), MaxIdleTime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_TIME", primaryDB.MaxIdleTime.String())), HealthCheckPeriod: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HEALTH_CHECK_PERIOD", primaryDB.HealthCheckPeriod.String())), } c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig) replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1] } // Update the specific replica switch property { case "host": replicaConfig.Host = value case "port": replicaConfig.Port = getEnvAsInt(key, 5432) case "username": replicaConfig.Username = value case "password": replicaConfig.Password = value case "database": replicaConfig.Database = value case "schema": replicaConfig.Schema = value case "sslmode": replicaConfig.SSLMode = value case "max_open_conns": replicaConfig.MaxOpenConns = getEnvAsInt(key, 25) case "max_idle_conns": replicaConfig.MaxIdleConns = getEnvAsInt(key, 25) case "conn_max_lifetime": replicaConfig.ConnMaxLifetime = parseDuration(value) case "require_ssl": replicaConfig.RequireSSL = getEnvAsBool(key, false) case "ssl_root_cert": replicaConfig.SSLRootCert = value case "ssl_cert": replicaConfig.SSLCert = value case "ssl_key": replicaConfig.SSLKey = value case "timeout": replicaConfig.Timeout = parseDuration(value) case "connect_timeout": replicaConfig.ConnectTimeout = parseDuration(value) case "read_timeout": replicaConfig.ReadTimeout = parseDuration(value) case "write_timeout": replicaConfig.WriteTimeout = parseDuration(value) case "statement_timeout": replicaConfig.StatementTimeout = parseDuration(value) case "max_lifetime": replicaConfig.MaxLifetime = parseDuration(value) case "max_idle_time": replicaConfig.MaxIdleTime = parseDuration(value) case "health_check_period": replicaConfig.HealthCheckPeriod = parseDuration(value) } } } } } func (c *Config) addSpecificDatabase(prefix, defaultType string) { connection := getEnv(strings.ToUpper(prefix)+"_CONNECTION", defaultType) host := getEnv(strings.ToUpper(prefix)+"_HOST", "") if host != "" { dbConfig := DatabaseConfig{ Name: prefix, Type: connection, Host: host, Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", getDefaultPort(connection)), Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""), Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""), Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)), Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", getDefaultSchema(connection)), SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", getDefaultSSLMode(connection)), MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", getDefaultMaxOpenConns(connection)), MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", getDefaultMaxIdleConns(connection)), ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", getDefaultConnMaxLifetime(connection))), // Security settings RequireSSL: getEnvAsBool(strings.ToUpper(prefix)+"_REQUIRE_SSL", false), SSLRootCert: getEnv(strings.ToUpper(prefix)+"_SSL_ROOT_CERT", ""), SSLCert: getEnv(strings.ToUpper(prefix)+"_SSL_CERT", ""), SSLKey: getEnv(strings.ToUpper(prefix)+"_SSL_KEY", ""), Timeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_WRITE_TIMEOUT", "30s")), StatementTimeout: parseDuration(getEnv(strings.ToUpper(prefix)+"_STATEMENT_TIMEOUT", "120s")), // Connection pool settings MaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv(strings.ToUpper(prefix)+"_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv(strings.ToUpper(prefix)+"_HEALTH_CHECK_PERIOD", "1m")), } c.Databases[prefix] = dbConfig } } // PostgreSQL database func (c *Config) addPostgreSQLConfigs() { // Support for custom PostgreSQL configurations with POSTGRES_ prefix envVars := os.Environ() for _, envVar := range envVars { parts := strings.SplitN(envVar, "=", 2) if len(parts) != 2 { continue } key := parts[0] // Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY]) if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") { segments := strings.Split(key, "_") if len(segments) >= 3 { dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) // Skip if it's a standard PostgreSQL configuration if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" { continue } // Create or update PostgreSQL configuration if _, exists := c.Databases[dbName]; !exists { c.Databases[dbName] = DatabaseConfig{ Name: dbName, Type: "postgres", Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"), Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432), Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""), Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""), Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName), Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"), SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), // Security settings RequireSSL: getEnvAsBool("POSTGRES_"+strings.ToUpper(dbName)+"_REQUIRE_SSL", false), SSLRootCert: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSL_ROOT_CERT", ""), SSLCert: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSL_CERT", ""), SSLKey: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSL_KEY", ""), Timeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_WRITE_TIMEOUT", "30s")), StatementTimeout: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_STATEMENT_TIMEOUT", "120s")), // Connection pool settings MaxLifetime: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")), } } } } } } // addMYSQLConfigs adds MYSQL database func (c *Config) addMySQLConfigs() { // Primary MySQL configuration defaultMySQLHost := getEnv("MYSQL_HOST", "") if defaultMySQLHost != "" { c.Databases["mysql"] = DatabaseConfig{ Name: "mysql", Type: getEnv("MYSQL_CONNECTION", "mysql"), Host: defaultMySQLHost, Port: getEnvAsInt("MYSQL_PORT", 3306), Username: getEnv("MYSQL_USERNAME", ""), Password: getEnv("MYSQL_PASSWORD", ""), Database: getEnv("MYSQL_DATABASE", "mysql"), SSLMode: getEnv("MYSQL_SSLMODE", "disable"), MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), // Security settings RequireSSL: getEnvAsBool("MYSQL_REQUIRE_SSL", false), SSLRootCert: getEnv("MYSQL_SSL_ROOT_CERT", ""), SSLCert: getEnv("MYSQL_SSL_CERT", ""), SSLKey: getEnv("MYSQL_SSL_KEY", ""), Timeout: parseDuration(getEnv("MYSQL_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv("MYSQL_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv("MYSQL_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv("MYSQL_WRITE_TIMEOUT", "30s")), // Connection pool settings MaxLifetime: parseDuration(getEnv("MYSQL_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("MYSQL_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("MYSQL_HEALTH_CHECK_PERIOD", "1m")), } } // Support for custom MySQL configurations with MYSQL_ prefix envVars := os.Environ() for _, envVar := range envVars { parts := strings.SplitN(envVar, "=", 2) if len(parts) != 2 { continue } key := parts[0] // Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY]) if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") { segments := strings.Split(key, "_") if len(segments) >= 3 { dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) // Skip if it's a standard MySQL configuration if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" { continue } // Create or update MySQL configuration if _, exists := c.Databases[dbName]; !exists { mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "") if mysqlHost != "" { c.Databases[dbName] = DatabaseConfig{ Name: dbName, Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"), Host: mysqlHost, Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306), Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""), Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""), Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName), SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), // Security settings RequireSSL: getEnvAsBool("MYSQL_"+strings.ToUpper(dbName)+"_REQUIRE_SSL", false), SSLRootCert: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSL_ROOT_CERT", ""), SSLCert: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSL_CERT", ""), SSLKey: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSL_KEY", ""), Timeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_WRITE_TIMEOUT", "30s")), // Connection pool settings MaxLifetime: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")), } } } } } } } // addMongoDBConfigs adds MongoDB database configurations from environment variables func (c *Config) addMongoDBConfigs() { // Primary MongoDB configuration mongoHost := getEnv("MONGODB_HOST", "") if mongoHost != "" { c.Databases["mongodb"] = DatabaseConfig{ Name: "mongodb", Type: getEnv("MONGODB_CONNECTION", "mongodb"), Host: mongoHost, Port: getEnvAsInt("MONGODB_PORT", 27017), Username: getEnv("MONGODB_USER", ""), Password: getEnv("MONGODB_PASS", ""), Database: getEnv("MONGODB_MASTER", "master"), SSLMode: getEnv("MONGODB_SSLMODE", "disable"), MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), // Security settings RequireSSL: getEnvAsBool("MONGODB_REQUIRE_SSL", false), SSLRootCert: getEnv("MONGODB_SSL_ROOT_CERT", ""), SSLCert: getEnv("MONGODB_SSL_CERT", ""), SSLKey: getEnv("MONGODB_SSL_KEY", ""), Timeout: parseDuration(getEnv("MONGODB_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv("MONGODB_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv("MONGODB_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv("MONGODB_WRITE_TIMEOUT", "30s")), // Connection pool settings MaxLifetime: parseDuration(getEnv("MONGODB_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("MONGODB_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("MONGODB_HEALTH_CHECK_PERIOD", "1m")), } } // Additional MongoDB configurations for local database mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "") if mongoLocalHost != "" { c.Databases["mongodb_local"] = DatabaseConfig{ Name: "mongodb_local", Type: getEnv("MONGODB_CONNECTION", "mongodb"), Host: mongoLocalHost, Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017), Username: getEnv("MONGODB_LOCAL_USER", ""), Password: getEnv("MONGODB_LOCAL_PASS", ""), Database: getEnv("MONGODB_LOCAL_DB", "local"), SSLMode: getEnv("MONGOD_SSLMODE", "disable"), MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), // Security settings RequireSSL: getEnvAsBool("MONGODB_LOCAL_REQUIRE_SSL", false), SSLRootCert: getEnv("MONGODB_LOCAL_SSL_ROOT_CERT", ""), SSLCert: getEnv("MONGODB_LOCAL_SSL_CERT", ""), SSLKey: getEnv("MONGODB_LOCAL_SSL_KEY", ""), Timeout: parseDuration(getEnv("MONGODB_LOCAL_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv("MONGODB_LOCAL_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv("MONGODB_LOCAL_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv("MONGODB_LOCAL_WRITE_TIMEOUT", "30s")), // Connection pool settings MaxLifetime: parseDuration(getEnv("MONGODB_LOCAL_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("MONGODB_LOCAL_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("MONGODB_LOCAL_HEALTH_CHECK_PERIOD", "1m")), } } // Support for custom MongoDB configurations with MONGODB_ prefix envVars := os.Environ() for _, envVar := range envVars { parts := strings.SplitN(envVar, "=", 2) if len(parts) != 2 { continue } key := parts[0] // Parse MongoDB configurations (format: MONGODB_[NAME]_[PROPERTY]) if strings.HasPrefix(key, "MONGODB_") && strings.Contains(key, "_") { segments := strings.Split(key, "_") if len(segments) >= 3 { dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) // Skip if it's a standard MongoDB configuration if dbName == "connection" || dbName == "dev" || dbName == "local" { continue } // Create or update MongoDB configuration if _, exists := c.Databases[dbName]; !exists { c.Databases[dbName] = DatabaseConfig{ Name: dbName, Type: "mongodb", Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"), Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017), Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""), Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""), Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName), SSLMode: getEnv("MONGOD_SSLMODE", "disable"), MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), // Security settings RequireSSL: getEnvAsBool("MONGODB_"+strings.ToUpper(dbName)+"_REQUIRE_SSL", false), SSLRootCert: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_SSL_ROOT_CERT", ""), SSLCert: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_SSL_CERT", ""), SSLKey: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_SSL_KEY", ""), Timeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_TIMEOUT", "30s")), ConnectTimeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_CONNECT_TIMEOUT", "10s")), ReadTimeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_READ_TIMEOUT", "30s")), WriteTimeout: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_WRITE_TIMEOUT", "30s")), // Connection pool settings MaxLifetime: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")), } } } } } } // addSQLiteConfigs adds SQLite database configurations from environment variables func (c *Config) addSQLiteConfigs() { // Support for custom SQLite configurations with SQLITE_ prefix envVars := os.Environ() for _, envVar := range envVars { parts := strings.SplitN(envVar, "=", 2) if len(parts) != 2 { continue } key := parts[0] // Parse SQLite configurations (format: SQLITE_[NAME]_[PROPERTY]) if strings.HasPrefix(key, "SQLITE_") && strings.Contains(key, "_") { segments := strings.Split(key, "_") if len(segments) >= 3 { dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) // Skip if it's a standard SQLite configuration if dbName == "connection" || dbName == "dev" || dbName == "default" { continue } // Create or update SQLite configuration if _, exists := c.Databases[dbName]; !exists { sqlitePath := getEnv("SQLITE_"+strings.ToUpper(dbName)+"_PATH", "") if sqlitePath != "" { c.Databases[dbName] = DatabaseConfig{ Name: dbName, Type: "sqlite", Path: sqlitePath, Database: getEnv("SQLITE_"+strings.ToUpper(dbName)+"_DATABASE", dbName), MaxOpenConns: getEnvAsInt("SQLITE_MAX_OPEN_CONNS", 25), MaxIdleConns: getEnvAsInt("SQLITE_MAX_IDLE_CONNS", 25), ConnMaxLifetime: parseDuration(getEnv("SQLITE_CONN_MAX_LIFETIME", "5m")), // Connection pool settings MaxLifetime: parseDuration(getEnv("SQLITE_"+strings.ToUpper(dbName)+"_MAX_LIFETIME", "1h")), MaxIdleTime: parseDuration(getEnv("SQLITE_"+strings.ToUpper(dbName)+"_MAX_IDLE_TIME", "5m")), HealthCheckPeriod: parseDuration(getEnv("SQLITE_"+strings.ToUpper(dbName)+"_HEALTH_CHECK_PERIOD", "1m")), } } } } } } } // Helper functions for getting default values based on database type func getDefaultPort(dbType string) int { switch dbType { case "postgres": return 5432 case "mysql": return 3306 case "sqlserver": return 1433 case "mongodb": return 27017 case "sqlite": return 0 // SQLite doesn't use port default: return 5432 } } func getDefaultSchema(dbType string) string { switch dbType { case "postgres": return "public" case "mysql": return "" case "sqlserver": return "dbo" case "mongodb": return "" case "sqlite": return "" default: return "public" } } func getDefaultSSLMode(dbType string) string { switch dbType { case "postgres": return "disable" case "mysql": return "false" case "sqlserver": return "false" case "mongodb": return "false" case "sqlite": return "" default: return "disable" } } func getDefaultMaxOpenConns(dbType string) int { switch dbType { case "postgres": return 25 case "mysql": return 25 case "sqlserver": return 25 case "mongodb": return 100 case "sqlite": return 1 // SQLite only supports one writer at a time default: return 25 } } func getDefaultMaxIdleConns(dbType string) int { switch dbType { case "postgres": return 25 case "mysql": return 25 case "sqlserver": return 25 case "mongodb": return 10 case "sqlite": return 1 // SQLite only supports one writer at a time default: return 25 } } func getDefaultConnMaxLifetime(dbType string) string { switch dbType { case "postgres": return "5m" case "mysql": return "5m" case "sqlserver": return "5m" case "mongodb": return "30m" case "sqlite": return "5m" default: return "5m" } } func getEnvFromMap(config map[string]string, key, defaultValue string) string { if value, exists := config[key]; exists { return value } return defaultValue } func getEnvAsIntFromMap(config map[string]string, key string, defaultValue int) int { if value, exists := config[key]; exists { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } func getEnvAsBoolFromMap(config map[string]string, key string, defaultValue bool) bool { if value, exists := config[key]; exists { if boolValue, err := strconv.ParseBool(value); err == nil { return boolValue } } return defaultValue } func parseDuration(durationStr string) time.Duration { if duration, err := time.ParseDuration(durationStr); err == nil { return duration } return 5 * time.Minute } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvAsInt(key string, defaultValue int) int { valueStr := getEnv(key, "") if value, err := strconv.Atoi(valueStr); err == nil { return value } return defaultValue } func getEnvAsBool(key string, defaultValue bool) bool { valueStr := getEnv(key, "") if value, err := strconv.ParseBool(valueStr); err == nil { return value } return defaultValue } // parseSchemes parses comma-separated schemes string into a slice func parseSchemes(schemesStr string) []string { if schemesStr == "" { return []string{"http"} } schemes := strings.Split(schemesStr, ",") for i, scheme := range schemes { schemes[i] = strings.TrimSpace(scheme) } return schemes } // parseStaticTokens parses comma-separated static tokens string into a slice func parseStaticTokens(tokensStr string) []string { if tokensStr == "" { return []string{} } tokens := strings.Split(tokensStr, ",") for i, token := range tokens { tokens[i] = strings.TrimSpace(token) // Remove empty tokens if tokens[i] == "" { tokens = append(tokens[:i], tokens[i+1:]...) i-- } } return tokens } func parseOrigins(originsStr string) []string { if originsStr == "" { return []string{"http://localhost:8080"} // Default untuk pengembangan } origins := strings.Split(originsStr, ",") for i, origin := range origins { origins[i] = strings.TrimSpace(origin) } return origins } func (c *Config) Validate() error { var errs []string if len(c.Databases) == 0 { errs = append(errs, "at least one database configuration is required") } for name, db := range c.Databases { if db.Type != "sqlite" && db.Host == "" { errs = append(errs, fmt.Sprintf("database host is required for %s", name)) } if db.Type != "sqlite" && db.Username == "" { errs = append(errs, fmt.Sprintf("database username is required for %s", name)) } if db.Type != "sqlite" && db.Password == "" { errs = append(errs, fmt.Sprintf("database password is required for %s", name)) } if db.Type == "sqlite" && db.Path == "" { errs = append(errs, fmt.Sprintf("database path is required for SQLite database %s", name)) } if db.Type != "sqlite" && db.Database == "" { errs = append(errs, fmt.Sprintf("database name is required for %s", name)) } } if c.Bpjs.BaseURL == "" { errs = append(errs, "BPJS Base URL is required") } if c.Bpjs.ConsID == "" { errs = append(errs, "BPJS Consumer ID is required") } if c.Bpjs.UserKey == "" { errs = append(errs, "BPJS User Key is required") } if c.Bpjs.SecretKey == "" { errs = append(errs, "BPJS Secret Key is required") } // Validate authentication configuration switch c.Auth.Type { case "keycloak": if !c.Keycloak.Enabled { errs = append(errs, "keycloak.enabled must be true when auth.type is 'keycloak'") } if c.Keycloak.Issuer == "" { errs = append(errs, "keycloak.issuer is required when auth.type is 'keycloak'") } if c.Keycloak.Audience == "" { errs = append(errs, "keycloak.audience is required when auth.type is 'keycloak'") } if c.Keycloak.JwksURL == "" { errs = append(errs, "keycloak.jwks_url is required when auth.type is 'keycloak'") } case "static": if len(c.Auth.StaticTokens) == 0 { errs = append(errs, "auth.static_tokens is required when auth.type is 'static'") } case "hybrid": if c.Auth.FallbackTo == "" { errs = append(errs, "auth.fallback_to is required when auth.type is 'hybrid'") } // Validate fallback configuration switch c.Auth.FallbackTo { case "keycloak": if !c.Keycloak.Enabled { errs = append(errs, "keycloak.enabled must be true when auth.fallback_to is 'keycloak'") } case "static": if len(c.Auth.StaticTokens) == 0 { errs = append(errs, "auth.static_tokens is required when auth.fallback_to is 'static'") } } } // Legacy validation for backward compatibility if c.Auth.Type != "keycloak" && c.Keycloak.Enabled { if c.Keycloak.Issuer == "" { errs = append(errs, "Keycloak issuer is required when Keycloak is enabled") } if c.Keycloak.Audience == "" { errs = append(errs, "Keycloak audience is required when Keycloak is enabled") } if c.Keycloak.JwksURL == "" { errs = append(errs, "Keycloak JWKS URL is required when Keycloak is enabled") } } // Validate SatuSehat configuration if c.SatuSehat.OrgID == "" { errs = append(errs, "SatuSehat Organization ID is required") } if c.SatuSehat.FasyakesID == "" { errs = append(errs, "SatuSehat Fasyankes ID is required") } if c.SatuSehat.ClientID == "" { errs = append(errs, "SatuSehat Client ID is required") } if c.SatuSehat.ClientSecret == "" { errs = append(errs, "SatuSehat Client Secret is required") } if c.SatuSehat.AuthURL == "" { errs = append(errs, "SatuSehat Auth URL is required") } if c.SatuSehat.BaseURL == "" { errs = append(errs, "SatuSehat Base URL is required") } if len(errs) > 0 { return fmt.Errorf("configuration validation failed: %s", strings.Join(errs, "; ")) } return nil }