mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 16:37:04 +00:00
feat(metablogizer): publisher wizard UI (5-step stepper)
Adds the Task-7 5-step Publish wizard (Content -> Version -> Route ->
Cert -> Backup) to www/metablogizer/index.html, wired to the Task-6
endpoints (POST /publish/wizard multipart, GET /publish/export/{name}).
Reuses the page's existing token()/API helpers and the
uploadSiteContent() multipart-fetch/401-retry pattern for consistency;
JSON result is rendered via textContent only.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
5b907bdf3a
commit
b755a90eec
|
|
@ -1,52 +1,92 @@
|
|||
# Task 7 report — Tor enhancement Phase 1 — webui `#tor` tab
|
||||
# Task 7 Report — MetaBlogizer Publish Wizard UI
|
||||
|
||||
**Status:** DONE
|
||||
**Status:** Done.
|
||||
|
||||
**Branch:** `feature/tor-enhancement-phase1`
|
||||
**Commit:** `b8d4e1b6` — `feat(toolbox): #tor tab — exit-country, Tor-VPN clients, obfs4 bridges, emancipate, .onion list + DNS status`
|
||||
**File touched:** `packages/secubox-toolbox/www/toolbox/index.html` (only file, as instructed) — 355 insertions / 2 deletions.
|
||||
(Note: this file previously held an unrelated stale Task-7 report for a
|
||||
different plan — "Tor enhancement Phase 1 — webui #tor tab" — overwritten
|
||||
here since it did not belong to this plan.)
|
||||
|
||||
(Note: this file previously held an unrelated stale Task-7 report from a different plan/openclaw dashboard — overwritten per instruction.)
|
||||
## What was added
|
||||
|
||||
## Panels added (inside the existing `#panel-tor` section, below the Tor-egress switch card, same P31-light skin)
|
||||
- `packages/secubox-metablogizer/www/metablogizer/index.html`:
|
||||
- New `<section class="card" id="publish-wizard">` placed right after the
|
||||
existing Sites card, inside `<main>` (before `</main>`), with the
|
||||
5-step stepper (`#wiz-steps`), name/domain/file inputs, Publish +
|
||||
Download-backup buttons, and a `<pre id="wiz-result" class="result">`
|
||||
output area.
|
||||
- New CSS block appended inside the page's existing `<style>` (after the
|
||||
touch-friendly-buttons media query): `.wizard-steps`, `.wizard-steps
|
||||
span.ok/.fail`, `#wiz-result.result`. Colors for ok/fail reuse the
|
||||
page's existing `var(--green)` / `var(--red)` tokens (the CRT phosphor
|
||||
palette) instead of the brief's hardcoded hex (`#148C66` / `#C04E24`),
|
||||
which would have clashed visually with the rest of the page.
|
||||
- New `<script>` block placed after the page's main `<script>` (which
|
||||
declares `token()` / `API` / `headers()` / `refresh()`) and before
|
||||
`crt-engine.js`. It is an IIFE that:
|
||||
- Reuses the page's real `token()` helper (reads both `sbx_token` and
|
||||
`secubox_token`) instead of the brief's illustrative `tok()` that
|
||||
only reads `sbx_token`.
|
||||
- Reuses the page's `API` constant (`/api/v1/metablogizer`) for the
|
||||
wizard/export URLs rather than hardcoding the full path.
|
||||
- Mirrors the exact multipart-fetch + 401-retry-without-header +
|
||||
redirect-to-login pattern already used by `uploadSiteContent()`
|
||||
elsewhere in the same file, for consistency.
|
||||
- Renders the JSON result via `out.textContent = JSON.stringify(...)`
|
||||
(never `innerHTML`) — safe against XSS from server data.
|
||||
- Calls the page's existing `refresh()` on successful publish so the
|
||||
Sites table picks up the new/updated site immediately.
|
||||
- Inputs use the page's existing bare `<input>` inside `.form-group`
|
||||
(no invented `form-input` class — the file's existing CSS rule
|
||||
`.form-group input { ... }` already styles any `<input>` inside a
|
||||
`.form-group`, so adding an unused class would have been dead weight).
|
||||
|
||||
1. **Exit-country** — `<select multiple>` populated from a curated static ISO 3166-1 alpha-2 list (`ISO_COUNTRIES`, ~72 countries, French labels, sorted). "Sélection actuelle" kv line + fail-closed warning banner (`StrictNodes 1` = no traffic if no exit exists in the chosen countries). Wired to `GET/POST /api/v1/toolbox/exit_country`.
|
||||
2. **Tor-VPN clients** — kind selector (ip/cidr/mac) + selector input + add button; table with per-row 🗑 remove. Prominent IPv6 warning banner (v4-only tunnel, advise disabling IPv6 on routed clients/RA). Client-side heuristic (selector contains `:` and kind≠mac) short-circuits with a clean IPv6-specific message before hitting the API; the error path also recognizes a `:`-selector 400 and rewords it, since the backend's generic `"invalid kind/selector"` detail isn't IPv6-specific. Wired to `GET /vpn/clients`, `POST/DELETE /vpn/client`.
|
||||
3. **obfs4 bridges** — paste-a-line input + add; list with per-row remove; hint pointing to Tor Browser moat / bridges.torproject.org. Wired to `GET /tor/bridges`, `POST/DELETE /tor/bridge`.
|
||||
4. **Emancipate** — "🚀 Publier en .onion" button → `POST /api/v1/exposure/tor/emancipate_webui` (cross-module, JWT-gated); shows the returned `.onion` (from `output.onion` / `tor.onion`) with a clipboard copy button.
|
||||
5. **Hidden services + .onion-DNS status** — `GET /api/v1/tor/hidden_services` table (name / onion / local port / state) and `GET /api/v1/tor/onion_dns` kv (dnsport_up / forward_zone_installed / resolves), fetched together via `Promise.all`.
|
||||
## Endpoint contract verified against source (read-only, no `.py` touched)
|
||||
|
||||
## Endpoints wired (confirmed against source, not guessed)
|
||||
|
||||
- Toolbox-native (same origin, cookie/vhost-gated exactly like the existing `torSet()`/`loadTor()` — `_require_tor_admin` blocks the public kbin vhost, no bearer needed): `/api/v1/toolbox/exit_country`, `/api/v1/toolbox/vpn/clients`, `/api/v1/toolbox/vpn/client`, `/api/v1/toolbox/tor/bridges`, `/api/v1/toolbox/tor/bridge`.
|
||||
- Cross-module (nginx-routed, confirmed via each module's `nginx/*.conf` location block): `/api/v1/exposure/tor/emancipate_webui` (requires JWT — `Depends(require_jwt)` in `packages/secubox-exposure/api/main.py`), `/api/v1/tor/hidden_services`, `/api/v1/tor/onion_dns` (no auth required server-side, bearer sent anyway for consistency).
|
||||
|
||||
## Auth approach
|
||||
|
||||
Two helpers, matching two different auth realities found in the source:
|
||||
- `Tj(path, opts)` — toolbox's own tor-* routes: `credentials: 'same-origin'`, no bearer (mirrors existing `torSet`/`torLeaks`/`loadFilters`).
|
||||
- `Xj(base, path, opts)` — cross-module (exposure + tor): reads `localStorage.getItem('sbx_token')`, sends `Authorization: Bearer <token>`, redirects to `/login.html` on 401 — this is the same pattern used by `packages/secubox-exposure/www/exposure/index.html` (`token()`/`headers()`/`api()`), and matches the fleet-wide `sbx_token` convention (a wrong localStorage key produces a login loop on other modules).
|
||||
|
||||
## XSS / robustness approach
|
||||
|
||||
- Every backend-derived string rendered into `innerHTML` goes through the existing global `esc()` (declared later in the file but hoisted — safe, since `function esc(s){}` statements hoist ahead of all script execution): error messages (`d.__error`/`dns.__error`/`hs.__error`), country codes, VPN client kind/selector, bridge lines, hidden-service name/onion_address, the emancipated `.onion` address.
|
||||
- No `onclick="fn('${…}')"` was introduced. Static buttons (apply/clear/add-client/add-bridge/emancipate) use `addEventListener` wired once near the bottom of the script. Per-row dynamic elements (VPN-client remove, bridge remove, onion copy) use `data-*` attributes + `.querySelectorAll(...).forEach(b => b.addEventListener(...))` immediately after each render — the exact idiom already used by the file's existing `loadSentinelC2()`/`c2Ignore()` pair.
|
||||
- Grep confirms the only `onclick="…${…}"` occurrences left in the file are pre-existing (lines for `setLevel`, `loadClientDetail`, `resetClient`, the "tout afficher" toggle, `quarantine`, `loadAdsClient`) — none are part of this change.
|
||||
- `switchTab('tor')` and the tab's "🔁 Refresh" button now call a new `refreshTorTab()` which loads all five new panels plus the existing egress state, so nothing is left stuck on `loading…` after a tab switch.
|
||||
Confirmed field names/response shape against
|
||||
`packages/secubox-metablogizer/api/routers/publish.py`:
|
||||
- `POST {API}/publish/wizard` — multipart fields `name`, `domain` (optional),
|
||||
`file`; JWT via `Depends(require_jwt)` (accepts Bearer **or** SSO session
|
||||
cookie, per `common/secubox_core/auth.py`). Response:
|
||||
`{ok, domain, steps: {content: {index_present}, version, route: {route_ok},
|
||||
cert}}` — matches the JS's `d.steps.content.index_present` /
|
||||
`d.steps.route.route_ok` reads exactly.
|
||||
- `GET {API}/publish/export/{name}` — also JWT-gated but accepts the SSO
|
||||
session cookie, so the plain `window.location = ...` navigation (no
|
||||
Authorization header possible on a top-level nav) works because the
|
||||
browser sends the session cookie automatically.
|
||||
|
||||
## Validation
|
||||
|
||||
- `node --check` on the extracted `<script>` block: **PASS**.
|
||||
- `grep -n 'onclick="[^"]*\${'` across the whole file: only pre-existing matches, none in the new code region.
|
||||
- Manual read-through confirms every dynamic value hitting `innerHTML` is `esc()`-wrapped; numeric fields (`local_port`) and boolean-derived static strings are not (not attacker-controlled free text, no escaping needed).
|
||||
- Not deployed to a board — that is Task 9's scope, explicitly excluded here.
|
||||
- Extracted the new `<script>` block (the one containing `wiz-go`) via a
|
||||
small Python regex script into a standalone `.js` file and ran
|
||||
`node --check` (node v22.20.0, available in this environment) —
|
||||
**no syntax error**.
|
||||
- Also checked: no duplicate `id=` attributes introduced anywhere in the
|
||||
file; `<section>`, `<script>`, `<style>` tag counts balanced
|
||||
(open == close) after the edit.
|
||||
- Did not run the app live (Task 11 is the manual/live verification step
|
||||
per the brief); this task's scope is UI-only markup/JS/CSS, and the API
|
||||
side (Task 6) was not modified.
|
||||
|
||||
## Concerns / follow-ups for later tasks
|
||||
## Helpers reused (per the "match existing conventions" instruction)
|
||||
|
||||
- The exit-country panel does not show a "live exit relay country" via geoIP (no such endpoint exists in the backend); it instead points the operator at the existing "Vérifier l'IP de sortie" button in the egress card above. Documented simplification, not a missing wire-up.
|
||||
- The IPv6-selector 400 from the toolbox API returns a generic `"invalid kind/selector"` detail (not IPv6-specific) — the client-side heuristic (colon-in-selector) covers the common case cleanly, but a genuinely malformed non-IPv6 selector will still show the generic backend message, which is correct behavior, just not IPv6-labeled.
|
||||
- The pre-existing deep-link array (`if (['overview','clients','filtres','social','ads','reseau','config'].includes(initial))`) still excludes `tor` and `sentinel` — a pre-existing gap unrelated to Task 7, left untouched per the "only touch this file for this task's scope" instruction (flagging it here rather than silently fixing it).
|
||||
- `token()` — the page's real dual-key localStorage reader (`sbx_token` /
|
||||
`secubox_token`), not the brief's single-key illustrative `tok()`.
|
||||
- `API` constant and the existing `refresh()` function.
|
||||
- The `uploadSiteContent()` multipart-fetch/401-retry/login-redirect idiom.
|
||||
- Existing `.card` / `.btn` / `.btn.primary` / `.form-group` classes; no new
|
||||
classes introduced beyond the wizard-specific `.wizard-steps` / `.result`
|
||||
that the brief itself calls for.
|
||||
|
||||
## Files touched
|
||||
## Concerns
|
||||
|
||||
- `packages/secubox-toolbox/www/toolbox/index.html` (extended, commit `b8d4e1b6`)
|
||||
- None blocking. Two intentional, documented deviations from the brief's
|
||||
literal snippet (both strict improvements for matching the page's real
|
||||
conventions, not functional gaps): `var(--green)`/`var(--red)` instead of
|
||||
hardcoded hex, and reuse of the page's `API` constant instead of a
|
||||
hardcoded absolute path.
|
||||
- The wizard's `content`/`route`/`cert` step markers assume the response
|
||||
shapes from `publish/routing.py` / `publish/certs.py` always include the
|
||||
keys the JS reads (`index_present`, `route_ok`); this mirrors the brief's
|
||||
own snippet verbatim and was cross-checked against the actual
|
||||
`publish_wizard()` handler in `api/routers/publish.py`, so no gap found.
|
||||
|
|
|
|||
|
|
@ -341,6 +341,13 @@
|
|||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Publish wizard */
|
||||
.wizard-steps { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
|
||||
.wizard-steps span { padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: .8rem; opacity: .7; }
|
||||
.wizard-steps span.ok { border-color: var(--green); color: var(--green); opacity: 1; }
|
||||
.wizard-steps span.fail { border-color: var(--red); color: var(--red); opacity: 1; }
|
||||
#wiz-result.result { max-height: 240px; overflow: auto; background: var(--bg-dark); padding: 10px; border-radius: 8px; font-size: 0.8rem; white-space: pre-wrap; word-break: break-word; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="crt-light hybrid-dark">
|
||||
|
|
@ -389,6 +396,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card" id="publish-wizard">
|
||||
<h2>🚀 Publish a site</h2>
|
||||
<div class="wizard-steps" id="wiz-steps">
|
||||
<span data-step="content">1 · Content</span>
|
||||
<span data-step="version">2 · Version</span>
|
||||
<span data-step="route">3 · Route</span>
|
||||
<span data-step="cert">4 · Cert</span>
|
||||
<span data-step="backup">5 · Backup</span>
|
||||
</div>
|
||||
<div class="form-group"><label>Site name</label>
|
||||
<input id="wiz-name" placeholder="zem"></div>
|
||||
<div class="form-group"><label>Domain</label>
|
||||
<input id="wiz-domain" placeholder="zem.gk2.secubox.in"></div>
|
||||
<div class="form-group"><label>Content (.zip or .html)</label>
|
||||
<input id="wiz-file" type="file" accept=".zip,.html"></div>
|
||||
<button class="btn primary" id="wiz-go">Publish</button>
|
||||
<button class="btn" id="wiz-backup" disabled>⬇ Download backup</button>
|
||||
<pre id="wiz-result" class="result"></pre>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<div class="modal" id="create-modal">
|
||||
|
|
@ -667,6 +694,51 @@
|
|||
refresh();
|
||||
installPolling();
|
||||
</script>
|
||||
<script>
|
||||
// Publish wizard: multipart upload to the Task-6 wizard endpoint, then
|
||||
// an on-demand backup download. Reuses the page's token()/API globals
|
||||
// (same realm as the script above) and the same manual-fetch + 401
|
||||
// fallback pattern as uploadSiteContent() for the multipart POST.
|
||||
(function () {
|
||||
function mark(step, ok) {
|
||||
var el = document.querySelector('#wiz-steps [data-step="' + step + '"]');
|
||||
if (el) el.classList.add(ok ? 'ok' : 'fail');
|
||||
}
|
||||
var lastName = '';
|
||||
document.getElementById('wiz-go').addEventListener('click', async function () {
|
||||
var name = document.getElementById('wiz-name').value.trim();
|
||||
var domain = document.getElementById('wiz-domain').value.trim();
|
||||
var f = document.getElementById('wiz-file').files[0];
|
||||
var out = document.getElementById('wiz-result');
|
||||
if (!name || !f) { out.textContent = 'name and file required'; return; }
|
||||
document.querySelectorAll('#wiz-steps span').forEach(function (s) { s.classList.remove('ok', 'fail'); });
|
||||
out.textContent = 'Publishing…';
|
||||
var fd = new FormData();
|
||||
fd.append('name', name); if (domain) fd.append('domain', domain); fd.append('file', f);
|
||||
var t = token();
|
||||
var auth = t ? { 'Authorization': 'Bearer ' + t } : {};
|
||||
try {
|
||||
let res = await fetch(API + '/publish/wizard',
|
||||
{ method: 'POST', headers: auth, body: fd, credentials: 'same-origin' });
|
||||
if (res.status === 401) {
|
||||
res = await fetch(API + '/publish/wizard',
|
||||
{ method: 'POST', body: fd, credentials: 'same-origin' });
|
||||
}
|
||||
if (res.status === 401) { window.location.href = '/login.html?redirect=' + encodeURIComponent(location.pathname); return; }
|
||||
const d = await res.json().catch(function () { return {}; });
|
||||
out.textContent = JSON.stringify(d, null, 2);
|
||||
if (d.steps) { ['content', 'version', 'route', 'cert'].forEach(function (s) {
|
||||
var v = d.steps[s]; mark(s, s === 'content' ? v.index_present : s === 'route' ? v.route_ok : true);
|
||||
}); }
|
||||
if (d.ok) { mark('backup', true); lastName = name;
|
||||
document.getElementById('wiz-backup').disabled = false; refresh(); }
|
||||
} catch (e) { out.textContent = 'error: ' + e; }
|
||||
});
|
||||
document.getElementById('wiz-backup').addEventListener('click', function () {
|
||||
if (lastName) window.location = API + '/publish/export/' + encodeURIComponent(lastName);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="/shared/crt-engine.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user