Arag / app /models /email_verification.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
2.26 kB
"""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"<EmailVerification user={self.user_id} purpose={self.purpose} valid={self.is_valid}>"