updates on the otp updates on the customer app

This commit is contained in:
2026-09-15 11:50:12 +05:30
parent e8f4c0a593
commit 89321c9e06
17 changed files with 1072 additions and 69 deletions

View File

@@ -1,7 +1,13 @@
package config
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"strings"
"doormile/utils"
)
type Config struct {
@@ -52,25 +58,93 @@ type Config struct {
}
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", "Package@321#"),
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", ""),
JWTSecret: getEnv("JWT_SECRET_KEY", "DoormileSuperSecretJWTKey2026!"),
NatsURL: getEnv("NATS_URL", "nats://66.116.226.161:4223"),
NatsUser: getEnv("NATS_USER", "doormile"),
NatsPassword: getEnv("NATS_PASSWORD", "Package@321#"),
AILayerBaseURL: getEnv("AI_LAYER_BASE_URL", "https://routemate.workolik.com"),
cfg := load()
cfg.hardenSecrets()
return cfg
}
RouteOptimizerURL: getEnv("ROUTE_OPTIMIZER_URL", "https://routes.workolik.com"),
// 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", ""),

142
config/secrets_test.go Normal file
View File

@@ -0,0 +1,142 @@
package config
import (
"strings"
"testing"
)
// DM-06: config.go used to default JWT_SECRET_KEY, the NATS URL and its
// credentials, and the AI/optimiser hosts to the REAL production values. Two
// consequences: anyone holding the repository could mint a valid token for any
// account against a deployment that had not overridden the secret, and any
// local run silently joined the live NATS stream.
// The literals that must never come back. Written out so a revert is a test
// failure rather than something noticed in review.
func TestProductionValuesAreNotDefaults(t *testing.T) {
for _, key := range []string{
"JWT_SECRET_KEY", "NATS_URL", "NATS_USER", "NATS_PASSWORD",
"AI_LAYER_BASE_URL", "ROUTE_OPTIMIZER_URL", "DB_PASSWORD",
} {
setEnv(t, key, "")
}
setEnv(t, "ENV", "development")
cfg := Load()
banned := map[string]string{
"NatsURL": cfg.NatsURL,
"NatsUser": cfg.NatsUser,
"NatsPassword": cfg.NatsPassword,
"AILayerBaseURL": cfg.AILayerBaseURL,
"RouteOptimizerURL": cfg.RouteOptimizerURL,
"DBPassword": cfg.DBPassword,
}
for field, got := range banned {
if got != "" {
t.Errorf("%s defaulted to %q — production values must not be defaults", field, got)
}
}
// The old hardcoded secret must not be what we sign with.
if cfg.JWTSecret == "DoormileSuperSecretJWTKey2026!" {
t.Error("JWTSecret fell back to the literal that used to be in config.go")
}
}
// With no secret configured outside production the service still runs, but on
// a key that exists only for this process.
func TestUnsetSecretOutsideProductionIsEphemeralNotShared(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "")
setEnv(t, "ENV", "development")
first := Load().JWTSecret
second := Load().JWTSecret
if first == "" || second == "" {
t.Fatal("an unset secret produced an empty signing key; tokens would be forgeable")
}
if first == second {
t.Error("two loads produced the same generated key — it is not ephemeral")
}
if len(first) < 32 {
t.Errorf("generated key is %d chars, too short to be a signing key", len(first))
}
}
// In production an absent secret is NOT quietly replaced — it is left absent so
// Validate can refuse the boot with a message that says why. Silently
// generating one would invalidate every live session on each restart.
func TestProductionRefusesToStartWithoutASecret(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "")
setEnv(t, "ENV", "production")
cfg := Load()
if cfg.JWTSecret != "" {
t.Errorf("production generated a secret (%q); it must stay empty so Validate fails", cfg.JWTSecret)
}
err := cfg.Validate()
if err == nil {
t.Fatal("Validate accepted an empty JWT secret in production")
}
if !strings.Contains(err.Error(), "JWT_SECRET_KEY") {
t.Errorf("Validate error does not name the variable: %v", err)
}
}
// Outside production the ephemeral key is enough to pass validation, so local
// development needs no configuration at all.
func TestValidatePassesOutsideProductionWithNoSecret(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "")
setEnv(t, "ENV", "development")
if err := Load().Validate(); err != nil {
t.Errorf("development should boot without a configured secret: %v", err)
}
}
// A configured secret always validates, production or not.
func TestValidatePassesWithAConfiguredSecret(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "a-real-configured-secret")
setEnv(t, "ENV", "production")
if err := Load().Validate(); err != nil {
t.Errorf("a configured secret must validate: %v", err)
}
}
// An explicitly configured secret is always used verbatim.
func TestConfiguredSecretIsUsedVerbatim(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "a-real-configured-secret")
setEnv(t, "ENV", "production")
if got := Load().JWTSecret; got != "a-real-configured-secret" {
t.Errorf("JWTSecret = %q, want the configured value", got)
}
}
func TestIsProductionIsCaseAndSpaceInsensitive(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "x")
for _, env := range []string{"production", "Production", "PRODUCTION", " production "} {
setEnv(t, "ENV", env)
if !Load().IsProduction() {
t.Errorf("ENV=%q was not treated as production", env)
}
}
for _, env := range []string{"development", "staging", "test", ""} {
setEnv(t, "ENV", env)
if Load().IsProduction() {
t.Errorf("ENV=%q was treated as production", env)
}
}
}
// The geocoder is a PUBLIC service, not a Doormile host, so it keeps its
// default — removing it would break place search for no security gain.
func TestGeocoderKeepsItsPublicDefault(t *testing.T) {
setEnv(t, "JWT_SECRET_KEY", "x")
setEnv(t, "GEOCODER_URL", "")
if got := Load().GeocoderURL; !strings.Contains(got, "nominatim") {
t.Errorf("GeocoderURL = %q, want the public Nominatim default", got)
}
}