← back to Professional Directory
agents/api-agent/routes/threads.js
119 lines
/**
* /threads — community discussion threads.
*
* GET /threads — list (any user)
* GET /threads/:id — single thread + posts
* POST /threads — create (paid tier)
* POST /threads/:id/posts — reply (paid tier)
* POST /admin/threads/:id/hide — admin moderation
*/
const express = require('express');
const { query } = require('../../shared/db');
const { requireRole, requireTier } = require('../auth');
const router = express.Router();
router.get('/', async (req, res, next) => {
try {
const limit = Math.min(Number(req.query.limit) || 50, 100);
const cat = req.query.category ? String(req.query.category) : null;
const city = req.query.city ? String(req.query.city) : null;
const where = ['hidden_by_admin = false']; const params = [];
if (cat) { params.push(cat); where.push(`category = $${params.length}`); }
if (city) { params.push(city); where.push(`city ILIKE $${params.length}`); }
const r = await query(`
SELECT t.id, t.title, t.body, t.category, t.city, t.zip, t.organization_id,
t.reply_count, t.last_post_at, t.created_at,
u.display_name AS author, u.role AS author_role
FROM threads t
LEFT JOIN users u ON u.id = t.created_by_user_id
WHERE ${where.join(' AND ')}
ORDER BY t.pinned DESC, t.last_post_at DESC
LIMIT ${limit}
`, params);
res.json({ count: r.rowCount, rows: r.rows });
} catch (e) { next(e); }
});
router.get('/:id', async (req, res, next) => {
try {
const id = Number(req.params.id);
const t = (await query(`
SELECT t.*, u.display_name AS author, u.role AS author_role
FROM threads t
LEFT JOIN users u ON u.id = t.created_by_user_id
WHERE t.id = $1 AND t.hidden_by_admin = false`, [id])).rows[0];
if (!t) return res.status(404).json({ error: 'not found' });
const posts = (await query(`
SELECT p.id, p.body, p.created_at,
u.display_name AS author, u.role AS author_role
FROM thread_posts p
LEFT JOIN users u ON u.id = p.author_user_id
WHERE p.thread_id = $1 AND p.hidden_by_admin = false
ORDER BY p.created_at`, [id])).rows;
res.json({ thread: t, posts });
} catch (e) { next(e); }
});
// Authed write routes.
router.use((req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'auth required' });
next();
});
router.post('/', requireTier('paid'), async (req, res, next) => {
try {
const b = req.body || {};
const title = String(b.title || '').trim().slice(0, 250);
const body = String(b.body || '').trim().slice(0, 10_000);
if (title.length < 5) return res.status(400).json({ error: 'title 5+ chars' });
const cat = ['general','recommendations','specialty','insurance','pediatrics','eldercare','mental_health','dental','optometry','question','rant'].includes(b.category) ? b.category : 'general';
const city = b.city ? String(b.city).slice(0, 80) : null;
const orgId = b.organization_id ? Number(b.organization_id) : null;
const r = await query(`
INSERT INTO threads (title, body, city, organization_id, category, created_by_user_id)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at`,
[title, body || null, city, orgId, cat, req.user.id]);
res.json({ ok: true, thread_id: r.rows[0].id });
} catch (e) { next(e); }
});
router.post('/:id/posts', requireTier('paid'), async (req, res, next) => {
try {
const id = Number(req.params.id);
const body = String(req.body?.body || '').trim();
if (body.length < 2 || body.length > 10_000) return res.status(400).json({ error: 'body 2-10000 chars' });
const t = (await query(`SELECT id FROM threads WHERE id=$1 AND hidden_by_admin=false`, [id])).rows[0];
if (!t) return res.status(404).json({ error: 'thread not found' });
const r = await query(`
INSERT INTO thread_posts (thread_id, author_user_id, body)
VALUES ($1, $2, $3) RETURNING id, created_at`,
[id, req.user.id, body]);
res.json({ ok: true, post_id: r.rows[0].id });
} catch (e) { next(e); }
});
// ─── /admin/threads moderation ─────────────────────────────────────────────
const adminRouter = express.Router();
adminRouter.use(requireRole('admin'));
adminRouter.post('/:id/hide', async (req, res, next) => {
try {
const id = Number(req.params.id);
const flag = req.body?.hidden !== false;
await query(`UPDATE threads SET hidden_by_admin=$2, updated_at=now() WHERE id=$1`, [id, flag]);
res.json({ ok: true, hidden: flag });
} catch (e) { next(e); }
});
adminRouter.post('/posts/:id/hide', async (req, res, next) => {
try {
const id = Number(req.params.id);
const flag = req.body?.hidden !== false;
await query(`UPDATE thread_posts SET hidden_by_admin=$2, updated_at=now() WHERE id=$1`, [id, flag]);
res.json({ ok: true, hidden: flag });
} catch (e) { next(e); }
});
module.exports = { router, adminRouter };