From 4fc7f7f03315f3b7591442130137462d2e4c63f2 Mon Sep 17 00:00:00 2001 From: ifedan-ed Date: Sat, 21 Mar 2026 19:32:40 -0400 Subject: [PATCH] Security: CSP, CORS origin lock, 1mb body limit, auth brute-force rate limits - Enable Helmet CSP (scripts/styles/fonts/connect restricted to self + CDNs) - Lock CORS to APP_URL origin in production (open in dev) - Reduce JSON body limit from 50mb to 1mb - Add per-route rate limits: login 10/15min, register 5/hr, forgot-pw 5/hr - Add README with full setup, Docker Hub, provider switching, env reference - Bump version to 3.0.0 --- README.md | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ server.js | 78 +++++++++++++++++-- 2 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..97c8c94 --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# 🏥 Pediatric AI Scribe v3 + +AI-powered clinical documentation platform for pediatric medicine. Generates HPIs, hospital courses, chart reviews, SOAP notes, and developmental milestone assessments from voice recordings or dictation. + +## Features + +- **Live Encounter → HPI** — record doctor-patient conversation, AI generates structured HPI +- **Voice Dictation → HPI / SOAP** — dictate your narrative, AI restructures it +- **Hospital Course Generator** — paste progress notes by date, AI generates organized discharge summary (prose, day-by-day, organ-system, or psych format) +- **Chart Review / Precharting** — summarize outpatient, subspecialty, and ED notes +- **SOAP Note Generator** — full SOAP or subjective-only from dictation +- **Developmental Milestones** — AAP/Nelson milestone tracker with narrative output +- **Admin Panel** — user management, registration control, usage stats +- **Per-tab model selector** — choose fast vs. smart models per task +- **Nextcloud integration** — export documents to your Nextcloud instance +- **2FA** — TOTP-based two-factor authentication + +--- + +## Quick Start (Docker) + +### 1. Clone and configure + +```bash +git clone https://github.com/ifedan-ed/pediatric-ai-scribe-v3.git +cd pediatric-ai-scribe-v3 +cp .env.example .env +``` + +Edit `.env` — at minimum set: + +```env +OPENROUTER_API_KEY=sk-or-v1-... +OPENAI_API_KEY=sk-... # for Whisper transcription +JWT_SECRET=<64-char random string> +DB_PASSWORD= +APP_URL=https://your-domain.com +``` + +Generate a strong JWT secret: +```bash +openssl rand -hex 32 +``` + +### 2. Start + +```bash +docker compose up -d +``` + +App runs on **port 3552** by default. The first user to register becomes admin automatically. + +### 3. Admin CLI (inside container) + +```bash +docker exec pediatric-ai-scribe node admin-cli.js list-users +docker exec pediatric-ai-scribe node admin-cli.js create-admin admin@example.com password123 "Dr. Admin" +docker exec pediatric-ai-scribe node admin-cli.js make-admin user@example.com +docker exec pediatric-ai-scribe node admin-cli.js reset-password user@example.com newpassword +docker exec pediatric-ai-scribe node admin-cli.js toggle-registration +docker exec pediatric-ai-scribe node admin-cli.js stats +``` + +--- + +## Docker Hub + +```bash +docker pull danielonyejesi/pediatric-ai-scribe-v3:latest +``` + +### Minimal docker-compose without building + +```yaml +services: + app: + image: danielonyejesi/pediatric-ai-scribe-v3:latest + ports: + - "3552:3000" + env_file: .env + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: pedscribe + POSTGRES_USER: pedscribe + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pedscribe"] + interval: 10s + retries: 5 + +volumes: + pgdata: +``` + +--- + +## AI Provider Configuration + +Switch providers by changing `AI_PROVIDER` in `.env`. No code changes needed. + +### OpenRouter (default — cheapest, NOT HIPAA) + +```env +AI_PROVIDER=openrouter +OPENROUTER_API_KEY=sk-or-v1-... +``` + +### AWS Bedrock (HIPAA compliant with BAA) + +```env +AI_PROVIDER=bedrock +AWS_BEDROCK_REGION=us-east-1 +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=... +``` + +Or use an IAM role (no keys needed when running on EC2/ECS — just set the region). + +Available Bedrock models (auto-selected when `AI_PROVIDER=bedrock`): +- vendor model Sonnet 4 (`anthropic.agent-config-sonnet-4-20250514-v1:0`) +- vendor model 3.5 Sonnet (`anthropic.agent-config-3-5-sonnet-20241022-v2:0`) +- vendor model 3 Haiku — default, cheapest (`anthropic.agent-config-3-haiku-20240307-v1:0`) +- Llama 3.1 70B / 8B +- Mistral Large + +### Azure OpenAI (HIPAA compliant with BAA) + +```env +AI_PROVIDER=azure +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com +AZURE_OPENAI_API_KEY=... +AZURE_DEPLOYMENT_NAME=gpt-4o-mini +AZURE_OPENAI_API_VERSION=2024-02-01 +``` + +--- + +## Whisper Transcription + +Always uses OpenAI Whisper regardless of the AI provider setting: + +```env +OPENAI_API_KEY=sk-... +``` + +--- + +## Email (optional — for verification & password reset) + +Without SMTP configured, email verification is skipped and users are auto-verified on registration. + +```env +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password +SMTP_FROM=noreply@yourdomain.com +``` + +--- + +## Environment Variables Reference + +| Variable | Required | Description | +|---|---|---| +| `OPENROUTER_API_KEY` | If using OpenRouter | OpenRouter API key | +| `AI_PROVIDER` | No | `openrouter` (default), `bedrock`, or `azure` | +| `AWS_BEDROCK_REGION` | If using Bedrock | e.g. `us-east-1` | +| `AWS_ACCESS_KEY_ID` | If using Bedrock (no IAM role) | AWS access key | +| `AWS_SECRET_ACCESS_KEY` | If using Bedrock (no IAM role) | AWS secret key | +| `AZURE_OPENAI_ENDPOINT` | If using Azure | Azure OpenAI endpoint URL | +| `AZURE_OPENAI_API_KEY` | If using Azure | Azure API key | +| `AZURE_DEPLOYMENT_NAME` | If using Azure | Deployment name, e.g. `gpt-4o-mini` | +| `OPENAI_API_KEY` | For transcription | OpenAI key (Whisper) | +| `ELEVENLABS_API_KEY` | No | ElevenLabs TTS (optional) | +| `JWT_SECRET` | **Yes** | Random 64-char string — keep secret | +| `DATABASE_URL` | No | PostgreSQL URL (auto-set by docker-compose) | +| `DB_PASSWORD` | **Yes** | PostgreSQL password | +| `APP_URL` | Recommended | Public URL e.g. `https://scribe.example.com` (used for CORS, emails) | +| `PORT` | No | Internal port, default `3000` | +| `SMTP_HOST` | No | SMTP server for email | +| `SMTP_PORT` | No | Default `587` | +| `SMTP_USER` | No | SMTP username | +| `SMTP_PASS` | No | SMTP password / app password | +| `SMTP_FROM` | No | From address for emails | + +--- + +## HIPAA Notice + +This application processes data through third-party AI APIs. + +- ✅ All connections use HTTPS/TLS +- ✅ Authentication required for all AI endpoints +- ✅ 2FA available +- ✅ No patient data stored on server (only audit logs) +- ⚠️ **OpenRouter does not offer a BAA** — do not use with real PHI +- ✅ **AWS Bedrock** and **Azure OpenAI** offer BAAs — suitable for PHI with proper configuration + +**Recommendation:** Do not enter real patient data until your organization has executed BAAs with all AI providers in use. + +--- + +## Development + +```bash +npm install +cp .env.example .env # edit with your keys +# Requires a running PostgreSQL instance (see DATABASE_URL in .env) +node server.js +``` diff --git a/server.js b/server.js index 942877c..0bed21a 100644 --- a/server.js +++ b/server.js @@ -11,11 +11,77 @@ const loggingMiddleware = require('./src/middleware/logging'); const app = express(); const server = http.createServer(app); -app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false })); -app.use(cors()); +// ============================================================ +// SECURITY — Helmet with CSP +// ============================================================ +app.use(helmet({ + crossOriginEmbedderPolicy: false, + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline needed for onclick handlers in HTML + styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'], + fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'], + imgSrc: ["'self'", 'data:', 'blob:'], + mediaSrc: ["'self'", 'blob:'], + connectSrc: ["'self'"], + workerSrc: ["'self'"], + frameSrc: ["'none'"], + objectSrc: ["'none'"], + } + } +})); + +// ============================================================ +// CORS — restrict to APP_URL origin in production +// ============================================================ +var allowedOrigin = process.env.APP_URL ? process.env.APP_URL.replace(/\/$/, '') : null; +app.use(cors({ + origin: function(origin, callback) { + // Allow requests with no origin (mobile apps, curl, server-to-server) + if (!origin) return callback(null, true); + // In development (no APP_URL set) allow all + if (!allowedOrigin) return callback(null, true); + if (origin === allowedOrigin) return callback(null, true); + callback(new Error('CORS: origin not allowed')); + }, + credentials: true +})); + app.use(cookieParser()); -app.use(express.json({ limit: '50mb' })); -app.use('/api/', rateLimit({ windowMs: 60000, max: 60, message: { error: 'Too many requests' } })); + +// ============================================================ +// BODY LIMITS — 1mb for JSON, transcribe uses multipart (no limit here) +// ============================================================ +app.use(express.json({ limit: '1mb' })); + +// ============================================================ +// RATE LIMITING +// ============================================================ +// General API: 60 req/min +app.use('/api/', rateLimit({ + windowMs: 60000, max: 60, + message: { error: 'Too many requests' }, + standardHeaders: true, legacyHeaders: false +})); + +// Auth routes: 10 attempts/15min per IP (brute-force protection) +app.use('/api/auth/login', rateLimit({ + windowMs: 15 * 60 * 1000, max: 10, + message: { error: 'Too many login attempts. Try again in 15 minutes.' }, + standardHeaders: true, legacyHeaders: false +})); +app.use('/api/auth/register', rateLimit({ + windowMs: 60 * 60 * 1000, max: 5, + message: { error: 'Too many registration attempts.' }, + standardHeaders: true, legacyHeaders: false +})); +app.use('/api/auth/forgot-password', rateLimit({ + windowMs: 60 * 60 * 1000, max: 5, + message: { error: 'Too many password reset attempts.' }, + standardHeaders: true, legacyHeaders: false +})); + app.use(loggingMiddleware); app.use(express.static(path.join(__dirname, 'public'))); @@ -39,7 +105,7 @@ app.get('/api/models', (req, res) => res.json({ models: getAvailableModels(), pr const { activeProvider } = require('./src/utils/ai'); app.get('/api/health', (req, res) => { res.json({ - status: 'running', version: '2.1.0', provider: activeProvider, + status: 'running', version: '3.0.0', provider: activeProvider, timestamp: new Date().toISOString(), openrouter: process.env.OPENROUTER_API_KEY ? 'configured' : 'missing', bedrock: process.env.AWS_BEDROCK_REGION ? 'configured' : 'not configured', @@ -54,7 +120,7 @@ const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(''); console.log('=========================================='); - console.log('🏥 PEDIATRIC AI SCRIBE v2.1'); + console.log('🏥 PEDIATRIC AI SCRIBE v3.0'); console.log('=========================================='); console.log('🌐 http://localhost:' + PORT); console.log('🤖 Provider: ' + activeProvider);