Implement Bellhop: Matrix-authenticated *arr request portal
- 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
This commit is contained in:
73
app/database.py
Normal file
73
app/database.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.config import DATABASE_PATH
|
||||
|
||||
_db: aiosqlite.Connection | None = None
|
||||
|
||||
|
||||
async def get_db() -> aiosqlite.Connection:
|
||||
global _db
|
||||
if _db is None:
|
||||
_db = await aiosqlite.connect(DATABASE_PATH)
|
||||
_db.row_factory = aiosqlite.Row
|
||||
await _db.execute("PRAGMA journal_mode=WAL")
|
||||
await _db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
matrix_user_id TEXT NOT NULL,
|
||||
matrix_access_token TEXT NOT NULL,
|
||||
created_at REAL NOT NULL,
|
||||
last_seen REAL NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
await _db.commit()
|
||||
return _db
|
||||
|
||||
|
||||
async def close_db() -> None:
|
||||
global _db
|
||||
if _db is not None:
|
||||
await _db.close()
|
||||
_db = None
|
||||
|
||||
|
||||
async def create_session(matrix_user_id: str, matrix_access_token: str) -> str:
|
||||
db = await get_db()
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
now = time.time()
|
||||
await db.execute(
|
||||
"INSERT INTO sessions (session_id, matrix_user_id, matrix_access_token, created_at, last_seen) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(session_id, matrix_user_id, matrix_access_token, now, now),
|
||||
)
|
||||
await db.commit()
|
||||
return session_id
|
||||
|
||||
|
||||
async def get_session(session_id: str) -> dict | None:
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT session_id, matrix_user_id, matrix_access_token, created_at, last_seen "
|
||||
"FROM sessions WHERE session_id = ?",
|
||||
(session_id,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
await db.execute(
|
||||
"UPDATE sessions SET last_seen = ? WHERE session_id = ?",
|
||||
(time.time(), session_id),
|
||||
)
|
||||
await db.commit()
|
||||
return dict(row)
|
||||
|
||||
|
||||
async def delete_session(session_id: str) -> None:
|
||||
db = await get_db()
|
||||
await db.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user