Per-client ICS calendar feed, replacing the shared ICS_TOKEN (#22)
Test backoffice (smb-crm) / test (push) Has been cancelled

Each client now gets their own clients.ics_token (lazily generated on
first /owner/settings visit), which both authenticates and scopes
/api/bookings.ics -- closing the gap where any shared-token holder
could view another client's bookings by swapping the client_id query
param. The owner settings page now surfaces a copyable subscribe URL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 11:55:35 +02:00
parent 319218ce21
commit 24d9aca812
9 changed files with 196 additions and 20 deletions
+8 -5
View File
@@ -87,8 +87,10 @@ Flask app (`app.py`) + `waitress`, backed by Postgres (`smb-db`, `postgres:16-al
- `PATCH /api/<entity>/<id>` — partial update, same auth.
- `DELETE /api/<entity>/<id>` — delete, same auth.
- `GET /api/bookings.ics` — read-only iCal feed for calendar apps (Apple/Google Calendar can't
send custom headers, so this is gated by a separate `?token=$ICS_TOKEN` query param instead of
the header token). Supports `?client_id=` to scope to one client.
send custom headers, so this is gated by a `?token=` query param instead of the header token).
The token is per-client (`clients.ics_token`, lazily generated on first visit to
`/owner/settings`), so it both authenticates and scopes the feed to that one client — there's
no separate `client_id` param to swap.
- `GET /healthz` — DB connectivity check.
- `GET /` — the dashboard (`static/index.html`; **Leads**, **Clients** and **Credentials** tabs —
read + add (+Neu) + edit (✎) + delete (🗑), all token-gated, all audit-logged. The Credentials
@@ -192,9 +194,10 @@ see `deploy/clients/README.md` and each group's own compose file for the exact r
- Browser-facing surfaces (`onboard.mivanchenko.de`, dashboards) sit behind Caddy basic-auth.
- Machine-to-machine writes (n8n → CRM) are gated by the `CRM_API_TOKEN` header, checked in
`backoffice/app/app.py::authed()`.
- The iCal feed is gated by a separate query-string token (`ICS_TOKEN`) since calendar clients
can't send custom headers — treat that token as effectively public-linkable and rotate it if a
feed URL leaks.
- The iCal feed is gated by a per-client query-string token (`clients.ics_token`) since calendar
clients can't send custom headers — treat that token as effectively public-linkable and rotate
it (clear the column, a fresh one is generated on next `/owner/settings` visit) if a feed URL
leaks.
- Credentials for client-owned accounts are recorded two ways: `clients.vault_ref` points to a
Vaultwarden item for anything the operator manually stashes there; the `credentials` table
holds secrets the *system itself* generates (currently: the Easy!Appointments provider login