"""SQLAlchemy models — full schema for BulkGmailMailer. All OAuth secrets are stored encrypted (Fernet). Sensitive columns: oauth_tokens.access_token / refresh_token gmail_accounts.client_secret (per-account imported config) """ from datetime import datetime, timezone from sqlalchemy import (JSON, Boolean, Column, DateTime, Enum, Float, ForeignKey, Index, Integer, LargeBinary, String, Text, UniqueConstraint) from sqlalchemy.orm import DeclarativeBase, relationship from app.core import encryption def utcnow(): return datetime.now(timezone.utc) class Base(DeclarativeBase): pass class TimestampMixin: created_at = Column(DateTime(timezone=True), default=utcnow, nullable=False, index=True) updated_at = Column(DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False) class User(Base, TimestampMixin): __tablename__ = "users" id = Column(Integer, primary_key=True) email = Column(String(320), unique=True, nullable=False, index=True) password_hash = Column(String(255), nullable=False) full_name = Column(String(200), default="") is_admin = Column(Boolean, default=False, nullable=False) is_active = Column(Boolean, default=True, nullable=False) last_login_at = Column(DateTime(timezone=True)) gmail_accounts = relationship("GmailAccount", back_populates="user", cascade="all, delete-orphan") audit_logs = relationship("AuditLog", back_populates="user") class OAuthClientConfig(Base, TimestampMixin): """An imported Google OAuth client configuration (from client_secret JSON). Secrets stored encrypted. Multiple configs may exist (e.g. per project).""" __tablename__ = "oauth_client_configs" id = Column(Integer, primary_key=True) name = Column(String(200), nullable=False) client_id = Column(String(300), nullable=False) client_secret_enc = Column(String(600), nullable=False) # encrypted redirect_uri = Column(String(500), default="") project_id = Column(String(200), default="") is_active = Column(Boolean, default=True, nullable=False) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE")) @property def client_secret(self) -> str: return encryption.decrypt(self.client_secret_enc) @client_secret.setter def client_secret(self, value: str) -> None: