Add credentials store to the CRM, docs cleanup, deploy pipeline TODO
Adds a `credentials` entity to the back office (never mirrored to Sheets, gated by the CRM token even to read) so client logins like the auto-generated Easy!Appointments provider password can be viewed and copied from the dashboard instead of getting lost — the actual cause of the happynails password going missing. Onboarding now saves that generated password instead of discarding it. Also adds Documentation.md, brings README/TODO in line with the current Postgres-first architecture, and tidies the backlog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+16
-7
@@ -21,8 +21,9 @@ every client regardless of tier.
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Postgres (smb-db) │
|
||||
│ clients · leads · projects · bookings · │
|
||||
│ invoices · activity_log — SOURCE OF TRUTH │
|
||||
│ clients · leads · projects · bookings · │
|
||||
│ invoices · activity_log · credentials — │
|
||||
│ SOURCE OF TRUTH (credentials never mirror) │
|
||||
└───────────────▲───────────────┬────────────┘
|
||||
│ SQL │ one-way mirror
|
||||
JSON API │ ▼
|
||||
@@ -70,6 +71,7 @@ Six entities, defined once in `backoffice/app/db.py::TABLES` and mirrored 1:1 in
|
||||
| `bookings` | `booking_id` | Every appointment, synced from Easy!Appointments via `booking-sync`. |
|
||||
| `invoices` | `invoice_id` | Billing records (manual today — no automated invoicing workflow yet). |
|
||||
| `activity_log` | `id` (serial) | Append-only audit trail; every mutation (API or workflow) writes one row. |
|
||||
| `credentials` | `cred_id` (`CR-<epoch-ms>`) | Per-client login credentials (e.g. the auto-generated Easy!Appointments provider login) — `client_id`, `label`, `username`, `secret`, `notes`. **The one entity excluded from the Sheets mirror** (`mirror: False` in `TABLES`) so secrets never leave Postgres; reading it also requires `X-CRM-Token` (every other entity's list is read-only-open behind Caddy basic-auth alone). Surfaced in the dashboard's **Credentials** tab with a masked value, a reveal toggle, and a copy-to-clipboard button. |
|
||||
|
||||
`db.coerce_row()` is the single place that types/normalizes incoming values (dates, timestamps,
|
||||
numbers, booleans) so the CRUD API, the n8n ingest path and the one-time Sheets importer can never
|
||||
@@ -91,8 +93,10 @@ Flask app (`app.py`) + `waitress`, backed by Postgres (`smb-db`, `postgres:16-al
|
||||
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.
|
||||
- `GET /healthz` — DB connectivity check.
|
||||
- `GET /` — the dashboard (`static/index.html`; currently **Leads** and **Clients** tabs only —
|
||||
read + add (+Neu) + edit (✎) + delete (🗑), all token-gated, all audit-logged).
|
||||
- `GET /` — the dashboard (`static/index.html`; **Leads**, **Clients** and **Credentials** tabs —
|
||||
read + add (+Neu) + edit (✎) + delete (🗑), all token-gated, all audit-logged. The Credentials
|
||||
tab masks the `secret` column by default with a per-row 👁 reveal toggle and a 📋 copy-to-
|
||||
clipboard button).
|
||||
|
||||
Every write is audit-logged to `activity_log` and enqueues an async, best-effort mirror of that
|
||||
entity (and of `activity_log` itself) into the linked Google Sheet — mirror failures never fail
|
||||
@@ -154,7 +158,7 @@ keeps the 14 most recent dumps in `/home/mivanchenko/backups/smb-crm/`.
|
||||
| Workflow | Trigger | Does |
|
||||
|---|---|---|
|
||||
| `lead-intake.json` | webhook | Normalize a lead payload → `POST /api/leads` → Telegram notify. Used by every demo/client lead form and the callback widget. |
|
||||
| `onboarding.json` | webhook (`onboard.mivanchenko.de` form) | Compute client+project rows → `POST /api/clients` → `POST /api/projects` → Telegram notify → **provision Easy!Appointments** (create service, create provider with a generated login, build the booking embed URL) → `PATCH` the client's `stack_notes` with that embed URL + EA login. Fully automates "sign a client" end to end. |
|
||||
| `onboarding.json` | webhook (`onboard.mivanchenko.de` form) | Compute client+project rows → `POST /api/clients` → `POST /api/projects` → Telegram notify → **provision Easy!Appointments** (create service, create provider with a generated login, build the booking embed URL) → `PATCH` the client's `stack_notes` with that embed URL + EA username → `POST /api/credentials` with the EA username **and password**. Fully automates "sign a client" end to end, including capturing the generated password so it isn't lost (it used to be discarded after the EA API call — see `TODO.md`). |
|
||||
| `booking-sync.json` | webhook (EA) | Normalize a booking event → `POST /api/bookings` → Telegram notify. |
|
||||
| `renewal-reminder.json` | daily 08:00 schedule | Read `Clients` from Sheets → find renewals due soon → Telegram notify → append a row to `Activity Log`. **Note:** still reads from the Sheets mirror rather than the DB directly — safe today because the mirror is kept current, but a re-point to the DB would remove that indirection. |
|
||||
|
||||
@@ -207,8 +211,13 @@ see `deploy/clients/README.md` and each group's own compose file for the exact r
|
||||
feed URL leaks.
|
||||
- Sheet cell values are defended against formula injection (`_cell()` in `app.py` prefixes
|
||||
values starting with `=+-@` with a `'`).
|
||||
- Credentials for client-owned accounts are never stored directly — `clients.vault_ref` stores a
|
||||
pointer into Vaultwarden only.
|
||||
- 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
|
||||
created during onboarding), gated by `X-CRM-Token` even to read and deliberately excluded from
|
||||
the Sheets mirror. Stored as plaintext in Postgres today — same trust boundary as the rest of
|
||||
the CRM (Caddy basic-auth + host security); revisit with column-level encryption (pgcrypto) if
|
||||
the dashboard is ever exposed more broadly (see `TODO.md`).
|
||||
- The DB→Sheets mirror is clear-then-write, not atomic — a reader can theoretically catch a
|
||||
cleared tab mid-sync. Accepted as low-risk (human overview, not a system of record) — see
|
||||
`TODO.md`.
|
||||
|
||||
Reference in New Issue
Block a user