Repository navigation
feat(moderation): ✨ scoped moderator permissions, moderation registry and report API - #872
aXenDeveloper wants to merge 5 commits into
Conversation
… and report API Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7
… migration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7
…s; lint fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
React Doctor found 16 new issues in 10 files · 1 error & 15 warnings · score 83 / 100 (Needs work) · 0 fixed · vs Errors
15 warnings
Reviewed by React Doctor for commit |
| if (moved.length === 0) return current; | ||
|
|
||
| for (const report of moved) { | ||
| await c |
There was a problem hiding this comment.
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.
| .where(inArray(core_users.id, assigneeIds)); | ||
|
|
||
| for (const assigneeId of assigneeIds) { | ||
| const user = users.find(row => row.id === assigneeId); |
There was a problem hiding this comment.
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
| const lost = reports.filter(report => report.assigneeId === assigneeId); | ||
| await c.get("db").transaction(async tx => { | ||
| for (const report of lost) { | ||
| const [updated] = await tx |
There was a problem hiding this comment.
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.
| const scope = findModerationScope(registry, family); | ||
| const found = new Set( | ||
| scope | ||
| ? (await scope.definition.find(c, [...ids])).map(item => item.id) |
There was a problem hiding this comment.
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.
| userId: core_moderators_permissions.userId, | ||
| }) | ||
| .from(core_moderators_permissions); | ||
| const rootRoles = await db |
There was a problem hiding this comment.
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.
| * Chooses where one scoped permission applies. Lazy-loaded: nothing about a | ||
| * family's resources is fetched until an administrator opens this. | ||
| */ | ||
| export const StaffScopeDialogContent = ({ |
There was a problem hiding this comment.
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.
| <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)} |
There was a problem hiding this comment.
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
| ); | ||
| }; | ||
|
|
||
| const ModcpReportDetail = ({ id }: { id: number }) => { |
There was a problem hiding this comment.
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.
| ); | ||
| }; | ||
|
|
||
| const ModcpReportDetail = ({ id }: { id: number }) => { |
There was a problem hiding this comment.
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.
| ) => { | ||
| setBusy(true); | ||
| const result = await work(); | ||
| setBusy(false); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| .where(and(eq(table.id, report.id), eq(table.version, report.version))); | ||
| report.version += 1; |
There was a problem hiding this comment.
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 👍 / 👎.
| if ( | ||
| (await countRecentReports(c, user.id)) >= | ||
| MODERATION_LIMITS.reportsPerUserPerHour | ||
| ) { |
There was a problem hiding this comment.
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 👍 / 👎.
| for (const target of targets) { | ||
| if (moderator[target.permissionModule]) continue; | ||
| moderator[target.permissionModule] = moderationPermissionEntries(target); |
There was a problem hiding this comment.
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 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01U58HQh7q7zUuoKUnmgeKW7