package config import ( "encoding/base64" "encoding/json" "errors" "net" "net/url" "os" "strconv" "strings" "github.com/joho/godotenv" ) type Config struct { Addr string GinMode string Auth AuthConfig Firebase FirebaseConfig DB DBConfig } type AuthConfig struct { Enabled bool } type FirebaseConfig struct { ProjectID string CredentialsJSON string CredentialsFile string } type DBConfig struct { Host string Port string User string Password string Name string SSLMode string MaxConns int32 AppName string } func Load() Config { _ = godotenv.Load() cfg := Config{ Addr: getenv("ADDR", ":8082"), GinMode: getenv("GIN_MODE", "release"), Auth: AuthConfig{ Enabled: getenvBool("AUTH_ENABLED", false), }, Firebase: FirebaseConfig{ ProjectID: getenv("FIREBASE_PROJECT_ID", ""), CredentialsJSON: os.Getenv("FIREBASE_CREDENTIALS_JSON"), CredentialsFile: os.Getenv("FIREBASE_CREDENTIALS_FILE"), }, DB: DBConfig{ Host: must("PGHOST"), Port: must("POSTGRES_PORT"), User: must("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD"), Name: must("POSTGRES_DB"), SSLMode: getenv("PGSSLMODE", "disable"), AppName: getenv("DB_APP_NAME", "current-api"), }, } if n := getenv("DB_MAX_CONNS", ""); n != "" { if v, err := strconv.Atoi(n); err == nil && v > 0 { cfg.DB.MaxConns = int32(v) } } if cfg.Auth.Enabled { if cfg.Firebase.ProjectID == "" { panic("missing FIREBASE_PROJECT_ID with auth enabled") } if _, err := cfg.Firebase.CredentialsBytes(); err != nil { panic("invalid firebase credentials: " + err.Error()) } } return cfg } func (d DBConfig) DSN() string { // DATABASE_URL wins if present if raw := os.Getenv("DATABASE_URL"); raw != "" { return raw } u := &url.URL{ Scheme: "postgres", User: url.UserPassword(d.User, d.Password), Host: net.JoinHostPort(d.Host, d.Port), Path: "/" + d.Name, } q := url.Values{} q.Set("sslmode", d.SSLMode) if d.AppName != "" { q.Set("application_name", d.AppName) } u.RawQuery = q.Encode() return u.String() } func RedactDSN(raw string) string { u, err := url.Parse(raw) if err != nil || u.User == nil { return raw } if _, ok := u.User.Password(); ok { u.User = url.UserPassword(u.User.Username(), "*****") } return u.String() } func getenv(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func must(k string) string { v := os.Getenv(k) if v == "" { panic("missing required env: " + k) } return v } func getenvBool(k string, def bool) bool { if v := os.Getenv(k); v != "" { if b, err := strconv.ParseBool(v); err == nil { return b } } return def } func (f FirebaseConfig) CredentialsBytes() ([]byte, error) { if path := strings.TrimSpace(f.CredentialsFile); path != "" { return os.ReadFile(path) } raw := strings.TrimSpace(f.CredentialsJSON) if raw == "" { return nil, errors.New("missing FIREBASE_CREDENTIALS_JSON or FIREBASE_CREDENTIALS_FILE") } if json.Valid([]byte(raw)) { return []byte(raw), nil } decoded, err := base64.StdEncoding.DecodeString(raw) if err != nil { return nil, errors.New("firebase credentials must be JSON or base64-encoded JSON") } if !json.Valid(decoded) { return nil, errors.New("decoded firebase credentials are not valid JSON") } return decoded, nil }