mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 15:37:03 +00:00
feat(metablogizer): publisher wizard endpoints (wizard/export/import)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
4305908ad5
commit
cc06d72b57
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
# secubox-metablogizer routers
|
||||
84
packages/secubox-metablogizer/api/routers/publish.py
Normal file
84
packages/secubox-metablogizer/api/routers/publish.py
Normal 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})
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue
Block a user