Commit Graph

20 Commits

Author SHA1 Message Date
f9302df075 feat(billets): emoji hashtags, quick views, and feed resumes
Authors already typed #hashtags in their bodies (37 of the 46 billets on gk2
carry one), so tags are EXTRACTED from the body rather than added via a picker:
tagging costs the author nothing and applies retroactively.

- migration 0004: tag(slug,emoji,label) + billet_tag, indexed on tag_slug.
  Extracted into rows rather than re-parsed per request, so filtering hits an
  index instead of LIKE-scanning every body. slug is accent-folded + lowercased
  (#Réseau == #reseau); emoji is STORED so a later curation change never
  silently restyles an already-published billet.
- services/tags.py: curated map + extraction. The lookbehind keeps a URL anchor
  (page#anchor) from becoming a tag; unknown tags fall back to a neutral badge
  rather than being rejected. Map covers this box's real content (podcast/media)
  as well as the security/box vocabulary; genuinely ambiguous topics are left on
  the default rather than editorialised.
- quick views: ?tag=slug on / and /feed.json, plus /tags.json and
  /admin/api/tags for the chip bar. Uses EXISTS, not a JOIN, so keyset paging
  cannot duplicate a row per matching tag; the pager carries the active tag.
- feed resumes: long billets show a short excerpt + "Lire la suite"; short ones
  render whole. `is_long` is measured on the markdown-collapsed text, so a short
  billet padded with link syntax is not "resumed" into a copy of itself.
- feed.json gains `summary` + `tags` (both JSON Feed 1.1 standard) with the
  emoji in the spec-legal `_secubox` extension; optional keys are omitted, not
  nulled.
- GET /admin/api/billets: the authoring list. The public feed cannot serve it —
  its item id is a permalink URL (so edit/delete could not address a row, which
  is why they 404'd) and it hides drafts, which an authoring client must see.
- manage.py backfill-tags: idempotent; also re-applies the curated map to
  existing tags as an explicit operator step.

Verified on gk2: backfill tagged 37/46 billets; ?tag=podcast→37, jfk→4, nope→0;
a draft's tag correctly stays out of the public chip bar; delete cascades.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:35:24 +02:00
db7fc2449e feat(billets): JWT admin surface + Companion authoring & image upload
Add packages/secubox-billets/api/routes/jwt_admin.py — a JWT-authed JSON
admin surface (secubox_core.auth.require_jwt: Bearer OR secubox_session
cookie) that reuses the same repo/media layer as the session HTML admin.
Endpoints: POST/PUT/DELETE /admin/api/billets, POST
/admin/api/billets/{id}/media (multipart, EXIF-strip via services.media),
GET /admin/api/comments, approve/delete comment moderation. No-op if
secubox_core is absent (isolated unit tests, session admin unaffected).

Wire it into api/main.py (register_jwt_admin after register_admin).

postinst: drop a sorted-last zz-secubox-system.pth into the venv so the
isolated billets venv can import the .deb-managed secubox_core without
shadowing its own fastapi/pydantic wheels.

Companion billets module: point EP map at /feed.json + /admin/api/*, add
an image file input to the editor and upload it after the billet is
created (POST /admin/api/billets/{id}/media, multipart). Fixes the "New
billet missing upload" gap. Bump service worker v6 -> v7.

Verified live on gk2: create -> feed, valid PNG upload -> {url,thumb},
delete — all 200.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:48:12 +02:00
ae98a76fe5 fix(billets): communiqué always-light + CSP-safe health-banner opt-out on public blog (ref #851)
Backport of two board-live fixes so a package reinstall can't revert them:

- billets-communique.css: the communiqué permalink extends the blog base
  (billets.css flips dark under prefers-color-scheme:dark), which leaked a dark
  ground into the always-light press-release. Pin color-scheme:light + html/body
  light. Bump billet_communique.html css ref to ?v=2.
- health-banner.js: public pages can now opt out of the operator banner with
  <meta name="sbx-no-health-banner"> (reliable — the WAF injects the loader
  after the page head; and CSP-safe, unlike an inline window flag). base.html
  emits it, so the public blog no longer shows the banner while admin pages
  (standalone templates) keep it. Replaces a board-only hostname hardcode.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:42:00 +02:00
965377388f fix(billets): SSRF — validate embed_url public-IP before Chromium screenshot (ref #851)
Review HIGH: the headless-browser snapshot path drove Chromium to embed_url with
no SSRF check (only the og:image fallback was guarded), so an author could
screenshot an internal service (127.0.0.1/RFC1918/::1/169.254.169.254). Now
ssrf.validate_url() gates the navigation (reject → fall back to og:image), plus a
Playwright route aborts any redirect/sub-resource to a non-public host. 12 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:05:35 +02:00
a95f157eb1 feat(billets): communiqué style + embed snapshot vignette + portable HTML archive (ref #851)
- Per-billet 'style' (default|communique). Communiqué permalink = the poster
  mockup (Space Grotesk/JetBrains Mono, ROUTE side-band, TRANSMISSION frame,
  slogans/footer); funky feed unchanged.
- Embed snapshot vignette: api/services/snapshot.py tries headless Chromium
  (guarded/optional), falls back to SSRF-guarded og:image, re-encoded via the
  media service. Captured off-loop on save (communiqué billets, cached per
  embed_url). Public render shows the vignette; click-to-load the live iframe.
- Portable single-HTML archive: GET /admin/billets/{id}/archive.html — self-
  contained (inlined CSS, system fonts, media+snapshot as base64 data-URIs,
  embed = snapshot linking to original, no iframe). Off-loop render.
- migration 0003 (style + embed_snapshot); models/repo/admin/main wired.
- CSP: style-src adds 'unsafe-inline' so the WAF-injected health-banner's
  dynamic <style> renders (was blocked → banner fell unstyled to page bottom);
  script-src stays 'self'. Google-Fonts relaxation page-scoped to communiqué.
- 118 tests (11 new: snapshot fallback/SSRF, communiqué render, archive self-containment).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 07:57:54 +02:00
77435afbc9 fix(billets): review — export off-loop, override bytes-compare, upload feedback
Adversarial review follow-ups:
- /admin/export.sbxsite: base64 + json.dumps now run in a thread (asyncio.to_thread),
  not on the shared event loop — a photo-heavy backup no longer stalls the module.
- /admin/override: compare the operator secret on bytes; a non-ASCII input used to
  raise TypeError in hmac.compare_digest → 500. Now a clean rejection + audit event.
- Uploads: surface the skipped-file count (?media_skipped=N + dashboard flash)
  instead of silently dropping invalid/oversized images. 107 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-12 08:28:18 +02:00
b80f445629 feat(billets): complete operator panel + generate-password override
- /billets/ operator panel: full Administration card linking every admin
  surface (Dashboard, Nouveau billet, Modération, Backup .sbxsite, Change-pw,
  Flux) instead of a couple of buttons.
- Forgotten-password OVERRIDE for the operator: GET/POST /admin/override — no
  session, gated by the module operator secret (/etc/secubox/secrets/billets,
  root-only, constant-time compare, rate-limited 5/h). Generates a strong
  random password (token_urlsafe), shows it once (rendered in the POST response,
  never in a URL), audit-logged (author.password_overridden / override_failed).
  Linked from the panel; the reset page is same-origin on billets (no CORS,
  no cross-origin secret handling).
- billets-admin.css: flash.good + code tokens. 106 tests (2 new override).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-12 08:20:02 +02:00
db74e02ffc feat(billets): image upload with zoomable vignette gallery + portable export
- Upload images (png/jpeg/webp/gif ≤5 MiB) alongside the text of a billet:
  multi-image mini-gallery. New api/services/media.py validates + FULLY
  re-encodes every upload via Pillow (drops EXIF/GPS, neutralises polyglots;
  SVG refused), producing a cleaned original + a ≤480px thumbnail.
- Storage under BILLETS_MEDIA_DIR (/var/lib/secubox/billets/media), served by
  nginx /media/ alias (in-process StaticFiles fallback). img-src 'self' already
  covers it — no third party, no CSP relaxation.
- Public: vignette grid in feed + permalink, pure-JS zoomable lightbox with
  ◀▶ keyboard/arrow navigation (graceful degradation: links open the full
  image with JS off). First image drives og:image / twitter:image.
- Admin editor: multipart file input + existing-media thumbnails with delete.
  New media table (migration 0002, ON DELETE CASCADE), repo CRUD, media delete
  route; billet delete removes files. All Pillow work off the event loop
  (asyncio.to_thread).
- Portable backup: GET /admin/export.sbxsite emits every billet + media inlined
  as base64 (single re-importable file).
- nginx client_max_body_size 6m; requirements pillow==11.1.0. 104 tests (11 new).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-12 08:11:43 +02:00
0d50cf6a51 fix(billets): reskin /billets/ panel to WebUI guideline + per-client login rate-limit
- /billets/ management panel rebuilt to the SecuBox WebUI Panel Guidelines
  (certs reference): restrained header + stat grid + glass cards + dense list
  + toast, dropping the funky hero/blobs. Cyan, Courier Prime, emoji glyphs.
- Login rate-limit now keys on the real client via X-Forwarded-For (admin
  _client_ip) instead of the shared proxy IP, so one operator's attempts no
  longer lock out everyone. nginx preserves the XFF chain (X-Real-IP +
  $proxy_add_x_forwarded_for).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-12 07:51:44 +02:00
0c74eddc0e feat(billets): admin password reset + guideline reskin (cyan hybrid-dark) + doc
- Admin webui: POST /admin/password (verify current, argon2, event-logged,
  forces re-login). Change-password card in the dashboard.
- Reskin billets admin (login/dashboard/editor/moderation) + the /billets/
  management panel to the SecuBox WebUI Panel Guidelines: Courier Prime, cyan
  #00d4ff accent, glass cards, emoji (certs as reference). Public blog stays funky.
- New self-contained api/static/billets-admin.css (module-vhost variant of the
  guideline, no /shared/).
- Docs: WEBUI-PANEL-GUIDELINES.md v1.1 (canonical DEFAULT for webui module
  management, billets added as reference, 2 variants documented) + CLAUDE.md pointer.
- 93 tests (3 new password-reset).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-12 07:15:15 +02:00
ac6df22258 feat(billets): funky/party public webapp — inline media + link chips + quick-share
Public feed + permalink redesigned funky/party (theme-aware gradients, emoji
accents, playful cards). Embeds now render INLINE in the feed too (responsive
16:9-ish media, CSP frame-src augmented for self-hosted hosts). Inline embedded
reference links shown as 🌐 chips. Prominent emoji quick-share row (Bluesky/X/
WhatsApp/Telegram/Reddit/LinkedIn/email + Mastodon/copy via billets.js) on every
billet in the feed and permalink (replaces the hidden Republier details).
Asset URLs versioned (?v=2) to bust the sbxwaf media cache. 90 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 16:35:56 +02:00
5e77b9ebbd feat(billets): SecuBox webui panel + navbar integration
menu.d/415-billets.json (category mind) surfaces Billets in the SecuBox sidebar.
A funky, emoji-rich hybrid-dark panel at /billets/ (admin webui) with live stat
count-ups, reaction breakdown, latest billets and action buttons — fed by a new
CORS-enabled GET /stats.json (public aggregate counts). Fixed a latent import-time
BILLETS_DB read (now resolved at connect()). 89 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 16:19:35 +02:00
084120c858 fix(billets): deploy hotfixes — disable markdown-it linkify (optional dep), drop failing ExecStartPre, remove debian/compat
Found during live deploy to billets.gk2.secubox.in:
- markdown-it linkify=True needs linkify-it-py (not in requirements) -> 500 on any
  body with a URL; spec only needs markdown [links], so linkify is off (bare-URL
  autolink stays for comments via linkify_plain).
- systemd ExecStartPre tried to chown the shared /run/secubox as the secubox user
  -> service failed to start; removed (tmpfiles already provides 1777 /run/secubox).
- debian/compat conflicted with debhelper-compat in control; removed.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 16:04:24 +02:00
68608a3857 feat(billets): step 7 — ASGI entrypoint, systemd/nginx/debian deploy, README
api/asgi.py (uvicorn api.asgi:app) with a lifespan that opens the WAL DB + runs
migrations; api/manage.py CLI (create-author/set-password/seed). Debian package
(control/rules/postinst: secubox user, venv+pinned requirements, signing secret,
nginx vhost, www-data->secubox group for socket access) + standalone deploy/
(install.sh, billets.service, nginx.conf). README documents install/config/backup
and every 'tranche et documente' decision. 88 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 15:52:08 +02:00
0f3906dbe4 feat(billets): step 6 — Atom/JSON feeds, OG/Twitter cards, outbound oEmbed, Republier
services/feeds: title/excerpt derivation (billets have no title field) + Atom and
JSON Feed 1.1 builders (XML-escaped, well-formed). Routes: /feed.xml, /feed.json,
and /oembed (outbound — only our own permalinks, returns a self-contained
blockquote, never an iframe). Full OpenGraph + Twitter Card meta per billet +
json+oembed discovery link. Republier share menu (Bluesky/X/Facebook/LinkedIn/
WhatsApp/Telegram/Reddit/email server-rendered; Mastodon-instance via localStorage
+ copy-link in billets.js). 85 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 15:46:20 +02:00
5074d6589a feat(billets): step 5 — comments (moderated, anti-spam) + emoji reactions
Public write surface, CSRF-guarded (double-submit). Reactions: 6 closed emojis,
anonymous signed visitor cookie (only its hash stored), toggle via same-origin
fetch fragment swap with a no-JS POST+redirect fallback. Comments: honeypot +
signed min-think-time token + per-ip_hash rate-limit (5/h), pending-by-default
moderation with auto-approve for a returning (ip_hash,name) already vetted;
emails BLAKE2b-hashed, bodies escaped + autolinked rel=nofollow ugc. Admin
moderation queue (approve/reject -> event_log). 76 tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 15:41:28 +02:00
1107462580 feat(billets): step 4 — SSRF-guarded oEmbed + sanitization + link-card
Critical security step. services/ssrf: https-only, all-resolved-IPs-must-be-public
(blocks private/loopback/link-local/reserved/metadata), re-validated redirects,
5s timeout + size cap. services/sanitize: nh3 + iframe host-allowlist, forced
sandbox+loading=lazy, non-allowlisted/non-https/srcdoc iframes dropped.
services/oembed: provider allowlist + same-origin discovery (Mastodon/PeerTube).
services/linkcard: OpenGraph fallback (local card, no iframe). Wired into admin
save (cache embed_html/provider/fetched_at) + manual refetch; CSP frame-src
limited to the embed allowlist (+ self-hosted host per-page). 63 tests
(21 new SSRF/XSS/oEmbed).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 15:21:49 +02:00
4e42edcc09 feat(billets): step 3 — admin auth + billet CRUD (event-log + gitea revision per mutation)
Session login: argon2id (verify off-loop via to_thread), optional TOTP, signed
session cookie (HttpOnly/SameSite=Lax/Secure), in-memory login rate-limit,
double-submit CSRF on every admin POST. Billet CRUD (draft/publish/edit/archive/
delete) via a single URL field (ref|embed). Each mutation appends to the
BLAKE2b-chained event_log AND commits a Gitea revision (content+style) off the
event loop. IPs are BLAKE2b-hashed, never stored raw. 40 tests pass.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 15:11:43 +02:00
e9488cedf9 feat(billets): step 2 — public feed + permalinks + gitea revision store
Public read surface: keyset (cursor) pagination on (published_at,id), permalinks
with view counting, restricted-markdown -> nh3-sanitized HTML (raw HTML escaped,
no live script), theme-aware CSS, h-entry microformats, strict CSP + security
headers. Plus per-user request: a git-backed revision store (services/revisions)
versioning each billet's content+style into a Gitea repo (front-matter .md,
best-effort push, full git-log history). 31 tests pass.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 15:02:17 +02:00
1d1457eefc feat(billets): step 1 — data layer (schema, models, db, ULID, chained event log)
New SecuBox module secubox-billets (micro-blog gateway). Async SQLite (WAL) via
aiosqlite — mandatory since billets is aggregator-served on the shared loop.
ULID ids (time-sortable for cursor pagination), Pydantic v2 models (https-only
URLs, size limits, closed emoji enum, honeypot), numbered .sql migrations, and
the append-only BLAKE2b-chained event_log invariant. 20 tests pass.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-11 14:52:22 +02:00