pediatric-ai-scribe-v3/src/routes/adminMilestones.js
Daniel dca1ebb6fe fix: admin routers authenticate themselves; track the audit in TODO
- adminMilestones was reached only because adminConfig is mounted on
  /api/admin ahead of it and guards the whole path. adminMiddleware checks
  req.user.role and nothing else, so it failed closed (403) rather than
  open — but on mount order, not intent. It now states the requirement,
  with a test covering all four admin routers.
- TODO.md records the whole audit: what was verified working (live
  transcription round trip, voice mode wiring), what was fixed, the two
  advisories that are unreachable and why, and the CI/CD and Kubernetes
  work worth doing before scaling out.

Security review found nothing else exploitable: parameterised SQL
throughout (the one interpolated table name is allowlisted), CORS refuses
to start open in production, JWT_SECRET refuses to start unset in
production, rate limits on /api and each auth route, a real CSP, and no
secrets in the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-10 16:03:20 +02:00

155 lines
4.6 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { authMiddleware, adminMiddleware } = require('../middleware/auth');
const db = require('../db/database');
// adminMiddleware only checks req.user.role — it does not authenticate. These
// routes were reached only because adminConfig is mounted on /api/admin ahead
// of them and runs authMiddleware for the whole path. That is mount order, not
// a guarantee: state the requirement here so it cannot be reordered away.
router.use(authMiddleware);
// Get all milestones (optionally filtered by age group)
router.get('/milestones', adminMiddleware, async (req, res) => {
try {
const { age_group } = req.query;
let query = 'SELECT * FROM developmental_milestones';
let params = [];
if (age_group) {
query += ' WHERE age_group = $1';
params.push(age_group);
}
query += ' ORDER BY age_group, domain, sort_order, id';
const result = await db.query(query, params);
res.json({ success: true, milestones: result.rows });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Get unique age groups and domains
router.get('/milestones/meta', adminMiddleware, async (req, res) => {
try {
const ageGroups = await db.query(
'SELECT DISTINCT age_group FROM developmental_milestones ORDER BY age_group'
);
const domains = await db.query(
'SELECT DISTINCT domain FROM developmental_milestones ORDER BY domain'
);
res.json({
success: true,
age_groups: ageGroups.rows.map(r => r.age_group),
domains: domains.rows.map(r => r.domain)
});
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Create new milestone
router.post('/milestones', adminMiddleware, async (req, res) => {
try {
const { age_group, domain, milestone_text, sort_order } = req.body;
if (!age_group || !domain || !milestone_text) {
return res.status(400).json({ error: 'Missing required fields' });
}
const result = await db.query(
`INSERT INTO developmental_milestones (age_group, domain, milestone_text, sort_order)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[age_group, domain, milestone_text, sort_order || 0]
);
res.json({ success: true, milestone: result.rows[0] });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Update milestone
router.put('/milestones/:id', adminMiddleware, async (req, res) => {
try {
const { id } = req.params;
const { age_group, domain, milestone_text, sort_order } = req.body;
if (!age_group || !domain || !milestone_text) {
return res.status(400).json({ error: 'Missing required fields' });
}
const result = await db.query(
`UPDATE developmental_milestones
SET age_group = $1, domain = $2, milestone_text = $3, sort_order = $4, updated_at = NOW()
WHERE id = $5
RETURNING *`,
[age_group, domain, milestone_text, sort_order || 0, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Milestone not found' });
}
res.json({ success: true, milestone: result.rows[0] });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Delete milestone
router.delete('/milestones/:id', adminMiddleware, async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(
'DELETE FROM developmental_milestones WHERE id = $1 RETURNING *',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Milestone not found' });
}
res.json({ success: true, message: 'Milestone deleted' });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Bulk import milestones (from static data)
router.post('/milestones/bulk-import', adminMiddleware, async (req, res) => {
try {
const { milestones, clearExisting } = req.body;
if (!Array.isArray(milestones)) {
return res.status(400).json({ error: 'Expected array of milestones' });
}
// Clear existing if requested
if (clearExisting) {
await db.query('DELETE FROM developmental_milestones');
}
let imported = 0;
for (const m of milestones) {
if (!m.age_group || !m.domain || !m.milestone_text) continue;
await db.query(
`INSERT INTO developmental_milestones (age_group, domain, milestone_text, sort_order)
VALUES ($1, $2, $3, $4)`,
[m.age_group, m.domain, m.milestone_text, m.sort_order || 0]
);
imported++;
}
res.json({ success: true, imported });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;