From 622864c6b3148aefba3afdf14f02c821e2bc8052 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 09:14:45 -0500 Subject: [PATCH] docs(readme): document /v1/customer/search + the two CSV export endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three real endpoints existed in the router but were missing from the README's endpoint table: - GET /v1/customer/search (substring search, 2-char min) - GET /v1/customer/export.csv (master must scope, OWASP guard) - GET /v1/timeentry/export.csv (same shape as customer export) Add a row for each. The CSV-export rows also call out the formula-injection guard (#266 / #267) so operators reading the README can see the safety property without grepping the controllers. Pure docs — no code, no tests. 760 still passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8ef811c..1f38c4d 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,12 @@ Working example at [node.timetrackerapi.com](http://node.timetrackerapi.com). | `GET /v1/customer/:id` | yes (`authKey`) | Single customer lookup. Master key sees all; non-master only sees customers in its own company. | | `GET /v1/customer/bycompany/:id` | yes (`authKey`) | Customers in a company (paginated). Master sees any; non-master only its own. Query params: `limit` (default 100, max 500), `offset` (default 0). Archived customers (`custArch = true`) are filtered out. | | `POST /v1/customer` | yes (`authKey`) | Create a customer. Master key may target any `custCompId`; non-master keys can only create within their own company (and `custCompId` defaults to that). Returns 201 + the created customer. | +| `GET /v1/customer/search` | yes (`authKey`) | Case-insensitive substring search across `custCompanyName`, `custFName`, `custLName`. Query params: `q` (2-char minimum), `companyId` (master-only — non-master keys are auto-scoped and a mismatched `companyId` returns 403), `limit` (default 100, max 500), `offset` (default 0). | +| `GET /v1/customer/export.csv` | yes (`authKey`) | CSV export of customers in a company. Master keys must supply `companyId`; non-master keys are auto-scoped. `limit` (default 5000, max 5000). Cells starting with `=`, `+`, `-`, `@`, tab, or CR are prefixed with a single quote to defuse OWASP CSV-formula injection on spreadsheet-app open. | | `POST /v1/timeentry` | yes (`authKey`) | Create a time entry. Body: `teCustId` (required), `teStartedAt` (required, ISO 8601), `teEndedAt` (optional — in-flight entries allowed), `teDescription`, `teBillable` (default true). `teMinutes` is computed server-side on close. | | `GET /v1/timeentry/:id` | yes (`authKey`) | Single time entry lookup. Company-scoped. Archived (soft-deleted) entries return 404. | | `GET /v1/timeentry/bycompany/:id` | yes (`authKey`) | List time entries for a company. Query params: `customerId` (filter), `from` / `to` (ISO 8601 date range on `teStartedAt`), `limit` (default 100, max 500). Ordered most-recent first. | +| `GET /v1/timeentry/export.csv` | yes (`authKey`) | CSV export of time entries. Same auth contract + CSV-injection guard as `/v1/customer/export.csv`. Query params: `companyId` (master-only), `customerId` (filter), `from` / `to` (ISO 8601), `limit` (default 5000, max 5000). | | `PATCH /v1/timeentry/:id` | yes (`authKey`) | Partial update. Updatable: `teDescription`, `teStartedAt`, `teEndedAt`, `teBillable`. `teMinutes` is recomputed on bound change. | | `DELETE /v1/timeentry/:id` | yes (`authKey`) | Soft-delete (sets `teArch = true`). Entries are never physically removed via the API. | | `* /v1/worker/*` | yes (`authKey`) | Full CRUD for Workers (`workerId`, `workerFName`, `workerLName`, `workerTitle`, `workerDefaultBillType`, `workerCompId`, `workerArch`). Direct company scoping via `workerCompId`. Endpoints: `POST /v1/worker`, `GET /v1/worker/:id`, `GET /v1/worker/bycompany/:id`, `PATCH /v1/worker/:id`, `DELETE /v1/worker/:id`. |