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
This commit is contained in:
parent
c30708f1cc
commit
4fc7f7f033
2 changed files with 292 additions and 6 deletions
220
README.md
Normal file
220
README.md
Normal file
|
|
@ -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=<strong 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
|
||||
```
|
||||
78
server.js
78
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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue