772 lines
13 KiB
Markdown
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)
|