commit 44dfe3fdc4fd1a14ba750b7c93d0e9236172e653 Author: Experiments DB Dev Date: Wed Apr 15 10:38:41 2026 -0400 chore: initialize project structure, Docker setup, docker-compose on port 52867 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c2682fd --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +POSTGRES_USER=expuser +POSTGRES_PASSWORD=exppassword +POSTGRES_DB=experiments_db +DATABASE_URL=postgresql://expuser:exppassword@postgres:5432/experiments_db +NODE_ENV=production diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..925690a --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Environment +.env +.env.local +.env.*.local + +# Node +node_modules/ +npm-debug.log* + +# Build outputs +dist/ +build/ +.next/ + +# Prisma +backend/prisma/migrations/dev.db + +# Test artifacts +coverage/ +test-results/ +playwright-report/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..c3aab1a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine AS base +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + +FROM base AS dev +RUN npm ci +COPY . . +CMD ["npm", "run", "dev"] + +FROM base AS prod +COPY . . +RUN npx prisma generate +CMD ["npm", "start"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..50c3eb4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,31 @@ +version: '3.9' + +# Development override — hot reload for backend, Vite dev server for frontend +# Usage: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +services: + postgres: + ports: + - "5432:5432" + + backend: + build: + target: dev + volumes: + - ./backend:/app + - /app/node_modules + environment: + NODE_ENV: development + command: npm run dev + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + ports: + - "52867:5173" + volumes: + - ./frontend:/app + - /app/node_modules + environment: + VITE_API_URL: http://localhost:3001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..30bfcaa --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3.9' + +services: + postgres: + image: postgres:15-alpine + container_name: experiments_postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-expuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-exppassword} + POSTGRES_DB: ${POSTGRES_DB:-experiments_db} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-expuser} -d ${POSTGRES_DB:-experiments_db}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: ./backend + target: prod + container_name: experiments_backend + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-expuser}:${POSTGRES_PASSWORD:-exppassword}@postgres:5432/${POSTGRES_DB:-experiments_db} + NODE_ENV: production + PORT: 3001 + ports: + - "3001:3001" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + + frontend: + build: + context: ./frontend + target: prod + container_name: experiments_frontend + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "52867:80" + +volumes: + postgres_data: + name: experiments_postgres_data diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..49ea44b --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:alpine AS prod +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..ca7d95a --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..b442ac4 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Serve static files, fall back to index.html for SPA routing + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API calls to backend + location /api/ { + proxy_pass http://backend:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/memory.md b/memory.md new file mode 100644 index 0000000..bf39f90 --- /dev/null +++ b/memory.md @@ -0,0 +1,126 @@ +# 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 +_To be filled after Phase 3 testing is complete._