import uuid from datetime import datetime, timezone from typing import Optional, TYPE_CHECKING from sqlalchemy import String, DateTime, ForeignKey, Boolean, Integer, CheckConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base if TYPE_CHECKING: from app.models.session import Session from app.models.user import User from app.models.account import Account class SessionShare(Base): __tablename__ = "session_shares" __table_args__ = ( CheckConstraint( "visibility IN ('public', 'account')", name='ck_session_shares_visibility' ), ) id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) session_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False, index=True ) account_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True, comment="Account that owns this share (denormalized from session at creation)" ) share_token: Mapped[str] = mapped_column( String(64), unique=True, nullable=False, index=True, comment="URL-safe random token (48 bytes -> 64 base64 chars)" ) share_name: Mapped[Optional[str]] = mapped_column( String(100), nullable=True, comment="Optional label: 'Training link', 'Customer escalation #1234'" ) visibility: Mapped[str] = mapped_column( String(20), nullable=False, default="public", comment="public = anyone with link, account = account members only" ) created_by: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc) ) expires_at: Mapped[Optional[datetime]] = mapped_column( DateTime(timezone=True), nullable=True, index=True, comment="Optional expiration for time-limited shares" ) view_count: Mapped[int] = mapped_column( Integer, nullable=False, default=0 ) last_viewed_at: Mapped[Optional[datetime]] = mapped_column( DateTime(timezone=True), nullable=True ) is_active: Mapped[bool] = mapped_column( Boolean, nullable=False, default=True, index=True ) # Relationships session: Mapped["Session"] = relationship("Session", back_populates="shares") account: Mapped["Account"] = relationship("Account") creator: Mapped["User"] = relationship("User", foreign_keys=[created_by]) views: Mapped[list["SessionShareView"]] = relationship( "SessionShareView", back_populates="share", cascade="all, delete-orphan" ) class SessionShareView(Base): __tablename__ = "session_share_views" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) share_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("session_shares.id", ondelete="CASCADE"), nullable=False, index=True ) session_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False, index=True, comment="Denormalized from share for analytics queries" ) viewer_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True, comment="NULL for public shares (unauthenticated views)" ) viewed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True ) viewer_ip: Mapped[Optional[str]] = mapped_column( String(45), # IPv6 max length nullable=True ) viewer_user_agent: Mapped[Optional[str]] = mapped_column( String(500), nullable=True ) # Relationships share: Mapped["SessionShare"] = relationship("SessionShare", back_populates="views") viewer: Mapped[Optional["User"]] = relationship("User")