Files
doormile_backend/config/config.go
Suriya 2c26cbe4ba fix: panic recovery, rate limiting, transaction error handling, pagination
Hardening pass over the API surface. No route's auth requirements change.

Resilience:
- Add recover middleware. There was none, so an unhandled panic in any
  handler propagated out of the process instead of becoming a 500.
- Add a centralized ErrorHandler so errors and recovered panics return the
  same {success,message} envelope as the utils helpers, not Fiber's default
  plain-text body. 5xx responses are logged with method and path.

Rate limiting:
- Global 300/min per IP as an abuse backstop, exempting health/readiness
  probes and websocket upgrades.
- 10/min shared across every credential endpoint (customer/miler/admin/hub
  login, verify-pin, reset-pin, email OTP). PINs are 4 digits, so the whole
  keyspace was previously walkable in seconds. One shared limiter instance
  means rotating between endpoints doesn't reset the budget.
- Add TRUSTED_PROXIES config. Limits key on c.IP(), which behind a TLS
  terminator is the proxy, collapsing every client into one bucket. When set,
  X-Forwarded-For is honoured only from those proxies so the header can't be
  spoofed to dodge the limit. Logs a warning when unset.

Transactions:
- Check the error on all 51 previously-unchecked tx.Save/Create/Delete/
  Model(...).Update/Commit calls across 6 controllers. A failed write inside
  a transaction was silently ignored and the request still reported success;
  an unchecked Commit could fail with the caller told everything worked.
  Each site now rolls back and returns a specific message.

Pagination:
- Add utils.ParsePage/Paginated, reusing the pageno/pagesize convention
  GetAdminBookings already established. Default 500, hard cap 1000.
- Apply to the previously unbounded consignments, tripsheets, exceptions,
  app-users and clients endpoints. Defaults are high so existing consoles
  that don't paginate keep working; the cap only stops a growing table from
  being loaded wholesale. total is now a real COUNT, not len(data).
- GetClients also loaded the entire auth table to join in memory; it now
  fetches only the current page's rows.

Tests (first in the repo):
- Extract the hyperlocal pincode rule out of BookingPickupComplete into
  isHyperlocal so it is testable, covering the short/empty pincode fallback.
- Cover calculateVolumetricWeight and the ParsePage clamping rules.

Repo hygiene:
- Tag scratch/*.go with //go:build ignore. Each declared its own main(), so
  `go build ./...` failed on redeclaration; it now passes repo-wide.
- Untrack scratch/node_modules (216 files) and ignore node_modules, test
  artifacts, and the `doormile` binary `go build .` emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:13:39 +05:30

71 lines
2.2 KiB
Go

package config
import (
"os"
)
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)
// 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 {
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"),
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
}