From 04aac379ed3a250b3e5235c6540e633ee5b5d36b Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 5 Feb 2026 16:18:45 +0200 Subject: [PATCH] Add RefreshToken model for user session management --- surfsense_backend/app/db.py | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 5cdb712db..a4da1a575 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -1361,6 +1361,13 @@ if config.AUTH_TYPE == "GOOGLE": display_name = Column(String, nullable=True) avatar_url = Column(String, nullable=True) + # Refresh tokens for this user + refresh_tokens = relationship( + "RefreshToken", + back_populates="user", + cascade="all, delete-orphan", + ) + else: class User(SQLAlchemyBaseUserTableUUID, Base): @@ -1426,6 +1433,57 @@ else: display_name = Column(String, nullable=True) avatar_url = Column(String, nullable=True) + # Refresh tokens for this user + refresh_tokens = relationship( + "RefreshToken", + back_populates="user", + cascade="all, delete-orphan", + ) + + +class RefreshToken(Base, TimestampMixin): + """ + Stores refresh tokens for user session management. + + Refresh tokens are long-lived tokens (2 weeks) used to obtain new + access tokens without requiring re-authentication. + """ + + __tablename__ = "refresh_tokens" + + id = Column(Integer, primary_key=True, autoincrement=True) + + # User relationship + user_id = Column( + UUID(as_uuid=True), + ForeignKey("user.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + user = relationship("User", back_populates="refresh_tokens") + + # Token hash (stored hashed, not plaintext) + token_hash = Column(String(256), unique=True, nullable=False, index=True) + + # Token expiration + expires_at = Column(TIMESTAMP(timezone=True), nullable=False, index=True) + + # Revocation flag + is_revoked = Column(Boolean, default=False, nullable=False) + + # Token family for rotation tracking (detect reuse attacks) + family_id = Column(UUID(as_uuid=True), nullable=False, index=True) + + @property + def is_expired(self) -> bool: + """Check if the token has expired.""" + return datetime.now(UTC) >= self.expires_at + + @property + def is_valid(self) -> bool: + """Check if the token is valid (not expired and not revoked).""" + return not self.is_expired and not self.is_revoked + engine = create_async_engine(DATABASE_URL) async_session_maker = async_sessionmaker(engine, expire_on_commit=False)