Files
playchat/backend/app/api/auth.py
colcaroliandClaude Sonnet 5 1ad8b130c7 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>
2026-08-18 23:04:01 +09:00

51 lines
1.6 KiB
Python

import secrets
from fastapi import APIRouter, HTTPException, Request, Response
from pydantic import BaseModel
from app.core.config import SESSION_COOKIE_SECURE, SESSION_MAX_AGE_DAYS, SITE_PASSWORD
from app.core.site_auth import COOKIE_NAME, create_session_token, verify_session_token
router = APIRouter(prefix="/api/auth", tags=["auth"])
class LoginRequest(BaseModel):
password: str
class AuthStatus(BaseModel):
required: bool
authenticated: bool
@router.get("/status", response_model=AuthStatus)
def status(request: Request):
if not SITE_PASSWORD:
return AuthStatus(required=False, authenticated=True)
token = request.cookies.get(COOKIE_NAME)
return AuthStatus(required=True, authenticated=bool(token and verify_session_token(token)))
@router.post("/login", response_model=AuthStatus)
def login(req: LoginRequest, response: Response):
if not SITE_PASSWORD:
raise HTTPException(status_code=403, detail="SITE_PASSWORD is not configured on the server")
if not secrets.compare_digest(req.password, SITE_PASSWORD):
raise HTTPException(status_code=401, detail="Invalid password")
response.set_cookie(
COOKIE_NAME,
create_session_token(),
max_age=SESSION_MAX_AGE_DAYS * 86400,
httponly=True,
secure=SESSION_COOKIE_SECURE,
samesite="lax",
path="/",
)
return AuthStatus(required=True, authenticated=True)
@router.post("/logout", response_model=AuthStatus)
def logout(response: Response):
response.delete_cookie(COOKIE_NAME, path="/")
return AuthStatus(required=bool(SITE_PASSWORD), authenticated=False)