← 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;