diff --git a/src/backend/services/auth_service.py b/src/backend/services/auth_service.py index 5b2df80..8d9c5ae 100644 --- a/src/backend/services/auth_service.py +++ b/src/backend/services/auth_service.py @@ -92,6 +92,10 @@ async def issue_api_key(db: AsyncSession, user: User) -> str: """Create a new session key for the user and return it in clear (once).""" key = generate_api_key() now = utcnow() + # Housekeeping: drop this user's expired keys so the table does not grow forever. + await db.execute( + delete(ApiKey).where(ApiKey.user_id == user.id, ApiKey.expires_at <= now) + ) db.add( ApiKey( user_id=user.id, diff --git a/src/backend/tests/test_auth.py b/src/backend/tests/test_auth.py index fd0ba54..11781e0 100644 --- a/src/backend/tests/test_auth.py +++ b/src/backend/tests/test_auth.py @@ -269,3 +269,24 @@ class TestRevokeAllKeys: await self._all_401(client, keys) new = resp.json()["api_key"] assert (await client.get("/api/auth/me", headers={"X-API-Key": new})).status_code == 200 + + +class TestExpiredKeyCleanup: + async def test_expired_key_removed_at_next_login(self, client, db_session): + from datetime import timedelta + from sqlalchemy import select, update + from src.backend.models.orm.api_key import ApiKey + from src.backend.services.auth_service import hash_api_key, utcnow + + user = await _create_user(db_session, username="cleanup", password="CleanPass123") + old_hash = hash_api_key(user.plain_key) + await db_session.execute( + update(ApiKey).values(expires_at=utcnow() - timedelta(minutes=1)) + ) + resp = await client.post( + "/api/auth/login", json={"username": "cleanup", "password": "CleanPass123"} + ) + assert resp.status_code == 200 + hashes = (await db_session.execute(select(ApiKey.key_hash))).scalars().all() + assert old_hash not in hashes + assert len(hashes) == 1