Config via env, abuse/resource limits, and production serving so the app is safe to expose publicly: - Fail-fast on missing SECRET_KEY when MULTI_USER=true - quickjs per-execution time/memory limits (while(true) can't hang server) - Per-user/per-IP rate limiting on turn/script/auth endpoints - Request body size limit + per-user row caps - Security headers (CSP, X-Frame-Options, nosniff, referrer-policy) incl. SSE - Debug router 403 and /docs disabled in multi-user mode - DATABASE_URL support (defaults to Neon Postgres) alongside SQLite - Documented all env vars in backend/.env.example Verified locally via uvicorn (MULTI_USER=1, SQLite); see plan/09-phase-hardening.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017e6tQuojBLYPetUfmhit4X
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
|
|
# AIDND_DB_PATH lets deployments (Docker volume, hosted disk) relocate the
|
|
# SQLite database; default stays backend/data.db for local runs. The parent
|
|
# directory also hosts the auto-generated secret.key (see security.py), so
|
|
# DB_PATH stays defined even when Postgres is in use.
|
|
_env_db_path = os.environ.get("AIDND_DB_PATH")
|
|
DB_PATH = (
|
|
Path(_env_db_path).resolve()
|
|
if _env_db_path
|
|
else Path(__file__).resolve().parent.parent / "data.db"
|
|
)
|
|
|
|
# AIDND_DATABASE_URL (or the platform-conventional DATABASE_URL) switches the
|
|
# app to a server database — any SQLAlchemy URL works, but Postgres is what
|
|
# hosted deploys use (Phase 9 decision: Neon). Unset = SQLite, as always.
|
|
DATABASE_URL = (
|
|
os.environ.get("AIDND_DATABASE_URL", "").strip()
|
|
or os.environ.get("DATABASE_URL", "").strip()
|
|
)
|
|
|
|
|
|
def _normalize_url(url: str) -> str:
|
|
"""Map the postgres:// / postgresql:// schemes hosts hand out to the
|
|
psycopg3 driver installed in requirements.txt."""
|
|
for prefix in ("postgres://", "postgresql://"):
|
|
if url.startswith(prefix):
|
|
return "postgresql+psycopg://" + url[len(prefix):]
|
|
return url
|
|
|
|
|
|
if DATABASE_URL:
|
|
engine = create_engine(
|
|
_normalize_url(DATABASE_URL),
|
|
# Serverless Postgres (Neon) suspends idle databases; pre-ping
|
|
# replaces silently-dead pooled connections instead of erroring.
|
|
pool_pre_ping=True,
|
|
# Store/read naive UTC like SQLite does, regardless of server default.
|
|
connect_args={"options": "-c timezone=UTC"},
|
|
)
|
|
else:
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
engine = create_engine(
|
|
f"sqlite:///{DB_PATH}",
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
|
|
@event.listens_for(engine, "connect")
|
|
def _enable_sqlite_foreign_keys(dbapi_connection, _record):
|
|
# SQLite ships with foreign keys OFF per connection; without this every
|
|
# ondelete=CASCADE/SET NULL in models.py is silently ignored.
|
|
cursor = dbapi_connection.cursor()
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
|
|
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|