| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 |
- """
- RefreshToken model - stores refresh tokens for users.
- """
- from datetime import datetime, timezone
- from sqlalchemy import DateTime, ForeignKey, String
- from sqlalchemy.dialects.postgresql import JSONB
- from sqlalchemy.orm import Mapped, mapped_column, relationship
- from app.models.base import Base
- class RefreshToken(Base):
- """
- Refresh token model for JWT token rotation.
- Stores hashed refresh tokens and device info.
- Tokens can be revoked by setting revoked_at.
- """
- __tablename__ = "refresh_tokens"
- id: Mapped[int] = mapped_column(primary_key=True)
- user_id: Mapped[int] = mapped_column(
- ForeignKey("users.id", ondelete="CASCADE"), nullable=False
- )
- # Token (should be hashed in production, but for MVP we'll store plain)
- token: Mapped[str] = mapped_column(String(512), unique=True, nullable=False)
- expires_at: Mapped[datetime] = mapped_column(
- DateTime(timezone=True), nullable=False
- )
- # Device tracking (user agent, IP, etc.)
- device_info: Mapped[dict | None] = mapped_column(JSONB)
- # Revocation
- revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
- # Relationships
- user: Mapped["User"] = relationship("User", back_populates="refresh_tokens")
- @property
- def is_valid(self) -> bool:
- """Check if token is valid (not expired and not revoked)."""
- now = datetime.now(timezone.utc)
- return self.expires_at > now and self.revoked_at is None
- def __repr__(self) -> str:
- return f"<RefreshToken {self.id} for user {self.user_id}>"
|