costco-grocery-list/API_DOCUMENTATION.md
2026-01-04 16:02:19 -08:00

772 lines
13 KiB
Markdown

# Costco Grocery List API Documentation
Base URL: `http://localhost:5000/api`
## Table of Contents
- [Authentication](#authentication)
- [Grocery List Management](#grocery-list-management)
- [User Management](#user-management)
- [Admin Operations](#admin-operations)
- [Role-Based Access Control](#role-based-access-control)
- [Error Responses](#error-responses)
---
## Authentication
All authenticated endpoints require a JWT token in the `Authorization` header:
```
Authorization: Bearer <token>
```
### POST /auth/register
Register a new user account.
**Access:** Public
**Request Body:**
```json
{
"username": "string (lowercase)",
"password": "string",
"name": "string"
}
```
**Response:** `200 OK`
```json
{
"message": "User registered",
"user": {
"id": 1,
"username": "johndoe",
"name": "John Doe",
"role": "viewer"
}
}
```
**Error:** `400 Bad Request`
```json
{
"message": "Registration failed",
"error": "Error details"
}
```
---
### POST /auth/login
Authenticate and receive a JWT token.
**Access:** Public
**Request Body:**
```json
{
"username": "string (lowercase)",
"password": "string"
}
```
**Response:** `200 OK`
```json
{
"token": "jwt_token_string",
"username": "johndoe",
"role": "editor"
}
```
**Errors:**
- `401 Unauthorized` - User not found or invalid credentials
```json
{
"message": "User not found"
}
```
or
```json
{
"message": "Invalid credentials"
}
```
---
## Grocery List Management
### GET /list
Retrieve all unbought grocery items.
**Access:** Authenticated (All roles)
**Query Parameters:** None
**Response:** `200 OK`
```json
[
{
"id": 1,
"item_name": "milk",
"quantity": 2,
"bought": false,
"item_image": "base64_encoded_string",
"image_mime_type": "image/jpeg",
"added_by_users": ["John Doe", "Jane Smith"],
"modified_on": "2026-01-03T12:00:00Z",
"item_type": "dairy",
"item_group": "milk",
"zone": "DAIRY"
}
]
```
**Fields:**
- `id` - Unique item identifier
- `item_name` - Name of grocery item (lowercase)
- `quantity` - Number of items needed
- `bought` - Purchase status (always false for this endpoint)
- `item_image` - Base64 encoded image (nullable)
- `image_mime_type` - MIME type of image (nullable)
- `added_by_users` - Array of user names who added/modified this item
- `modified_on` - Last modification timestamp
- `item_type` - Classification type (nullable)
- `item_group` - Classification group (nullable)
- `zone` - Store zone location (nullable)
---
### GET /list/item-by-name
Retrieve a specific item by name (case-insensitive).
**Access:** Authenticated (All roles)
**Query Parameters:**
- `itemName` (required) - Name of the item to search
**Example:** `GET /list/item-by-name?itemName=milk`
**Response:** `200 OK`
```json
{
"id": 1,
"item_name": "milk",
"quantity": 2,
"bought": false,
"item_image": "base64_encoded_string",
"image_mime_type": "image/jpeg"
}
```
Returns `null` if item not found.
---
### GET /list/suggest
Get item name suggestions based on partial input.
**Access:** Authenticated (All roles)
**Query Parameters:**
- `query` (required) - Partial item name to search
**Example:** `GET /list/suggest?query=mil`
**Response:** `200 OK`
```json
[
{ "item_name": "milk" },
{ "item_name": "almond milk" },
{ "item_name": "oat milk" }
]
```
**Behavior:**
- Case-insensitive partial matching (ILIKE)
- Searches all items in `grocery_list` (bought and unbought)
- Returns up to 10 suggestions
- Ordered by database query result
---
### GET /list/recently-bought
Retrieve items bought within the last 24 hours.
**Access:** Authenticated (All roles)
**Query Parameters:** None
**Response:** `200 OK`
```json
[
{
"id": 5,
"item_name": "bread",
"quantity": 1,
"bought": true,
"item_image": "base64_encoded_string",
"image_mime_type": "image/jpeg",
"added_by_users": ["John Doe"],
"modified_on": "2026-01-03T10:30:00Z",
"item_type": "bakery",
"item_group": "bread",
"zone": "BAKERY"
}
]
```
**Behavior:**
- Returns items with `bought = true`
- Filters by `modified_on` within last 24 hours
- Includes classification data if available
---
### GET /list/item/:id/classification
Get classification data for a specific item.
**Access:** Authenticated (All roles)
**Path Parameters:**
- `id` (required) - Item ID
**Example:** `GET /list/item/1/classification`
**Response:** `200 OK`
```json
{
"item_type": "dairy",
"item_group": "milk",
"zone": "DAIRY",
"confidence": 1.0,
"source": "user"
}
```
Returns empty object `{}` if no classification exists.
**Fields:**
- `item_type` - Classification type (e.g., "dairy", "produce", "meat")
- `item_group` - Sub-group within type (e.g., "milk", "cheese")
- `zone` - Store zone (e.g., "DAIRY", "PRODUCE", "MEAT")
- `confidence` - Classification confidence (0.0 to 1.0)
- `source` - Origin of classification ("user" or "auto")
---
### POST /list/add
Add a new item or update quantity of existing item.
**Access:** Editor, Admin
**Content-Type:** `multipart/form-data`
**Request Body:**
```
itemName: string (required)
quantity: number (required)
image: file (optional) - Image file
```
**Response:** `200 OK`
```json
{
"message": "Item added/updated",
"addedBy": 1
}
```
**Behavior:**
- If item exists and is unbought: updates quantity
- If item exists and is bought: marks as unbought with new quantity
- If item doesn't exist: creates new item
- Adds record to `grocery_history` table
- Processes and optimizes uploaded image (800x800px, JPEG 85% quality)
**Image Processing:**
- Accepted formats: JPEG, PNG, GIF, WebP
- Max file size: 5MB
- Output: 800x800px JPEG at 85% quality
- Stored as binary in database
---
### POST /list/update-image
Update or add image to an existing item.
**Access:** Editor, Admin
**Content-Type:** `multipart/form-data`
**Request Body:**
```
id: number (required)
itemName: string (required)
quantity: number (required)
image: file (required) - Image file
```
**Response:** `200 OK`
```json
{
"message": "Image updated successfully"
}
```
**Error:** `400 Bad Request`
```json
{
"message": "No image provided"
}
```
---
### POST /list/mark-bought
Mark an item as purchased.
**Access:** Editor, Admin
**Request Body:**
```json
{
"id": 1
}
```
**Response:** `200 OK`
```json
{
"message": "Item marked bought"
}
```
**Behavior:**
- Sets `bought = true`
- Updates `modified_on` timestamp
- Item moves to "Recently Bought" list
---
### PUT /list/item/:id
Update item details and/or classification.
**Access:** Editor, Admin
**Path Parameters:**
- `id` (required) - Item ID
**Request Body:**
```json
{
"itemName": "string (optional)",
"quantity": 2,
"classification": {
"item_type": "dairy",
"item_group": "milk",
"zone": "DAIRY"
}
}
```
**Response:** `200 OK`
```json
{
"message": "Item updated successfully"
}
```
**Errors:**
- `400 Bad Request` - Invalid classification values
```json
{
"message": "Invalid item_type"
}
```
or
```json
{
"message": "Invalid item_group for selected item_type"
}
```
or
```json
{
"message": "Invalid zone"
}
```
**Classification Validation:**
- `item_type`: Must be one of the valid types defined in `backend/constants/classifications.js`
- `item_group`: Must be valid for the selected `item_type`
- `zone`: Must be one of the predefined store zones
**Valid Zones:**
- ENTRANCE, PRODUCE, MEAT, DELI, BAKERY, DAIRY, FROZEN, DRY_GOODS, BEVERAGES, SNACKS, HOUSEHOLD, HEALTH, CHECKOUT
---
## User Management
### GET /users/exists
Check if a username already exists.
**Access:** Public
**Query Parameters:**
- `username` (required) - Username to check
**Example:** `GET /users/exists?username=johndoe`
**Response:** `200 OK`
```json
{
"exists": true
}
```
or
```json
{
"exists": false
}
```
---
### GET /users/test
Health check endpoint for user routes.
**Access:** Public
**Response:** `200 OK`
```json
{
"message": "User route is working"
}
```
---
## Admin Operations
### GET /admin/users
Retrieve all registered users.
**Access:** Admin only
**Query Parameters:** None
**Response:** `200 OK`
```json
[
{
"id": 1,
"username": "johndoe",
"name": "John Doe",
"role": "editor"
},
{
"id": 2,
"username": "janesmith",
"name": "Jane Smith",
"role": "viewer"
}
]
```
---
### PUT /admin/users
Update a user's role.
**Access:** Admin only
**Request Body:**
```json
{
"id": 1,
"role": "admin"
}
```
**Valid Roles:**
- `viewer` - Read-only access
- `editor` - Can add/modify/buy items
- `admin` - Full access including user management
**Response:** `200 OK`
```json
{
"message": "Role updated",
"id": 1,
"role": "admin"
}
```
**Errors:**
- `400 Bad Request` - Invalid role
```json
{
"error": "Invalid role"
}
```
- `404 Not Found` - User not found
```json
{
"error": "User not found"
}
```
- `500 Internal Server Error`
```json
{
"error": "Failed to update role"
}
```
---
### DELETE /admin/users
Delete a user account.
**Access:** Admin only
**Path Parameters:**
- `id` (required) - User ID
**Example:** `DELETE /admin/users?id=5`
**Response:** `200 OK`
```json
{
"message": "User deleted",
"id": 5
}
```
**Errors:**
- `404 Not Found`
```json
{
"error": "User not found"
}
```
- `500 Internal Server Error`
```json
{
"error": "Failed to delete user"
}
```
---
## Role-Based Access Control
### Roles and Permissions
| Role | View Lists | Add/Edit Items | Buy Items | User Management |
|------|-----------|----------------|-----------|-----------------|
| **viewer** | ✅ | ❌ | ❌ | ❌ |
| **editor** | ✅ | ✅ | ✅ | ❌ |
| **admin** | ✅ | ✅ | ✅ | ✅ |
### Protected Endpoints by Role
**All Authenticated Users (viewer, editor, admin):**
- GET /list
- GET /list/item-by-name
- GET /list/suggest
- GET /list/recently-bought
- GET /list/item/:id/classification
**Editor and Admin Only:**
- POST /list/add
- POST /list/update-image
- POST /list/mark-bought
- PUT /list/item/:id
**Admin Only:**
- GET /admin/users
- PUT /admin/users
- DELETE /admin/users
---
## Error Responses
### Standard Error Format
```json
{
"message": "Error description",
"error": "Detailed error information (optional)"
}
```
### Common HTTP Status Codes
| Code | Meaning | Common Causes |
|------|---------|---------------|
| 200 | OK | Successful request |
| 400 | Bad Request | Invalid input data, missing required fields |
| 401 | Unauthorized | Missing/invalid token, invalid credentials |
| 403 | Forbidden | Insufficient permissions for operation |
| 404 | Not Found | Resource doesn't exist |
| 500 | Internal Server Error | Server-side error |
### Authentication Errors
**Missing Token:**
```json
{
"message": "No token provided"
}
```
**Invalid Token:**
```json
{
"message": "Invalid or expired token"
}
```
**Insufficient Permissions:**
```json
{
"message": "Access denied"
}
```
---
## Data Models
### User
```typescript
{
id: number
username: string (lowercase, unique)
password: string (bcrypt hashed)
name: string
role: "viewer" | "editor" | "admin"
}
```
### Grocery Item
```typescript
{
id: number
item_name: string (lowercase)
quantity: number
bought: boolean
item_image: Buffer | null (binary image data)
image_mime_type: string | null
added_by: number (user id)
modified_on: timestamp
}
```
### Item Classification
```typescript
{
id: number (references grocery_list.id)
item_type: string | null
item_group: string | null
zone: string | null
confidence: number (0.0 - 1.0)
source: "user" | "auto"
}
```
### Grocery History
```typescript
{
id: number
list_item_id: number (references grocery_list.id)
quantity: number
added_by: number (user id)
added_on: timestamp
}
```
---
## Notes
### Case Sensitivity
- Usernames are stored and compared in **lowercase**
- Item names are stored in **lowercase**
- All searches are **case-insensitive** (ILIKE in PostgreSQL)
### Token Expiration
- JWT tokens expire after **1 year**
- Include token expiration handling in client applications
### Image Optimization
- Images are automatically processed:
- Resized to 800x800px (maintains aspect ratio)
- Converted to JPEG format
- Compressed to 85% quality
- Max upload size: 5MB
### Database Transaction Handling
- Item additions/updates include history tracking
- Classification updates are atomic operations
### CORS Configuration
- Allowed origins configured via `ALLOWED_ORIGINS` environment variable
- Supports both static origins and IP range patterns (e.g., `192.168.*.*`)
---
## Frontend Integration Examples
### Login Flow
```javascript
const response = await axios.post('/api/auth/login', {
username: 'johndoe',
password: 'password123'
});
const { token, role, username } = response.data;
localStorage.setItem('token', token);
localStorage.setItem('role', role);
localStorage.setItem('username', username);
```
### Authenticated Request
```javascript
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:5000/api',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const items = await api.get('/list');
```
### Adding Item with Image
```javascript
const formData = new FormData();
formData.append('itemName', 'milk');
formData.append('quantity', 2);
formData.append('image', imageFile); // File object from input
await api.post('/list/add', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
```
---
## Version Information
- API Version: 1.0
- Last Updated: January 3, 2026
- Backend Framework: Express 5.1.0
- Database: PostgreSQL 8.16.0
- Authentication: JWT (jsonwebtoken 9.0.2)