193e0d3c77
* feat: ratelimit, ghcr package and proper project refactor from askarzh * fix(bridge): validate media path * update: dependencies * fix: added LID to contact number migration
138 lines
3.5 KiB
Go
138 lines
3.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
type dbConfig struct {
|
|
User string
|
|
Pass string
|
|
Host string
|
|
Port string
|
|
IsPostgres bool
|
|
}
|
|
|
|
type Config struct {
|
|
DB dbConfig
|
|
JWTSecret []byte
|
|
APIKey string
|
|
WebhookUrl string
|
|
Host string
|
|
Port int
|
|
AuthLoginRate string
|
|
}
|
|
|
|
func LoadConfig() (*Config, error) {
|
|
isPostgres := os.Getenv("IS_POSTGRES") == "true"
|
|
|
|
var user, pass, host, port string
|
|
if isPostgres {
|
|
var ok bool
|
|
user, ok = os.LookupEnv("POSTGRES_USER")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing POSTGRES_USER")
|
|
}
|
|
pass, ok = os.LookupEnv("POSTGRES_PASS")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing POSTGRES_PASS")
|
|
}
|
|
host, ok = os.LookupEnv("POSTGRES_HOST")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing POSTGRES_HOST")
|
|
}
|
|
port, ok = os.LookupEnv("POSTGRES_PORT")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing POSTGRES_PORT")
|
|
}
|
|
}
|
|
|
|
jwtSecret, ok := lookupEither("WHATSAPP_JWT_SECRET", "JWT_SECRET")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing WHATSAPP_JWT_SECRET")
|
|
}
|
|
apiKey, ok := lookupEither("WHATSAPP_API_KEY", "API_KEY")
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing WHATSAPP_API_KEY")
|
|
}
|
|
if err := validateSecret("WHATSAPP_JWT_SECRET (or deprecated alias JWT_SECRET)", jwtSecret); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateSecret("WHATSAPP_API_KEY (or deprecated alias API_KEY)", apiKey); err != nil {
|
|
return nil, err
|
|
}
|
|
webhookUrl := os.Getenv("WEBHOOK_URL")
|
|
|
|
serverHost := os.Getenv("HOST")
|
|
serverPort := 8080
|
|
if v, ok := os.LookupEnv("PORT"); ok {
|
|
p, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid PORT %q: %w", v, err)
|
|
}
|
|
serverPort = p
|
|
}
|
|
|
|
authLoginRate := os.Getenv("AUTH_LOGIN_RATE") // parsed in auth package; empty -> default
|
|
|
|
return &Config{
|
|
DB: dbConfig{
|
|
User: user,
|
|
Pass: pass,
|
|
Host: host,
|
|
Port: port,
|
|
IsPostgres: isPostgres,
|
|
},
|
|
JWTSecret: []byte(jwtSecret),
|
|
APIKey: apiKey,
|
|
WebhookUrl: webhookUrl,
|
|
Host: serverHost,
|
|
Port: serverPort,
|
|
AuthLoginRate: authLoginRate,
|
|
}, nil
|
|
}
|
|
|
|
const minSecretLen = 32
|
|
|
|
// knownPlaceholders are the example values shipped in docker-compose.yaml.
|
|
// Operators who forget to override them must see startup fail loudly.
|
|
var knownPlaceholders = []string{
|
|
"c3VwZXItbG9uZy1yYW5kb20tc3RyaW5nLW1pbmltdW0tb2YtNjQtY2hhcmFjdGVycy15b3UtbmVlZC10by1wYXN0ZS1oZXJl",
|
|
"YW5vdGhlci1zdXBlci1sb25nLXJhbmRvbS1zdHJpbmctbWluaW11bS1vZi02NC1jaGFyYWN0ZXJzLXlvdS1uZWVkLXRvLXBhc3RlLWhlcmU=",
|
|
}
|
|
|
|
func validateSecret(name, value string) error {
|
|
for _, ph := range knownPlaceholders {
|
|
if value == ph {
|
|
return fmt.Errorf("%s is set to a placeholder value; generate a real one with `openssl rand -base64 48`", name)
|
|
}
|
|
}
|
|
if len(value) < minSecretLen {
|
|
return fmt.Errorf("%s is too short (%d chars, need ≥%d)", name, len(value), minSecretLen)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// envWarnFn is replaced in tests; in production it logs via slog.
|
|
var envWarnFn = func(msg string, args ...any) {
|
|
slog.Warn(msg, args...)
|
|
}
|
|
|
|
// lookupEither returns the value for `primary` if set, otherwise falls back
|
|
// to `deprecated` and emits a deprecation warning. Returns ("", false) only
|
|
// when neither is set.
|
|
func lookupEither(primary, deprecated string) (string, bool) {
|
|
if v, ok := os.LookupEnv(primary); ok {
|
|
return v, true
|
|
}
|
|
if v, ok := os.LookupEnv(deprecated); ok {
|
|
envWarnFn("env var is deprecated, use the new name",
|
|
"deprecated", deprecated,
|
|
"use_instead", primary)
|
|
return v, true
|
|
}
|
|
return "", false
|
|
}
|