refresh_token.py 1.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. """
  2. RefreshToken model - stores refresh tokens for users.
  3. """
  4. from datetime import datetime, timezone
  5. from sqlalchemy import DateTime, ForeignKey, String
  6. from sqlalchemy.dialects.postgresql import JSONB
  7. from sqlalchemy.orm import Mapped, mapped_column, relationship
  8. from app.models.base import Base
  9. class RefreshToken(Base):
  10. """
  11. Refresh token model for JWT token rotation.
  12. Stores hashed refresh tokens and device info.
  13. Tokens can be revoked by setting revoked_at.
  14. """
  15. __tablename__ = "refresh_tokens"
  16. id: Mapped[int] = mapped_column(primary_key=True)
  17. user_id: Mapped[int] = mapped_column(
  18. ForeignKey("users.id", ondelete="CASCADE"), nullable=False
  19. )
  20. # Token (should be hashed in production, but for MVP we'll store plain)
  21. token: Mapped[str] = mapped_column(String(512), unique=True, nullable=False)
  22. expires_at: Mapped[datetime] = mapped_column(
  23. DateTime(timezone=True), nullable=False
  24. )
  25. # Device tracking (user agent, IP, etc.)
  26. device_info: Mapped[dict | None] = mapped_column(JSONB)
  27. # Revocation
  28. revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
  29. # Relationships
  30. user: Mapped["User"] = relationship("User", back_populates="refresh_tokens")
  31. @property
  32. def is_valid(self) -> bool:
  33. """Check if token is valid (not expired and not revoked)."""
  34. now = datetime.now(timezone.utc)
  35. return self.expires_at > now and self.revoked_at is None
  36. def __repr__(self) -> str:
  37. return f"<RefreshToken {self.id} for user {self.user_id}>"