muse drives a download client and writes to the media library, so exposing it publicly needs more than a login form. It fails closed. Auth is on unless MUSE_AUTH=off is set explicitly, and when it is on with incomplete config the app refuses to start rather than serving open. The mode worth engineering against is not a broken login page, it is a misconfigured deploy quietly publishing an unauthenticated download client. Sessions are server-side: the cookie carries a 256-bit random token and nothing else, and only its SHA-256 is stored. There is no signed payload to get subtly wrong, a stolen database yields no usable cookies, and logout revokes for real. The allowlist is re-checked on every request so removing a user takes effect immediately rather than at session expiry. OAuth state is compared with secrets.compare_digest, so a callback the user did not initiate is rejected. MUSE_BASE_URL comes from config rather than X-Forwarded-* because those headers are attacker-controllable and this one is a redirect target. CSP is default-src 'self' with no unsafe-inline, which is why the stylesheet and script moved out of index.html into static files -- an injected <script> now cannot execute. Also frame-ancestors 'none', base-uri 'none', nosniff, DENY, no-referrer and HSTS. FastAPI's /docs, /redoc and /openapi.json are disabled so the login page does not hand out a map of every endpoint. /healthz is the only unauthenticated route and returns a bare ok, so the container healthcheck works without leaking anything. Verified: misconfigured auth refuses to boot; unauthenticated / redirects and /api/* returns 401; a forged state is rejected 400; a junk cookie is 401; a valid session for a non-allowlisted user is 401; all headers present; and the UI renders with no CSP violations in the console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44 lines
1.3 KiB
HTML
44 lines
1.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>muse</title>
|
|
<link rel="stylesheet" href="/static/app.css">
|
|
<script src="/static/app.js" defer></script>
|
|
</head>
|
|
<body>
|
|
<header><div class="wrap">
|
|
<h1>muse <small>search a song → it lands in Navidrome</small>
|
|
<span class="who" id="who"></span></h1>
|
|
<form id="f">
|
|
<input id="q" placeholder="artist, song title…" autocomplete="off" autofocus>
|
|
<button id="go">Search</button>
|
|
</form>
|
|
<details id="imp">
|
|
<summary>Import a list — paste many songs at once</summary>
|
|
<textarea id="bulk" placeholder="One per line:
|
|
|
|
Radiohead - Creep
|
|
Juice WRLD - Lucid Dreams
|
|
Tory Lanez - The Color Violet
|
|
|
|
Or paste a Deezer album / playlist URL to expand it."></textarea>
|
|
<div class="btnrow">
|
|
<button id="imprun" type="button">Queue all</button>
|
|
<span class="hint" id="imphint">Top Deezer match is taken automatically — check the list below.</span>
|
|
</div>
|
|
<div id="impout"></div>
|
|
</details>
|
|
</div></header>
|
|
|
|
<main class="wrap">
|
|
<div id="results"></div>
|
|
<h2>Requests <span class="count" id="qcount"></span></h2>
|
|
<div class="bar hidden" id="bar"><i></i></div>
|
|
<div id="jobs"><div class="empty">nothing requested yet</div></div>
|
|
</main>
|
|
|
|
</body>
|
|
</html>
|