- FastAPI backend with async SQLite session storage - Matrix homeserver authentication (login/logout/session validation via whoami) - Server-side proxy for Radarr, Sonarr, and Lidarr search & add APIs - Fire-and-forget Matrix audit log bot for request tracking - Rate-limited login endpoint (5 req/min per IP via slowapi) - Single-page Alpine.js frontend with dark theme, media type tabs, search grid - Dockerfile for single-container deployment - Secure session cookies (httponly, samesite=strict, secure) https://claude.ai/code/session_018iMt5qm4j9bk8wzytLC3E6
52 lines
1.3 KiB
Python
52 lines
1.3 KiB
Python
"""Matrix audit log — fire-and-forget messages to an unencrypted room."""
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import httpx
|
|
|
|
from app.config import (
|
|
MATRIX_AUDIT_ROOM_ID,
|
|
MATRIX_BOT_ACCESS_TOKEN,
|
|
MATRIX_HOMESERVER_URL,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def send_audit_message(message: str) -> None:
|
|
"""Schedule an audit message to be sent without blocking the caller."""
|
|
if not MATRIX_AUDIT_ROOM_ID or not MATRIX_BOT_ACCESS_TOKEN:
|
|
logger.debug("Audit log not configured, skipping")
|
|
return
|
|
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
return
|
|
|
|
loop.create_task(_send(message))
|
|
|
|
|
|
async def _send(message: str) -> None:
|
|
import time
|
|
|
|
room_id = MATRIX_AUDIT_ROOM_ID
|
|
txn_id = str(int(time.time() * 1000))
|
|
|
|
url = (
|
|
f"{MATRIX_HOMESERVER_URL.rstrip('/')}/_matrix/client/v3/rooms/"
|
|
f"{room_id}/send/m.room.message/{txn_id}"
|
|
)
|
|
body = {
|
|
"msgtype": "m.text",
|
|
"body": message,
|
|
}
|
|
headers = {"Authorization": f"Bearer {MATRIX_BOT_ACCESS_TOKEN}"}
|
|
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
await client.put(url, json=body, headers=headers, timeout=10.0)
|
|
except Exception:
|
|
logger.warning("Failed to send audit message: %s", message, exc_info=True)
|