feat(moderation): appeals (6c) + Discord reversal on approve (6d)
Players whose linked Discord identity was banned or muted can now submit an appeal from the portal and track it; staff get a queue in the admin moderation section to claim and resolve (approve/deny) appeals. Approving a ban/mute appeal best-effort asks the Discord bot to reverse the action (unban / clear timeout) via the internal API and posts a mod-log embed; a down bot never fails the resolution (reversal_status is recorded). - Schema: new server-owned `appeals` table (no cross-owner FK to mod_actions; existence validated in app code). - Server: model/appeals/* + player appeals controller (submit/mine/ eligible/withdraw) and admin queue handlers (list/claim/resolve/ per-user) under the existing admin+moderator gate; one-active-appeal enforced app-side; eligibility keyed on the caller's linked Discord id. - 6d: bot POST /internal/mod-reverse (+ modLog.postReversal) and server botInternalClient.reverseModAction, wired into resolve(). - Client: admin Appeals queue + resolve modal, ModerationUser appeals tab, player Appeals page (submit/withdraw), nav + routes + api methods. - Docs: swagger annotations + component schemas, regenerated output. - Tests: appeals controller + pure suites (server npm test 224 green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
This commit is contained in:
@@ -573,6 +573,35 @@ CREATE TABLE IF NOT EXISTS mod_actions (
|
||||
INDEX idx_mod_actions_target (guild_id, target_user_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Player-submitted moderation appeals (Phase 6c). Unlike mod_actions above, this
|
||||
-- table is SERVER-owned — written and read only by the main site (the player
|
||||
-- appeals controller and the admin moderation queue), never by the bot. A player
|
||||
-- appeals one of their own ban/mute mod_actions; staff triage the queue, and an
|
||||
-- approval optionally triggers an automatic Discord reversal (Phase 6d) whose
|
||||
-- outcome is recorded in reversal_status. mod_action_id is a plain column with NO
|
||||
-- hard FK to the bot-owned mod_actions table (cross-owner FK avoided on purpose,
|
||||
-- matching posts.announce_job_id) — existence is validated in app code. user_id
|
||||
-- is the appealing site account; discord_user_id is the snowflake the appeal is
|
||||
-- for (snapshotted from mod_actions.target_user_id at submit time).
|
||||
CREATE TABLE IF NOT EXISTS appeals (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mod_action_id INT NOT NULL,
|
||||
discord_user_id VARCHAR(32) NOT NULL,
|
||||
action_type ENUM('ban','mute') NOT NULL,
|
||||
user_id INT NULL,
|
||||
status ENUM('pending','under_review','approved','denied','withdrawn') NOT NULL DEFAULT 'pending',
|
||||
submitted_text TEXT NOT NULL,
|
||||
staff_response TEXT NULL,
|
||||
handled_by_user_id INT NULL,
|
||||
handled_by_tag VARCHAR(120) NULL,
|
||||
reversal_status ENUM('none','done','failed') NOT NULL DEFAULT 'none',
|
||||
submitted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
resolved_at DATETIME NULL,
|
||||
CONSTRAINT fk_appeal_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_appeals_status (status, submitted_at),
|
||||
INDEX idx_appeals_action (mod_action_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Standing warnings, separate from mod_actions so /warnings can list active
|
||||
-- warnings per user. expires_at is unused in Phase 2 (no decay/escalation
|
||||
-- yet — deferred, see mute/warn command comments) but the column is cheap to
|
||||
|
||||
Reference in New Issue
Block a user