190e0ef4b6
The web app had no auth — anyone reaching the host could browse the library, trigger downloads, and read/overwrite the stored credentials in Settings. Adds an opt-in single-shared-password gate: - Next.js middleware redirects unauthenticated page requests to /login and returns 401 for /api/*; static assets are excluded via matcher. - Auth is enabled ONLY when LYRA_PASSWORD is set (unset ⇒ app stays open, so existing deployments are unchanged). Documented in README "Security". - The session cookie is httpOnly and carries HMAC-SHA256(password) keyed by LYRA_SECRET_KEY — unforgeable, and the plaintext password never touches the client. Edge-safe Web Crypto so one lib/auth.ts serves middleware + route. - Cookie is NOT Secure: Lyra serves plain HTTP on the LAN; a Secure cookie would loop. Works behind an HTTPS proxy too. - /login page + form, a Sign out button (shown only when auth is enabled), and chrome (nav + worker-status poll) suppressed on /login. - docker-compose passes LYRA_PASSWORD to web; .env.example documents it. Verified live end-to-end (built + next start): no-cookie page→307 /login, API→401, wrong password→401, correct→200+cookie, authed page/API→200, forged cookie→401, and the auth-disabled (no LYRA_PASSWORD) path stays fully open. web 133 tests green, tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
1.1 KiB
TypeScript
40 lines
1.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { usePathname } from "next/navigation";
|
|
|
|
/** Live worker liveness in the masthead — polls /api/worker (heartbeat recency). */
|
|
export function WorkerStatus() {
|
|
const [online, setOnline] = useState<boolean | null>(null);
|
|
const pathname = usePathname();
|
|
const onLogin = pathname === "/login";
|
|
|
|
useEffect(() => {
|
|
if (onLogin) return; // don't poll (would 401) on the login screen
|
|
let alive = true;
|
|
async function check() {
|
|
try {
|
|
const d = await (await fetch("/api/worker")).json();
|
|
if (alive) setOnline(!!d.online);
|
|
} catch {
|
|
if (alive) setOnline(false);
|
|
}
|
|
}
|
|
check();
|
|
const id = setInterval(check, 10000);
|
|
return () => {
|
|
alive = false;
|
|
clearInterval(id);
|
|
};
|
|
}, [onLogin]);
|
|
|
|
if (onLogin) return null;
|
|
const label = online === null ? "…" : online ? "listening" : "offline";
|
|
return (
|
|
<div className={`worker-status${online === false ? " off" : ""}`}>
|
|
<span className="live-dot" />
|
|
Worker · {label}
|
|
</div>
|
|
);
|
|
}
|