// Admin moderation dashboard (Phase 6). Read-only views over the bot's // mod_actions log plus server-owned staff notes. Mounted behind the // admin+moderator RBAC gate (see admin.routes.js). The only mutation here is // adding a staff note; admin_only notes are further restricted to the admin role. const moderation = require('../../../model/moderation/moderation.model') const modNotes = require('../../../model/modNotes/modNotes.model') const modNotesDb = require('../../../model/modNotes/modNotes.db') const activity = require('../../../model/activity/activity.model') const log = require('../../../utils/logger')('moderation') const VALID_TYPES = new Set(['ban', 'kick', 'mute', 'warn']) const MAX_LIMIT = 200 const DEFAULT_LIMIT = 50 // Parse ?limit/&offset the same way the activity log does: numeric, capped. function pageParams(req) { const limit = Math.min(Number(req.query.limit) || DEFAULT_LIMIT, MAX_LIMIT) const offset = Number(req.query.offset) || 0 return { limit, offset } } // Optional ?type filter — ignored unless it is a known action type. function typeParam(req) { const t = req.query.type return VALID_TYPES.has(t) ? t : null } function isAdmin(req) { return req.user && req.user.role === 'admin' } async function getSummary(req, res) { try { return res.json(await moderation.summary()) } catch (err) { log.error('summary failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function getRecent(req, res) { try { const { limit, offset } = pageParams(req) return res.json(await moderation.recent({ type: typeParam(req), limit, offset })) } catch (err) { log.error('recent failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function search(req, res) { try { const term = (req.query.q || '').trim() if (!term) return res.json([]) return res.json(await moderation.search(term, { limit: 20 })) } catch (err) { log.error('search failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } // ── Phase 6b event feeds ────────────────────────────────────────────── const MEMBER_TYPES = new Set(['join', 'leave']) async function getMembers(req, res) { try { const { limit, offset } = pageParams(req) const t = MEMBER_TYPES.has(req.query.type) ? req.query.type : null return res.json(await moderation.members({ type: t, limit, offset })) } catch (err) { log.error('members failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function getFilterHits(req, res) { try { const { limit, offset } = pageParams(req) return res.json(await moderation.filterHits({ limit, offset })) } catch (err) { log.error('filterHits failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function getSpamHits(req, res) { try { const { limit, offset } = pageParams(req) return res.json(await moderation.spamHits({ limit, offset })) } catch (err) { log.error('spamHits failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function getUser(req, res) { try { const summary = await moderation.userSummary(req.params.discordId) const notesCount = await modNotesDb.countForUser(req.params.discordId, { includeAdminOnly: isAdmin(req), }) return res.json({ ...summary, notes_count: notesCount }) } catch (err) { log.error('getUser failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function getUserActions(req, res) { try { const { limit, offset } = pageParams(req) return res.json( await moderation.userActions(req.params.discordId, { type: typeParam(req), limit, offset }), ) } catch (err) { log.error('getUserActions failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function getUserNotes(req, res) { try { const notes = await modNotes.listForUser(req.params.discordId, { includeAdminOnly: isAdmin(req), }) return res.json(notes) } catch (err) { log.error('getUserNotes failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } async function addUserNote(req, res) { try { const visibility = req.body.visibility === 'admin_only' ? 'admin_only' : 'staff_only' // admin_only notes can carry sensitive judgement calls — restrict to admins. if (visibility === 'admin_only' && !isAdmin(req)) { return res.status(403).json({ message: 'Only admins can add admin-only notes' }) } const note = await modNotes.add({ discordUserId: req.params.discordId, author: req.user, body: req.body.body, visibility, }) await activity.log({ req, action: 'moderation.note.add', detail: { discordUserId: req.params.discordId, visibility }, }) return res.status(201).json(note) } catch (err) { log.error('addUserNote failed', { error: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } } module.exports = { getSummary, getRecent, search, getMembers, getFilterHits, getSpamHits, getUser, getUserActions, getUserNotes, addUserNote, }