""" Database Models and Session Management Uses SQLAlchemy async ORM for database operations. Keeps existing schema: users, audit_log tables. """ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy import String, Integer, Boolean, DateTime, Text, func, select from datetime import datetime from typing import Optional import os # Database URL from environment DATABASE_URL = os.getenv( "DATABASE_URL", "sqlite+aiosqlite:///./oliver_metadata.db" ) # Create async engine engine = create_async_engine( DATABASE_URL, echo=os.getenv("DEBUG") == "true", # Log SQL queries in debug mode future=True ) # Create async session factory AsyncSessionLocal = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, autocommit=False, autoflush=False ) # Base class for models class Base(DeclarativeBase): pass # ===== Models ===== class User(Base): """User model - keeps existing schema from Flask app""" __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True) password_hash: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # Nullable for SSO users email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) full_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) auth_method: Mapped[str] = mapped_column(String(20), default="local", nullable=False) # 'local' or 'sso' is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) def to_dict(self): """Convert model to dict for JSON serialization""" return { "id": self.id, "username": self.username, "email": self.email, "full_name": self.full_name, "auth_method": self.auth_method, "is_active": self.is_active, "created_at": self.created_at.isoformat() if self.created_at else None, "last_login": self.last_login.isoformat() if self.last_login else None, } class AuditLog(Base): """Audit log model - tracks user actions""" __tablename__ = "audit_log" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) action: Mapped[str] = mapped_column(String(100), nullable=False, index=True) details: Mapped[Optional[str]] = mapped_column(Text, nullable=True) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True) def to_dict(self): """Convert model to dict for JSON serialization""" return { "id": self.id, "user_id": self.user_id, "action": self.action, "details": self.details, "timestamp": self.timestamp.isoformat() if self.timestamp else None, } # ===== Database Initialization ===== async def init_db(): """ Initialize database - create tables if they don't exist. Called on application startup. """ async with engine.begin() as conn: # Create all tables await conn.run_sync(Base.metadata.create_all) # ===== Database Session Dependency ===== async def get_db() -> AsyncSession: """ FastAPI dependency to get database session. Use as: db: AsyncSession = Depends(get_db) """ async with AsyncSessionLocal() as session: try: yield session finally: await session.close() # ===== Database Helper Functions ===== class UserRepository: """Repository pattern for User operations""" @staticmethod async def get_by_id(db: AsyncSession, user_id: int) -> Optional[User]: """Get user by ID""" result = await db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none() @staticmethod async def get_by_username(db: AsyncSession, username: str) -> Optional[User]: """Get user by username""" result = await db.execute(select(User).where(User.username == username)) return result.scalar_one_or_none() @staticmethod async def get_by_email(db: AsyncSession, email: str) -> Optional[User]: """Get user by email""" result = await db.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() @staticmethod async def create_user( db: AsyncSession, username: str, password_hash: Optional[str], email: Optional[str], full_name: Optional[str], auth_method: str = "local" ) -> User: """Create new user""" user = User( username=username, password_hash=password_hash, email=email, full_name=full_name, auth_method=auth_method, is_active=True ) db.add(user) await db.commit() await db.refresh(user) return user @staticmethod async def update_last_login(db: AsyncSession, user_id: int): """Update user's last login timestamp""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user: user.last_login = datetime.utcnow() await db.commit() @staticmethod async def get_all_users(db: AsyncSession) -> list[User]: """Get all users""" result = await db.execute(select(User)) return list(result.scalars().all()) class AuditLogRepository: """Repository pattern for AuditLog operations""" @staticmethod async def log_action( db: AsyncSession, user_id: int, action: str, details: Optional[str] = None ) -> AuditLog: """Create audit log entry""" log_entry = AuditLog( user_id=user_id, action=action, details=details ) db.add(log_entry) await db.commit() await db.refresh(log_entry) return log_entry @staticmethod async def get_user_activity( db: AsyncSession, user_id: int, limit: int = 100 ) -> list[AuditLog]: """Get user activity logs""" result = await db.execute( select(AuditLog) .where(AuditLog.user_id == user_id) .order_by(AuditLog.timestamp.desc()) .limit(limit) ) return list(result.scalars().all()) @staticmethod async def get_all_activity( db: AsyncSession, limit: int = 1000 ) -> list[AuditLog]: """Get all activity logs""" result = await db.execute( select(AuditLog) .order_by(AuditLog.timestamp.desc()) .limit(limit) ) return list(result.scalars().all())