6 Commits
Author SHA1 Message Date
Juan Denis 55d8fb1e44 Read route bodies through json_object()
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.

Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
2026-10-10 20:53:43 -04:00
Juan Denis 6296887bad Remove internal plan citations from app code
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
2026-10-10 15:50:45 -04:00
Juan Denis 97a96d963b Give common API errors stable codes and service wording
Toasts showed backend prose verbatim, with no code to translate
against and "application" where the UI says "service". Not-found,
missing-field, permission, conflict and agent-offline errors now come
from factories in app/exceptions.py that pair one fixed message with
a dotted code (not_found.<resource>, validation.required,
permission.denied, conflict.exists, agent.offline), so the client can
translate them.

The shared app-loading helpers and the apps, domains, servers,
databases and backups routes raise those errors instead of hand
shaping bodies, which drops the hand-shaped body count from 1132 to
850. Five routes picked an HTTP status by comparing error text; they
now key off a result code or a sentinel so rewording can't change a
status. Five broad excepts that echoed str(exc) now log the crash and
return the request id instead.
2026-10-04 18:59:25 -04:00
Juan DenisandClaude Fable 5 28dcd63f4d refactor(themes): registry install raises typed errors
theme_registry_service.install returns the theme dict and raises
NotFound/Validation/DependencyUnavailable; the route's not-found prose
sniff is gone. import_theme keeps its tuple contract for the import
route. Sniffing census 7 -> 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 23:56:15 -04:00
Juan DenisandClaude Opus 5 6cd2ee6b95 refactor(auth): one mid-route admin gate instead of three contracts
Plan 76 milestone A, drift/bug row 3. Five API modules had grown a private
_require_admin() with *three mutually incompatible* return contracts:

  jobs.py        -> returns the user; None means DENIED
  dns_cutover.py -> returns None on success; a response tuple means denied
  waf.py         -> same as dns_cutover
  manifests.py   -> (user, None) on success, (None, response) on denial
  themes.py      -> same as manifests

jobs.py's is the exact inverse of dns_cutover.py's, so a guard copied between
the two reads identically at the call site and fails open. Nothing in the code
told you which contract you were looking at.

Replace all five with rbac.require_admin_user(), which raises a typed
PermissionDeniedError the global ApplicationError handler already renders. A
gate that raises has no return contract to get backwards.

Two convergences ride along, both toward the stricter shared behaviour:

- manifests.py gated on `user.role != 'admin'` where the others used
  `is_admin`. These are the same predicate today (is_admin is defined as
  role == ROLE_ADMIN), so no route changes status, but there is now one
  truth source rather than two.
- none of the five checked is_active, so a deactivated admin still passed
  them; @admin_required has always checked it. The shared door checks it, so
  these 25 call sites now reject deactivated admins like every other
  admin route.

The unknown-user case keeps its 403 'Admin access required' rather than
becoming a 401/404: a non-admin caller should not learn from the status code
whether the account behind the token still exists.

Gate: tests/test_jobs.py, test_themes.py, test_waf.py, test_dns_cutover_api.py
(82 passed) and -k manifest (152 passed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:22:01 -04:00
Juan DenisandClaude Opus 4.8 c20a9f14f2 feat(themes): backend model, API, and registry pipeline (plan 60 Phase 2-3)
Themes are data, not code — so the backend is a simple validated-JSON store,
no zips/checksums/permissions.

- Theme model + migration 078_themes
- theme_tokens.py: server-side mirror of the token whitelist + validators
  (the gate: a malicious/malformed theme never reaches the DB)
- theme_service: bundled-seed loading, import/list/delete, panel default,
  pre-auth public/active
- /api/v1/themes: /installed, /import (admin), DELETE (admin), /default
  get+set (admin), unauthenticated /public/active for login/setup, plus
  /registry + /registry/<slug>/install
- theme_registry_service: offline-tolerant registry fetch (TTL + last-good +
  bundled fallback), mirrors the extension registry pattern
- bundled seed themes synced to app/data/themes + themes_index.json
- 18 proving tests (server-side validation sanitizes url()/;/} smuggling,
  admin gate, panel-default → public/active, offline fallback, install flow)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:24:00 -04:00