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()
|
env_name = _environment_name()
|
||||||
if env_name not in PRODUCTION_ENV_NAMES:
|
if env_name not in PRODUCTION_ENV_NAMES:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
|
||||||
broker_token_key = (os.getenv("BROKER_TOKEN_KEY") or "").strip()
|
broker_token_key = (os.getenv("BROKER_TOKEN_KEY") or "").strip()
|
||||||
if not broker_token_key:
|
if not broker_token_key:
|
||||||
raise RuntimeError("BROKER_TOKEN_KEY must be configured in production")
|
missing.append("BROKER_TOKEN_KEY")
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
Fernet(broker_token_key.encode("utf-8"))
|
Fernet(broker_token_key.encode("utf-8"))
|
||||||
except Exception:
|
except Exception:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"BROKER_TOKEN_KEY is set but invalid — must be a 32-byte URL-safe base64 key. "
|
"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 (os.getenv("ENABLE_SUPER_ADMIN_BOOTSTRAP") or "").strip() in {"1", "true", "yes"}:
|
||||||
if not (os.getenv("SUPER_ADMIN_EMAIL") or "").strip():
|
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"))
|
RESET_OTP_TTL_MINUTES = int(os.getenv("RESET_OTP_TTL_MINUTES", "10"))
|
||||||
PASSWORD_HASHER = PasswordHasher()
|
PASSWORD_HASHER = PasswordHasher()
|
||||||
LEGACY_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
LEGACY_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||||
RESET_OTP_SECRET = (os.getenv("RESET_OTP_SECRET") or "").strip()
|
def _get_reset_otp_secret() -> str:
|
||||||
if not RESET_OTP_SECRET:
|
secret = (os.getenv("RESET_OTP_SECRET") or "").strip()
|
||||||
raise RuntimeError("RESET_OTP_SECRET must be configured")
|
if not secret:
|
||||||
|
raise RuntimeError("RESET_OTP_SECRET is not configured on this server")
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
def _now_utc() -> datetime:
|
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:
|
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()
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user