feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
114
backend/app/database.py
Normal file
114
backend/app/database.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Database engine, session factory, and dependency injection."""
|
||||
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""Base class for all SQLAlchemy ORM models."""
|
||||
pass
|
||||
|
||||
|
||||
# Primary engine using postgres superuser (for migrations/admin)
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=settings.DEBUG,
|
||||
pool_pre_ping=True,
|
||||
pool_size=settings.DB_ADMIN_POOL_SIZE,
|
||||
max_overflow=settings.DB_ADMIN_MAX_OVERFLOW,
|
||||
)
|
||||
|
||||
# App user engine (enforces RLS — no superuser bypass)
|
||||
app_engine = create_async_engine(
|
||||
settings.APP_USER_DATABASE_URL,
|
||||
echo=settings.DEBUG,
|
||||
pool_pre_ping=True,
|
||||
pool_size=settings.DB_POOL_SIZE,
|
||||
max_overflow=settings.DB_MAX_OVERFLOW,
|
||||
)
|
||||
|
||||
# Session factory for the app_user connection (RLS enforced)
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
app_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
# Admin session factory (for bootstrap/migrations only)
|
||||
AdminAsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
|
||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""
|
||||
Dependency that yields an async database session using app_user (RLS enforced).
|
||||
|
||||
The tenant context (SET LOCAL app.current_tenant) must be set by
|
||||
tenant_context middleware before any tenant-scoped queries.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def get_admin_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""
|
||||
Dependency that yields an admin database session (bypasses RLS).
|
||||
USE ONLY for bootstrap operations and internal system tasks.
|
||||
"""
|
||||
async with AdminAsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def set_tenant_context(session: AsyncSession, tenant_id: Optional[str]) -> None:
|
||||
"""
|
||||
Set the PostgreSQL session variable for RLS enforcement.
|
||||
|
||||
This MUST be called before any tenant-scoped query to activate RLS policies.
|
||||
Uses SET LOCAL so the context resets at transaction end.
|
||||
"""
|
||||
if tenant_id:
|
||||
# Allow 'super_admin' as a special RLS context value for cross-tenant access.
|
||||
# Otherwise validate tenant_id is a valid UUID to prevent SQL injection.
|
||||
# SET LOCAL cannot use parameterized queries in PostgreSQL.
|
||||
if tenant_id != "super_admin":
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid tenant_id format: {tenant_id!r}")
|
||||
await session.execute(text(f"SET LOCAL app.current_tenant = '{tenant_id}'"))
|
||||
else:
|
||||
# For super_admin users: set empty string which will not match any tenant
|
||||
# The super_admin uses the admin engine which bypasses RLS
|
||||
await session.execute(text("SET LOCAL app.current_tenant = ''"))
|
||||
Reference in New Issue
Block a user