"""Device registration only. This module deliberately contains no push sender.""" import hashlib from uuid import UUID from fastapi import Request from fastapi.responses import JSONResponse from pydantic import BaseModel, Field 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] cursor.execute('DELETE FROM push_devices WHERE session_id IN ' '(SELECT id FROM sessions WHERE expires_at<=CURRENT_TIMESTAMP) ' 'OR last_seen_at