3d5de06b44
AI-powered web app that recommends unwatched movies from a Jellyfin library based on natural language mood input. Jellyfin auth, modular LLM backend (Claude/OpenAI/Ollama), two-tier pre-filter + AI ranking, mobile-responsive dark theme UI with poster cards and deep links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
import secrets
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, Response
|
|
|
|
from app.database import get_db
|
|
from app.models import LoginRequest
|
|
from app.services.jellyfin import authenticate_user
|
|
|
|
router = APIRouter()
|
|
|
|
SESSION_DURATION_DAYS = 30
|
|
|
|
|
|
async def get_current_user(request: Request) -> dict:
|
|
"""Extract and validate session from cookie. Returns user info or raises 401."""
|
|
session_id = request.cookies.get("session_id")
|
|
if not session_id:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
|
|
db = await get_db()
|
|
try:
|
|
cursor = await db.execute(
|
|
"SELECT user_id, username, jellyfin_token, expires_at FROM sessions WHERE session_id = ?",
|
|
(session_id,),
|
|
)
|
|
row = await cursor.fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=401, detail="Invalid session")
|
|
|
|
if datetime.fromisoformat(row["expires_at"]) < datetime.now(timezone.utc):
|
|
await db.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
|
await db.commit()
|
|
raise HTTPException(status_code=401, detail="Session expired")
|
|
|
|
return {
|
|
"id": row["user_id"],
|
|
"name": row["username"],
|
|
"token": row["jellyfin_token"],
|
|
}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
@router.post("/login")
|
|
async def login(request: LoginRequest, response: Response):
|
|
result = await authenticate_user(request.username, request.password)
|
|
if not result:
|
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
|
|
|
session_id = secrets.token_urlsafe(32)
|
|
now = datetime.now(timezone.utc)
|
|
expires = now + timedelta(days=SESSION_DURATION_DAYS)
|
|
|
|
db = await get_db()
|
|
try:
|
|
await db.execute(
|
|
"INSERT INTO sessions (session_id, user_id, username, jellyfin_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
(session_id, result["user_id"], result["username"], result["token"], now.isoformat(), expires.isoformat()),
|
|
)
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
|
|
response.set_cookie(
|
|
key="session_id",
|
|
value=session_id,
|
|
httponly=True,
|
|
samesite="lax",
|
|
max_age=SESSION_DURATION_DAYS * 86400,
|
|
)
|
|
return {"id": result["user_id"], "name": result["username"]}
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout(request: Request, response: Response):
|
|
session_id = request.cookies.get("session_id")
|
|
if session_id:
|
|
db = await get_db()
|
|
try:
|
|
await db.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
|
await db.commit()
|
|
finally:
|
|
await db.close()
|
|
response.delete_cookie("session_id")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/me")
|
|
async def me(request: Request):
|
|
user = await get_current_user(request)
|
|
return {"id": user["id"], "name": user["name"]}
|