feat(metablogizer): publisher wizard endpoints (wizard/export/import)

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-11 13:14:40 +02:00
parent 4305908ad5
commit cc06d72b57
4 changed files with 139 additions and 1 deletions

View File

@ -52,6 +52,8 @@ from webhook import (
verify_signature,
_record_deploy,
)
from routers.publish import router as publish_router
app.include_router(publish_router)
logger = logging.getLogger("metablogizer")

View File

@ -1 +0,0 @@
# secubox-metablogizer routers

View File

@ -0,0 +1,84 @@
# packages/secubox-metablogizer/api/routers/publish.py
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""MetaBlogizer publisher wizard: upload -> version -> route -> cert -> backup."""
from __future__ import annotations
import tempfile
from pathlib import Path
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, JSONResponse
from secubox_core.auth import require_jwt
from publish.content import extract_archive, ContentError
from publish.routing import apply_route
from publish.certs import provision_cert
from publish.backup import export_site, import_site
from webhook import git_commit_push
# Mirror the constants owned by api/main.py (kept in sync intentionally).
SITES_ROOT = Path("/srv/metablogizer/sites")
DEFAULT_DOMAIN_SUFFIX = ".gk2.secubox.in"
BASE_PORT = 8900
router = APIRouter()
def _site_dir(name: str) -> Path:
if not name.replace("-", "").replace("_", "").isalnum():
raise HTTPException(400, "invalid site name")
d = SITES_ROOT / name
return d
@router.post("/publish/wizard")
async def publish_wizard(
name: str = Form(...),
domain: str = Form(None),
file: UploadFile = File(...),
user=Depends(require_jwt),
):
site = _site_dir(name)
docroot = site / "public"
docroot.mkdir(parents=True, exist_ok=True)
domain = domain or f"{name}{DEFAULT_DOMAIN_SUFFIX}"
steps: dict = {}
data = await file.read()
try:
steps["content"] = extract_archive(docroot, data, file.filename or "index.html")
except ContentError as e:
raise HTTPException(400, f"unsafe upload: {e}")
steps["version"] = git_commit_push(site, f"publish {name} via wizard")
steps["route"] = apply_route(domain, BASE_PORT)
steps["cert"] = provision_cert(domain)
ok = bool(steps["content"].get("index_present")) and bool(steps["route"].get("route_ok"))
return {"ok": ok, "domain": domain, "steps": steps}
@router.get("/publish/export/{name}")
async def publish_export(name: str, user=Depends(require_jwt)):
site = _site_dir(name)
if not site.exists():
raise HTTPException(404, "site not found")
manifest = {"name": name, "domain": f"{name}{DEFAULT_DOMAIN_SUFFIX}",
"base_port": BASE_PORT}
out = Path(tempfile.mkdtemp())
art = export_site(site, manifest, out)
return FileResponse(str(art), filename=art.name, media_type="application/octet-stream")
@router.post("/publish/import")
async def publish_import(file: UploadFile = File(...), user=Depends(require_jwt)):
data = await file.read()
tmp = Path(tempfile.mkdtemp())
art = tmp / "upload.sbxsite"
art.write_bytes(data)
try:
manifest = import_site(art, SITES_ROOT)
except Exception as e: # noqa: BLE001 — surface a clean 400
raise HTTPException(400, f"import failed: {e}")
return JSONResponse({"ok": True, "manifest": manifest})

View File

@ -0,0 +1,53 @@
# packages/secubox-metablogizer/api/tests/test_publish_router.py
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
import io
import zipfile
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
@pytest.fixture
def client(tmp_path, monkeypatch):
monkeypatch.setenv("SECUBOX_JWT_SECRET", "test-secret")
from secubox_core import config as sbx_config
monkeypatch.setattr(sbx_config, "_CONF_PATHS", [])
monkeypatch.setattr(sbx_config, "_CONFIG", None)
import importlib
import routers.publish as rp
importlib.reload(rp)
# Point the site root at tmp and stub privileged deps.
monkeypatch.setattr(rp, "SITES_ROOT", tmp_path / "sites")
(tmp_path / "sites" / "zem" / "public").mkdir(parents=True)
monkeypatch.setattr(rp, "apply_route", lambda domain, port=8900: {"route_ok": True, "vhost": {}, "waf": {}})
monkeypatch.setattr(rp, "provision_cert", lambda domain: {"mode": "wildcard", "detail": ""})
monkeypatch.setattr(rp, "git_commit_push", lambda d, m: {"pushed": True, "committed": True, "commit": "abc", "reason": "ok"})
# Bypass auth
from secubox_core.auth import require_jwt
app = FastAPI()
app.dependency_overrides[require_jwt] = lambda: {"sub": "tester"}
app.include_router(rp.router)
return TestClient(app)
def _zip_bytes():
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr("index.html", "<h1>zem</h1>")
return buf.getvalue()
def test_wizard_runs_all_steps(client):
r = client.post("/publish/wizard",
data={"name": "zem"},
files={"file": ("site.zip", _zip_bytes(), "application/zip")})
assert r.status_code == 200, r.text
body = r.json()
assert body["ok"] is True
assert body["domain"] == "zem.gk2.secubox.in"
assert body["steps"]["content"]["index_present"] is True
assert body["steps"]["route"]["route_ok"] is True
assert body["steps"]["cert"]["mode"] == "wildcard"