165 lines
6.1 KiB
Go
165 lines
6.1 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"doormile/utils"
|
|
)
|
|
|
|
type Config struct {
|
|
Env string
|
|
Port string
|
|
DBName string
|
|
DBUser string
|
|
DBPassword string
|
|
DBPort string
|
|
DBHost string
|
|
RedisHost string
|
|
RedisPort string
|
|
RedisUser string
|
|
RedisPassword string
|
|
JWTSecret string
|
|
NatsURL string
|
|
NatsUser string
|
|
NatsPassword string
|
|
AILayerBaseURL string // AI decision-engine service base URL (e.g. http://rider-api:8082)
|
|
|
|
// RouteOptimizerURL is the Route Optimization API that orders a rider's
|
|
// stops (Valhalla-backed road sequencing). Empty disables sequencing: stops
|
|
// stay unordered rather than assignment failing.
|
|
RouteOptimizerURL string
|
|
|
|
// GeocoderURL is the Nominatim-compatible geocoding service the customer
|
|
// app'''s place search and reverse geocode are proxied through. Proxied on
|
|
// purpose: the legacy rider app shipped a Google Maps key inside the
|
|
// binary and it had to be revoked, so the customer app is never handed a
|
|
// key at all — it asks this service and this service asks the geocoder.
|
|
GeocoderURL string
|
|
// GeocoderEmail is the contact address Nominatim'''s usage policy asks
|
|
// callers to identify themselves with. Sent as the User-Agent contact;
|
|
// requests without one are throttled or blocked.
|
|
GeocoderEmail string
|
|
|
|
// TrustedProxies is a comma-separated list of reverse-proxy IPs/CIDRs that
|
|
// are allowed to set X-Forwarded-For. Rate limiting keys on the client IP,
|
|
// so behind a proxy this MUST be set — otherwise every request appears to
|
|
// come from the proxy and the whole fleet shares one limit bucket.
|
|
// Empty means "no proxy": the socket peer address is used as-is.
|
|
TrustedProxies string
|
|
SMTPHost string
|
|
SMTPPort string
|
|
SMTPUser string
|
|
SMTPPassword string
|
|
SMTPFrom string
|
|
}
|
|
|
|
func Load() *Config {
|
|
cfg := load()
|
|
cfg.hardenSecrets()
|
|
return cfg
|
|
}
|
|
|
|
// IsProduction reports whether this process is running as production. Used by
|
|
// the guards that must behave differently there — a fixed OTP, a missing JWT
|
|
// secret — rather than scattering string comparisons.
|
|
func (c *Config) IsProduction() bool {
|
|
return strings.EqualFold(strings.TrimSpace(c.Env), "production")
|
|
}
|
|
|
|
// hardenSecrets refuses to let the service run on a guessable signing key.
|
|
//
|
|
// JWT_SECRET_KEY used to default to a literal in this file. Anyone holding the
|
|
// repository could mint a token for any user id and any role, against any
|
|
// deployment that had not overridden it — which is the whole authorisation
|
|
// model, given away by a git clone.
|
|
//
|
|
// In production an unset secret is fatal: booting with a known key is worse
|
|
// than not booting, because nothing external shows that anything is wrong.
|
|
// Anywhere else it becomes a random per-process key, so local development
|
|
// works without configuration while tokens stop surviving a restart and can
|
|
// never be valid anywhere but this process.
|
|
func (c *Config) hardenSecrets() {
|
|
if strings.TrimSpace(c.JWTSecret) != "" {
|
|
return
|
|
}
|
|
// In production an absent secret is left absent, so Validate can refuse the
|
|
// boot with a clear message. Generating one here would be worse than the
|
|
// old default: every restart would invalidate every live session, and
|
|
// nothing would say why.
|
|
if c.IsProduction() {
|
|
return
|
|
}
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
// Leave it empty; Validate turns this into a refusal to start.
|
|
return
|
|
}
|
|
c.JWTSecret = hex.EncodeToString(b)
|
|
utils.Warn("JWT_SECRET_KEY is not set — generated an ephemeral key for this process only. " +
|
|
"Tokens will not survive a restart. Set JWT_SECRET_KEY for a stable local session.")
|
|
}
|
|
|
|
// Validate reports configuration that must prevent the service from starting.
|
|
// Called by main; kept separate from Load so that loading stays free of side
|
|
// effects and the package remains testable.
|
|
func (c *Config) Validate() error {
|
|
if strings.TrimSpace(c.JWTSecret) == "" {
|
|
return fmt.Errorf("JWT_SECRET_KEY is not set (ENV=%s): refusing to start, because "+
|
|
"booting on a default or empty signing key lets anyone holding this repository "+
|
|
"mint a valid token for any account", c.Env)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func load() *Config {
|
|
return &Config{
|
|
Env: getEnv("ENV", "development"),
|
|
Port: getEnv("APP_PORT", "8081"),
|
|
DBName: getEnv("DB_NAME", "logistics"),
|
|
DBUser: getEnv("DB_USER", "admin"),
|
|
DBPassword: getEnv("DB_PASSWORD", ""),
|
|
DBPort: getEnv("DB_PORT", "5433"),
|
|
DBHost: getEnv("DB_HOST", "127.0.0.1"),
|
|
RedisHost: getEnv("REDIS_HOST", "127.0.0.1"),
|
|
RedisPort: getEnv("REDIS_PORT", "6379"),
|
|
RedisUser: getEnv("REDIS_USER", ""),
|
|
RedisPassword: getEnv("REDIS_PASSWORD", ""),
|
|
// No default. See hardenSecrets below — an unset secret is either a
|
|
// refusal to boot or an ephemeral per-process key, never a shared one
|
|
// baked into the source.
|
|
JWTSecret: getEnv("JWT_SECRET_KEY", ""),
|
|
|
|
// These defaulted to the real production hosts and credentials, which
|
|
// meant `go run .` on a laptop silently joined the live NATS stream and
|
|
// competed with the production workers for the same durable consumer.
|
|
// Empty now: InitNATS skips connecting, routing.BaseURL == "" disables
|
|
// sequencing, and the AI layer falls back to legacy scoring. Fail
|
|
// closed, so reaching production is something you opt into.
|
|
NatsURL: getEnv("NATS_URL", ""),
|
|
NatsUser: getEnv("NATS_USER", ""),
|
|
NatsPassword: getEnv("NATS_PASSWORD", ""),
|
|
AILayerBaseURL: getEnv("AI_LAYER_BASE_URL", ""),
|
|
|
|
RouteOptimizerURL: getEnv("ROUTE_OPTIMIZER_URL", ""),
|
|
GeocoderURL: getEnv("GEOCODER_URL", "https://nominatim.openstreetmap.org"),
|
|
GeocoderEmail: getEnv("GEOCODER_EMAIL", ""),
|
|
TrustedProxies: getEnv("TRUSTED_PROXIES", ""),
|
|
SMTPHost: getEnv("SMTP_HOST", ""),
|
|
SMTPPort: getEnv("SMTP_PORT", "465"),
|
|
SMTPUser: getEnv("SMTP_USER", ""),
|
|
SMTPPassword: getEnv("SMTP_PASSWORD", ""),
|
|
SMTPFrom: getEnv("SMTP_FROM", ""),
|
|
}
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|