Compare commits

2 Commits

Author SHA1 Message Date
Antoni Nuñez Romeu 076ca2d590 Fixes in deployment and passwords
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Successful in 2m2s
Run Tests on Branches / Detect Changes (push) Successful in 14s
Run Tests on Branches / Backend Tests (push) Successful in 1m58s
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
2026-07-22 18:28:47 +02:00
Antoni Nuñez Romeu 1f340c1aa1 docs: record dependency hardening baseline 2026-07-22 17:46:49 +02:00
10 changed files with 2681 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
{"pid":568607,"startedAt":1784046408782}
{"pid":3854791,"startedAt":1784736547988}
+83 -4
View File
@@ -184,6 +184,44 @@ cp .env.example .env
docker compose up --build
```
### Production Environment Variables (IMPORTANT)
The root `.env` file is the **source of truth** for all Docker Compose services. The `docker-compose.yml` uses `${VAR:?...}` syntax which reads from this file. **Do not use placeholder values in production** — the backend validates them on startup and will crash.
Required variables to set with real secrets:
```env
# PostgreSQL password (used by postgres, backend, n8n, exporters)
PG_PASSWORD=<strong-random-hex>
# Backend session secret (required, non-placeholder)
SESSION_SECRET=<strong-random-hex>
# Backend CORS origin (must be your real domain, not localhost)
CORS_ORIGIN=https://farmacias.hacecalor.net
# N8N admin password
N8N_PASSWORD=<strong-random-string>
# Parapharmacy API keys (required for product ingestion)
INGEST_API_KEY=<strong-random-hex>
ADMIN_API_KEY=<strong-random-hex>
```
Generate secrets with:
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
**After changing `PG_PASSWORD`**, you must reset the PostgreSQL volume:
```bash
docker compose down
docker volume rm farmafinder_postgres_data
docker compose up -d
```
Then re-seed data (see "First Run" below).
### Services
| Service | URL | Description |
@@ -201,10 +239,27 @@ docker compose up --build
docker compose exec backend node create-admin.js
# Default: admin / admin123
# Seed sample pharmacies
# Seed sample pharmacies (SQLite — for local dev)
docker compose exec backend node seed.js
# Seed parapharmacy products (requires INGEST_API_KEY)
# The n8n-init container handles this automatically on first run.
# To re-seed manually after a PostgreSQL reset:
docker compose exec parapharmacy-api node -e "
const http = require('http');
const fs = require('fs');
const seed = JSON.parse(fs.readFileSync('/home/node/seed.json','utf8'));
const body = JSON.stringify(seed);
const req = http.request('http://localhost:3002/api/products/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-service-key': process.env.INGEST_API_KEY, 'Content-Length': Buffer.byteLength(body) }
}, res => { let d=''; res.on('data',c=>d+=c); res.on('end',()=>console.log(d)); });
req.write(body); req.end();
"
```
After a PostgreSQL volume reset, the n8n-init container will re-import workflows automatically. The backend re-creates its PG tables on startup (`initDatabase()` in `server.js`).
### N8N Setup
N8N auto-creates an admin account on first start:
@@ -225,6 +280,16 @@ docker compose down
docker compose down -v
```
### Reset PostgreSQL Only (keep other data)
```bash
docker compose down
docker volume rm farmafinder_postgres_data
docker compose up -d
# n8n-init re-imports workflows; backend re-creates tables on startup
# Re-seed parapharmacy products (see First Run above)
```
## Manual Setup
### 1. Install Redis
@@ -325,13 +390,27 @@ See [Parapharmacy Documentation](docs/parapharmacy.md) for details.
## Database Schema
### SQLite Tables
In production (Docker), the backend uses **PostgreSQL**. In local dev without PG, it falls back to **SQLite**.
**pharmacies**: `id`, `name`, `address`, `phone`, `latitude`, `longitude`
### PostgreSQL Tables (Production)
**pharmacies**: `id`, `name`, `address`, `phone`, `latitude`, `longitude`, `opening_hours`
**pharmacy_medicines**: `id`, `pharmacy_id`, `medicine_nregistro`, `medicine_name`, `price`, `stock`
**users**: `id`, `username`, `password_hash`, `created_at`
**users**: `id`, `username`, `password_hash`, `is_admin`, `address`, `latitude`, `longitude`, `created_at`
**user_alerts**: `id`, `user_id`, `type`, `medicine_nregistro`, `title`, `detail`, `schedule`, `created_at`, `updated_at`
**push_subscriptions**: `id`, `user_id`, `medicine_nregistro`, `medicine_name`, `endpoint`, `p256dh`, `auth`, `created_at`
**push_subscriptions_pharmacy**: `id`, `user_id`, `medicine_nregistro`, `medicine_name`, `pharmacy_id`, `endpoint`, `p256dh`, `auth`, `created_at`
**expo_push_tokens**: `id`, `user_id`, `expo_token`, `created_at`
### SQLite Tables (Local Dev Fallback)
Same schema as above minus foreign key constraints and PostgreSQL-specific types.
### Redis Cache
+1 -1
View File
@@ -88,7 +88,7 @@ const sessionConfig = {
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production' || process.env.COOKIE_SECURE === 'true',
secure: process.env.COOKIE_SECURE !== 'false' && (process.env.NODE_ENV === 'production' || process.env.COOKIE_SECURE === 'true'),
sameSite: 'lax',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
+3
View File
@@ -30,6 +30,9 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Copy package files
COPY apps/parapharmacy-api/package*.json ./
#Run npm install to avoid npm ci issues
RUN npm install
# Install dependencies from the committed lockfile (production only)
RUN npm ci --omit=dev
+2 -2
View File
@@ -7,11 +7,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /app
COPY pyproject.toml ./
COPY apps/pip-platform/pyproject.toml ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir .
COPY . .
COPY apps/pip-platform/ .
EXPOSE 8000
+5
View File
@@ -62,6 +62,11 @@ plugins = ["pydantic.mypy"]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pip-audit>=2.10.1",
]
[tool.hatch.build.targets.wheel]
packages = ["src"]
@@ -29,7 +29,7 @@ class TestCircuitBreaker:
return await coro
import asyncio
result = asyncio.get_event_loop().run_until_complete(_run())
result = asyncio.run(_run())
assert result == "ok"
def test_open_raises_service_unavailable(self):
@@ -42,7 +42,7 @@ class TestCircuitBreaker:
import asyncio
with pytest.raises(ServiceUnavailableException):
asyncio.get_event_loop().run_until_complete(_run())
asyncio.run(_run())
def test_half_open_after_recovery_timeout(self):
cb = CircuitBreaker(name="test", failure_threshold=1, recovery_timeout=0.01)
@@ -126,7 +126,7 @@ class TestWithRetry:
nonlocal call_count
call_count += 1
if call_count < 3:
raise Exception("temp fail")
raise ConnectionError("temp fail")
return "ok"
policy = RetryPolicy(max_attempts=3, base_delay=0.01, max_delay=0.1)
@@ -141,7 +141,7 @@ class TestWithRetry:
async def _always_fail():
nonlocal call_count
call_count += 1
raise Exception("always fail")
raise ConnectionError("always fail")
policy = RetryPolicy(max_attempts=2, base_delay=0.01, max_delay=0.1)
with pytest.raises(Exception, match="always fail"):
+2532
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -35,6 +35,7 @@ services:
NODE_ENV: production
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
CORS_ORIGIN: ${CORS_ORIGIN:?CORS_ORIGIN must be set}
COOKIE_SECURE: ${COOKIE_SECURE:-false}
REDIS_HOST: redis
REDIS_PORT: "6379"
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
@@ -0,0 +1,49 @@
# Dependency Hardening Baseline
**Baseline date:** 2026-07-22
**Branch:** `security/dependency-hardening-2026-07-22`
**Lockfile:** root `package-lock.json`, installed with `npm ci`
## Toolchain
- Node.js `v22.22.1` (the production Dockerfiles target Node 20 and 24)
- npm `9.2.0`
- Python `3.14.4`
- uv `0.5.9`
- Docker `29.6.1`
- Docker Compose `v5.3.1`
The local Node version produces an expected engine warning for `@zxing/library@0.23.0`, which requires Node 24 or newer. Production and CI must use the declared Node image/version rather than this local Node 22 runtime.
## Audit counts
Reports were generated online and saved outside the repository. Counts are advisory snapshots, not a substitute for the CI gate.
| Scope | Critical | High | Moderate | Low | Total |
|---|---:|---:|---:|---:|---:|
| Workspace root | 3 | 13 | 60 | 3 | 79 |
| Backend workspace | 1 | 10 | 41 | 2 | 54 |
| Parapharmacy API | 0 | 2 | 0 | 1 | 3 |
| Frontend workspace | 1 | 6 | 20 | 1 | 28 |
| Scraper workspace | 0 | 1 | 0 | 0 | 1 |
The parapharmacy and scraper reports resolve through the root workspace lockfile. Their package-local audit invocation fails without a package-local lockfile; the workspace-scoped audit is the authoritative current result.
## Direct dependency families selected for review
- Backend OpenTelemetry: `@opentelemetry/auto-instrumentations-node ^0.52.0`, exporters/SDKs primarily on `^0.55.0`, resources/API families on `^1.x`.
- Backend native dependencies: `bcrypt ^5.1.1`, `sqlite3 ^5.1.6`, and `connect-sqlite3 ^0.9.16`.
- Parapharmacy: `mongoose ^8.8.0`, `puppeteer ^22.0.0`; current installed Puppeteer is `22.15.0`.
- Scraper: `puppeteer ^24.40.0`, `puppeteer-extra ^3.3.6`, and stealth plugin `^2.11.2`.
- Frontend: `vite ^5.0.8`, `vitest ^1.6.0`, `vite-plugin-pwa ^1.3.0`, and Grafana Faro/OpenTelemetry packages on the `^1.x` family.
## Upgrade order and known risks
1. Resolve Python dependencies and audit them independently.
2. Pin runtime images and verify lockfile-enforced Docker builds.
3. Upgrade backend OpenTelemetry as one family, then native bcrypt/SQLite packages.
4. Upgrade parapharmacy Mongoose/`fast-uri` and scraper Puppeteer separately.
5. Upgrade frontend Vite/Rollup/esbuild/Vitest without enabling Vitest UI.
6. Re-run all audits and record any residual advisory with production reachability, mitigation, owner, and expiry/review date.
Do not run `npm audit fix --force`: the baseline reports breaking-version fixes for several families, including OpenTelemetry, SQLite, and Vite/Vitest. Each such upgrade requires its own compatibility test and commit.