feat: GDPR compliance — anonymization, unsubscribe, consent tracking
- Add consent_given_at and anonymized_at fields + Alembic migration (002) - Add GET /waitlist/unsubscribe endpoint (HMAC token, anonymizes PII) - Add cleanup.py: cron-able script to anonymize unconfirmed entries after 48h - Clear IP address on email confirmation (no longer needed) - Add unsubscribe link in confirmation email footer - Record consent timestamp on signup - Add 4 new tests (unsubscribe, consent timestamp) - Update .env.example, schemas
This commit is contained in:
58
app/cleanup.py
Normal file
58
app/cleanup.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Periodic cleanup: anonymize unconfirmed waitlist entries older than CONFIRM_TOKEN_EXPIRY_HOURS.
|
||||
|
||||
Run as a cron job:
|
||||
python -m app.cleanup
|
||||
|
||||
Or integrate into docker-compose with a one-shot service.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
from app.config import settings
|
||||
from app.db import async_session
|
||||
from app.models import WaitlistEntry
|
||||
from app.routes import _anonymize_entry
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def anonymize_expired() -> int:
|
||||
"""Anonymize all unconfirmed entries past the token expiry window. Returns count."""
|
||||
cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
|
||||
hours=settings.CONFIRM_TOKEN_EXPIRY_HOURS
|
||||
)
|
||||
|
||||
async with async_session() as db:
|
||||
result = await db.execute(
|
||||
select(WaitlistEntry).where(
|
||||
and_(
|
||||
WaitlistEntry.confirmed == False, # noqa: E712
|
||||
WaitlistEntry.anonymized_at == None, # noqa: E711
|
||||
WaitlistEntry.created_at < cutoff,
|
||||
)
|
||||
)
|
||||
)
|
||||
entries = result.scalars().all()
|
||||
|
||||
for entry in entries:
|
||||
_anonymize_entry(entry)
|
||||
|
||||
await db.commit()
|
||||
|
||||
if entries:
|
||||
logger.info("Anonymized %d expired unconfirmed entries", len(entries))
|
||||
else:
|
||||
logger.info("No expired unconfirmed entries to anonymize")
|
||||
|
||||
return len(entries)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
count = asyncio.run(anonymize_expired())
|
||||
raise SystemExit(0)
|
||||
Reference in New Issue
Block a user