"""Author RAG — Email Verification Token Model. Used for: password reset requests. Token is HMAC-signed (same pattern as subscription tokens), single-use (used_at is set on consumption), and expires after 1 hour. NOT used for email verification at signup — accounts are created only after successful Stripe payment, so the email is already confirmed via the Stripe Checkout flow. """ from datetime import datetime from sqlalchemy import Boolean, DateTime, String from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, generate_uuid class EmailVerification(Base): """Single-use HMAC-signed token for password reset.""" __tablename__ = "email_verifications" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) user_id: Mapped[str] = mapped_column( String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) # HMAC-signed token — never store the raw token, only the hash token_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) purpose: Mapped[str] = mapped_column( String(30), nullable=False, default="password_reset" ) # "password_reset" | "email_change" # Lifecycle created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) expires_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, index=True ) # Typically now + 1 hour used_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) # Set on consumption — single-use enforcement # IP for audit trail requested_from_ip: Mapped[str | None] = mapped_column(String(50), nullable=True) @property def is_expired(self) -> bool: from datetime import timezone return datetime.now(timezone.utc) > self.expires_at @property def is_used(self) -> bool: return self.used_at is not None @property def is_valid(self) -> bool: return not self.is_expired and not self.is_used def __repr__(self) -> str: return f""