7 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 8ca1f3d07e refactor(auth): enforce one policy boundary per route 2026-08-18 02:42:39 -04:00
Juan Denis c242d4cab5 fix(ssl): a failed certificate check no longer reports fine
Plan 75 §A5. The frontend could always render unknown (a real 'unknown'
state, an n/a chip) — the backend starved it of the signal:

- Doctor's cert check read Domain.ssl_expires_at, a column nothing in
  production writes — structurally incapable of anything but a green
  'No certificates tracked.' Empty now warns: expiry is not monitored.
- openssl -checkend exits non-zero for 'expires soon' AND for
  'unreadable' — an unreadable cert reported needs_renewal=True, and
  the error path vanished the cert from the expiring list entirely.
  Unreadable now returns an error result, and the status endpoint
  surfaces check_errors instead of dropping them.
- A failed monitor cert probe bumped cert_checked_at while keeping the
  old cert_expires_at — a green 'valid / N days' chip from weeks-old
  data next to a fresh checked timestamp. The stale value is cleared;
  the UI falls back to n/a until a probe succeeds.
- get_expiry_alerts dropped unprobeable certs by omission; they now
  appear with severity 'unknown'. get_cert_health reported grade 'F'
  for a DNS timeout; probe failure now yields grade None + error.
- A failed certbot inventory reported '0 certificates, nothing
  expiring' with HTTP 200; list_certificates_report carries the
  errors and the status endpoint exposes them as list_errors.
2026-08-17 22:48:27 -04:00
Juan DenisandClaude Opus 5 fc000a3b97 feat(ssl): remember one Let's Encrypt contact instead of asking every time
Plan 73 item 5. The Domains SSL modal asked for a contact email on every
single certificate, and the backend hard-required one per call. The app-detail
Settings tab did remember it -- in localStorage, so the memory was per browser
and invisible to the other surface. The two never agreed, and neither could
help the paths with no form at all: domain attach passed `admin@<host>` when
it had nothing else, an address that usually does not exist, which is where
every expiry notice for those certificates went to die.

There is now a single panel-wide ACME contact in system settings.
get_acme_contact() resolves an explicit address first and falls back to the
stored one; remember_acme_contact() writes it back whenever a request carries
an explicit address. Both are best-effort on the settings side by design: a
settings failure must not break issuance, and failing to remember an address
must not turn a successful certificate into an error. An empty input never
clears a stored contact, since the automatic paths pass whatever they were
given and that is frequently nothing.

Wired into every issuance entry point -- the domain SSL enable route, the
generic certificate route, and domain attach, which now consults the stored
contact before falling back to the admin@<host> guess. The address is only
remembered after issuance actually succeeds; remembering one Let's Encrypt
rejected would be worse than remembering none.

Prefill needed its own endpoint. The settings routes are all admin-gated, but
issuing a certificate for an app you can edit is not an admin action, so
GET /ssl/acme-contact is jwt_required only -- it returns an address the
operator chose for notices, not a credential. The Domains modal and the
Settings tab both prefill from it, and the Settings tab keeps its localStorage
seed so a browser that already has the value shows it without a flash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:22:33 -04:00
Juan Denis 9f9d868085 refactor(backend): extract reusable format_bytes, admin_required, and validate_app_name
- Add app/utils/formatting.py with format_bytes() and delegate 9 duplicated
  human-readable byte formatters across services.
- Import admin_required from app.middleware.rbac in 15 API modules instead of
  redefining it locally.
- Extend app/utils/slug.py with validate_app_name() and use it for template
  install/validation and apps slug normalization.
2026-06-21 02:11:47 -04:00
Juan Denis 06d63cbc52 Add standalone WordPress and SSL management UI
Implements backend endpoints and service logic for managing standalone WordPress via Docker, including install, uninstall, start, stop, and restart. Adds Nginx integration for WordPress at /wordpress. Introduces a new frontend page for SSL certificate management with Certbot install, obtain, renew, and revoke actions. Updates the WordPress frontend page to support one-click install, status, and lifecycle management. Updates documentation and install instructions for improved clarity.
2026-01-25 22:48:12 -05:00
Juan Denis c0a2a01fc1 Initial project structure and core API implementation
Add backend and frontend foundations for ServerKit, including Flask API structure, SQLAlchemy models, service layers, and React app with authentication context and service hooks. Implement core API endpoints for authentication, application management, Docker, databases, and system operations. Update README with project overview, tech stack, and setup instructions. Add development roadmap and example environment configuration.
2026-01-17 04:22:12 -05:00