Persist runtime config in app db and seed from env

This commit is contained in:
2026-04-20 15:56:10 +02:00
parent 3f7c9e43d9
commit 179aae264e
21 changed files with 921 additions and 125 deletions
+15 -8
View File
@@ -18,6 +18,7 @@ from app.services.auth import (
revoke_session,
validate_csrf_token,
)
from app.services.config_page import build_config_sections
logger = logging.getLogger(__name__)
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parents[2] / "templates"))
@@ -33,7 +34,7 @@ def login_page(
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
) -> Response:
if current_auth is not None:
return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER)
csrf_token = issue_login_csrf_token()
response = templates.TemplateResponse(
@@ -79,7 +80,7 @@ def login_submit(
)
auth_session, raw_token = create_session(session, user=user, settings=settings)
response = RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER)
response = RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER)
response.delete_cookie(LOGIN_CSRF_COOKIE_NAME, path="/login")
response.set_cookie(
key=settings.auth_session_cookie_name,
@@ -94,7 +95,7 @@ def login_submit(
return response
@router.post("/admin/change-password", response_class=HTMLResponse)
@router.post("/config/change-password", response_class=HTMLResponse)
def change_password_submit(
request: Request,
current_password: str = Form(),
@@ -110,9 +111,10 @@ def change_password_submit(
if not validate_csrf_token(expected=current_auth.session.csrf_token, actual=csrf_token):
logger.warning("Rejected password change attempt due to CSRF validation failure")
return _render_admin_page(
return _render_config_page(
request,
settings=settings,
auth_db_session=session,
current_auth=current_auth,
status_code=status.HTTP_400_BAD_REQUEST,
password_change_error="invalid password change request",
@@ -132,16 +134,17 @@ def change_password_submit(
current_auth.user.username,
exc,
)
return _render_admin_page(
return _render_config_page(
request,
settings=settings,
auth_db_session=session,
current_auth=current_auth,
status_code=status.HTTP_400_BAD_REQUEST,
password_change_error="password change failed",
)
logger.info("Password updated for user '%s'", current_auth.user.username)
return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/logout")
@@ -200,17 +203,18 @@ def _set_login_csrf_cookie(response: HTMLResponse, *, settings: Settings, token:
)
def _render_admin_page(
def _render_config_page(
request: Request,
*,
settings: Settings,
auth_db_session: Session,
current_auth: AuthenticatedSession,
status_code: int,
password_change_error: str | None,
) -> HTMLResponse:
return templates.TemplateResponse(
request,
"admin.html",
"config.html",
{
"app_name": settings.app_name,
"app_env": settings.app_env,
@@ -218,6 +222,9 @@ def _render_admin_page(
"csrf_token": current_auth.session.csrf_token,
"force_password_change": current_auth.user.force_password_change,
"password_change_error": password_change_error,
"config_error": None,
"config_saved": False,
"config_sections": build_config_sections(auth_db_session, settings),
},
status_code=status_code,
)
+87 -11
View File
@@ -1,30 +1,45 @@
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from fastapi.templating import Jinja2Templates
from app.config import Settings
from app.dependencies import get_app_settings, get_current_auth_session
from app.config import Settings, get_settings
from app.dependencies import get_app_settings, get_auth_db, get_current_auth_session
from app.services.auth import AuthenticatedSession
from app.services.config_page import ConfigSaveError, build_config_sections, save_config_updates
from sqlalchemy.orm import Session
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parents[2] / "templates"))
router = APIRouter(tags=["pages"])
logger = logging.getLogger(__name__)
@router.get("/", response_class=HTMLResponse)
def home(request: Request, settings: Settings = Depends(get_app_settings)) -> HTMLResponse:
context = {
"app_name": settings.app_name,
"app_env": settings.app_env,
"notion_status": "Legacy scope, removed from the Python rewrite target.",
}
return templates.TemplateResponse(request, "home.html", context)
def home(
request: Request,
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
) -> RedirectResponse:
if current_auth is None:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER)
@router.get("/admin", response_class=HTMLResponse)
def admin_page(
def admin_redirect(
request: Request,
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
) -> RedirectResponse:
if current_auth is None:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/config", status_code=status.HTTP_303_SEE_OTHER)
@router.get("/config", response_class=HTMLResponse)
def config_page(
request: Request,
auth_db_session: Session = Depends(get_auth_db),
settings: Settings = Depends(get_app_settings),
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
) -> Response:
@@ -38,5 +53,66 @@ def admin_page(
"csrf_token": current_auth.session.csrf_token,
"force_password_change": current_auth.user.force_password_change,
"password_change_error": None,
"config_error": None,
"config_saved": request.query_params.get("saved") == "1",
"config_sections": build_config_sections(auth_db_session, settings),
}
return templates.TemplateResponse(request, "admin.html", context)
return templates.TemplateResponse(request, "config.html", context)
@router.post("/config", response_class=HTMLResponse)
async def config_submit(
request: Request,
auth_db_session: Session = Depends(get_auth_db),
settings: Settings = Depends(get_app_settings),
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
) -> Response:
if current_auth is None:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
form = await request.form()
csrf_token = form.get("csrf_token")
if csrf_token != current_auth.session.csrf_token:
logger.warning("Rejected config update due to CSRF validation failure")
context = {
"app_name": settings.app_name,
"app_env": settings.app_env,
"current_username": current_auth.user.username,
"csrf_token": current_auth.session.csrf_token,
"force_password_change": current_auth.user.force_password_change,
"password_change_error": None,
"config_error": "invalid config update request",
"config_saved": False,
"config_sections": build_config_sections(auth_db_session, settings),
}
return templates.TemplateResponse(
request,
"config.html",
context,
status_code=status.HTTP_400_BAD_REQUEST,
)
try:
save_config_updates(auth_db_session, dict(form), settings)
except ConfigSaveError:
logger.warning("Rejected config update due to invalid submitted values")
refreshed_settings = build_runtime_settings(auth_db_session, get_settings())
context = {
"app_name": refreshed_settings.app_name,
"app_env": refreshed_settings.app_env,
"current_username": current_auth.user.username,
"csrf_token": current_auth.session.csrf_token,
"force_password_change": current_auth.user.force_password_change,
"password_change_error": None,
"config_error": "invalid config submission",
"config_saved": False,
"config_sections": build_config_sections(auth_db_session, refreshed_settings),
}
return templates.TemplateResponse(
request,
"config.html",
context,
status_code=status.HTTP_400_BAD_REQUEST,
)
return RedirectResponse(url="/config?saved=1", status_code=status.HTTP_303_SEE_OTHER)