cookie-tracker/CHANGELOG.md
adamp a4d5461a8c Whitelist SPA routes so unknown paths return 404
Replace the file-extension check with a regex whitelist of known client
routes. Only whitelisted paths serve index.html for React Router — all
other paths return a real 404.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 00:35:19 -06:00

359 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Changelog
All notable changes to the Cookie Tracker are documented in this file.
---
## 2026-02-09 (404 Fix)
### Bugfix: Return Proper 404s for Non-Existent Routes
**Commit:** `5074f3d`*Return proper 404s for unmatched API routes and static files*
**Problem**: Nessus flagged that requesting a non-existent page returned a 200 with `index.html` instead of a proper 404. The SPA catch-all (`app.get('*')`) was serving `index.html` for everything — including unmatched `/api/*` routes and non-existent static files like `/foo.js`.
**Fix** (`server/index.js`):
- Added a `/api` catch-all after all API route mounts that returns `404 JSON` (`{"error":"Not found"}`) for any unmatched API route. When auth is enabled, unauthenticated requests to non-existent API routes return 401 (the auth middleware intercepts before the 404 handler, avoiding route enumeration).
- Replaced the file-extension check with a regex whitelist of known client routes (`/`, `/inventory`, `/customers`, `/orders`, `/orders/new`, `/orders/:id`, `/restock`, `/reports`). Only these paths serve `index.html` for React Router. All other paths — extensionless (`/foo`), with special characters (`/it's`), or non-existent files (`/missing.js`) — return a real 404.
---
## 2026-02-09 (Security Hardening)
### Enhancement: Security Headers and Rate Limiting
**Commit:** `a4ef21d`*Add security headers via helmet and improve rate limiting*
Added `helmet` and `express-rate-limit` packages to harden the application against common web vulnerabilities flagged by security scanners (e.g. Nessus).
**Server** (`server/index.js`):
- Added `helmet()` middleware before all routes. This sets security headers: `Content-Security-Policy`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Strict-Transport-Security`, `Referrer-Policy: no-referrer`, and removes `X-Powered-By`.
- Added a global API rate limiter on `/api` — 100 requests per minute per IP — using `express-rate-limit`. Returns standard `RateLimit-*` headers.
**Server** (`server/routes/auth.js`):
- Removed the hand-rolled in-memory rate limiter (~25 lines): `loginAttempts` Map, `isRateLimited()`, `recordAttempt()` functions, and per-request IP tracking/recording.
- Replaced with a dedicated `express-rate-limit` instance applied as middleware on the `POST /login` route: 5 attempts per minute per IP. Same behavior, cleaner implementation.
**Already solid (no changes needed):**
- Session cookies: httpOnly, SameSite=lax, Secure in production
- HMAC signing with timing-safe comparison
- CSRF: SameSite=lax (the `csurf` package is deprecated)
- SQL injection: parameterized queries throughout
- XSS: React escapes output
---
## 2026-02-09 (README Fix)
### Fix: Clone URL in Deployment Instructions
**Commit:** `b1d3e69`*Fix clone URL in README deployment instructions*
- `README.md` — Updated the placeholder `git clone https://your-git-host/your-repo/cookie-tracker.git` to the actual repository URL `https://git.raylos.net/adamp/cookie-tracker.git` in the deployment setup instructions.
---
## 2026-02-09 (Mobile Reports Fix)
### Bugfix: Reports Page Mobile Layout
**Commit:** `de71565`*Fix Reports page mobile layout*
**Problem**: On mobile (iPhone 13 Pro Max portrait, and any screen < 640px), the Reports page tables were squished and unreadable. The existing CSS converts `.table-wrap` tables to a stacked card layout on small screens by hiding `<thead>` and showing `data-label` attributes on each `<td>`, but the Reports tables were missing those attributes entirely. This meant cells rendered without labels and the 45 column tables were crammed into the narrow viewport.
**Fix**:
- `client/src/pages/Reports.jsx` Added `data-label` attributes to every `<td>` across all 5 report tables:
- **Sales by Product**: Product, Units sold, Revenue (including `<tfoot>` total row)
- **Top Customers**: Customer, Orders, Total spent
- **Revenue Over Time**: Date, Orders, Revenue
- **Order Status Breakdown**: Status, Count
- **Inventory Summary**: Product, Current stock, Total restocked, Total sold, Restock count
- `client/src/pages/Reports.jsx` Added `className="bar-cell"` to the Revenue Over Time inline bar chart column.
- `client/src/index.css` Added `display: block` for `tbody` and `tfoot` inside `.table-wrap` at the `max-width: 639px` breakpoint so `<tfoot>` rows also get the mobile card treatment.
- `client/src/index.css` Added `.table-wrap td.bar-cell { display: none }` at the mobile breakpoint to hide the bar chart column (doesn't translate well to stacked layout).
---
## 2026-02-09
### Feature: Restock History Tracking
**Commit:** `1ed2642` *Add restock history tracking, payment tracking, and reports page*
Every stock change is now logged to a `stock_adjustments` table, creating a full audit trail for inventory movements.
**Schema** (`server/db.js`):
- Added `stock_adjustments` table with columns: `id`, `product_id` (FK products), `adjustment` (integer, positive or negative), `reason` (text), `reference_id` (nullable, links to order ID), `created_at`.
**Server changes**:
- `server/routes/products.js` `PATCH /:id/stock` now inserts a row into `stock_adjustments` with `reason='restock'` after updating the product quantity.
- `server/routes/products.js` Added `GET /stock-history` endpoint (placed before `/:id` to avoid route collision). Accepts optional query params: `product_id`, `start`, `end`, `limit`. Returns stock adjustment rows joined with product name, ordered by most recent first.
- `server/routes/orders.js` Stock adjustments are logged in all three transaction paths:
- **POST `/`** (create order): logs `reason='order_created'`, `adjustment=-qty`, `reference_id=orderId` for each item.
- **PUT `/:id`** (update order): logs `reason='order_updated'` with positive adjustment when restoring old stock, then negative adjustment when deducting new stock.
- **DELETE `/:id`** (delete order): logs `reason='order_deleted'` with positive adjustment for each restored item.
---
### Feature: Payment Tracking
**Commit:** `1ed2642` *Add restock history tracking, payment tracking, and reports page*
Orders now track how they were paid and how much was paid, enabling balance-due tracking.
**Schema** (`server/db.js`):
- Added `payment_method` column (TEXT, nullable) to `orders` table. Valid values: `'cash'`, `'card'`, `'venmo'`, `'check'`, `'other'`, or `null`.
- Added `amount_paid` column (REAL, NOT NULL, default 0) to `orders` table.
- Both columns added via `ALTER TABLE` with try-catch for idempotency on existing databases. (Later migrated to versioned schema migrations see below.)
**Server changes** (`server/routes/orders.js`):
- **POST `/`**: Accepts `payment_method` and `amount_paid` in request body. Validates `payment_method` against allowed values. Includes both in the INSERT statement.
- **PUT `/:id`**: Accepts `payment_method` and `amount_paid`. Includes in UPDATE statement, preserving existing values when not provided.
- GET endpoints already use `SELECT o.*`, so new columns are returned automatically.
**Client changes**:
- `client/src/pages/OrderNew.jsx`:
- Added `paymentMethod` state (dropdown: None/Cash/Card/Venmo/Check/Other) and `amountPaid` state (number input).
- Payment method and amount paid are on separate rows: Status + Payment method share a `form-row`, Amount paid sits below with `maxWidth: 200` to prevent overflow.
- When a payment method is selected and amount is empty, auto-fills with current order total.
- Both fields included in submit payload.
- `client/src/pages/OrderDetail.jsx`:
- **Display mode**: Shows payment method (capitalized or "Not set"), amount paid, and a red "Balance due" warning when `amount_paid < order total`.
- **Edit mode**: Payment method dropdown and amount paid input, initialized from order data in `load()`. When switching from "0" to a payment method, auto-fills with total. Both included in `handleUpdate` payload.
---
### Feature: Reports Page
**Commit:** `1ed2642` *Add restock history tracking, payment tracking, and reports page*
A new Reports page provides five aggregated views of sales, customer, and inventory data, with date range filtering.
**Server** (`server/routes/reports.js` new file):
- `GET /api/reports` accepts optional `start` and `end` query params (YYYY-MM-DD format).
- Returns a single JSON object with five sections:
| Section | Description | Key columns |
|---------|-------------|-------------|
| `salesByProduct` | LEFT JOIN products order_items orders (via subquery for date filtering). All products shown, zero-filled. | `product_name`, `units_sold`, `revenue` |
| `topCustomers` | JOIN customers orders order_items. Sorted by total spent descending. | `customer_name`, `order_count`, `total_spent` |
| `revenueOverTime` | JOIN orders order_items, grouped by `date(created_at)`. | `date`, `revenue`, `order_count` |
| `orderStatusBreakdown` | GROUP BY `orders.status`. | `status`, `count` |
| `inventorySummary` | Products with correlated subqueries on `stock_adjustments` for restock totals, sold totals, and restock count. | `product_name`, `current_stock`, `low_stock_threshold`, `total_restocked`, `total_sold`, `restock_count` |
- Date filtering uses positional `?` params (not named params, which caused errors with better-sqlite3 when not all params are used in every query). Order-based queries use `orderParams`, inventory summary uses `invParams` (SA params repeated 3x for 3 subqueries).
- `salesByProduct` uses a subquery in the LEFT JOIN to properly filter order_items by date range (putting date filter directly on the LEFT JOIN ON clause didn't filter correctly).
**Server** (`server/index.js`):
- Imported and mounted `reportsRouter` at `/api/reports` (after dashboard mount, behind auth middleware).
**Client** (`client/src/pages/Reports.jsx` new file):
- Date range toolbar with preset dropdown (All time / This week / This month / Custom) and conditional date inputs for custom range.
- Fetches `/api/reports` with start/end params, re-fetches when dates change via `useEffect`.
- Five sections:
1. **Sales by Product** table with totals row in `<tfoot>`.
2. **Top Customers** table sorted by total spent.
3. **Revenue Over Time** table with inline bar indicators (`div` with width proportional to max revenue).
4. **Order Status Breakdown** horizontal colored bar segments (yellow=pending, green=paid, blue=delivered) plus a table.
5. **Inventory Summary** table with low-stock products highlighted (red background, "(low)" label).
**Client** (`client/src/App.jsx`):
- Imported `Reports` component, added `<Route path="/reports" element={<Reports />} />`.
**Client** (`client/src/components/Layout.jsx`):
- Added `{ path: '/reports', label: 'Reports' }` to `navItems` array.
---
### Enhancement: Girl Scouts Trefoil Logo
**Commit:** `1ed2642` *Add restock history tracking, payment tracking, and reports page*
- Extracted the trefoil icon paths from the full Girl Scouts wordmark SVG and created `client/src/assets/gs-trefoil.svg` with a `viewBox="0 0 13 11.7"` containing just the two trefoil `<path>` elements.
- `client/src/components/Layout.jsx`: Imported the SVG as `trefoilLogo` and added an `<img>` tag next to "Cookie Tracker" in the header link, styled at `height: 1.5em` with `verticalAlign: 'middle'`.
---
### Enhancement: Mobile Network Access
**Commit:** `1ed2642` *Add restock history tracking, payment tracking, and reports page*
- `client/vite.config.js`: Added `host: true` to the Vite dev server config so it binds to all network interfaces, allowing access from phones on the same Wi-Fi network.
---
### Bug Fixes and Iteration During Development
These issues were discovered during testing and fixed before the feature commit:
1. **Reports named params error**: better-sqlite3 threw "Missing named parameter" when using `$start`/`$saStart` named params across queries that didn't all use every param. Fixed by switching to positional `?` params with separate `orderParams` and `saParams` arrays.
2. **salesByProduct date filtering**: The initial LEFT JOIN approach with date filter on the orders ON clause didn't properly filter order_items products still showed all-time sales data. Fixed by restructuring to use a subquery: `LEFT JOIN (SELECT ... FROM order_items JOIN orders WHERE date_filter) oi ON oi.product_id = p.id`.
3. **OrderNew layout overflow**: Three `flex: 1` form groups (Status, Payment method, Amount paid) in a single `form-row` inside a `maxWidth: 640` card caused the amount paid input to overflow the card boundary, especially on mobile. Fixed by moving Amount paid to its own row below the Status/Payment method row, with `maxWidth: 200`.
---
## 2026-02-09 (Code Review Fixes)
### Bugfix: OrderDetail Product Change Overwrites product_id
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P0
**Problem**: In `client/src/pages/OrderDetail.jsx` (edit mode), changing the product in a line item called `updateLine` twice once for `product_id`, once for `price_at_sale`. Under React's state batching, the second `setItems` call used stale state from before the first call, so it overwrote the `product_id` back to the old value.
**Fix**: Replaced the two `updateLine` calls with a single `setItems` call that updates both `product_id` and `price_at_sale` in one state transition:
```jsx
setItems((prev) => prev.map((ln, idx) =>
idx === i ? { ...ln, product_id: newId, price_at_sale: p?.price ?? ln.price_at_sale } : ln
));
```
---
### Bugfix: Validate All `:id` Route Parameters
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P0
**Problem**: `req.params.id` was passed directly to SQLite queries without validation. Values like `"abc"`, `"-1"`, or `"0"` could produce 500 errors or unexpected behavior.
**Fix**:
- Created `server/utils.js` with a `parseId(raw)` helper that returns a positive integer or `null`.
- Added `parseId` validation at the top of every `:id` route handler in `products.js`, `customers.js`, and `orders.js` (10 routes total). Invalid IDs return `400` with a clear error message.
- All subsequent DB calls in those routes use the parsed integer `id` instead of `req.params.id`.
**Files changed**: `server/utils.js` (new), `server/routes/products.js`, `server/routes/customers.js`, `server/routes/orders.js`.
---
### Bugfix: Product/Customer Delete with Existing References
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P0
**Problem**: Deleting a product used in `order_items` or a customer with existing `orders` hit the SQLite foreign key constraint, producing an unhandled 500 error with a raw database error message.
**Fix**:
- `server/routes/products.js` DELETE `/:id` now checks `SELECT COUNT(*) FROM order_items WHERE product_id = ?` before deleting. If references exist, returns `409 Conflict` with message "Cannot delete product that has been used in orders".
- `server/routes/customers.js` DELETE `/:id` now checks `SELECT COUNT(*) FROM orders WHERE customer_id = ?` before deleting. If references exist, returns `409 Conflict` with message "Cannot delete customer that has existing orders".
- The Inventory page's delete handler (`client/src/pages/Inventory.jsx`) already had `.catch((e) => setError(e.message))`, so the 409 error message is now displayed to the user automatically.
---
### Improvement: Disallow quantity_on_hand in Product PUT
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P1
**Problem**: Product `quantity_on_hand` could be changed via two paths: `PUT /products/:id` (not logged) and `PATCH /products/:id/stock` (logged to `stock_adjustments`). This created an unauditable backdoor for stock changes.
**Fix**:
- `server/routes/products.js` PUT `/:id` no longer accepts or applies `quantity_on_hand`. Only `name`, `price`, and `low_stock_threshold` are updated. The UPDATE query was changed from 4 columns to 3.
- `client/src/pages/Inventory.jsx` Removed the "Qty" input field from the inline edit form. Removed `quantity_on_hand` from the `handleUpdate` payload. Stock changes now exclusively go through the "Adjust stock" button (which uses PATCH).
---
### Improvement: Global Error Handler
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P1
**Fix** (`server/index.js`):
- Added Express error middleware (4-arg `(err, req, res, next)`) after all routes. Logs the error stack and returns `500` with either the error message (development) or a generic "Internal server error" (production).
- Checks `res.headersSent` to avoid double-sending responses.
- Added `process.on('unhandledRejection')` handler that logs the rejection reason.
---
### Improvement: Validate Report Dates and Stock-History Limit
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P2
**Fix**:
- `server/utils.js` Added `isValidDate(str)` (validates YYYY-MM-DD format and that it parses to a real date) and `parseLimit(raw, max)` (returns a positive integer capped at `max`, default 1000, or `null`).
- `server/routes/reports.js` Validates `start` and `end` query params with `isValidDate()` before using them in queries. Returns 400 with clear message on invalid format.
- `server/routes/products.js` Stock-history endpoint validates `start`, `end` with `isValidDate()`, `product_id` with `parseId()`, and `limit` with `parseLimit()`.
---
### Improvement: Client-Side Robustness
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P2
1. **api.js 204 handling** (`client/src/api.js`): Added `res.jsonSafe()` method to the response object. Returns `null` for 204 responses (no body), otherwise calls `res.json()`. This prevents parse errors if a caller accidentally tries to parse a 204 response body.
2. **OrderNew setSubmitting** (`client/src/pages/OrderNew.jsx`): Changed error-only `setSubmitting(false)` in the `.catch()` to a `.finally(() => setSubmitting(false))` so the button always resets, even if navigation is slow or fails.
3. **Login error messages** (`client/src/pages/Login.jsx`): Now checks `res.status === 429` and shows "Too many login attempts. Try again later." instead of the generic message. Other auth failures show "Invalid credentials" (generic, doesn't leak information about whether a user exists).
---
### Improvement: Schema Migrations
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P3
**Problem**: New columns were added with `try { database.exec("ALTER TABLE ..."); } catch (e) {}`, which silently swallows all errors including unexpected ones.
**Fix** (`server/db.js`):
- Added `schema_version` table (single row with `version` integer) in the main `CREATE TABLE IF NOT EXISTS` block.
- Added `getSchemaVersion()`, `setSchemaVersion()`, and `runMigrations()` functions.
- Migrations are version-gated: `if (version < 1) { ... setSchemaVersion(database, 1); }`. Future migrations increment the version number.
- ALTER TABLE errors are now only caught if they contain "duplicate column" (expected on re-run); unexpected errors are re-thrown.
---
### Improvement: OrderDetail useEffect Dependency
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
**Priority:** P3
**Fix** (`client/src/pages/OrderDetail.jsx`): Wrapped `load` function in `useCallback` with `[id]` dependency. Changed `useEffect` dependency from `[id]` to `[load]`. This satisfies React's exhaustive-deps lint rule.
---
### Documentation: README Deployment Instructions
**Commit:** `7068ea3` *Fix bugs, harden validation, and improve robustness*
Expanded the "Deployment (self-hosted)" section of `README.md` from 4 bullet points to comprehensive instructions:
- Prerequisites (Node.js, PM2, reverse proxy)
- Initial setup steps (clone, install, configure .env, PM2 start/save/startup)
- Deploying updates (`git pull`, `npm run build`, `pm2 restart`)
- Nginx reverse proxy example config with HTTPS
- Backup cron job example
Also updated the Features section to include Reports, Stock Audit Trail, and payment/balance tracking.
---
### Documentation: CLAUDE.md Architecture Update
**Commit:** `c00bb90` *Update CLAUDE.md with current architecture and documentation policy*
- Added change documentation requirement
- Added `utils.js` to server architecture docs
- Updated routes description to include reports endpoint
- Updated pages list to include Reports, Restock
- Updated key data flow to reference `atomicDeductStock()` (corrected from `deductStockForOrder()`), stock_adjustments logging, and PUT restriction on quantity_on_hand
- Added validation patterns documentation
- Added schema migration approach documentation
- Updated database schema section: 6 tables (was 4), payment fields, stock_adjustments