Files
saas_backend/app/models/system/user_session_model.py
T
2026-08-31 20:39:41 -04:00

51 lines
1.9 KiB
Python

import uuid
from sqlalchemy import Column, DateTime, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import INET, UUID
from app.config.database import Base
class UserSession(Base):
"""One signed-in session, and the refresh token currently standing for it.
Revocation used to live only in a Redis blacklist. That failed open — a
Redis outage made `verify_refresh_token` log a warning and accept the token
anyway — and it did not survive a flush, so every revoked token quietly came
back. It also meant nobody could be shown where they were signed in, because
nothing recorded it.
A session row is the durable fact. Redis stays useful as a fast negative
cache, but it is no longer the only thing standing between a stolen token
and an account.
"""
__tablename__ = "user_sessions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, index=True,
)
tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True)
current_jti = Column(UUID(as_uuid=True), nullable=False, unique=True, index=True)
previous_jti = Column(UUID(as_uuid=True), nullable=True, index=True)
user_agent = Column(String(512), nullable=True)
ip_address = Column(INET, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
last_used_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False)
revoked_at = Column(DateTime(timezone=True), nullable=True)
revoked_reason = Column(String(40), nullable=True)
@property
def is_active(self) -> bool:
return self.revoked_at is None
def __repr__(self) -> str:
return f"<UserSession {self.user_id} {'active' if self.is_active else 'revoked'}>"