Add moderation dashboard, user history & notes (Phase 6a)
Surface the Discord bot's moderation data on the admin panel: a read-only
staff dashboard over the existing mod_actions log, per-user history, staff
notes, and a new moderator role. No bot changes.
Schema
- users.role ENUM gains 'moderator' (CREATE + idempotent ALTER for existing DBs)
- new server-owned mod_notes table (staff_only/admin_only visibility)
Server
- model/moderation: read mod_actions via the shared pool (documented read-only
cross of the bot/server ownership boundary), correlate accounts through
user_identities (provider='discord'), flag automated actions via
staff_user_id === bot_config.application_id; pure reshaping helpers isolated
in moderation.pure.js so they unit-test without opening a DB pool
- model/modNotes: list/add with role-gated admin_only visibility
- admin/moderation.controller + routes under /api/v1/admin/moderation/* gated by
requireRole('admin','moderator'); admin_only note writes require admin
- allow assigning 'moderator' in the user create/update validators
Client
- /admin/moderation overview (window tiles, type-filterable recent feed, user
lookup) and /user/:discordId history (tabs + notes with add-note)
- RoleGate; AdminLayout filters nav and confines moderators to their section
- moderator badge + action-type/auto badges
Deferred (see plan): 6b bot event capture (joins/leaves/filter/spam), 6c appeals
(needs public accounts), 6d /internal/mod-reverse bot reversal callback.
Verified: 116 server unit tests, client build, DB-backed model smoke, full
HTTP/RBAC e2e, and a browser click-through of the dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
This commit is contained in:
51
server/src/model/modNotes/modNotes.db.js
Normal file
51
server/src/model/modNotes/modNotes.db.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Staff notes on a Discord user (server-owned, see db/schema.sql mod_notes).
|
||||
// Notes are never user-visible; admin_only notes are filtered out for non-admin
|
||||
// callers at this layer via includeAdminOnly.
|
||||
const { query } = require('../../utils/db')
|
||||
|
||||
async function listForUser(discordId, { includeAdminOnly = false } = {}) {
|
||||
const visClause = includeAdminOnly ? '' : "AND n.visibility = 'staff_only'"
|
||||
return query(
|
||||
`SELECT n.id, n.discord_user_id, n.author_user_id, n.author_tag,
|
||||
n.body, n.visibility, n.created_at,
|
||||
u.username AS author_username
|
||||
FROM mod_notes n
|
||||
LEFT JOIN users u ON u.id = n.author_user_id
|
||||
WHERE n.discord_user_id = ? ${visClause}
|
||||
ORDER BY n.id DESC`,
|
||||
[discordId],
|
||||
)
|
||||
}
|
||||
|
||||
async function insert({ discordUserId, authorUserId = null, authorTag = null, body, visibility = 'staff_only' }) {
|
||||
const res = await query(
|
||||
`INSERT INTO mod_notes (discord_user_id, author_user_id, author_tag, body, visibility)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[discordUserId, authorUserId, authorTag, body, visibility],
|
||||
)
|
||||
return res.insertId
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const rows = await query(
|
||||
`SELECT n.id, n.discord_user_id, n.author_user_id, n.author_tag,
|
||||
n.body, n.visibility, n.created_at,
|
||||
u.username AS author_username
|
||||
FROM mod_notes n
|
||||
LEFT JOIN users u ON u.id = n.author_user_id
|
||||
WHERE n.id = ? LIMIT 1`,
|
||||
[id],
|
||||
)
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
async function countForUser(discordId, { includeAdminOnly = false } = {}) {
|
||||
const visClause = includeAdminOnly ? '' : "AND visibility = 'staff_only'"
|
||||
const rows = await query(
|
||||
`SELECT COUNT(*) AS c FROM mod_notes WHERE discord_user_id = ? ${visClause}`,
|
||||
[discordId],
|
||||
)
|
||||
return Number(rows[0].c)
|
||||
}
|
||||
|
||||
module.exports = { listForUser, insert, getById, countForUser }
|
||||
18
server/src/model/modNotes/modNotes.model.js
Normal file
18
server/src/model/modNotes/modNotes.model.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const modNotesDb = require('./modNotes.db')
|
||||
|
||||
async function listForUser(discordId, { includeAdminOnly = false } = {}) {
|
||||
return modNotesDb.listForUser(discordId, { includeAdminOnly })
|
||||
}
|
||||
|
||||
async function add({ discordUserId, author, body, visibility = 'staff_only' }) {
|
||||
const id = await modNotesDb.insert({
|
||||
discordUserId,
|
||||
authorUserId: author ? author.id : null,
|
||||
authorTag: author ? author.username : null,
|
||||
body,
|
||||
visibility,
|
||||
})
|
||||
return modNotesDb.getById(id)
|
||||
}
|
||||
|
||||
module.exports = { listForUser, add }
|
||||
Reference in New Issue
Block a user