docs: add authentication, API reference, and MCP server guides
Some checks failed
CI / ci (push) Failing after 11s
Some checks failed
CI / ci (push) Failing after 11s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
642
docs/api.md
Normal file
642
docs/api.md
Normal file
@@ -0,0 +1,642 @@
|
||||
# API Reference
|
||||
|
||||
Base URL: `http://localhost:3000`
|
||||
|
||||
**Auth:** GET endpoints are public. POST, PUT, PATCH, and DELETE require authentication via session cookie or `X-API-Key` header. See [authentication.md](authentication.md) for details.
|
||||
|
||||
**Prices** are stored and returned in cents (e.g., `2500` = $25.00).
|
||||
**Weights** are in grams.
|
||||
**Timestamps** are unix epoch integers.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Health](#health)
|
||||
- [Items](#items)
|
||||
- [Categories](#categories)
|
||||
- [Threads](#threads)
|
||||
- [Setups](#setups)
|
||||
- [Images](#images)
|
||||
- [Settings](#settings)
|
||||
- [Totals](#totals)
|
||||
- [Auth](#auth)
|
||||
|
||||
---
|
||||
|
||||
## Health
|
||||
|
||||
### `GET /api/health`
|
||||
|
||||
Returns server status. Always public.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Items
|
||||
|
||||
### `GET /api/items`
|
||||
|
||||
List all items in the collection.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Revelate Tangle Frame Bag",
|
||||
"categoryId": 2,
|
||||
"weightGrams": 185,
|
||||
"priceCents": 12000,
|
||||
"notes": "Medium size, fits 2020 Surly Straggler",
|
||||
"productUrl": "https://revelatedesigns.com/...",
|
||||
"imageFilename": "1710000000000-uuid.jpg",
|
||||
"imageSourceUrl": "https://example.com/image.jpg",
|
||||
"createdAt": 1710000000,
|
||||
"updatedAt": 1710000000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/items/:id`
|
||||
|
||||
Get a single item by ID.
|
||||
|
||||
**Response:** item object (see above), or `404 { "error": "Item not found" }`.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/items`
|
||||
|
||||
Create a new item. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "Revelate Tangle Frame Bag",
|
||||
"categoryId": 2,
|
||||
"weightGrams": 185,
|
||||
"priceCents": 12000,
|
||||
"notes": "Medium size",
|
||||
"productUrl": "https://revelatedesigns.com/...",
|
||||
"imageFilename": "1710000000000-uuid.jpg",
|
||||
"imageSourceUrl": "https://example.com/image.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-----------------|---------|----------|--------------------------------------|
|
||||
| `name` | string | yes | Item name |
|
||||
| `categoryId` | integer | yes | ID of an existing category |
|
||||
| `weightGrams` | number | no | Weight in grams (non-negative) |
|
||||
| `priceCents` | integer | no | Price in cents (non-negative) |
|
||||
| `notes` | string | no | Free-text notes |
|
||||
| `productUrl` | string | no | URL to product page |
|
||||
| `imageFilename` | string | no | Filename from a prior image upload |
|
||||
| `imageSourceUrl`| string | no | Original URL the image came from |
|
||||
|
||||
**Response:** `201` — created item object.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/items/:id`
|
||||
|
||||
Update an existing item. All fields are optional. Auth required.
|
||||
|
||||
**Request:** same fields as POST, all optional.
|
||||
|
||||
**Response:** updated item object, or `404`.
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/items/:id`
|
||||
|
||||
Delete an item and clean up its image file if one exists. Auth required.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
Returns `404` if item not found.
|
||||
|
||||
---
|
||||
|
||||
## Categories
|
||||
|
||||
### `GET /api/categories`
|
||||
|
||||
List all categories.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{ "id": 1, "name": "Uncategorized", "icon": "package" },
|
||||
{ "id": 2, "name": "Bags", "icon": "backpack" }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/categories`
|
||||
|
||||
Create a new category. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "name": "Bags", "icon": "backpack" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|--------|--------|----------|--------------------------------------|
|
||||
| `name` | string | yes | Category name |
|
||||
| `icon` | string | no | Icon name (defaults to `"package"`) |
|
||||
|
||||
**Response:** `201` — created category object.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/categories/:id`
|
||||
|
||||
Update a category. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "name": "Carry", "icon": "bag" }
|
||||
```
|
||||
|
||||
**Response:** updated category object, or `404`.
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/categories/:id`
|
||||
|
||||
Delete a category. Auth required.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threads
|
||||
|
||||
Research threads track multiple candidate options for a single gear slot before committing to a purchase.
|
||||
|
||||
### `GET /api/threads`
|
||||
|
||||
List threads. By default only active (unresolved) threads are returned.
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-------------------|---------|---------|------------------------------------|
|
||||
| `includeResolved` | boolean | `false` | Set to `true` to include resolved threads |
|
||||
|
||||
**Example:** `GET /api/threads?includeResolved=true`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Handlebar bag",
|
||||
"categoryId": 2,
|
||||
"status": "active",
|
||||
"resolvedCandidateId": null,
|
||||
"createdAt": 1710000000,
|
||||
"updatedAt": 1710000000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/threads`
|
||||
|
||||
Create a new research thread. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "name": "Handlebar bag", "categoryId": 2 }
|
||||
```
|
||||
|
||||
**Response:** `201` — created thread object.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/threads/:id`
|
||||
|
||||
Get a thread with all its candidates.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Handlebar bag",
|
||||
"categoryId": 2,
|
||||
"status": "active",
|
||||
"resolvedCandidateId": null,
|
||||
"candidates": [
|
||||
{
|
||||
"id": 10,
|
||||
"threadId": 1,
|
||||
"name": "Revelate Sweetroll",
|
||||
"categoryId": 2,
|
||||
"weightGrams": 290,
|
||||
"priceCents": 13500,
|
||||
"notes": "16L, fits most drop bars",
|
||||
"productUrl": "https://revelatedesigns.com/...",
|
||||
"imageFilename": null,
|
||||
"imageSourceUrl": null,
|
||||
"status": "researching",
|
||||
"pros": "Waterproof, large capacity",
|
||||
"cons": "Pricey",
|
||||
"sortOrder": 0,
|
||||
"createdAt": 1710000000,
|
||||
"updatedAt": 1710000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Returns `404` if thread not found.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/threads/:id`
|
||||
|
||||
Update a thread's name or category. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "name": "Bar bag", "categoryId": 2 }
|
||||
```
|
||||
|
||||
**Response:** updated thread object, or `404`.
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/threads/:id`
|
||||
|
||||
Delete a thread and all its candidates. Cleans up any candidate image files. Auth required.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/threads/:id/candidates`
|
||||
|
||||
Add a candidate to a thread. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "Revelate Sweetroll",
|
||||
"categoryId": 2,
|
||||
"weightGrams": 290,
|
||||
"priceCents": 13500,
|
||||
"notes": "16L capacity",
|
||||
"productUrl": "https://revelatedesigns.com/...",
|
||||
"imageFilename": "1710000000000-uuid.jpg",
|
||||
"imageSourceUrl": "https://example.com/image.jpg",
|
||||
"status": "researching",
|
||||
"pros": "Waterproof, large capacity",
|
||||
"cons": "Expensive"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-----------------|---------|----------|----------------------------------------------------|
|
||||
| `name` | string | yes | Candidate name |
|
||||
| `categoryId` | integer | yes | Category ID |
|
||||
| `weightGrams` | number | no | Weight in grams |
|
||||
| `priceCents` | integer | no | Price in cents |
|
||||
| `notes` | string | no | Notes |
|
||||
| `productUrl` | string | no | Product URL |
|
||||
| `imageFilename` | string | no | Filename from a prior image upload |
|
||||
| `imageSourceUrl`| string | no | Original image URL |
|
||||
| `status` | string | no | `"researching"`, `"ordered"`, or `"arrived"` |
|
||||
| `pros` | string | no | Pros of this option |
|
||||
| `cons` | string | no | Cons of this option |
|
||||
|
||||
**Response:** `201` — created candidate object. Returns `404` if thread not found.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/threads/:threadId/candidates/:candidateId`
|
||||
|
||||
Update a candidate. All fields optional. Auth required.
|
||||
|
||||
**Request:** same fields as POST, all optional.
|
||||
|
||||
**Response:** updated candidate object, or `404`.
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/threads/:threadId/candidates/:candidateId`
|
||||
|
||||
Delete a candidate and clean up its image. Auth required.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `PATCH /api/threads/:id/candidates/reorder`
|
||||
|
||||
Reorder candidates within a thread. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "orderedIds": [12, 10, 11] }
|
||||
```
|
||||
|
||||
`orderedIds` is an array of candidate IDs in the desired display order. All IDs must belong to the thread.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
Returns `400` with an error message if validation fails.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/threads/:id/resolve`
|
||||
|
||||
Resolve a thread by selecting the winning candidate. Auth required.
|
||||
|
||||
The winning candidate's data is copied into a new item in the collection. The thread status is set to `"resolved"` and `resolvedCandidateId` is set.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "candidateId": 10 }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"item": {
|
||||
"id": 42,
|
||||
"name": "Revelate Sweetroll",
|
||||
"categoryId": 2,
|
||||
"weightGrams": 290,
|
||||
"priceCents": 13500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returns `400` if the candidate does not belong to the thread or another error occurs.
|
||||
|
||||
---
|
||||
|
||||
## Setups
|
||||
|
||||
Setups are named collections of items (e.g., "Bikepacking weekend", "Commute loadout").
|
||||
|
||||
### `GET /api/setups`
|
||||
|
||||
List all setups with item counts and weight/cost totals.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Bikepacking weekend",
|
||||
"itemCount": 12,
|
||||
"totalWeightGrams": 4800,
|
||||
"totalPriceCents": 285000,
|
||||
"createdAt": 1710000000,
|
||||
"updatedAt": 1710000000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setups`
|
||||
|
||||
Create a new setup. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "name": "Bikepacking weekend" }
|
||||
```
|
||||
|
||||
**Response:** `201` — created setup object.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/setups/:id`
|
||||
|
||||
Get a setup with its full item list.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Bikepacking weekend",
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Revelate Tangle Frame Bag",
|
||||
"weightGrams": 185,
|
||||
"priceCents": 12000,
|
||||
"categoryId": 2,
|
||||
"classification": "base"
|
||||
}
|
||||
],
|
||||
"totalWeightGrams": 185,
|
||||
"totalPriceCents": 12000
|
||||
}
|
||||
```
|
||||
|
||||
Returns `404` if setup not found.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/setups/:id`
|
||||
|
||||
Update a setup's name. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "name": "Bikepacking overnighter" }
|
||||
```
|
||||
|
||||
**Response:** updated setup object, or `404`.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/setups/:id/items`
|
||||
|
||||
Replace all items in a setup atomically. Deletes all existing setup-item associations and re-inserts with the provided IDs. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "itemIds": [1, 5, 12, 17] }
|
||||
```
|
||||
|
||||
Pass an empty array to remove all items from the setup.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /api/setups/:id`
|
||||
|
||||
Delete a setup. Does not delete the items themselves. Auth required.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Images
|
||||
|
||||
### `POST /api/images`
|
||||
|
||||
Upload an image file. Auth required.
|
||||
|
||||
**Request:** `multipart/form-data` with an `image` field.
|
||||
|
||||
```
|
||||
POST /api/images
|
||||
Content-Type: multipart/form-data; boundary=...
|
||||
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="image"; filename="bag.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
<binary data>
|
||||
--boundary--
|
||||
```
|
||||
|
||||
Constraints:
|
||||
- Accepted types: `image/jpeg`, `image/png`, `image/webp`
|
||||
- Maximum size: 5MB
|
||||
- Files are saved to `./uploads/` with a UUID-based filename
|
||||
|
||||
**Response:** `201`
|
||||
```json
|
||||
{ "filename": "1710000000000-550e8400-e29b-41d4-a716-446655440000.jpg" }
|
||||
```
|
||||
|
||||
Use the returned `filename` as `imageFilename` when creating or updating items or candidates.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/images/from-url`
|
||||
|
||||
Fetch an image from a remote URL and save it locally. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "url": "https://example.com/product-image.jpg" }
|
||||
```
|
||||
|
||||
**Response:** `201`
|
||||
```json
|
||||
{
|
||||
"filename": "1710000000000-uuid.jpg",
|
||||
"sourceUrl": "https://example.com/product-image.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
Returns `400` for invalid URLs, unsupported content types, files over 5MB, or non-200 HTTP responses.
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
Key-value store for application settings.
|
||||
|
||||
### `GET /api/settings/:key`
|
||||
|
||||
Get a setting by key. Public.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "key": "collectionName", "value": "My Bikepacking Gear" }
|
||||
```
|
||||
|
||||
Returns `404` if the key does not exist.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/settings/:key`
|
||||
|
||||
Create or update a setting. Auth required.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "value": "My Bikepacking Gear" }
|
||||
```
|
||||
|
||||
**Response:** updated setting object.
|
||||
|
||||
---
|
||||
|
||||
## Totals
|
||||
|
||||
### `GET /api/totals`
|
||||
|
||||
Global and per-category weight and cost totals. Computed from current item data. Public.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"totalWeightGrams": 12500,
|
||||
"totalPriceCents": 450000,
|
||||
"itemCount": 34
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"categoryId": 2,
|
||||
"name": "Bags",
|
||||
"icon": "backpack",
|
||||
"totalWeightGrams": 1200,
|
||||
"totalPriceCents": 78000,
|
||||
"itemCount": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
|
||||
Authentication endpoints are documented in [authentication.md](authentication.md).
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|-------------------------|----------|---------------------------|
|
||||
| GET | `/api/auth/me` | public | Current session state |
|
||||
| POST | `/api/auth/setup` | public | Create first admin user |
|
||||
| POST | `/api/auth/login` | public | Log in, receive cookie |
|
||||
| POST | `/api/auth/logout` | public | Clear session |
|
||||
| PUT | `/api/auth/password` | session | Change password |
|
||||
| GET | `/api/auth/keys` | required | List API keys |
|
||||
| POST | `/api/auth/keys` | required | Create API key |
|
||||
| DELETE | `/api/auth/keys/:id` | required | Revoke API key |
|
||||
Reference in New Issue
Block a user