diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..d3a41f0 --- /dev/null +++ b/backend/.env.example @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py index 0f85f90..4ddd8d2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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") - try: - from cryptography.fernet import Fernet - Fernet(broker_token_key.encode("utf-8")) - except Exception: + 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: 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( - "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())\"" + 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(): diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 813cbac..733b7fe 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -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()