fix(moderation): let an appointed moderator see what they moderate - #117
Merged
Conversation
The Authorizer grants content.viewUnapproved and content.viewDeleted to a
forum's own moderator, but every content-scope call site built its target
from { forumId, forum } alone, so target.isForumModerator was never set and
that branch could not fire. A moderator appointed on a forum — not an admin,
not a super-moderator, not in a group carrying the columns — saw neither held
nor soft-deleted content anywhere: the queue's links landed on pages that
filtered the item out, and the inline approve and restore controls rendered
with nothing to select.
Authorizer.moderatorTargetIn resolves the actor's standing in one forum into
a target, and contentScopeIn derives the visibility scope from it. The forum
listing and thread pages already resolve that target for their moderation
tools, so they now resolve it before the scope and pass the same one to both;
the thread page's metadata and the two REST call sites take contentScopeIn.
moderatorTargetFor keeps its callers and delegates to the one implementation.
Guests and ordinary members are untouched — moderatorRightsIn returns no
rights without an appointment, and no rights means the public scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kP43A5shJWmAKqkBovhaF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes MEI-52 (audit finding A6).
The bug
Authorizer.canForumgrantscontent.viewUnapprovedandcontent.viewDeletedwhentarget.isForumModerator === true(packages/authorization/src/authorizer.ts). EverycontentScopecall site built its target from{ forumId, forum }alone, so that flag was never set and the branch was dead code:apps/community/app/(board)/[slug]/page.tsx— the forum listingapps/community/app/(board)/thread/[slug]/page.tsx— the thread's posts, and itsgenerateMetadataapps/community/app/api/v1/[...path]/route.ts—threadScope, andGET /forums/:forumId/threadsA moderator appointed on a forum — not an administrator, not a super-moderator, not in a group carrying
canViewUnapproved/canViewDeleted— therefore saw neither held nor soft-deleted threads or posts anywhere. The moderation queue's links landed on pages that filtered the item out, and the inline Approve/Restore controls rendered with no selectable rows, because moderator rights were computed with appointments elsewhere on those same pages (the thread page's tools, the listing's inline rights). The only view of the content was the queue's 300-character excerpt.The fix
Two methods on
Authorizer, so the resolution exists once:moderatorTargetIn(actor, forumId, forum)— resolves the actor's standing in that one forum into aModeratedTarget(moderatorRights+isForumModerator).contentScopeIn(actor, forumId, forum)— derives the visibility scope from that target.Both pages already resolved the same target a few lines further down for their moderation tools, so they now resolve it before the scope and pass the one target to both — no extra queries. The thread page's
generateMetadataand the two REST sites callcontentScopeIndirectly.moderatorTargetForinsrc/server/modcp.tskeeps its signature and callers and delegates tomoderatorTargetIn, sopost-scope.tsis unchanged.Guests and ordinary members are untouched by construction:
moderatorRightsInreturns early with no rights for a nulluserIdand grants nothing without a covering appointment, and no rights meansPUBLIC_CONTENT. The group-column path (forum.canViewUnapproved) and the administrator/super-moderator bypasses are unchanged.Out of scope, deliberately: the board-wide aggregates (
search.ts,stats.ts,board-latest.tsx,presence.ts,discovery.ts) build a cross-forum staff scope rather than a per-forum one, and are left alone.The caching rule in
docs/nextjs-conventions.mdholds — every touched read already takes anactorand runs per request; none is inside a cached region, andno-request-state-in-cachepasses.Validation
packages/authorization/src/content-scope.test.ts— nine cases following thethread-tool-rights.test.tspattern: an appointee's scope carriesunapprovedanddeletedin the forum they moderate; the same appointee getsPUBLIC_CONTENTin a forum they do not moderate; the appointment reaches a subforum only when it cascades; any right at all widens the scope, not justcanApproveContent; an ordinary member (including one whose group holds an appointment elsewhere) and a guest stay onPUBLIC_CONTENT; the group-column path and the staff bypasses still widen; a banned member gets nothing.isForumModeratorunconditionally true reddens the four negative cases; dropping theforum.canView*half of the branch reddens the group-column case; removing the banned check and the staff bypasses reddens the last two. Restored after each.pnpm verify— exit 0 (311 files, 5726 tests; only the two pre-existing warnings).DATA_SOURCE=fixture pnpm build— exit 0, required because the diff touchesapp/. No client component was added or changed; the one"use client"underapp/is stillerror.tsx.pnpm test:e2enot run — fixed ports are shared with sibling agents.No documentation change: nothing under
docs/claimed anything about who the content scope admits, so no doc statement moves with this behaviour. The moderation sections ofdocs/mybb-parity.mdwere checked and describe the queue and inline tools rather than visibility.🤖 Generated with Claude Code
Generated by Claude Code