# Dash API ``` http://localhost:4208/api ``` ## CORS & cookies All browser requests that need auth must send cookies: ```js fetch('http://localhost:4208/api/auth/me', { credentials: 'include', }); ``` Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h). The browser must talk to the API over **HTTPS** for login to work — `Secure` cookies are not sent on plain HTTP. ## Auth flow 1. Call [`GET /auth/me`](routes/auth/me.md) to get `googleClientId` (included in 200 and 401 responses). 2. Sign in with Google on the frontend (Google Identity Services / One Tap). 3. POST the Google ID token to [`POST /auth/login`](routes/auth/login.md). 4. The API sets the session cookie. 5. Call protected routes with `credentials: 'include'`. The cookie is refreshed automatically when it has less than 2 hours left. ## Routes Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/routes/). Source of truth is the code; regenerate with `npm run docs:api`. | File | Endpoint | |------|----------| | [`routes/article/create.md`](routes/article/create.md) | `POST /article` | | [`routes/article/export.zip.md`](routes/article/export.zip.md) | `GET /article/export.zip` | | [`routes/article/delete.md`](routes/article/delete.md) | `DELETE /article/{sku}` | | [`routes/article/get.md`](routes/article/get.md) | `GET /article/{sku}` | | [`routes/article/update.md`](routes/article/update.md) | `PATCH /article/{sku}` | | [`routes/article/set-brand.md`](routes/article/set-brand.md) | `PUT /article/{sku}/brand` | | [`routes/article/set-categories.md`](routes/article/set-categories.md) | `PUT /article/{sku}/categories` | | [`routes/article/set-eans.md`](routes/article/set-eans.md) | `PUT /article/{sku}/eans` | | [`routes/article/history.md`](routes/article/history.md) | `GET /article/{sku}/history` | | [`routes/article/add-image.md`](routes/article/add-image.md) | `POST /article/{sku}/image` | | [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` | | [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` | | [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` | | [`routes/article/_sku/print.md`](routes/article/_sku/print.md) | `POST /article/{sku}/print` | | [`routes/article/supplier/list.md`](routes/article/supplier/list.md) | `GET /article/{sku}/supplier` | | [`routes/article/supplier/create.md`](routes/article/supplier/create.md) | `POST /article/{sku}/supplier` | | [`routes/article/supplier/order.md`](routes/article/supplier/order.md) | `PUT /article/{sku}/supplier/order` | | [`routes/article/supplier/delete.md`](routes/article/supplier/delete.md) | `DELETE /article/{sku}/supplier/{id}` | | [`routes/article/supplier/update.md`](routes/article/supplier/update.md) | `PATCH /article/{sku}/supplier/{id}` | | [`routes/article/_sku/track_stock.md`](routes/article/_sku/track_stock.md) | `PUT /article/{sku}/track_stock` | | [`routes/article/set-weight.md`](routes/article/set-weight.md) | `PUT /article/{sku}/weight` | | [`routes/articleIndex.md`](routes/articleIndex.md) | `GET /articleIndex` | | [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` | | [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` | | [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` | | [`routes/auth/selected-company.md`](routes/auth/selected-company.md) | `PUT /auth/selected-company` | | [`routes/brand/list.md`](routes/brand/list.md) | `GET /brand` | | [`routes/brand/upsert.md`](routes/brand/upsert.md) | `PUT /brand` | | [`routes/brand/delete.md`](routes/brand/delete.md) | `DELETE /brand/{id}` | | [`routes/category/create.md`](routes/category/create.md) | `POST /category` | | [`routes/category/export.zip.md`](routes/category/export.zip.md) | `GET /category/export.zip` | | [`routes/category/order.md`](routes/category/order.md) | `PUT /category/order` | | [`routes/category/tree.md`](routes/category/tree.md) | `GET /category/tree` | | [`routes/category/delete.md`](routes/category/delete.md) | `DELETE /category/{id}` | | [`routes/category/update.md`](routes/category/update.md) | `PATCH /category/{id}` | | [`routes/category/articles.md`](routes/category/articles.md) | `GET /category/{id}/articles` | | [`routes/category/delete-image.md`](routes/category/delete-image.md) | `DELETE /category/{id}/image` | | [`routes/category/add-image.md`](routes/category/add-image.md) | `POST /category/{id}/image` | | [`routes/company/list.md`](routes/company/list.md) | `GET /company` | | [`routes/company/create.md`](routes/company/create.md) | `POST /company` | | [`routes/companysetting/list.md`](routes/companysetting/list.md) | `GET /company/setting` | | [`routes/companysetting/get.md`](routes/companysetting/get.md) | `GET /company/setting/{key}` | | [`routes/companysetting/set.md`](routes/companysetting/set.md) | `PUT /company/setting/{key}` | | [`routes/company/delete.md`](routes/company/delete.md) | `DELETE /company/{id}` | | [`routes/company/update.md`](routes/company/update.md) | `PUT /company/{id}` | | [`routes/customergroup/list.md`](routes/customergroup/list.md) | `GET /customergroup` | | [`routes/customergroup/create.md`](routes/customergroup/create.md) | `POST /customergroup` | | [`routes/customergroup/order.md`](routes/customergroup/order.md) | `PUT /customergroup/order` | | [`routes/customergroup/delete.md`](routes/customergroup/delete.md) | `DELETE /customergroup/{id}` | | [`routes/customergroup/update.md`](routes/customergroup/update.md) | `PATCH /customergroup/{id}` | | [`routes/delivery/list.md`](routes/delivery/list.md) | `GET /delivery` | | [`routes/delivery/create.md`](routes/delivery/create.md) | `POST /delivery` | | [`routes/delivery/parse.md`](routes/delivery/parse.md) | `POST /delivery/parse` | | [`routes/delivery/delete.md`](routes/delivery/delete.md) | `DELETE /delivery/{id}` | | [`routes/delivery/get.md`](routes/delivery/get.md) | `GET /delivery/{id}` | | [`routes/delivery/update.md`](routes/delivery/update.md) | `PATCH /delivery/{id}` | | [`routes/delivery/file.md`](routes/delivery/file.md) | `GET /delivery/{id}/file/{fileId}` | | [`routes/entity/list.md`](routes/entity/list.md) | `GET /entity` | | [`routes/entity/create.md`](routes/entity/create.md) | `POST /entity` | | [`routes/entity/next-number.md`](routes/entity/next-number.md) | `GET /entity/next-number` | | [`routes/entity/address/list.md`](routes/entity/address/list.md) | `GET /entity/{entityId}/address` | | [`routes/entity/address/create.md`](routes/entity/address/create.md) | `POST /entity/{entityId}/address` | | [`routes/entity/address/delete.md`](routes/entity/address/delete.md) | `DELETE /entity/{entityId}/address/{id}` | | [`routes/entity/address/update.md`](routes/entity/address/update.md) | `PATCH /entity/{entityId}/address/{id}` | | [`routes/entity/contact/list.md`](routes/entity/contact/list.md) | `GET /entity/{entityId}/contact` | | [`routes/entity/contact/create.md`](routes/entity/contact/create.md) | `POST /entity/{entityId}/contact` | | [`routes/entity/contact/delete.md`](routes/entity/contact/delete.md) | `DELETE /entity/{entityId}/contact/{id}` | | [`routes/entity/contact/update.md`](routes/entity/contact/update.md) | `PATCH /entity/{entityId}/contact/{id}` | | [`routes/entity/delete.md`](routes/entity/delete.md) | `DELETE /entity/{id}` | | [`routes/entity/get.md`](routes/entity/get.md) | `GET /entity/{id}` | | [`routes/entity/update.md`](routes/entity/update.md) | `PATCH /entity/{id}` | | [`routes/events.md`](routes/events.md) | `GET /events` | | [`routes/health.md`](routes/health.md) | `GET /health` | | [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` | | [`routes/labeltemplate/list.md`](routes/labeltemplate/list.md) | `GET /labeltemplate` | | [`routes/labeltemplate/create.md`](routes/labeltemplate/create.md) | `POST /labeltemplate` | | [`routes/labeltemplate/delete.md`](routes/labeltemplate/delete.md) | `DELETE /labeltemplate/{id}` | | [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) | `GET /labeltemplate/{id}` | | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PATCH /labeltemplate/{id}` | | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PUT /labeltemplate/{id}` | | [`routes/ledger.get.md`](routes/ledger.get.md) | `GET /ledger` | | [`routes/ledger.post.md`](routes/ledger.post.md) | `POST /ledger` | | [`routes/ledger/manifestarticle/_manifestarticleId.md`](routes/ledger/manifestarticle/_manifestarticleId.md) | `GET /ledger/manifestarticle/{manifestarticleId}` | | [`routes/ledger/sku/_sku.md`](routes/ledger/sku/_sku.md) | `GET /ledger/sku/{sku}` | | [`routes/number/list.md`](routes/number/list.md) | `GET /number` | | [`routes/number/update.md`](routes/number/update.md) | `PATCH /number/{id}` | | [`routes/order/list.md`](routes/order/list.md) | `GET /order` | | [`routes/order/create.md`](routes/order/create.md) | `POST /order` | | [`routes/order/delete.md`](routes/order/delete.md) | `DELETE /order/{id}` | | [`routes/order/get.md`](routes/order/get.md) | `GET /order/{id}` | | [`routes/order/update.md`](routes/order/update.md) | `PATCH /order/{id}` | | [`routes/posterminal/list.md`](routes/posterminal/list.md) | `GET /posterminal` | | [`routes/posterminal/upsert.md`](routes/posterminal/upsert.md) | `PUT /posterminal` | | [`routes/posterminal/delete.md`](routes/posterminal/delete.md) | `DELETE /posterminal/{id}` | | [`routes/posterminal/setOffAll.md`](routes/posterminal/setOffAll.md) | `DELETE /posterminal/{id}/all` | | [`routes/posterminal/setOnAll.md`](routes/posterminal/setOnAll.md) | `PUT /posterminal/{id}/all` | | [`routes/posterminal/setOff.md`](routes/posterminal/setOff.md) | `DELETE /posterminal/{id}/category/{categoryId}` | | [`routes/posterminal/setOn.md`](routes/posterminal/setOn.md) | `PUT /posterminal/{id}/category/{categoryId}` | | [`routes/printer/list.md`](routes/printer/list.md) | `GET /printer` | | [`routes/printer/create.md`](routes/printer/create.md) | `POST /printer` | | [`routes/printer/order.md`](routes/printer/order.md) | `PUT /printer/order` | | [`routes/printer/delete.md`](routes/printer/delete.md) | `DELETE /printer/{id}` | | [`routes/printer/get.md`](routes/printer/get.md) | `GET /printer/{id}` | | [`routes/printer/update.md`](routes/printer/update.md) | `PATCH /printer/{id}` | | [`routes/printer/_id.put.md`](routes/printer/_id.put.md) | `PUT /printer/{id}` | | [`routes/quote/list.md`](routes/quote/list.md) | `GET /quote` | | [`routes/quote/create.md`](routes/quote/create.md) | `POST /quote` | | [`routes/quote/delete.md`](routes/quote/delete.md) | `DELETE /quote/{id}` | | [`routes/quote/get.md`](routes/quote/get.md) | `GET /quote/{id}` | | [`routes/quote/update.md`](routes/quote/update.md) | `PATCH /quote/{id}` | | [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` | | [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` | | [`routes/stock/adjust.md`](routes/stock/adjust.md) | `POST /stock/adjust` | | [`routes/supplier/list.md`](routes/supplier/list.md) | `GET /supplier` | | [`routes/supplier/create.md`](routes/supplier/create.md) | `POST /supplier` | | [`routes/supplier/delete.md`](routes/supplier/delete.md) | `DELETE /supplier/{id}` | | [`routes/supplier/update.md`](routes/supplier/update.md) | `PATCH /supplier/{id}` | | [`routes/supplier/articles.get.md`](routes/supplier/articles.get.md) | `GET /supplier/{id}/articles` | | [`routes/supplier/articles.post.md`](routes/supplier/articles.post.md) | `POST /supplier/{id}/articles` | | [`routes/thumb/get.md`](routes/thumb/get.md) | `GET /thumb/{fullHash}` | | [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` | | [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` | | [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` | | [`routes/warehouse/list.md`](routes/warehouse/list.md) | `GET /warehouse` | | [`routes/warehouse/create.md`](routes/warehouse/create.md) | `POST /warehouse` | | [`routes/warehouse/order.md`](routes/warehouse/order.md) | `PUT /warehouse/order` | | [`routes/warehouse/delete.md`](routes/warehouse/delete.md) | `DELETE /warehouse/{id}` | | [`routes/warehouse/get.md`](routes/warehouse/get.md) | `GET /warehouse/{id}` | | [`routes/warehouse/update.md`](routes/warehouse/update.md) | `PATCH /warehouse/{id}` | | [`routes/warehouse/_id.put.md`](routes/warehouse/_id.put.md) | `PUT /warehouse/{id}` | Machine-readable spec: [`openapi.json`](openapi.json). ## Signals (SSE) Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). After a successful mutation, the API emits a named SSE event (signal) to subscribers of the same selected company. | Signal | Emitted by | |--------|------------| | `article-` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `POST /article/{sku}/supplier`, `PUT /article/{sku}/supplier/order`, `DELETE /article/{sku}/supplier/{id}`, `PATCH /article/{sku}/supplier/{id}`, `PUT /article/{sku}/track_stock`, `PUT /article/{sku}/weight` | | `articles` | `POST /article`, `DELETE /article/{sku}`, `PATCH /article/{sku}` | | `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` | ## DESIGN - for every route we have a file in /src/api/routes - that should loosly bound to services in /src/services - for every service we have a CLI tester in /scripts/ - all DB SQL code shall be in /src/db - with SQL staements in /src/db/ - API docs: `@openapi` JSDoc on handlers → `npm run docs:api` → `docs/api/`