Harden server restart: remove import-time crashes, centralise secret validation
- auth_service.py: RESET_OTP_SECRET no longer crashes at import; read lazily inside _hash_otp() so the module always loads cleanly - main.py: _validate_runtime_secrets() now checks both BROKER_TOKEN_KEY and RESET_OTP_SECRET together, reporting all missing vars in one clear message - .env.example: documents every required/optional env var with generation commands With load_dotenv() + .env file, all secrets survive pm2 restarts automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0a7e038be9
commit
ae3a335ea1
31
backend/.env.example
Normal file
31
backend/.env.example
Normal file
@ -0,0 +1,31 @@
|
||||
# ── Required secrets (server will NOT start without these in production) ──────
|
||||
|
||||
# 32-byte URL-safe base64 Fernet key for encrypting Zerodha access tokens.
|
||||
# Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
BROKER_TOKEN_KEY=
|
||||
|
||||
# Secret used to HMAC-sign password-reset OTPs. Any long random string works.
|
||||
# Generate: python -c "import secrets; print(secrets.token_hex(32))"
|
||||
RESET_OTP_SECRET=
|
||||
|
||||
# ── Environment ───────────────────────────────────────────────────────────────
|
||||
APP_ENV=production
|
||||
CORS_ORIGINS=https://quantfortune.com,https://app.quantfortune.com
|
||||
|
||||
# ── Database ──────────────────────────────────────────────────────────────────
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/quantfortune
|
||||
|
||||
# ── Email (optional — auto-login OTP emails) ─────────────────────────────────
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
|
||||
# ── Zerodha auto-login (optional) ────────────────────────────────────────────
|
||||
# ZERODHA_API_KEY=
|
||||
# ZERODHA_API_SECRET=
|
||||
|
||||
# ── Tuning (all have safe defaults, only set if you need to override) ─────────
|
||||
# SESSION_TTL_SECONDS=604800
|
||||
# RESET_OTP_TTL_MINUTES=10
|
||||
# LIVE_EQUITY_SNAPSHOT_HOUR=15
|
||||
@ -108,16 +108,29 @@ def _validate_runtime_secrets():
|
||||
env_name = _environment_name()
|
||||
if env_name not in PRODUCTION_ENV_NAMES:
|
||||
return
|
||||
|
||||
missing = []
|
||||
|
||||
broker_token_key = (os.getenv("BROKER_TOKEN_KEY") or "").strip()
|
||||
if not broker_token_key:
|
||||
raise RuntimeError("BROKER_TOKEN_KEY must be configured in production")
|
||||
missing.append("BROKER_TOKEN_KEY")
|
||||
else:
|
||||
try:
|
||||
from cryptography.fernet import Fernet
|
||||
Fernet(broker_token_key.encode("utf-8"))
|
||||
except Exception:
|
||||
raise RuntimeError(
|
||||
"BROKER_TOKEN_KEY is set but invalid — must be a 32-byte URL-safe base64 key. "
|
||||
"Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
||||
"Generate: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
||||
)
|
||||
|
||||
if not (os.getenv("RESET_OTP_SECRET") or "").strip():
|
||||
missing.append("RESET_OTP_SECRET")
|
||||
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"Missing required environment variables: {', '.join(missing)}. "
|
||||
"Add them to the .env file in the backend directory."
|
||||
)
|
||||
if (os.getenv("ENABLE_SUPER_ADMIN_BOOTSTRAP") or "").strip() in {"1", "true", "yes"}:
|
||||
if not (os.getenv("SUPER_ADMIN_EMAIL") or "").strip():
|
||||
|
||||
@ -17,9 +17,11 @@ SESSION_REFRESH_WINDOW_SECONDS = int(
|
||||
RESET_OTP_TTL_MINUTES = int(os.getenv("RESET_OTP_TTL_MINUTES", "10"))
|
||||
PASSWORD_HASHER = PasswordHasher()
|
||||
LEGACY_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
RESET_OTP_SECRET = (os.getenv("RESET_OTP_SECRET") or "").strip()
|
||||
if not RESET_OTP_SECRET:
|
||||
raise RuntimeError("RESET_OTP_SECRET must be configured")
|
||||
def _get_reset_otp_secret() -> str:
|
||||
secret = (os.getenv("RESET_OTP_SECRET") or "").strip()
|
||||
if not secret:
|
||||
raise RuntimeError("RESET_OTP_SECRET is not configured on this server")
|
||||
return secret
|
||||
|
||||
|
||||
def _now_utc() -> datetime:
|
||||
@ -43,7 +45,7 @@ def _is_legacy_password_hash(password_hash: str | None) -> bool:
|
||||
|
||||
|
||||
def _hash_otp(email: str, otp: str) -> str:
|
||||
payload = f"{email}:{otp}:{RESET_OTP_SECRET}"
|
||||
payload = f"{email}:{otp}:{_get_reset_otp_secret()}"
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user