security: httpOnly cookies, rate limiting, nginx headers, SSE sanitization

Auth / token storage:
- JWT is now set as an httpOnly Secure SameSite=Lax cookie on login
- Add POST /auth/logout endpoint that clears the cookie
- get_current_member falls back to rh_token cookie when no Authorization header
- WebSocket auth now accepts cookie (rh_token) or optional ?token= query param
- Frontend removes all localStorage JWT access; uses credentials:"include" on
  every fetch so the httpOnly cookie is sent automatically
- Replace clearToken() with logout() that calls the server logout endpoint
- Non-sensitive rh_session flag in localStorage used only for client-side routing

Rate limiting:
- Add slowapi>=0.1.9 dependency
- /auth/login limited to 10 req/min per IP
- /auth/register limited to 5 req/min per IP

Nginx security headers:
- Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
  X-XSS-Protection, Permissions-Policy to all responses

SSE error leakage:
- songs.py nc-scan/stream no longer leaks str(exc) to clients

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mistral Vibe
2026-03-30 21:11:53 +02:00
parent 68da26588a
commit c1941ed9ac
14 changed files with 109 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { api } from "../api/client";
import { api, isLoggedIn } from "../api/client";
interface InviteInfo {
id: string;
@@ -19,7 +19,7 @@ export function InvitePage() {
const [accepting, setAccepting] = useState(false);
const [done, setDone] = useState(false);
const isLoggedIn = !!localStorage.getItem("rh_token");
const loggedIn = isLoggedIn();
useEffect(() => {
if (!token) return;
@@ -74,7 +74,7 @@ export function InvitePage() {
{invite.used_at && " · Already used"}
</p>
{isLoggedIn ? (
{loggedIn ? (
<button
onClick={accept}
disabled={accepting || !!invite.used_at}