const express = require('express'); const { requirePermission, requireAdmin } = require('../middleware/auth'); const { logCuratorAction } = require('../audit-log'); const { listLinks, createLink, deleteLink, listDeleteVotes, castDeleteVote, withdrawDeleteVote, seedDefaultsIfEmpty, } = require('../movement-lineage-service'); const router = express.Router(); /** Optional staff session for vote annotations on list (public otherwise). */ async function optionalStaff(req, _res, next) { try { if (req.session?.userId) { const { loadStaffUser } = require('../middleware/auth'); const user = await loadStaffUser(req.session.userId); if (user && user.is_active) { req.curatorUser = user; } } } catch { /* ignore — list stays public */ } next(); } /** Public: timeline / tree consumers. */ router.get('/', optionalStaff, async (req, res) => { try { await seedDefaultsIfEmpty(); const q = typeof req.query.q === 'string' ? req.query.q : ''; const limit = Number(req.query.limit) || 500; const offset = Number(req.query.offset) || 0; const data = await listLinks({ q, limit, offset, viewerUserId: req.curatorUser?.id || null, }); res.json(data); } catch (err) { console.error(err); res.status(500).json({ error: 'Failed to list movement links' }); } }); router.post('/', requirePermission('influences'), async (req, res) => { try { const item = await createLink(req.body || {}, { userId: req.session?.userId }); await logCuratorAction({ userId: req.session.userId, action: 'movement_link.create', resourceType: 'movement_lineage_link', resourceId: item.id, details: { source_movement_id: item.source_movement_id, target_movement_id: item.target_movement_id, proof_url: item.proof_url, }, req, }); res.status(201).json({ item }); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to create movement link' }); } }); router.get('/:id/delete-votes', requirePermission('influences'), async (req, res) => { try { const data = await listDeleteVotes(req.params.id); res.json(data); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to list delete votes' }); } }); router.post('/:id/delete-votes', requirePermission('influences'), async (req, res) => { try { const vote = await castDeleteVote(req.params.id, req.body || {}, { userId: req.session.userId, }); await logCuratorAction({ userId: req.session.userId, action: 'movement_link.delete_vote', resourceType: 'movement_lineage_link', resourceId: Number(req.params.id), details: { reason: vote.reason }, req, }); res.status(201).json({ vote }); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to cast delete vote' }); } }); router.delete('/:id/delete-votes/me', requirePermission('influences'), async (req, res) => { try { const result = await withdrawDeleteVote(req.params.id, { userId: req.session.userId }); await logCuratorAction({ userId: req.session.userId, action: 'movement_link.delete_vote_withdraw', resourceType: 'movement_lineage_link', resourceId: result.linkId, details: {}, req, }); res.json({ ok: true, ...result }); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to withdraw delete vote' }); } }); /** Hard delete — admin only. Curators vote instead. */ router.delete('/:id', requireAdmin, async (req, res) => { try { const votes = await listDeleteVotes(req.params.id).catch(() => ({ votes: [] })); const result = await deleteLink(req.params.id); await logCuratorAction({ userId: req.session.userId, action: 'movement_link.delete', resourceType: 'movement_lineage_link', resourceId: result.id, details: { prior_votes: (votes.votes || []).map((v) => ({ username: v.username, reason: v.reason, })), }, req, }); res.json({ ok: true, id: result.id }); } catch (err) { const status = err.status || 500; if (status >= 500) console.error(err); res.status(status).json({ error: err.message || 'Failed to delete movement link' }); } }); module.exports = router;