Merge branch 'main' into feat/password-reset
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m59s
PR Checks / client-build (pull_request) Successful in 9m26s
PR Checks / bot-install (pull_request) Successful in 9m31s

This commit is contained in:
2026-07-19 09:04:43 +00:00
23 changed files with 3083 additions and 8 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,7 @@ const doc = {
{ name: 'Admin · Account', description: 'Self-service account security (2FA, linked identities)' },
{ name: 'Player', description: 'Self-service player accounts (register, credentials, 2FA, linked identities)' },
{ name: 'Player · Shard', description: 'Link an in-game account and read its roster / vendors (uo-link)' },
{ name: 'Player · Appeals', description: 'Player-submitted moderation appeals' },
{ name: 'Admin · Dashboard', description: 'Dashboard summary and site mode' },
{ name: 'Admin · Posts', description: 'News / five-on-friday / newsletter / screenshots + uploads' },
{ name: 'Admin · Wiki', description: 'Wiki pages, categories, tags and revisions' },
@@ -421,6 +422,93 @@ const doc = {
type: 'object',
properties: { ok: { type: 'boolean', example: true } },
},
// ── Moderation appeals (Phase 6c/6d) ────────────────────────────────────
Appeal: {
type: 'object',
description: 'A player-submitted moderation appeal (as returned to the player and in the staff queue).',
properties: {
id: { type: 'integer', example: 12 },
mod_action_id: { type: 'integer', example: 340 },
discord_user_id: { type: 'string', example: '216734083584917504' },
action_type: { type: 'string', enum: ['ban', 'mute'], example: 'ban' },
user_id: { type: 'integer', nullable: true, example: 42 },
status: {
type: 'string',
enum: ['pending', 'under_review', 'approved', 'denied', 'withdrawn'],
example: 'pending',
},
submitted_text: { type: 'string', example: 'I was banned by mistake — please review.' },
staff_response: { type: 'string', nullable: true, example: null },
handled_by_user_id: { type: 'integer', nullable: true, example: null },
handled_by_tag: { type: 'string', nullable: true, example: null },
reversal_status: {
type: 'string',
enum: ['none', 'done', 'failed'],
description: 'Discord-reversal outcome. done/failed only after an approval; none otherwise.',
example: 'none',
},
submitted_at: { type: 'string', format: 'date-time' },
resolved_at: { type: 'string', format: 'date-time', nullable: true, example: null },
action_target_tag: { type: 'string', nullable: true, example: 'Rogue#1234', description: 'Snapshot of the original action target tag (from mod_actions).' },
action_reason: { type: 'string', nullable: true, example: 'Spam' },
action_created_at: { type: 'string', format: 'date-time', nullable: true },
action_duration_seconds: { type: 'integer', nullable: true, example: 86400 },
submitter_username: { type: 'string', nullable: true, example: 'newplayer' },
},
},
AppealQueueItem: {
allOf: [{ $ref: '#/components/schemas/Appeal' }],
description: 'A staff-queue appeal row — identical shape to Appeal, with the joined action/submitter columns populated.',
},
AppealResolveResult: {
allOf: [
{ $ref: '#/components/schemas/Appeal' },
{
type: 'object',
properties: {
reversal: {
type: 'object',
description: 'What the approval attempted against Discord.',
properties: {
attempted: { type: 'boolean', example: true },
ok: { type: 'boolean', example: true },
reversal_status: { type: 'string', enum: ['none', 'done', 'failed'], example: 'done' },
bot_status: { type: 'integer', nullable: true, example: 200, description: 'HTTP status from the bot internal call, or null when no call was made.' },
error: { type: 'string', nullable: true, example: null },
},
},
},
},
],
},
AppealEligibleAction: {
type: 'object',
description: 'A ban/mute mod_action the caller may appeal (no active appeal outstanding).',
properties: {
id: { type: 'integer', example: 340, description: 'mod_action id — pass as mod_action_id when submitting.' },
action_type: { type: 'string', enum: ['ban', 'mute'], example: 'ban' },
target_tag: { type: 'string', nullable: true, example: 'Rogue#1234' },
reason: { type: 'string', nullable: true, example: 'Spam' },
duration_seconds: { type: 'integer', nullable: true, example: 86400 },
created_at: { type: 'string', format: 'date-time' },
},
},
CreateAppealRequest: {
type: 'object',
required: ['mod_action_id', 'submitted_text'],
properties: {
mod_action_id: { type: 'integer', example: 340, description: 'The ban/mute mod_action to appeal (must belong to the caller).' },
submitted_text: { type: 'string', minLength: 1, maxLength: 4000, example: 'I was banned by mistake — please review.' },
},
},
ResolveAppealRequest: {
type: 'object',
required: ['status'],
properties: {
status: { type: 'string', enum: ['approved', 'denied'], example: 'approved' },
staff_response: { type: 'string', maxLength: 4000, nullable: true, example: 'Reviewed — reversing the ban.' },
},
},
TotpCodeRequest: {
type: 'object',
required: ['code'],