# Animal Experiments Database — Architecture Memory ## Project Overview Full-stack application for managing animal experimental data. - **Backend**: Node.js + Express + Prisma ORM - **Frontend**: React (Vite) + TailwindCSS - **Database**: PostgreSQL 15 - **Containerization**: Docker + Docker Compose - **Main port**: 52867 (frontend), 3001 (backend API) ## Directory Structure ``` experiments-database/ ├── backend/ # Node.js + Express + Prisma │ ├── src/ │ │ ├── routes/ # Express route handlers │ │ ├── middleware/ # Audit logging, error handling │ │ └── prisma/ # Schema & migrations │ ├── tests/ # Jest unit + integration tests │ └── Dockerfile ├── frontend/ # React + Vite + TailwindCSS │ ├── src/ │ │ ├── components/ │ │ ├── pages/ │ │ └── api/ # Axios API client │ ├── tests/ # RTL unit tests │ ├── e2e/ # Playwright E2E tests │ └── Dockerfile ├── docker-compose.yml └── memory.md ``` ## Database Schema ### Experiments | Column | Type | Notes | |------------|-----------|-------------------| | id | UUID | PK, auto-generated | | title | TEXT | NOT NULL | | created_at | TIMESTAMP | auto-set | ### Animals | Column | Type | Notes | |------------------|------|----------------------| | id | UUID | PK, auto-generated | | experiment_id | UUID | FK → Experiments.id | | animal_id_string | TEXT | Researcher's own ID | | animal_name | TEXT | | ### Daily_Statuses | Column | Type | Notes | |------------------------|-----------|---------------------| | id | UUID | PK, auto-generated | | animal_id | UUID | FK → Animals.id | | date | DATE | NOT NULL | | experiment_description | TEXT | | | vitals | TEXT | | | treatment | TEXT | | | notes | TEXT | | ### Audit_Logs | Column | Type | Notes | |------------|-----------|----------------------------| | id | UUID | PK, auto-generated | | table_name | TEXT | Which table was affected | | record_id | UUID | The affected record's id | | action | TEXT | CREATE / UPDATE / DELETE | | changes | JSONB | before/after snapshot | | timestamp | TIMESTAMP | auto-set | ## API Routes ### Experiments - `GET /api/experiments` — list all - `POST /api/experiments` — create - `GET /api/experiments/:id` — get one - `PUT /api/experiments/:id` — update (audited) - `DELETE /api/experiments/:id` — delete (audited) ### Animals - `GET /api/animals?experimentId=` — list for experiment - `POST /api/animals` — create - `GET /api/animals/:id` — get one - `PUT /api/animals/:id` — update (audited) - `DELETE /api/animals/:id` — delete (audited) ### Daily Statuses - `GET /api/daily-statuses?animalId=` — list for animal - `POST /api/daily-statuses` — create - `GET /api/daily-statuses/:id` — get one - `PUT /api/daily-statuses/:id` — update (audited) - `DELETE /api/daily-statuses/:id` — delete (audited) ### Audit Logs - `GET /api/audit-logs?tableName=&recordId=` — query logs ## Audit Middleware All PUT and DELETE routes pass through `auditMiddleware` which: 1. Fetches the current record (before state) 2. Executes the route handler 3. Writes an `Audit_Log` entry with table_name, record_id, action, and changes (JSONB diff) ## Port Mapping | Service | Internal | External | |-----------|----------|----------| | Frontend | 80 | 52867 | | Backend | 3001 | 3001 | | PostgreSQL| 5432 | 5432 | ## Git Branch Strategy - `main` — stable, tested code - `feature/docker-init` — Docker & project scaffolding - `feature/database-setup` — Prisma schema + migrations - `feature/backend-api` — Express routes + audit middleware - `feature/frontend` — React UI - `feature/tests` — Jest + RTL + Playwright tests - `feature/red-team-fixes` — Security & UX patches ## Testing Notes - Backend tests use Jest + Supertest against a test PostgreSQL instance - Frontend unit tests use React Testing Library - E2E tests use Playwright against the full Docker stack - All tests run inside Docker via `docker-compose.test.yml` ## Red Team Audit ### Findings #### 🔴 Critical 1. **CORS wildcard (`origin: '*'`)** — The backend accepts requests from any origin. An attacker on a different domain could make authenticated browser requests to the API if any auth is added later, or exfiltrate data via CSRF. - **Fix**: Restrict `CORS_ORIGIN` to the frontend's origin in production. 2. **Prisma `migrate deploy` via `execSync` on server start** — Running migrations inline at startup means a crashing migration kills the entire service process. Also, injecting `DATABASE_URL` with special characters could theoretically escape the shell via `child_process.execSync`. - **Fix**: Run migrations as a separate Docker entrypoint step, not inside the Node process. 3. **Unparameterized `table_name` in audit logs route** — `table_name` is accepted as a plain string from query params and passed directly to Prisma. Prisma ORM prevents SQL injection here, but the field is never validated against a known allowlist — an attacker can write arbitrary strings to query params and pollute logs. - **Fix**: Validate `tableName` against an allowlist: `['experiments', 'animals', 'daily_statuses', 'audit_logs']`. #### 🟡 Medium 4. **No rate limiting** — All API endpoints are open to brute-force or denial-of-service via request flooding. Express has no rate limiter middleware. - **Fix**: Add `express-rate-limit` on all `/api/*` routes. 5. **Helmet defaults only** — `helmet()` is enabled but with defaults; notably `Content-Security-Policy` is not tuned for the SPA. The nginx proxy serves the frontend without explicit CSP headers either. - **Fix**: Add a CSP header in nginx and strengthen helmet config. 6. **Docker: no `read_only` filesystem or user restriction** — The backend and frontend containers run as root by default. - **Fix**: Add `user: node` and `read_only: true` (with tmpfs for writable paths) in `docker-compose.yml`. 7. **Postgres port exposed on host** — `docker-compose.yml` exposes port 5432 externally. In production, only the backend container needs DB access. - **Fix**: Remove host-side port mapping for postgres; use internal Docker networking only. 8. **`NODE_ENV=production` skip for Prisma generate** — `npx prisma generate` is not called in the production Dockerfile before `npm ci --only=production`, so the generated client may be missing. - **Fix**: Run `prisma generate` in the builder stage. #### 🟢 UX / Low 9. **Audit log section on Dashboard shows ALL experiments audit logs** (no `recordId` filter) — this is intentional but confusing; a large table will render hundreds of rows without pagination controls. - **Fix**: Add pagination controls to `AuditLogSection`. 10. **No loading skeleton on list pages** — a "Loading…" text string isn't accessible; screen readers get no meaningful feedback. - **Fix**: Add `aria-live="polite"` region for loading state. 11. **Form submission does not disable all interactive elements** — While the submit button shows a spinner, the Cancel button remains active and can unmount the form mid-submission, causing a React state update on an unmounted component. - **Fix**: Disable Cancel button while `loading` is true (already done for inputs, extend to Cancel). ### Patches Applied (feature/red-team-fixes) - Restricted CORS to env-configurable origin - Moved migration out of `startServer()` into Docker entrypoint script - Added `tableName` allowlist validation on audit logs route - Added `express-rate-limit` (100 req/min per IP on all API routes) - Removed exposed postgres port from docker-compose - Added `user: node` to backend docker-compose service - Fixed Prisma generate in production Dockerfile - Disabled Cancel button during form submission