Skip to content

feat(admin): moderation tools for limits and activity stats - #941

Open
Ferryx349 wants to merge 10 commits into
mainfrom
feat/admin-moderation-634
Open

Ferryx349 wants to merge 10 commits into
mainfrom
feat/admin-moderation-634

Conversation

@Ferryx349

Copy link
Copy Markdown
Collaborator

Description

This PR adds Admin Console Moderation :- view/update pubkey & kind lists and rate-limit bypass lists, quick ban/unban and IP bypass actions, and 7-day stats (top publishers, client IPs, rate-limit hits). Persists limit changes to settings with audit logging.

Related Issue

Closes #634

Motivation and Context

How Has This Been Tested?

Screenshots (if appropriate):

Screen.Recording.2026-10-23.at.13.38.14.mov

Types of changes

  • Non-functional change (docs, style, minor refactor)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my code changes.
  • I added a changeset, or this is docs-only and I added an empty changeset.
  • All new and existing tests passed.

Closes #634. Adds admin API and Moderation console for pubkey/kind lists,
rate-limit bypass, ban/unban actions, and publisher/IP/rate-limit stats.
@changeset-bot

changeset-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fddff7f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
nostream Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes affect relay event moderation within the authenticated admin workflow.

Concurrent moderation saves can silently undo bans, so this needs a fix before merging.

Findings

  1. P1 Concurrent saves undo bans ▶
  2. P2 Recent uploads miss activity totals ▶
  3. P2 Valid IPv6 bypasses do nothing ▶

Summary

Adds a Moderation page and admin APIs for limit lists, quick bans, bypass changes, and activity statistics.

  • Admins can change event limits and take quick moderation actions.
  • Admins can rank publishers and client IPs by recent event activity.
  • The relay counts rate-limit hits for moderation activity stats.

The supplied previous threads have no comment numbers. Earlier fixes preserve whitelist membership, isolate test settings, deep-merge defaults, aggregate rankings before limiting, and delete temporary Redis sets. Ferryx349 manually resolved the remaining rolling-window thread without explanation.

Diagram

sequenceDiagram
  participant Admin
  participant API as Admin API
  participant Settings as Settings file
  participant Workers as Relay workers
  participant Stores as PostgreSQL and Redis
  Admin->>API: Read limits and activity
  API->>Stores: Query activity and rate-limit hits
  Stores-->>Admin: Ranked counts
  Admin->>API: Ban or change bypass
  API->>Settings: Save updated lists
  Settings-->>Workers: File change triggers reload
  API-->>Admin: Success
Loading

Reviews (10) · Last reviewed commit: "Merge branch 'main' into feat/admin-mode..." · Reviewed by Greptile

Comment thread src/utils/admin-moderation.ts Outdated
Comment thread test/unit/utils/admin-moderation.spec.ts
Comment thread src/utils/moderation-rate-limit-stats.ts Outdated
@coveralls

coveralls commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 40060142350

Warning

No base build found for commit c40e570 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 75.733%

Details

  • Patch coverage: 22 uncovered changes across 5 files (268 of 290 lines covered, 92.41%).

Uncovered Changes

File Changed Covered %
src/utils/admin-moderation.ts 79 69 87.34%
src/controllers/admin/patch-moderation-limits-controller.ts 21 17 80.95%
src/controllers/admin/post-moderation-actions-controller.ts 25 21 84.0%
src/utils/admin-moderation-persist.ts 19 16 84.21%
src/utils/moderation-rate-limit-stats.ts 62 61 98.39%
Total (15 files) 290 268 92.41%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 11576
Covered Lines: 9174
Line Coverage: 79.25%
Relevant Branches: 5983
Covered Branches: 4124
Branch Coverage: 68.93%
Branches in Coverage %: Yes
Coverage Strength: 789.94 hits per line

💛 - Coveralls

Keep pubkey whitelist on ban; use daily Redis buckets for rate-limit hits;
isolate moderation tests with temp NOSTR_CONFIG_DIR; skip stats when cache has no eval.
Comment thread src/utils/moderation-rate-limit-stats.ts Outdated
Use ZREVRANGE with the requested limit instead of scanning full daily sets.
Comment thread src/utils/moderation-rate-limit-stats.ts Outdated
Union day buckets in Redis, then return top N so multi-day totals stay correct.
Comment thread src/utils/moderation-rate-limit-stats.ts Outdated
Drop the temporary ZUNIONSTORE destination immediately so stats refresh
does not retain duplicate daily bucket copies in Redis.
Improve coverage for moderation limits, actions, stats, and rate-limit helpers.
Comment thread test/unit/utils/moderation-rate-limit-stats.spec.ts Outdated
greptile-apps[bot]
greptile-apps Bot previously approved these changes Oct 23, 2026
Use callCount in rate-limit stats tests, cover query helpers and factory wiring, and add error-path cases for moderation actions and stats.
@greptile-apps
greptile-apps Bot dismissed their stale review October 23, 2026 18:28

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Oct 23, 2026
Comment thread src/utils/admin-moderation.ts Outdated
Comment thread src/utils/moderation-rate-limit-stats.ts Outdated
…dows

Deep-merge defaults before validateSettings so partial info overrides work, and include every UTC day bucket touched by the requested stats window.
@greptile-apps
greptile-apps Bot dismissed their stale review October 24, 2026 03:45

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

Comment on lines +138 to +147
const userSettings = loadUserSettings() as Settings
const nextUserSettings = applyLimitsToUserSettings(userSettings, normalized)
const mergedNext = mergeDeepRight(loadDefaults(), nextUserSettings) as Settings

const issues = validateSettings(mergedNext)
if (issues.length > 0) {
throw new ValidationError('Validation failed', issues)
}

saveSettings(nextUserSettings)

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 Concurrent saves undo bans

On relays with multiple workers, two moderation actions can overwrite each other. Each request reads every list, changes its copy, then replaces all lists without a shared lock. If two workers read before either saves, banning different pubkeys leaves only the last ban, even though both requests succeed.

Lock the full read-change-save operation across workers and read the current lists inside that lock.

.select(db.raw(`encode(event_pubkey, 'hex') as pubkey`))
.count('* as eventCount')
.whereNull('deleted_at')
.where('event_created_at', '>=', sinceUnixSeconds)

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 Recent uploads miss activity totals

Both activity queries filter by event_created_at, which copies the publisher's created_at, rather than when the relay received the event. With the default maxNegativeDelta: 0, clients can upload older events today without appearing in today's activity totals. Admins can therefore miss recent heavy publishers and client IPs.

Use first_seen for the receipt window in both queries.

Comment on lines +250 to +259
case 'whitelist-ip': {
const ip = fields.ip?.trim()
if (!ip || isIP(ip) === 0) {
throw new Error('ip is required and must be a valid IPv4 or IPv6 address')
}
return {
...current,
rateLimitBypass: {
...current.rateLimitBypass,
ipAddresses: [...new Set([...current.rateLimitBypass.ipAddresses, ip])],

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 Valid IPv6 bypasses do nothing

IP bypass actions accept any valid IPv6 spelling but save and remove it as an exact string. The socket uses SocketAddress, which returns a standard spelling, while the rate-limit check uses includes. Entering 2001:0db8:0:0:0:0:0:1 succeeds but does not match the client's 2001:db8::1. Another valid spelling can also fail to remove an existing bypass.

Normalize addresses the same way before adding, removing, and comparing them.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Admin Console] Phase 4: Moderation Tools

3 participants