Skip to content

feat(moderation): ✨ scoped moderator permissions, moderation registry and report API - #872

Open
aXenDeveloper wants to merge 5 commits into
canaryfrom
claude/moderation-mvp
Open

aXenDeveloper wants to merge 5 commits into
canaryfrom
claude/moderation-mvp

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7

@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 11, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T22:53:23.006861Z 9fb604d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown

React Doctor found 16 new issues in 10 files · 1 error & 15 warnings · score 83 / 100 (Needs work) · 0 fixed · vs canary

Errors

15 warnings

src/api/lib/moderation/access.ts

  • ⚠️ L175 await inside a loop async-await-in-loop
  • ⚠️ L226 array.find() inside a loop js-index-maps
  • ⚠️ L247 await inside a loop async-await-in-loop

src/api/modules/admin/staff/lib/scoped-grants.ts

  • ⚠️ L167 await inside a loop async-await-in-loop

src/api/modules/moderation/lib/candidates.ts

  • ⚠️ L24 Sequential independent awaits server-sequential-independent-await

src/api/modules/moderation/routes/list.route.ts

  • ⚠️ L82 Sequential independent awaits server-sequential-independent-await

src/api/modules/moderation/routes/workflow.routes.ts

  • ⚠️ L495 await inside a loop async-await-in-loop

src/content/server/report-target.ts

  • ⚠️ L312 Array lookup inside a loop js-set-map-lookups
  • ⚠️ L329 await inside a loop async-await-in-loop

src/views/admin/views/core/staff/edit/edit-staff-form-content.tsx

  • ⚠️ L338 Empty default prop breaks memo rerender-memo-with-default-value

src/views/admin/views/core/staff/edit/scope/scope-dialog.tsx

  • ⚠️ L68 React function has high control-flow complexity no-high-complexity-react-function
  • ⚠️ L231 Array lookup inside a loop js-set-map-lookups

src/views/moderation/modcp/report-content.tsx

  • ⚠️ L86 Large component is hard to read and change no-giant-component
  • ⚠️ L86 React function has high control-flow complexity no-high-complexity-react-function
  • ⚠️ L114 Loading flag reset outside finally no-loading-flag-reset-outside-finally

Reviewed by React Doctor for commit 9fb604d. See inline comments for fixes.

if (moved.length === 0) return current;

for (const report of moved) {
await c

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

.where(inArray(core_users.id, assigneeIds));

for (const assigneeId of assigneeIds) {
const user = users.find(row => row.id === assigneeId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-index-maps (warning)

This gets slow as your list grows because array.find() runs inside a loop, so build a Map once before the loop for instant lookups

Fix → Build a Map once before the loop instead of calling array.find(...) inside it

Docs

const lost = reports.filter(report => report.assigneeId === assigneeId);
await c.get("db").transaction(async tx => {
for (const report of lost) {
const [updated] = await tx

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

const scope = findModerationScope(registry, family);
const found = new Set(
scope
? (await scope.definition.find(c, [...ids])).map(item => item.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

userId: core_moderators_permissions.userId,
})
.from(core_moderators_permissions);
const rootRoles = await db

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/server-sequential-independent-await (warning)

This awaited initializer does not read the previous result. If the operations are independent asynchronous work, they may overlap; await syntax alone does not establish a speedup.

Fix → Consider Promise.all([...]) only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics.

Docs

* Chooses where one scoped permission applies. Lazy-loaded: nothing about a
* family's resources is fetched until an administrator opens this.
*/
export const StaffScopeDialogContent = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-high-complexity-react-function (warning)

StaffScopeDialogContent has cyclomatic complexity 17, cognitive complexity 22, and maximum nesting depth 3, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

<li key={item.id}>
<label className="hover:bg-muted/40 flex items-center gap-3 px-3 py-2">
<Checkbox
checked={selected.includes(item.id)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-set-map-lookups (warning)

This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.

Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time

Docs

);
};

const ModcpReportDetail = ({ id }: { id: number }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-giant-component (warning)

Component "ModcpReportDetail" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.

Fix → Pull each section into its own component so the parent is easier to read, test, and change.

Docs

);
};

const ModcpReportDetail = ({ id }: { id: number }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-high-complexity-react-function (warning)

ModcpReportDetail has cyclomatic complexity 32, cognitive complexity 49, and maximum nesting depth 3, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

) => {
setBusy(true);
const result = await work();
setBusy(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-loading-flag-reset-outside-finally (warning)

This resets a loading/busy flag only on the success path: if the awaited call rejects the reset never runs and the flag stays stuck truthy (a spinner that never stops, a button disabled forever). Move the reset into a finally block, or mirror it on every catch, so it clears on rejection too.

Fix → A trailing setLoading(false) after an await never runs if the awaited call rejects, so the flag stays stuck truthy; reset it in a finally block (or mirror the reset on every catch) so it clears on both paths.

Docs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fb604d46b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +183 to +184
.where(and(eq(table.id, report.id), eq(table.version, report.version)));
report.version += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retry scope updates that lose the version race

When a report is mutated after stale is read but before this optimistic update, the WHERE version = ... can update zero rows, yet the code increments the in-memory version and returns the newly resolved scope as though it succeeded. authorizeReport then reloads a report that can still contain its old scope and authorizes the current request against that stale scope, so a moderator limited to the old resource can briefly access or manage a report after its target moved. Check the affected row and retry the scope synchronization before authorizing.

Useful? React with 👍 / 👎.

Comment on lines +95 to +98
if (
(await countRecentReports(c, user.id)) >=
MODERATION_LIMITS.reportsPerUserPerHour
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the per-user report limit atomically

The hourly limit is checked by counting existing rows before the later insert, so concurrent requests for different targets can all observe a count below 20 and all succeed. A client can therefore bypass the documented per-user limit simply by submitting reports in parallel; the duplicate key only serializes submissions for the same target. Reserve the quota atomically or perform the count and insert under suitable locking/serialization.

Useful? React with 👍 / 👎.

Comment on lines +71 to +73
for (const target of targets) {
if (moderator[target.permissionModule]) continue;
moderator[target.permissionModule] = moderationPermissionEntries(target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Merge permissions for targets that share a module

If a plugin registers multiple custom report targets with the same permissionModule, processing the first target creates the module and this check silently skips every later target. Validation does not reject shared modules, so a later target with another scope family becomes inaccessible to restricted moderators, while a later hideable target may never expose can_view_hidden or can_hide in the catalog. Aggregate compatible entries from all targets or reject incompatible module reuse during registry validation.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants