"""Device registration. Background sending lives in notifications.py.""" import hashlib from uuid import UUID from fastapi import Request from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from i18n import current_language from notifications import save_language class DeviceRegistration(BaseModel): device_id: UUID token: str = Field(min_length=20, max_length=4096, pattern=r'^[A-Za-z0-9_:.-]+$') platform: str = Field(pattern=r'^android$') app_version: str = Field(default='', max_length=32, pattern=r'^[0-9A-Za-z.+_-]*$') session_tag: str = Field(pattern=r'^[a-f0-9]{64}$') def session_hash(request, cookie_name): return hashlib.sha256(request.cookies.get(cookie_name, '').encode()).hexdigest() def register_routes(app, get_db, get_user, cookie_name): @app.get('/api/push/session') def push_session(request: Request): user = get_user(request) payload = {'authenticated': bool(user)} if user: payload.update(user_id=user['id'], session_tag=session_hash(request, cookie_name)) return JSONResponse(payload, headers={'Cache-Control': 'no-store'}) @app.post('/api/push/devices') def register_device(request: Request, data: DeviceRegistration): user = get_user(request) if not user: return JSONResponse({'error': 'authentication_required'}, status_code=401) token_hash = session_hash(request, cookie_name) if data.session_tag != token_hash: return JSONResponse({'error': 'session_changed'}, status_code=409) with get_db() as connection: with connection.cursor() as cursor: # The row lock serializes registration against logout/session deletion. cursor.execute('SELECT id FROM sessions WHERE token_hash=%s AND user_id=%s ' 'AND expires_at>CURRENT_TIMESTAMP FOR UPDATE', (token_hash, user['id'])) session = cursor.fetchone() if not session: return JSONResponse({'error': 'session_expired'}, status_code=401) # Serialize token/device transfers across users as well as token rotations. cursor.execute('SELECT pg_advisory_xact_lock(71001)') cursor.execute('DELETE FROM push_devices WHERE token=%s AND device_id<>%s', (data.token, data.device_id)) cursor.execute(''' INSERT INTO push_devices(user_id, session_id, device_id, token, platform, app_version) VALUES (%s,%s,%s,%s,%s,%s) ON CONFLICT(device_id) DO UPDATE SET user_id=EXCLUDED.user_id, session_id=EXCLUDED.session_id, token=EXCLUDED.token, platform=EXCLUDED.platform, app_version=EXCLUDED.app_version, updated_at=CURRENT_TIMESTAMP, last_seen_at=CURRENT_TIMESTAMP RETURNING id ''', (user['id'], session[0], data.device_id, data.token, data.platform, data.app_version)) device_id = cursor.fetchone()[0] save_language(connection, user['id'], current_language.get()) cursor.execute('DELETE FROM push_devices WHERE session_id IN ' '(SELECT id FROM sessions WHERE expires_at<=CURRENT_TIMESTAMP) ' 'OR last_seen_at