from datetime import UTC, datetime from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import hash_token from app.models.refresh_token import RefreshToken async def store( db: AsyncSession, *, user_id: int, jti: str, raw_token: str, expires_at: datetime, ) -> RefreshToken: record = RefreshToken( jti=jti, token_hash=hash_token(raw_token), user_id=user_id, expires_at=expires_at, ) db.add(record) await db.commit() await db.refresh(record) return record async def get_active( db: AsyncSession, *, jti: str, raw_token: str ) -> RefreshToken | None: """Return the token row only if it exists, matches, is not revoked, not expired.""" result = await db.execute(select(RefreshToken).where(RefreshToken.jti == jti)) record = result.scalar_one_or_none() if record is None or record.revoked: return None if record.token_hash != hash_token(raw_token): return None expires_at = record.expires_at if expires_at.tzinfo is None: expires_at = expires_at.replace(tzinfo=UTC) if expires_at < datetime.now(UTC): return None return record async def revoke(db: AsyncSession, record: RefreshToken) -> None: record.revoked = True await db.commit() async def revoke_by_jti(db: AsyncSession, jti: str) -> None: await db.execute( update(RefreshToken).where(RefreshToken.jti == jti).values(revoked=True) ) await db.commit() async def revoke_all_for_user(db: AsyncSession, user_id: int) -> None: await db.execute( update(RefreshToken) .where(RefreshToken.user_id == user_id, RefreshToken.revoked.is_(False)) .values(revoked=True) ) await db.commit()