from __future__ import annotations import uuid from datetime import UTC, datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.base import ensure_aware_utc from app.models.refresh_token import RefreshToken class RefreshTokenRepository: def __init__(self, db: AsyncSession) -> None: self.db = db async def create( self, *, user_id: uuid.UUID, token_hash: str, expires_at: datetime ) -> RefreshToken: record = RefreshToken(user_id=user_id, token_hash=token_hash, expires_at=expires_at) self.db.add(record) await self.db.flush() return record async def get_valid_by_hash(self, token_hash: str) -> RefreshToken | None: result = await self.db.execute( select(RefreshToken).where(RefreshToken.token_hash == token_hash) ) record = result.scalar_one_or_none() if record is None: return None if record.revoked_at is not None: return None if ensure_aware_utc(record.expires_at) < datetime.now(UTC): return None return record async def revoke(self, record: RefreshToken) -> None: record.revoked_at = datetime.now(UTC) await self.db.flush() async def revoke_all_for_user(self, user_id: uuid.UUID) -> None: """Invalidates every active session for this user - used after a password reset, since an attacker who had a valid refresh token shouldn't stay logged in past the password change that locked them out going forward.""" result = await self.db.execute( select(RefreshToken).where( RefreshToken.user_id == user_id, RefreshToken.revoked_at.is_(None) ) ) now = datetime.now(UTC) for record in result.scalars().all(): record.revoked_at = now await self.db.flush()