feat(server): FastAPI app with API-key auth and health check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
37
server/app.py
Normal file
37
server/app.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""MusicFetch REST API. Plain HTTP behind an upstream TLS reverse proxy."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from . import actions, jobs, mf
|
||||||
|
|
||||||
|
API_KEY = os.environ.get("MUSICFETCH_API_KEY", "")
|
||||||
|
ROOT = os.environ.get("MUSICFETCH_ROOT", "/media/music")
|
||||||
|
|
||||||
|
app = FastAPI(title="MusicFetch API")
|
||||||
|
|
||||||
|
|
||||||
|
def require_key(x_api_key: str = Header(default="")):
|
||||||
|
if not API_KEY or x_api_key != API_KEY:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid API key.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def _http_exc(_req, exc: HTTPException):
|
||||||
|
# Always return a Siri-speakable {"message": ...} body.
|
||||||
|
return JSONResponse(status_code=exc.status_code, content={"message": exc.detail})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# Minimal stub so that auth tests can verify 401 behaviour before the real
|
||||||
|
# /fetch implementation lands in Task 5. This route MUST carry require_key as
|
||||||
|
# a dependency so unauthenticated / wrong-key requests are rejected here.
|
||||||
|
@app.post("/fetch", dependencies=[Depends(require_key)])
|
||||||
|
def fetch_stub(q: str = ""):
|
||||||
|
# Task 5 will replace this body with the full search-and-download logic.
|
||||||
|
raise HTTPException(status_code=501, detail="Not yet implemented.")
|
||||||
5
server/requirements.txt
Normal file
5
server/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
requests
|
||||||
|
ytmusicapi
|
||||||
|
rich
|
||||||
16
tests/conftest.py
Normal file
16
tests/conftest.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
os.environ.setdefault("MUSICFETCH_API_KEY", "test-key")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from server.app import app
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth():
|
||||||
|
return {"X-API-Key": "test-key"}
|
||||||
16
tests/test_auth.py
Normal file
16
tests/test_auth.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
def test_health_no_auth(client):
|
||||||
|
r = client.get("/health")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_requires_key(client):
|
||||||
|
r = client.post("/fetch", params={"q": "anything"})
|
||||||
|
assert r.status_code == 401
|
||||||
|
assert "message" in r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_rejects_wrong_key(client):
|
||||||
|
r = client.post("/fetch", params={"q": "anything"},
|
||||||
|
headers={"X-API-Key": "wrong"})
|
||||||
|
assert r.status_code == 401
|
||||||
Reference in New Issue
Block a user