costco-grocery-list/.github/copilot-instructions.md
2026-01-01 22:55:39 -08:00

198 lines
8.6 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
### Key Design Patterns
**Three-tier RBAC system** (`viewer`, `editor`, `admin`):
- `viewer`: Read-only access to grocery lists
- `editor`: Can add items and mark as bought
- `admin`: Full user management via admin panel
- Roles defined in [backend/models/user.model.js](backend/models/user.model.js) and mirrored in [frontend/src/constants/roles.js](frontend/src/constants/roles.js)
**Middleware chain pattern** for protected routes:
```javascript
router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.addItem);
```
- `auth` middleware extracts JWT from `Authorization: Bearer <token>` header
- `requireRole` checks if user's role matches allowed roles
- See [backend/routes/list.routes.js](backend/routes/list.routes.js) for examples
**Frontend route protection**:
- `<PrivateRoute>`: Requires authentication, redirects to `/login` if no token
- `<RoleGuard allowed={[ROLES.ADMIN]}>`: Requires specific role(s), redirects to `/` if unauthorized
- Example in [frontend/src/App.jsx](frontend/src/App.jsx)
## Database Schema
**PostgreSQL server runs externally** - not in Docker Compose. Connection configured in [backend/.env](backend/.env) via standard environment variables.
**Tables** (inferred from models, no formal migrations):
- **users**: `id`, `username`, `password` (bcrypt hashed), `name`, `role`
- **grocery_list**: `id`, `item_name`, `quantity`, `bought`, `added_by`
- **grocery_history**: Junction table tracking which users added which items
**Important patterns**:
- No 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.js](backend/models/list.model.js))
## 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, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js))
2. Frontend stores in `localStorage` and `AuthContext` ([frontend/src/context/AuthContext.jsx](frontend/src/context/AuthContext.jsx))
3. Axios interceptor auto-attaches `Authorization: Bearer <token>` header ([frontend/src/api/axios.js](frontend/src/api/axios.js))
4. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js))
5. 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` + `requireRole(...)` middleware
2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `<PrivateRoute>` and/or `<RoleGuard>`
**Access user info in backend controller**:
```javascript
const { id, role } = req.user; // Set by auth middleware
```
**Query grocery items with contributors**:
Use the JOIN pattern in [backend/models/list.model.js](backend/models/list.model.js) - aggregates user names via `grocery_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