← back to Professional Directory
agents/api-agent/routes/messages.js
105 lines
/**
* /messages — direct messages between any two patients/doctors.
*
* GET /messages — my DM pair index
* GET /messages/:pairId — message history
* POST /messages/:pairId/read — mark all incoming as read
* POST /messages — send (paid tier required)
*
* Disclaimer banner enforced client-side; server-side adds the
* "not medical advice" prefix to every outgoing message body.
*/
const express = require('express');
const { query } = require('../../shared/db');
const { requireTier } = require('../auth');
const router = express.Router();
router.use((req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'auth required' });
next();
});
// GET /messages — list my DM pairs with last-message preview + unread count.
router.get('/', async (req, res, next) => {
try {
const me = req.user.id;
const r = await query(`
SELECT p.id, p.user_a_id, p.user_b_id, p.last_message_at,
other.display_name AS other_display, other.role AS other_role,
other.id AS other_id,
(SELECT body FROM messages m WHERE m.dm_pair_id = p.id ORDER BY m.sent_at DESC LIMIT 1) AS last_body,
(SELECT COUNT(*) FROM messages m WHERE m.dm_pair_id = p.id AND m.author_user_id <> $1 AND m.read_at IS NULL) AS unread
FROM dm_pairs p
JOIN users other ON other.id = (CASE WHEN p.user_a_id = $1 THEN p.user_b_id ELSE p.user_a_id END)
WHERE p.user_a_id = $1 OR p.user_b_id = $1
ORDER BY p.last_message_at DESC NULLS LAST
LIMIT 100`, [me]);
res.json({ count: r.rowCount, rows: r.rows });
} catch (e) { next(e); }
});
// GET /messages/:pairId — message history.
router.get('/:pairId', async (req, res, next) => {
try {
const pid = Number(req.params.pairId);
const me = req.user.id;
const pair = (await query(`SELECT * FROM dm_pairs WHERE id=$1`, [pid])).rows[0];
if (!pair || (pair.user_a_id != me && pair.user_b_id != me && req.user.role !== 'admin')) {
return res.status(404).json({ error: 'not found' });
}
const msgs = (await query(`
SELECT id, author_user_id, body, sent_at, read_at
FROM messages WHERE dm_pair_id = $1 ORDER BY sent_at LIMIT 500`, [pid])).rows;
res.json({ pair, messages: msgs });
} catch (e) { next(e); }
});
// POST /messages — send a DM. body: { to_user_id, body }
router.post('/', requireTier('paid'), async (req, res, next) => {
try {
const toUser = Number(req.body?.to_user_id);
const myId = Number(req.user.id); // PG bigint comes back as string
const body = String(req.body?.body || '').trim();
if (!toUser || toUser === myId) return res.status(400).json({ error: 'invalid to_user_id (cannot DM yourself)' });
if (body.length < 1 || body.length > 5000) return res.status(400).json({ error: 'body 1-5000 chars' });
// Verify target user exists and isn't deleted.
const u = (await query(`SELECT id, role, deleted_at FROM users WHERE id=$1`, [toUser])).rows[0];
if (!u || u.deleted_at) return res.status(404).json({ error: 'recipient not found' });
// Canonical pair: smaller id first.
const aId = Math.min(myId, toUser);
const bId = Math.max(myId, toUser);
let pair = (await query(`SELECT id FROM dm_pairs WHERE user_a_id=$1 AND user_b_id=$2`, [aId, bId])).rows[0];
if (!pair) {
pair = (await query(`INSERT INTO dm_pairs (user_a_id, user_b_id) VALUES ($1, $2) RETURNING id`, [aId, bId])).rows[0];
}
// Server-side disclaimer prefix on every message — defensive.
const safeBody = body.startsWith('[Not medical advice]')
? body
: '[Not medical advice — for clinical questions, call the office.]\n\n' + body;
const m = await query(`
INSERT INTO messages (dm_pair_id, author_user_id, body) VALUES ($1, $2, $3)
RETURNING id, sent_at`, [pair.id, req.user.id, safeBody]);
res.json({ ok: true, message_id: m.rows[0].id, dm_pair_id: pair.id });
} catch (e) { next(e); }
});
// POST /messages/:pairId/read — mark incoming as read.
router.post('/:pairId/read', async (req, res, next) => {
try {
const pid = Number(req.params.pairId);
await query(`
UPDATE messages SET read_at = now()
WHERE dm_pair_id = $1 AND author_user_id <> $2 AND read_at IS NULL`,
[pid, req.user.id]);
res.json({ ok: true });
} catch (e) { next(e); }
});
module.exports = router;