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:
@@ -0,0 +1,50 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user