← back to AbramsOS
routes/biometrics.js
78 lines
// Biometrics — per-person ID / emergency profile (kids + adults). Sensitive PII; every
// write audited. Doubles as a child-ID record. One row per person (upsert).
const express = require('express');
const db = require('../lib/db');
const audit = require('../lib/audit');
const { id } = require('../lib/ids');
const bio = require('../lib/biometrics');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
async function profiles(userId) {
const rows = (await db.query(
`SELECT p.id AS person_id, p.full_name, p.relation, b.*
FROM person p LEFT JOIN person_biometric b ON b.person_id = p.id AND b.user_id = $1
WHERE p.user_id = $1
ORDER BY (p.relation <> 'self'), p.full_name`,
[userId]
)).rows;
return rows.map((r) => ({ ...r, derived: bio.derive(r) }));
}
router.get('/biometrics', async (_req, res) => {
res.render('biometrics', { profiles: await profiles(DEV_USER_ID) });
});
router.get('/api/biometrics', async (_req, res) => {
res.json(await profiles(DEV_USER_ID));
});
const FIELDS = ['date_of_birth', 'sex', 'height_in', 'weight_lb', 'blood_type', 'eye_color',
'hair_color', 'distinguishing_marks', 'allergies', 'conditions', 'emergency_contact',
'emergency_notes', 'prints_on_file', 'prints_location'];
function clean(b) {
const out = {};
for (const f of FIELDS) {
if (!(f in b)) continue;
let v = b[f];
if (v === '' ) v = null;
if (f === 'prints_on_file') v = v === true || v === 'true' || v === 'on' || v === '1';
else if (['height_in', 'weight_lb'].includes(f)) v = (v == null ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
else if (v != null) v = String(v).slice(0, 1000);
out[f] = v;
}
return out;
}
// Upsert a person's biometric profile.
router.post('/api/biometrics/:personId', async (req, res) => {
const personId = req.params.personId;
const owns = await db.query(`SELECT 1 FROM person WHERE id = $1 AND user_id = $2`, [personId, DEV_USER_ID]);
if (!owns.rows.length) return res.status(404).json({ error: 'person not found' });
const c = clean(req.body || {});
const cols = Object.keys(c);
if (!cols.length) return res.status(400).json({ error: 'nothing to save' });
const existing = await db.query(`SELECT id FROM person_biometric WHERE user_id = $1 AND person_id = $2`, [DEV_USER_ID, personId]);
if (existing.rows.length) {
const sets = cols.map((c2, i) => `${c2} = $${i + 1}`);
const vals = cols.map((k) => c[k]);
sets.push('updated_at = now()');
vals.push(existing.rows[0].id, DEV_USER_ID);
await db.query(`UPDATE person_biometric SET ${sets.join(', ')} WHERE id = $${vals.length - 1} AND user_id = $${vals.length}`, vals);
} else {
const bid = id('biometric');
const allCols = ['id', 'user_id', 'person_id', ...cols];
const vals = [bid, DEV_USER_ID, personId, ...cols.map((k) => c[k])];
const ph = allCols.map((_, i) => `$${i + 1}`);
await db.query(`INSERT INTO person_biometric (${allCols.join(', ')}) VALUES (${ph.join(', ')})`, vals);
}
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'person_biometric', objectId: personId, eventType: 'biometric_saved', metadata: { medical: true, sensitive: true } }).catch(() => {});
res.json({ ok: true });
});
module.exports = router;