feat(web): minimal shared-password auth gate
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>
This commit is contained in:
@@ -172,6 +172,29 @@ nav.contents .sep { flex: 1; }
|
||||
.job .chip { grid-column: 2 / 3; justify-self: start; margin-top: 12px; }
|
||||
}
|
||||
|
||||
/* ── Login ────────────────────────────────────────────── */
|
||||
.login { display: flex; justify-content: center; padding: 48px 0; }
|
||||
.login-panel {
|
||||
border: 1.5px solid var(--rule-2); padding: 34px 32px; max-width: 380px; width: 100%;
|
||||
}
|
||||
.login-panel .eyebrow {
|
||||
font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.16em; text-transform: uppercase;
|
||||
color: var(--accent); margin: 0 0 8px;
|
||||
}
|
||||
.login-panel h1 { font-family: var(--serif); font-size: 1.9rem; margin: 0 0 6px; }
|
||||
.login-lede { color: var(--graphite); margin: 0 0 22px; font-size: 0.9rem; }
|
||||
.login-form { display: flex; flex-direction: column; gap: 18px; align-items: stretch; }
|
||||
.login-form .field input { min-width: 0; }
|
||||
.login-form .btn { align-self: flex-start; }
|
||||
.login-error {
|
||||
font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.04em; color: var(--accent); margin: 0;
|
||||
}
|
||||
.sign-out {
|
||||
font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.14em; text-transform: uppercase;
|
||||
color: var(--graphite); background: transparent; border: 0; cursor: pointer; padding: 0;
|
||||
}
|
||||
.sign-out:hover { color: var(--accent); }
|
||||
|
||||
/* ── Phase 2: page headers, list rows, tools ──────────── */
|
||||
.page-head { margin: 6px 0 26px; }
|
||||
.page-head .eyebrow {
|
||||
|
||||
Reference in New Issue
Block a user