150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from .auth import EditorAuthManager, create_editor_auth_router
|
|
from .manager import UIBuilderManager
|
|
from .router import DataProvider, create_ui_builder_router
|
|
|
|
|
|
def _seed_config(source: Path, destination: Path) -> None:
|
|
if destination.exists():
|
|
return
|
|
if source.exists():
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, destination)
|
|
|
|
|
|
def install_ui_builder(
|
|
app: FastAPI,
|
|
*,
|
|
settings_dir: Path,
|
|
data_providers: dict[str, DataProvider],
|
|
source_labels: dict[str, str] | None = None,
|
|
api_prefix: str = "/api/ui-builder",
|
|
assets_url: str = "/ui-builder-assets",
|
|
editor_url: str = "/editor",
|
|
runtime_url: str = "/",
|
|
settings_filename: str = "ui_builder.json",
|
|
draft_filename: str = "ui_builder_draft.json",
|
|
published_filename: str = "ui_builder_published.json",
|
|
custom_script_urls: list[str] | None = None,
|
|
custom_style_urls: list[str] | None = None,
|
|
) -> UIBuilderManager:
|
|
"""Install published runtime and PIN-protected editor."""
|
|
|
|
settings_dir = Path(settings_dir)
|
|
legacy = settings_dir / Path(settings_filename).name
|
|
draft_file = settings_dir / Path(draft_filename).name
|
|
published_file = settings_dir / Path(published_filename).name
|
|
|
|
_seed_config(legacy, draft_file)
|
|
_seed_config(legacy, published_file)
|
|
|
|
draft_manager = UIBuilderManager(settings_dir, filename=draft_file.name)
|
|
published_manager = UIBuilderManager(settings_dir, filename=published_file.name)
|
|
auth = EditorAuthManager(settings_dir)
|
|
|
|
assets_dir = Path(__file__).resolve().parent / "static"
|
|
app.mount(
|
|
assets_url,
|
|
StaticFiles(directory=assets_dir),
|
|
name="ui-builder-assets",
|
|
)
|
|
|
|
editor_api = f"{api_prefix.rstrip('/')}/editor"
|
|
runtime_api = f"{api_prefix.rstrip('/')}/runtime"
|
|
auth_api = f"{api_prefix.rstrip('/')}/auth"
|
|
|
|
app.include_router(
|
|
create_editor_auth_router(
|
|
auth,
|
|
prefix=auth_api,
|
|
editor_url=editor_url,
|
|
)
|
|
)
|
|
app.include_router(
|
|
create_ui_builder_router(
|
|
draft_manager,
|
|
data_providers,
|
|
source_labels=source_labels,
|
|
prefix=editor_api,
|
|
access_dependency=auth.require_editor,
|
|
publish_manager=published_manager,
|
|
)
|
|
)
|
|
app.include_router(
|
|
create_ui_builder_router(
|
|
published_manager,
|
|
data_providers,
|
|
source_labels=source_labels,
|
|
prefix=runtime_api,
|
|
read_only=True,
|
|
)
|
|
)
|
|
|
|
template = (assets_dir / "index.html").read_text(encoding="utf-8")
|
|
asset_digest = hashlib.sha256()
|
|
for asset_name in ("app.js", "styles.css"):
|
|
asset_path = assets_dir / asset_name
|
|
if asset_path.exists():
|
|
asset_digest.update(asset_path.read_bytes())
|
|
asset_version = asset_digest.hexdigest()[:12]
|
|
custom_script_urls = custom_script_urls or []
|
|
custom_style_urls = custom_style_urls or []
|
|
|
|
def render_page(mode: str) -> HTMLResponse:
|
|
page_api = editor_api if mode == "editor" else runtime_api
|
|
html = (
|
|
template.replace("__UI_BUILDER_ASSETS__", assets_url.rstrip("/"))
|
|
.replace("__UI_BUILDER_ASSET_VERSION__", asset_version)
|
|
.replace("__UI_BUILDER_API__", page_api)
|
|
.replace("__UI_BUILDER_AUTH_API__", auth_api)
|
|
.replace("__UI_BUILDER_EDITOR__", editor_url)
|
|
.replace("__UI_BUILDER_RUNTIME__", runtime_url)
|
|
.replace("__UI_BUILDER_MODE__", mode)
|
|
.replace("__UI_BUILDER_EDITOR_HOTKEY__", "Ctrl+Shift+E")
|
|
.replace(
|
|
"__UI_BUILDER_CUSTOM_STYLES__",
|
|
"\n".join(
|
|
f'<link rel="stylesheet" href="{url}">'
|
|
for url in custom_style_urls
|
|
),
|
|
)
|
|
.replace(
|
|
"__UI_BUILDER_CUSTOM_SCRIPTS__",
|
|
"\n".join(
|
|
f'<script src="{url}"></script>'
|
|
for url in custom_script_urls
|
|
),
|
|
)
|
|
)
|
|
return HTMLResponse(html)
|
|
|
|
@app.get(editor_url, include_in_schema=False)
|
|
async def ui_builder_editor(request: Request):
|
|
# Runtime is the default landing mode, even if an old bookmark points
|
|
# directly at /editor. The visual editor is opened only explicitly
|
|
# from the runtime toolbar and carries ?open=1 after PIN auth.
|
|
if request.query_params.get("open") != "1":
|
|
return RedirectResponse(runtime_url, status_code=302)
|
|
if not auth.is_request_authenticated(request):
|
|
return RedirectResponse(runtime_url, status_code=302)
|
|
return render_page("editor")
|
|
|
|
@app.get(runtime_url, include_in_schema=False)
|
|
async def ui_builder_runtime() -> HTMLResponse:
|
|
return render_page("runtime")
|
|
|
|
app.state.ui_builder_manager = draft_manager
|
|
app.state.ui_builder_draft_manager = draft_manager
|
|
app.state.ui_builder_published_manager = published_manager
|
|
app.state.ui_builder_auth = auth
|
|
return draft_manager
|