Update besar

This commit is contained in:
meninjar
2025-10-31 02:30:27 +00:00
parent 07d264c57e
commit 0002cf26be
20 changed files with 4939 additions and 1938 deletions

No files matched your search

+293 -60
View File
@@ -13,16 +13,20 @@ import (
"time"
"github.com/go-playground/validator/v10"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig
Databases map[string]DatabaseConfig
ReadReplicas map[string][]DatabaseConfig // For read replicas
Auth AuthConfig
Keycloak KeycloakConfig
Bpjs BpjsConfig
SatuSehat SatuSehatConfig
Swagger SwaggerConfig
Security SecurityConfig
Validator *validator.Validate
}
@@ -63,6 +67,25 @@ type DatabaseConfig struct {
ConnMaxLifetime time.Duration // Connection max lifetime
}
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
@@ -90,27 +113,30 @@ type SatuSehatConfig struct {
Timeout time.Duration `json:"timeout"`
}
// SetHeader generates required headers for BPJS VClaim API
// 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)
// }
// 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"`
}
// 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)
// RateLimitConfig berisi pengaturan untuk rate limiter
type RateLimitConfig struct {
RequestsPerMinute int `mapstructure:"requests_per_minute"`
Redis RedisConfig `mapstructure:"redis"`
}
// // to lowercase hexits
// hex.EncodeToString(hash.Sum(nil))
// // to base64
// xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil))
// 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"`
}
// return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature
// }
func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
timenow := time.Now().UTC()
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
@@ -149,6 +175,7 @@ func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) {
}
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),
@@ -156,12 +183,8 @@ func LoadConfig() *Config {
},
Databases: make(map[string]DatabaseConfig),
ReadReplicas: make(map[string][]DatabaseConfig),
Keycloak: KeycloakConfig{
Issuer: getEnv("KEYCLOAK_ISSUER", "https://keycloak.example.com/auth/realms/yourrealm"),
Audience: getEnv("KEYCLOAK_AUDIENCE", "your-client-id"),
JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"),
Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true),
},
Auth: loadAuthConfig(),
Keycloak: loadKeycloakConfig(),
Bpjs: BpjsConfig{
BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"),
ConsID: getEnv("BPJS_CONSID", ""),
@@ -194,8 +217,21 @@ func LoadConfig() *Config {
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:8080")),
MaxInputLength: getEnvAsInt("SECURITY_MAX_INPUT_LENGTH", 500),
RateLimit: RateLimitConfig{
RequestsPerMinute: getEnvAsInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60),
Redis: RedisConfig{
Host: getEnv("REDIS_HOST", "localhost"),
Port: getEnvAsInt("REDIS_PORT", 6379),
Password: getEnv("REDIS_PASSWORD", ""),
DB: getEnvAsInt("REDIS_DB", 0),
},
},
},
}
log.Printf("DEBUG: Final Config Object. MaxInputLength is: %d", config.Security.MaxInputLength)
// Initialize validator
config.Validator = validator.New()
@@ -205,28 +241,155 @@ func LoadConfig() *Config {
// 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() {
// Simplified approach: Directly load from environment variables
// This ensures we get the exact values specified in .env
// Primary database configuration
c.Databases["default"] = DatabaseConfig{
Name: "default",
Type: getEnv("DB_CONNECTION", "postgres"),
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 5432),
Username: getEnv("DB_USERNAME", ""),
Password: getEnv("DB_PASSWORD", ""),
Database: getEnv("DB_DATABASE", "satu_db"),
Schema: getEnv("DB_SCHEMA", "public"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
}
// // Primary database configuration
// c.Databases["default"] = DatabaseConfig{
// Name: "default",
// Type: getEnv("DB_CONNECTION", "postgres"),
// Host: getEnv("DB_HOST", "localhost"),
// Port: getEnvAsInt("DB_PORT", 5432),
// Username: getEnv("DB_USERNAME", ""),
// Password: getEnv("DB_PASSWORD", ""),
// Database: getEnv("DB_DATABASE", "satu_db"),
// Schema: getEnv("DB_SCHEMA", "public"),
// SSLMode: getEnv("DB_SSLMODE", "disable"),
// MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
// MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
// ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
// }
// SATUDATA database configuration
c.addPostgreSQLConfigs()
@@ -669,71 +832,141 @@ func parseSchemes(schemesStr string) []string {
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 {
log.Fatal("At least one database configuration is required")
errs = append(errs, "at least one database configuration is required")
}
for name, db := range c.Databases {
if db.Host == "" {
log.Fatalf("Database host is required for %s", name)
errs = append(errs, fmt.Sprintf("database host is required for %s", name))
}
if db.Username == "" {
log.Fatalf("Database username is required for %s", name)
errs = append(errs, fmt.Sprintf("database username is required for %s", name))
}
if db.Password == "" {
log.Fatalf("Database password is required for %s", name)
errs = append(errs, fmt.Sprintf("database password is required for %s", name))
}
if db.Database == "" {
log.Fatalf("Database name is required for %s", name)
errs = append(errs, fmt.Sprintf("database name is required for %s", name))
}
}
if c.Bpjs.BaseURL == "" {
log.Fatal("BPJS Base URL is required")
errs = append(errs, "BPJS Base URL is required")
}
if c.Bpjs.ConsID == "" {
log.Fatal("BPJS Consumer ID is required")
errs = append(errs, "BPJS Consumer ID is required")
}
if c.Bpjs.UserKey == "" {
log.Fatal("BPJS User Key is required")
errs = append(errs, "BPJS User Key is required")
}
if c.Bpjs.SecretKey == "" {
log.Fatal("BPJS Secret Key is required")
errs = append(errs, "BPJS Secret Key is required")
}
// Validate Keycloak configuration if enabled
if c.Keycloak.Enabled {
// 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 == "" {
log.Fatal("Keycloak issuer is required when Keycloak is enabled")
errs = append(errs, "keycloak.issuer is required when auth.type is 'keycloak'")
}
if c.Keycloak.Audience == "" {
log.Fatal("Keycloak audience is required when Keycloak is enabled")
errs = append(errs, "keycloak.audience is required when auth.type is 'keycloak'")
}
if c.Keycloak.JwksURL == "" {
log.Fatal("Keycloak JWKS URL is required when Keycloak is enabled")
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 == "" {
log.Fatal("SatuSehat Organization ID is required")
errs = append(errs, "SatuSehat Organization ID is required")
}
if c.SatuSehat.FasyakesID == "" {
log.Fatal("SatuSehat Fasyankes ID is required")
errs = append(errs, "SatuSehat Fasyankes ID is required")
}
if c.SatuSehat.ClientID == "" {
log.Fatal("SatuSehat Client ID is required")
errs = append(errs, "SatuSehat Client ID is required")
}
if c.SatuSehat.ClientSecret == "" {
log.Fatal("SatuSehat Client Secret is required")
errs = append(errs, "SatuSehat Client Secret is required")
}
if c.SatuSehat.AuthURL == "" {
log.Fatal("SatuSehat Auth URL is required")
errs = append(errs, "SatuSehat Auth URL is required")
}
if c.SatuSehat.BaseURL == "" {
log.Fatal("SatuSehat Base URL is required")
errs = append(errs, "SatuSehat Base URL is required")
}
if len(errs) > 0 {
return fmt.Errorf("configuration validation failed: %s", strings.Join(errs, "; "))
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
auth:
type: static # Options: jwt, keycloak, static, hybrid (for hybrid mode keycloak is primary and jwt is fallback)
static_tokens:
- token1
- token2
- token3
- token4
fallback_to: jwt # Options: keycloak, static, jwt (for hybrid mode keycloak is primary and jwt is fallback)
keycloak:
enabled: true
issuer: https://auth.rssa.top/realms/sandbox
audience: nuxtsim-pendaftaran
jwks_url: https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs