Add FCM device registration and Gitea bug reporter

This commit is contained in:
2026-09-15 01:01:08 +02:00
parent b2af866cf0
commit b96b1890d2
33 changed files with 1232 additions and 18 deletions
+79
View File
@@ -0,0 +1,79 @@
"""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<CURRENT_TIMESTAMP - INTERVAL \'90 days\'')
connection.commit()
return JSONResponse({'registered': True, 'id': device_id}, headers={'Cache-Control': 'no-store'})
@app.delete('/api/push/devices/{device_id}')
def unregister_device(request: Request, device_id: UUID):
user = get_user(request)
if not user:
return JSONResponse({'error': 'authentication_required'}, status_code=401)
with get_db() as connection:
with connection.cursor() as cursor:
cursor.execute('DELETE FROM push_devices WHERE device_id=%s AND user_id=%s '
'AND session_id IN (SELECT id FROM sessions WHERE token_hash=%s)',
(device_id, user['id'], session_hash(request, cookie_name)))
connection.commit()
return JSONResponse({'registered': False}, headers={'Cache-Control': 'no-store'})