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>
51 lines
1.6 KiB
Python
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)
|