Files
denya-onecare/app/core/config.py
T
root 3ae1062d65 P0 security batch: admin-only user mgmt, unified role model, login rate limiting, webhook secret, security headers, pagination caps
- Remove POST /api/auth/register (404); no sign-up UI; users are admin-managed
- Add admin-only POST/PATCH/DELETE /api/auth/users (forced canonical roles,
  self-lockout + reference guards)
- Unify role model in app/core/roles.py; reject unknown roles at creation and
  at login/JWT validation; startup normalizes unambiguous legacy aliases
- Login rate limiting ~5 fails/15 min per IP+email -> 429 (in-process, tunable)
- WhatsApp webhook requires X-Webhook-Secret; fail-closed when env unset;
  GET handshake uses constant-time verify token (403 on mismatch)
- GET /api/whatsapp/mock-log now requires auth
- Security headers middleware: X-Frame-Options DENY, nosniff, CSP on HTML,
  HSTS behind TLS
- Pagination: limit alias for page_size, hard cap enforced, both -> 422
2026-09-08 10:36:16 +00:00

70 lines
3.3 KiB
Python

"""Application configuration via Pydantic-settings environment variables."""
from __future__ import annotations
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── App ──────────────────────────────────────────────────────────
APP_NAME: str = "Denya OneCare"
DEBUG: bool = False
# ── Database ─────────────────────────────────────────────────────
DATABASE_URL: str = "sqlite+aiosqlite:///./denya_onecare.db"
# ── Auth ─────────────────────────────────────────────────────────
SECRET_KEY: str = ""
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 # Phase 1: raised 30 -> 60 for fewer re-logins
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days
# ── CORS ─────────────────────────────────────────────────────────
CORS_ORIGINS: str = "*"
# ── WhatsApp ─────────────────────────────────────────────────────
WHATSAPP_PHONE_NUMBER_ID: str = ""
WHATSAPP_ACCESS_TOKEN: str = ""
WHATSAPP_VERIFY_TOKEN: str = ""
META_GRAPH_BASE: str = "https://graph.facebook.com/v18.0"
# Shared secret for inbound webhook POSTs (header ``X-Webhook-Secret``).
# Fail-closed: when unset/empty the webhook rejects every message.
WHATSAPP_WEBHOOK_SECRET: str = ""
# ── Login rate limiting ──────────────────────────────────────────
LOGIN_RATE_LIMIT_MAX_ATTEMPTS: int = 5
LOGIN_RATE_LIMIT_WINDOW_SECONDS: int = 15 * 60
# ── Paths ────────────────────────────────────────────────────────
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
settings = Settings()
# ── Fail-closed secret validation (HARDENING.md P0.1) ─────────────────
# Refuse to boot without a real SECRET_KEY. Devs must create a local .env
# (see .env.example); production injects it via docker-compose env_file.
_KNOWN_PLACEHOLDER_SECRETS = {
"",
"change-me-in-production",
"change-me-in-production-use-a-real-secret",
"changeme",
"secret",
}
if settings.SECRET_KEY in _KNOWN_PLACEHOLDER_SECRETS or len(settings.SECRET_KEY) < 32:
raise RuntimeError(
"SECRET_KEY is missing, a known placeholder, or shorter than 32 chars. "
"Generate one with: openssl rand -hex 32 — and set it in .env "
"(dev) or the runtime environment (prod). Refusing to start."
)