diff --git a/surfsense_backend/app/celery_app.py b/surfsense_backend/app/celery_app.py index 704c9cf9b..331ed0f40 100644 --- a/surfsense_backend/app/celery_app.py +++ b/surfsense_backend/app/celery_app.py @@ -188,6 +188,7 @@ celery_app = Celery( "app.tasks.celery_tasks.document_reindex_tasks", "app.tasks.celery_tasks.stale_notification_cleanup_task", "app.tasks.celery_tasks.stripe_reconciliation_task", + "app.tasks.celery_tasks.refresh_token_cleanup_task", "app.tasks.celery_tasks.auto_reload_task", "app.tasks.celery_tasks.gateway_tasks", "app.etl_pipeline.cache.eviction.task", @@ -306,6 +307,11 @@ celery_app.conf.beat_schedule = { "schedule": crontab(hour="3", minute="17"), "options": {"expires": 600}, }, + "purge-refresh-tokens": { + "task": "purge_refresh_tokens", + "schedule": crontab(hour="3", minute="41"), + "options": {"expires": 600}, + }, # Prune the ETL parse cache (TTL + size budget) once daily, off-peak. "evict-etl-cache": { "task": "evict_etl_cache", diff --git a/surfsense_backend/app/tasks/celery_tasks/refresh_token_cleanup_task.py b/surfsense_backend/app/tasks/celery_tasks/refresh_token_cleanup_task.py new file mode 100644 index 000000000..7a17f1963 --- /dev/null +++ b/surfsense_backend/app/tasks/celery_tasks/refresh_token_cleanup_task.py @@ -0,0 +1,34 @@ +"""Celery task for pruning expired refresh-token rows.""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime, timedelta + +from sqlalchemy import delete, or_ + +from app.celery_app import celery_app +from app.config import config +from app.db import RefreshToken, async_session_maker + + +@celery_app.task(name="purge_refresh_tokens") +def purge_refresh_tokens() -> int: + return asyncio.run(_purge_refresh_tokens()) + + +async def _purge_refresh_tokens() -> int: + now = datetime.now(UTC) + revoked_cutoff = now - timedelta(seconds=config.REFRESH_ROTATION_GRACE_SECONDS) + + async with async_session_maker() as session: + result = await session.execute( + delete(RefreshToken).where( + or_( + RefreshToken.expires_at < now, + RefreshToken.revoked_at < revoked_cutoff, + ) + ) + ) + await session.commit() + return result.rowcount or 0