325 lines
14 KiB
Markdown
325 lines
14 KiB
Markdown
# Costco Grocery List - AI Agent Instructions
|
|
|
|
## Architecture Overview
|
|
|
|
This is a full-stack grocery list management app with **role-based access control (RBAC)**:
|
|
- **Backend**: Node.js + Express + PostgreSQL (port 5000)
|
|
- **Frontend**: React 19 + TypeScript + Vite (port 3000/5173)
|
|
- **Deployment**: Docker Compose with separate dev/prod configurations
|
|
|
|
## Mobile-First Design Principles
|
|
|
|
**CRITICAL**: All UI components MUST be designed for both mobile and desktop from the start.
|
|
|
|
**Responsive Design Requirements**:
|
|
- Use relative units (`rem`, `em`, `%`, `vh/vw`) over fixed pixels where possible
|
|
- Implement mobile breakpoints: `480px`, `768px`, `1024px`
|
|
- Test layouts at: 320px (small phone), 375px (phone), 768px (tablet), 1024px+ (desktop)
|
|
- Avoid horizontal scrolling on mobile devices
|
|
- Touch targets minimum 44x44px for mobile usability
|
|
- Use `max-width` with `margin: 0 auto` for content containers
|
|
- Stack elements vertically on mobile, use flexbox/grid for larger screens
|
|
- Hide/collapse navigation into hamburger menus on mobile
|
|
- Ensure modals/dropdowns work well on small screens
|
|
|
|
**Common Patterns**:
|
|
```css
|
|
/* Mobile-first approach */
|
|
.container {
|
|
padding: 1rem;
|
|
max-width: 100%;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.container {
|
|
padding: 2rem;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Key Design Patterns
|
|
|
|
**Dual RBAC System** - Two separate role hierarchies:
|
|
|
|
**1. System Roles** (users.role column):
|
|
- `system_admin`: Access to Admin Panel for system-wide management (stores, users)
|
|
- `user`: Regular system user (default for new registrations)
|
|
- Defined in [backend/models/user.model.js](backend/models/user.model.js)
|
|
- Used for Admin Panel access control
|
|
|
|
**2. Household Roles** (household_members.role column):
|
|
- `admin`: Can manage household members, change roles, delete household
|
|
- `user`: Can add/edit items, mark as bought (standard member permissions)
|
|
- Defined per household membership
|
|
- Used for household-level permissions (item management, member management)
|
|
|
|
**Important**: Always distinguish between system role and household role:
|
|
- **System role**: From `AuthContext` or `req.user.role` - controls Admin Panel access
|
|
- **Household role**: From `activeHousehold.role` or `household_members.role` - controls household operations
|
|
|
|
**Middleware chain pattern** for protected routes:
|
|
```javascript
|
|
// System-level protection
|
|
router.get("/stores", auth, requireRole("system_admin"), controller.getAllStores);
|
|
|
|
// Household-level checks done in controller
|
|
router.post("/lists/:householdId/items", auth, controller.addItem);
|
|
```
|
|
- `auth` middleware extracts JWT from `Authorization: Bearer <token>` header
|
|
- `requireRole` checks system role only
|
|
- Household role checks happen in controllers using `household.model.js` methods
|
|
|
|
**Frontend route protection**:
|
|
- `<PrivateRoute>`: Requires authentication, redirects to `/login` if no token
|
|
- `<RoleGuard allowed={[ROLES.SYSTEM_ADMIN]}>`: Requires system_admin role for Admin Panel
|
|
- Household permissions: Check `activeHousehold.role` in components (not route-level)
|
|
- Example in [frontend/src/App.jsx](frontend/src/App.jsx)
|
|
|
|
**Multi-Household Architecture**:
|
|
- Users can belong to multiple households
|
|
- Each household has its own grocery lists, stores, and item classifications
|
|
- `HouseholdContext` manages active household selection
|
|
- All list operations are scoped to the active household
|
|
|
|
## Database Schema
|
|
|
|
**PostgreSQL server runs externally** - not in Docker Compose. Connection configured in [backend/.env](backend/.env) via standard environment variables.
|
|
|
|
**Core Tables**:
|
|
|
|
**users** - System users
|
|
- `id` (PK), `username`, `password` (bcrypt), `name`, `display_name`
|
|
- `role`: `system_admin` | `user` (default: `viewer` - legacy)
|
|
- System-level authentication and authorization
|
|
|
|
**households** - Household entities
|
|
- `id` (PK), `name`, `invite_code`, `created_by`, `created_at`
|
|
- Each household is independent with own lists and members
|
|
|
|
**household_members** - Junction table (users ↔ households)
|
|
- `id` (PK), `household_id` (FK), `user_id` (FK), `role`, `joined_at`
|
|
- `role`: `admin` | `user` (household-level permissions)
|
|
- One user can belong to multiple households with different roles
|
|
|
|
**items** - Master item catalog
|
|
- `id` (PK), `name`, `default_image`, `default_image_mime_type`, `usage_count`
|
|
- Shared across all households, case-insensitive unique names
|
|
|
|
**stores** - Store definitions (system-wide)
|
|
- `id` (PK), `name`, `default_zones` (JSONB array)
|
|
- Managed by system_admin in Admin Panel
|
|
|
|
**household_stores** - Stores available to each household
|
|
- `id` (PK), `household_id` (FK), `store_id` (FK), `is_default`
|
|
- Links households to stores they use
|
|
|
|
**household_lists** - Grocery list items per household
|
|
- `id` (PK), `household_id` (FK), `store_id` (FK), `item_id` (FK)
|
|
- `quantity`, `bought`, `custom_image`, `custom_image_mime_type`
|
|
- `added_by`, `modified_on`
|
|
- Scoped to household + store combination
|
|
|
|
**household_list_history** - Tracks quantity contributions
|
|
- `id` (PK), `household_list_id` (FK), `quantity`, `added_by`, `added_on`
|
|
- Multi-contributor tracking (who added how much)
|
|
|
|
**household_item_classifications** - Item classifications per household/store
|
|
- `id` (PK), `household_id`, `store_id`, `item_id`
|
|
- `item_type`, `item_group`, `zone`, `confidence`, `source`
|
|
- Household-specific overrides of global classifications
|
|
|
|
**item_classification** - Global item classifications
|
|
- `id` (PK), `item_type`, `item_group`, `zone`, `confidence`, `source`
|
|
- System-wide defaults for item categorization
|
|
|
|
**Legacy Tables** (deprecated, may still exist):
|
|
- `grocery_list`, `grocery_history` - Old single-household implementation
|
|
|
|
**Important patterns**:
|
|
- No formal migration system - schema changes are manual SQL
|
|
- Items use case-insensitive matching (`ILIKE`) to prevent duplicates
|
|
- JOINs with `ARRAY_AGG` for multi-contributor queries (see [backend/models/list.model.v2.js](backend/models/list.model.v2.js))
|
|
- All list operations require `household_id` parameter for scoping
|
|
- Image storage: `bytea` columns for images with separate MIME type columns
|
|
|
|
## Development Workflow
|
|
|
|
### Local Development
|
|
```bash
|
|
# Start all services with hot-reload against LOCAL database
|
|
docker-compose -f docker-compose.dev.yml up
|
|
|
|
# Backend runs nodemon (watches backend/*.js)
|
|
# Frontend runs Vite dev server with HMR on port 3000
|
|
```
|
|
|
|
**Key dev setup details**:
|
|
- Volume mounts preserve `node_modules` in containers while syncing source code
|
|
- Backend uses `Dockerfile` (standard) with `npm run dev` override
|
|
- Frontend uses `Dockerfile.dev` with `CHOKIDAR_USEPOLLING=true` for file watching
|
|
- Both connect to **external PostgreSQL server** (configured in `backend/.env`)
|
|
- No database container in compose - DB is managed separately
|
|
|
|
### Production Build
|
|
```bash
|
|
# Local production build (for testing)
|
|
docker-compose -f docker-compose.prod.yml up --build
|
|
|
|
# Actual production uses pre-built images
|
|
docker-compose up # Pulls from private registry
|
|
```
|
|
|
|
### CI/CD Pipeline (Gitea Actions)
|
|
|
|
See [.gitea/workflows/deploy.yml](.gitea/workflows/deploy.yml) for full workflow:
|
|
|
|
**Build stage** (on push to `main`):
|
|
1. Run backend tests (`npm test --if-present`)
|
|
2. Build backend image with tags: `:latest` and `:<commit-sha>`
|
|
3. Build frontend image with tags: `:latest` and `:<commit-sha>`
|
|
4. Push both images to private registry
|
|
|
|
**Deploy stage**:
|
|
1. SSH to production server
|
|
2. Upload `docker-compose.yml` to deployment directory
|
|
3. Pull latest images and restart containers with `docker compose up -d`
|
|
4. Prune old images
|
|
|
|
**Notify stage**:
|
|
- Sends deployment status via webhook
|
|
|
|
**Required secrets**:
|
|
- `REGISTRY_USER`, `REGISTRY_PASS`: Docker registry credentials
|
|
- `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_KEY`: SSH deployment credentials
|
|
|
|
### Backend Scripts
|
|
- `npm run dev`: Start with nodemon
|
|
- `npm run build`: esbuild compilation + copy public assets to `dist/`
|
|
- `npm test`: Run Jest tests (currently no tests exist)
|
|
|
|
### Frontend Scripts
|
|
- `npm run dev`: Vite dev server (port 5173)
|
|
- `npm run build`: TypeScript compilation + Vite production build
|
|
|
|
### Docker Configurations
|
|
|
|
**docker-compose.yml** (production):
|
|
- Pulls pre-built images from private registry
|
|
- Backend on port 5000, frontend on port 3000 (nginx serves on port 80)
|
|
- Requires `backend.env` and `frontend.env` files
|
|
|
|
**docker-compose.dev.yml** (local development):
|
|
- Builds images locally from Dockerfile/Dockerfile.dev
|
|
- Volume mounts for hot-reload: `./backend:/app` and `./frontend:/app`
|
|
- Named volumes preserve `node_modules` between rebuilds
|
|
- Backend uses `backend/.env` directly
|
|
- Frontend uses `Dockerfile.dev` with polling enabled for cross-platform compatibility
|
|
|
|
**docker-compose.prod.yml** (local production testing):
|
|
- Builds images locally using production Dockerfiles
|
|
- Backend: Standard Node.js server
|
|
- Frontend: Multi-stage build with nginx serving static files
|
|
|
|
## Configuration & Environment
|
|
|
|
**Backend** ([backend/.env](backend/.env)):
|
|
- Database connection variables (host, user, password, database name)
|
|
- `JWT_SECRET`: Token signing key
|
|
- `ALLOWED_ORIGINS`: Comma-separated CORS whitelist (supports static origins + `192.168.*.*` IP ranges)
|
|
- `PORT`: Server port (default 5000)
|
|
|
|
**Frontend** (environment variables):
|
|
- `VITE_API_URL`: Backend base URL
|
|
|
|
**Config accessed via**:
|
|
- Backend: `process.env.VAR_NAME`
|
|
- Frontend: `import.meta.env.VITE_VAR_NAME` (see [frontend/src/config.ts](frontend/src/config.ts))
|
|
|
|
## Authentication Flow
|
|
|
|
1. User logs in → backend returns `{token, userId, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
|
|
- `role` is the **system role** (`system_admin` or `user`)
|
|
2. Frontend stores in `localStorage` and `AuthContext` ([frontend/src/context/AuthContext.jsx](frontend/src/context/AuthContext.jsx))
|
|
3. `HouseholdContext` loads user's households and sets active household
|
|
- Active household includes `household.role` (the **household role**)
|
|
4. Axios interceptor auto-attaches `Authorization: Bearer <token>` header ([frontend/src/api/axios.js](frontend/src/api/axios.js))
|
|
5. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js))
|
|
- Sets `req.user = { id, role, username }` with **system role**
|
|
6. Controllers check household membership/role using [backend/models/household.model.js](backend/models/household.model.js)
|
|
7. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login
|
|
|
|
## Critical Conventions
|
|
|
|
### Security Practices
|
|
- **Never expose credentials**: Do not hardcode or document actual values for `JWT_SECRET`, database passwords, API keys, or any sensitive configuration
|
|
- **No infrastructure details**: Avoid documenting specific IP addresses, domain names, deployment paths, or server locations in code or documentation
|
|
- **Environment variables**: Reference `.env` files conceptually - never include actual contents
|
|
- **Secrets in CI/CD**: Document that secrets are required, not their values
|
|
- **Code review**: Scan all changes for accidentally committed credentials before pushing
|
|
|
|
### Backend
|
|
- **No SQL injection**: Always use parameterized queries (`$1`, `$2`, etc.) with [backend/db/pool.js](backend/db/pool.js)
|
|
- **Password hashing**: Use `bcryptjs` for hashing (see [backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
|
|
- **CORS**: Dynamic origin validation in [backend/app.js](backend/app.js) allows configured origins + local IPs
|
|
- **Error responses**: Return JSON with `{message: "..."}` structure
|
|
|
|
### Frontend
|
|
- **Mixed JSX/TSX**: Some components are `.jsx` (JavaScript), others `.tsx` (TypeScript) - maintain existing file extensions
|
|
- **API calls**: Use centralized `api` instance from [frontend/src/api/axios.js](frontend/src/api/axios.js), not raw axios
|
|
- **Role checks**: Access role from `AuthContext`, compare with constants from [frontend/src/constants/roles.js](frontend/src/constants/roles.js)
|
|
- **Navigation**: Use React Router's `<Navigate>` for redirects, not `window.location` (except in interceptor)
|
|
|
|
## Common Tasks
|
|
|
|
**Add a new protected route**:
|
|
1. Backend: Add route with `auth` middleware (+ `requireRole(...)` if system role check needed)
|
|
2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `<PrivateRoute>` (and `<RoleGuard>` for Admin Panel)
|
|
|
|
**Access user info in backend controller**:
|
|
```javascript
|
|
const { id, role } = req.user; // Set by auth middleware (system role)
|
|
const userId = req.user.id;
|
|
```
|
|
|
|
**Check household permissions in backend controller**:
|
|
```javascript
|
|
const householdRole = await household.getUserRole(householdId, userId);
|
|
if (!householdRole) return res.status(403).json({ message: "Not a member of this household" });
|
|
if (householdRole !== 'admin') return res.status(403).json({ message: "Household admin required" });
|
|
```
|
|
|
|
**Check household permissions in frontend**:
|
|
```javascript
|
|
const { activeHousehold } = useContext(HouseholdContext);
|
|
const householdRole = activeHousehold?.role; // 'admin' or 'user'
|
|
|
|
// Allow all members except viewers (no viewer role in households)
|
|
const canManageItems = householdRole && householdRole !== 'viewer'; // Usually just check if role exists
|
|
|
|
// Admin-only actions
|
|
const canManageMembers = householdRole === 'admin';
|
|
```
|
|
|
|
**Query grocery items with contributors**:
|
|
Use the JOIN pattern in [backend/models/list.model.v2.js](backend/models/list.model.v2.js) - aggregates user names via `household_list_history` table.
|
|
|
|
## Testing
|
|
|
|
**Backend**:
|
|
- Jest configured at root level ([package.json](package.json))
|
|
- Currently **no test files exist** - testing infrastructure needs development
|
|
- CI/CD runs `npm test --if-present` but will pass if no tests found
|
|
- Focus area: API endpoint testing (use `supertest` with Express)
|
|
|
|
**Frontend**:
|
|
- ESLint only (see [frontend/eslint.config.js](frontend/eslint.config.js))
|
|
- No test runner configured
|
|
- Manual testing workflow in use
|
|
|
|
**To add backend tests**:
|
|
1. Create `backend/__tests__/` directory
|
|
2. Use Jest + Supertest pattern for API tests
|
|
3. Mock database calls or use test database
|