Add site-wide login gate protecting the whole app

Chat/memory/logs/models APIs were reachable without any auth even
though the app is deployed publicly behind Caddy HTTPS; only the
settings screen had a password. Add an optional SITE_PASSWORD-backed
session cookie gate in front of everything (settings included, on
top of its existing SETTINGS_PASSWORD), with a matching login screen
on the frontend. Stays fully backward compatible: with SITE_PASSWORD
unset the app behaves exactly as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 23:04:01 +09:00
co-authored by Claude Sonnet 5
parent 002282a8b7
commit 1ad8b130c7
12 changed files with 319 additions and 38 deletions
+40
View File
@@ -0,0 +1,40 @@
import hashlib
import hmac
import time
from fastapi import HTTPException, Request
from app.core.config import SESSION_MAX_AGE_DAYS, SESSION_SECRET_KEY, SITE_PASSWORD
COOKIE_NAME = "playchat_session"
def _sign(payload: str) -> str:
return hmac.new(SESSION_SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest()
def create_session_token() -> str:
issued_at = str(int(time.time()))
return f"{issued_at}.{_sign(issued_at)}"
def verify_session_token(token: str) -> bool:
issued_at, _, signature = token.partition(".")
if not issued_at or not signature:
return False
if not hmac.compare_digest(_sign(issued_at), signature):
return False
age_seconds = time.time() - int(issued_at)
return 0 <= age_seconds <= SESSION_MAX_AGE_DAYS * 86400
def require_site_session(request: Request) -> None:
"""サイト全体(設定画面以外の全API)を保護するセッションCookie認証。
SITE_PASSWORD が未設定の場合は既存のローカル開発体験を壊さないよう常に通す。
"""
if not SITE_PASSWORD:
return
token = request.cookies.get(COOKIE_NAME)
if not token or not verify_session_token(token):
raise HTTPException(status_code=401, detail="Login required")