diff --git a/main.py b/main.py index dae368d..1c795c7 100644 --- a/main.py +++ b/main.py @@ -7,10 +7,13 @@ import urllib.parse from typing import Optional, List, Dict, Any import requests -from fastapi import FastAPI, Request, HTTPException, Depends -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi import FastAPI, Request, HTTPException, Depends, Form +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel +import base64 +import hmac +import hashlib # Setup logging logging.basicConfig(level=logging.INFO) @@ -78,12 +81,67 @@ settings = Settings() class SettingsUpdate(BaseModel): stashdb_url: str stashdb_api_key: str - whisparr_url: str - whisparr_api_key: str - jellyfin_url: str whisparr_root_folder: Optional[str] = "" whisparr_quality_profile_id: Optional[str] = "" +def sign_data(data: str) -> str: + secret = (settings.whisparr_api_key or "default_secret").encode() + h = hmac.new(secret, data.encode(), hashlib.sha256).digest() + return f"{data}.{base64.urlsafe_b64encode(h).decode()}" + +def verify_data(signed_data: str) -> bool: + try: + data, sig = signed_data.rsplit(".", 1) + expected_sig = base64.urlsafe_b64encode(hmac.new((settings.whisparr_api_key or "default_secret").encode(), data.encode(), hashlib.sha256).digest()).decode() + return hmac.compare_digest(sig, expected_sig) + except Exception: + return False + +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + path = request.url.path + if path in ["/login", "/logout"] or path.startswith("/static"): + return await call_next(request) + + auth_cookie = request.cookies.get("huntarr_auth") + if not auth_cookie or not verify_data(auth_cookie): + if path.startswith("/api"): + return JSONResponse({"error": "Unauthorized"}, status_code=401) + return RedirectResponse("/login", status_code=303) + + return await call_next(request) + +@app.get("/login", response_class=HTMLResponse) +def login_get(request: Request): + return templates.TemplateResponse(request=request, name="login.html", context={}) + +@app.post("/login", response_class=HTMLResponse) +def login_post(request: Request, username: str = Form(...), password: str = Form(...)): + jf_url = settings.jellyfin_url.rstrip("/") + try: + res = requests.post( + f"{jf_url}/Users/AuthenticateByName", + json={"Username": username, "Pw": password}, + headers={"X-Emby-Authorization": 'MediaBrowser Client="Huntarr", Device="Server", DeviceId="Huntarr", Version="1.0"'}, + timeout=5 + ) + if res.status_code == 200: + data = res.json() + if data.get("AccessToken"): + response = RedirectResponse("/", status_code=303) + response.set_cookie("huntarr_auth", sign_data(username), httponly=True, max_age=86400*30) + return response + except Exception as e: + logger.error(f"Jellyfin auth error: {e}") + + return templates.TemplateResponse(request=request, name="login.html", context={"error": "Invalid username or password or Jellyfin unreachable."}) + +@app.get("/logout") +def logout(): + response = RedirectResponse("/login", status_code=303) + response.delete_cookie("huntarr_auth") + return response + # GraphQL / StashDB Client class StashDBClient: def __init__(self, url: str, api_key: str): @@ -626,9 +684,6 @@ def index_route( def update_settings(update: SettingsUpdate): settings.stashdb_url = update.stashdb_url settings.stashdb_api_key = update.stashdb_api_key - settings.whisparr_url = update.whisparr_url - settings.whisparr_api_key = update.whisparr_api_key - settings.jellyfin_url = update.jellyfin_url settings.whisparr_root_folder = update.whisparr_root_folder or "" settings.whisparr_quality_profile_id = update.whisparr_quality_profile_id or "" diff --git a/templates/layout.html b/templates/layout.html index 95593ab..8f632ff 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -718,14 +718,6 @@
Sign in with Jellyfin
+{{ error }}
+