Skip to content

Fix appeal submission for suspended users without session#22

Open
dbwg2009 wants to merge 43 commits into
mainfrom
claude/test-api-setup-XBHY0
Open

Fix appeal submission for suspended users without session#22
dbwg2009 wants to merge 43 commits into
mainfrom
claude/test-api-setup-XBHY0

Conversation

@dbwg2009
Copy link
Copy Markdown
Owner

@dbwg2009 dbwg2009 commented May 19, 2026

Summary

Fixed critical authentication bug preventing suspended/banned users from submitting appeals. Suspended users couldn't access the appeal endpoint because it required a valid authenticated session, which they never received due to intentional login blocking.

Changes

Backend (main.py)

  • Modified AppealSubmitIn model to include authHash field for credential verification
  • Updated POST /api/appeals endpoint to remove auth_dep requirement and instead verify credentials directly using email + authHash (matching login flow)
  • Credential verification mirrors the login endpoint: hash lookup, hasher.verify(), and session-less submission

Frontend (app.js)

  • Store authHash in state when login returns restriction response (user has already verified credentials)
  • Pass authHash in appeal form submission payload
  • No additional UI changes needed - authHash already derived during login

Technical Details

Suspended users follow this flow:

  1. Login with email + password
  2. Browser derives authHash via PBKDF2 (same as normal login)
  3. Backend verifies authHash, detects restriction, returns suspension response
  4. Frontend stores authHash in state for later use
  5. User navigates to appeal form
  6. Appeal submission includes stored authHash for credential verification
  7. Backend verifies credentials without requiring active session
  8. Appeal created successfully

Testing

  • Suspended user can submit appeal with correct credentials ✓
  • Banned user can submit appeal with correct credentials ✓
  • Invalid credentials rejected (401) ✓
  • Active users cannot appeal (400) ✓
  • Prevents duplicate pending appeals ✓

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Implemented moderator role system for user management and content moderation
    • Added account suspension and banning with automatic restoration for expired suspensions
    • Introduced user appeals system for disputing account restrictions
    • Enhanced reporting with message content analysis capability
    • Created moderator dashboard displaying metrics, pending appeals, reports, user management, and audit logs
    • Added suspension notification screen with appeal submission interface
    • Included privacy notice in message reporting about content handling
  • Chores

    • Refactored service worker registration configuration

Review Change Stack

Syme-6005 and others added 30 commits May 11, 2026 22:27
- Replace unsafe innerHTML assignments with safe DOM manipulation (createElement, appendChild, textContent)
- Fix moderation reports list rendering (line 748-785)
- Fix moderation users list rendering (line 787-843)
- Add try-catch error handling to loadModeration() function
- Add error handling to async user action event listeners
- Use textContent for user-controlled data instead of innerHTML templates

Fixes XSS issues reported in static/js/app.js related to reports and users lists
- Replace unsafe innerHTML assignments with replaceChildren()
- Use textContent instead of innerHTML for error messages
- Eliminate unencoded HTML in error display
- Improve XSS vulnerability prevention
…e checks, and session revocation

- Fix CSRF protection in require_active_user and require_moderator dependencies
- Add moderator privilege escalation prevention in mod_user_status
- Extend session revocation to suspended users (not just banned)
- Sync moderator tab visibility on boot() for existing sessions
- Fix toast message grammar for account actions (ban->banned, suspend->suspended, restore->restored)
- Prevent suspended users from accessing group messages by using require_active_user
- New super_moderator role with higher privileges than moderators
- Regular moderators cannot ban/suspend other moderators
- Suspension system with duration (1-365 days) and auto-restore on expiry
- Added suspended_until column with migration
- Fixed suspended user login blocking
- New /api/mod/users/{id}/role endpoint for role management
- Complete HTML injection audit: all user data properly escaped
- Improved role-based access controls throughout

Fixes privilege escalation where moderators could ban each other.
- Add mod_actions table with mod_id, target_id, action, reason, and created_at
- Create indexes on mod_id, target_id, and created_at for efficient queries
- Log all moderator actions (suspend, ban, restore, role changes) to audit table
- Add GET /api/mod/audit-log endpoint to retrieve audit logs with pagination
- Add audit log UI section to moderation dashboard
- Display audit entries showing action, moderator, target, reason, and timestamp

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add suspension duration modal with predefined options (1, 7, 30, 60 days)
- Add optional reason field when suspending accounts
- Update moderator action handler to use suspensionDialog for suspension actions
- Store suspension duration and reason in database for audit trail
- Allow moderators to specify why account is being suspended

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add search input to filter users by email
- Add status filter dropdown (active, suspended, banned, moderators)
- Implement real-time filtering as user types
- Refactor user list rendering to support dynamic filtering
- Users can quickly find and take bulk actions on specific users

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add appeals table to track user appeals with status, reason, and moderator response
- Create POST /api/appeals endpoint to allow users to submit appeals
- Create GET /api/mod/appeals endpoint for moderators to review pending appeals
- Create POST /api/mod/appeals/{id} endpoint to approve/reject appeals
- Add appeals UI section to moderation dashboard
- Add appeal review modal with decision tracking
- Add submit appeal modal for restricted users
- Auto-restore users when appeal is approved

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add unreviewed reports and pending appeals metric cards
- Display badge on appeals card showing count of pending appeals
- Automatically update appeal count when moderation dashboard loads
- Color-coded badges (red) for quick visual indication of pending work

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add search input to filter reports by keyword
- Search across reason, details, reporter email, and reported user email
- Real-time filtering as moderators type
- Shows 'No matching reports' when search yields no results

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add predefined reason templates for suspension, ban, and restore actions
- Templates include: spam, harassment, inappropriate content, guidelines violation, suspicious activity
- Allow custom reason input in addition to templates
- Reason templates auto-fill when moderators select from dropdown
- Consistent UX across all moderator actions

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Reduce report reason minimum length from 5 to 1 character
- Allows preset reasons like 'spam' (4 chars) to be submitted
- Fixes validation error when reporting with short reason text

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Fix event listener accumulation in loadModeration() using event delegation
- Add offset pagination parameter to mod_reports and mod_users endpoints
- Add reason field to ModUserActionIn for logging moderator actions
- Add super_moderator indicator in user list with orange styling
- Update moderator filter to include both moderator and super_moderator roles
- Apply require_active_user to report endpoint for consistency
- Add comprehensive documentation for CIPHER_MODERATOR_EMAILS environment variable
- Fix filter logic to correctly match super_moderators in list

Fixes:
- P0: Event listener accumulation (now uses event delegation)
- P1: Pagination support (added offset to mod_reports and mod_users)
- P1: Super moderator visibility (added orange indicator)
- P1: Reason field validation (added to ModUserActionIn)
- P1: Report submission protection (uses require_active_user)
- P1: Environment variable documentation (added comments)

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
Changed entry.modUsername/targetUsername to entry.mod_email/target_email to match API response format.
…stead

Restore now has its own flow with a simple prompt for restoration reason, helping other mods understand the context without the formal dialog format of ban actions.
Add a dedicated restore dialog with three preset reasons:
- Appeal approved
- Manual review - no violation found
- Mistaken suspension

Reason is now required for restore actions to help other mods understand context.
- Add warning message explaining message will be decrypted and sent unencrypted to mods
- Show preview of message being reported in report dialog
- Add confirmation dialog before submitting report
- Decrypt message using user's key and send decrypted content with report
- Store message_content in reports table
- Display message content in mod reports view
- Add migration for message_content column
Security Fixes:
- CRITICAL: Fix SQL injection in get_appeals endpoint by validating status parameter
  - Validate status against whitelist (pending, approved, rejected)
  - Use parameterized queries consistently without dynamic query building
  - Prevent malicious SQL injection via status parameter

- HIGH: Fix variable redeclaration of searchInput
  - Rename reports search input to reportsSearchInput
  - Prevents confusion and variable shadowing

- HIGH: Fix JavaScript linting issues
  - Update event listener to use renamed variable

The SQL injection vulnerability was in the dynamic query building for the appeals
endpoint where the status parameter could be used maliciously. Now uses explicit
queries for each case with proper parameter binding.

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Parallelize stats, appeals, reports, and users API calls in loadModeration using Promise.all for faster dashboard load
- Add exception chaining to hasher.verify error handlers for better debugging traceback information

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
dbwg2009 and others added 13 commits May 19, 2026 22:37
The inline script was blocked by Content-Security-Policy directive 'script-src self'.
Moving service worker registration to /static/js/sw-register.js resolves the CSP violation.

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add Service-Worker-Allowed header to allow SW scope of /
- Remove explicit scope from SW registration (uses SW directory scope by default)
- These fixes resolve SW registration error and allow offline support

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
The loadModeration function was missing its closing brace, causing the
entire app.js module to fail with 'Unexpected end of input' error.
This prevented the app from loading at all - no event listeners attached,
no login functionality, no UI initialization.
- Remove 'Moderation' from main tabs (was visually overflowing)
- Add 'mod' toggle button in topbar-end (visible only to moderators)
- Add separate mod-tabs nav with Dashboard/Reports/Users/Appeals/Audit
- Split monolithic moderation pane into 5 sub-panes controlled by setModRoute
- Fix usersList scoping bug: move declaration outside renderUsersList
  so event delegation can access it
- Support legacy #moderation hash by routing to enterModMode
- Remove 'Moderation' from main tabs (was visually overflowing)
- Add 'mod' toggle button in topbar-end (visible only to moderators)
- Add separate mod-tabs nav with Dashboard/Reports/Users/Appeals/Audit
- Split monolithic moderation pane into 5 sub-panes controlled by setModRoute
- Fix usersList scoping bug: move declaration outside renderUsersList
  so event delegation can access it
- Support legacy #moderation hash by routing to enterModMode
- Item 1: Message decryption for reports ✅
- Item 7: Restore dialog with preset reasons ✅

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add AppealSubmitIn model with appeal_type, email confirmation, min 50 char reason
- Add appeal_type column migration and endpoint support
- Add GET /api/appeals/status endpoint for users to check appeal status
- Modify POST /api/appeals to validate email and accept appeal_type
- Update login endpoint to return restriction info (banned/suspended) instead of throwing errors
- Return timeRemaining for suspended accounts to display on suspension screen
- Support auto-restore on appeal approval

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
- Add suspension screen (replaces login when user is banned/suspended)
- Show suspension time remaining with readable format
- Add appeal form page with:
  - Appeal type dropdown (mistaken, violated by mistake, circumstances changed)
  - Email confirmation field
  - Reason textarea (min 50 char, max 2000)
  - Character count display
- Handle appeal submission and show success screen
- Route to /appeal from suspension screen
- Update login flow to detect restrictions and show suspension screen

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
The suspension and appeal views weren't being toggled by setView(),
causing blank screen when user is suspended. Now properly shows
suspension screen with appeal option.

https://claude.ai/code/session_01Tq7iYMZVHmyeByUxiUj2iH
Suspended/banned users can't submit appeals through authenticated endpoints
since they never received a session. Modified the appeal endpoint to verify
credentials directly using email + authHash (matching login flow), and stored
the authHash from login in state for use during appeal submission.

Changes:
- Add authHash field to AppealSubmitIn request model
- Remove auth_dep requirement from POST /api/appeals
- Verify credentials directly in appeal endpoint using hasher.verify
- Store authHash in state when user is restricted
- Include authHash in appeal form submission
@github-actions github-actions Bot added area: frontend app/, components/, styles area: backend labels May 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

This PR implements a complete moderation and account restriction system. Users now have roles (moderator/super-moderator) and statuses (active/suspended/banned), with expired suspensions auto-restoring on login. Login detects restrictions and returns metadata; appeal forms and moderation dashboards provide UI for restricted users and moderators. Sensitive operations require active accounts. Moderators can manage users, review reports with decrypted content, and approve/reject appeals via audit-logged endpoints.

Changes

Moderation and Account Restriction System

Layer / File(s) Summary
Database schema and request models
backend/main.py
users table gains role, status, suspended_until columns; reports table adds message_content; new mod_actions and appeals tables for audit logging and appeals; migrations wired; Pydantic models for moderator actions and appeal submission.
Authentication guards and endpoint gating
backend/main.py
current_user selects role/status and treats banned/expired-suspended states as invalid; require_active_user rejects non-active accounts with 403; require_moderator requires moderator role; 10 messaging and group endpoints updated to require active accounts instead of generic auth.
User registration and login with status management
backend/main.py
Registration assigns role=moderator when email matches MODERATOR_EMAILS; login auto-restores expired suspensions, returns restricted metadata with restrictionType/timeRemaining for suspended/banned users, updates last_login_at on success; /api/auth/me includes role, status, isModerator.
Account restriction screens (suspension and appeals)
static/index.html, static/js/app.js
Adds suspension and appeal views with form fields, modals for workflows, and timed countdown messaging; login detects "restricted" response and routes to suspension screen; appeal form validates reason, counts characters, and submits to /api/appeals.
Moderator mode navigation and routing
static/index.html, static/js/app.js
Introduces moderator tab strip, toggle button, and moderation pane; extends app state with modMode/modRoute; implements setModRoute(), enterModMode(), exitModMode() to swap visibility and load moderator data; wires hash routing to support mod/* sub-routes.
Moderator dashboard UI and panes
static/index.html, static/js/app.js
Moderation tab displays dashboard tiles (users/messages/reports/appeals/badges), searchable report list, filterable user list, pending appeals with review modal, and audit log entries.
Moderator and appeal API endpoints
backend/main.py
Adds 10 endpoints for stats, reports, users, user status/role changes, audit log, appeal submission/status/review; logs all moderator actions to mod_actions; /api/report stores decrypted message_content.
Report modal message content decryption and preview
static/index.html, static/js/app.js
Report modal decrypts and previews message content (DM/group), displays decryption status, adds privacy notice, resets decryption state when switching threads, gates submission behind confirmation modal, sends decrypted messageContent to backend.
Service worker registration extraction
static/index.html, static/js/sw-register.js, backend/main.py
Moves service worker registration from inline script to external file; adds Service-Worker-Allowed header; checks navigator.serviceWorker and registers /static/js/sw.js with console logging.

Sequence Diagrams

sequenceDiagram
    participant RestrictedUser as Restricted User
    participant LoginEndpoint as Login Endpoint
    participant Database
    participant ClientApp as Client App
    participant SuspensionView as Suspension View
    RestrictedUser->>LoginEndpoint: email + password
    LoginEndpoint->>Database: verify user
    alt suspended_until expired
        Database->>Database: auto-restore to active
        LoginEndpoint-->>ClientApp: {restricted: false, ...}
    else status = suspended
        LoginEndpoint-->>ClientApp: {restricted: true, restrictionType: suspended, timeRemaining: ms}
        ClientApp->>SuspensionView: render with countdown
        SuspensionView-->>RestrictedUser: display remaining time
    else status = banned
        LoginEndpoint-->>ClientApp: {restricted: true, restrictionType: banned}
        ClientApp->>SuspensionView: render ban message
        SuspensionView-->>RestrictedUser: offer appeal or sign out
    end
Loading
sequenceDiagram
    participant Moderator
    participant ModDashboard as Mod Dashboard
    participant ModAPI as Mod API
    participant Database
    Moderator->>ModDashboard: enter mod mode
    ModDashboard->>ModAPI: fetch stats, reports, users, appeals
    ModAPI->>Database: query tables
    Database-->>ModAPI: data
    ModAPI-->>ModDashboard: metrics + lists
    ModDashboard-->>Moderator: render dashboard tiles + tables
    Moderator->>ModDashboard: select user, click suspend
    ModDashboard->>ModDashboard: show suspension modal
    Moderator->>ModDashboard: enter duration + reason
    ModDashboard->>ModAPI: POST /api/mod/users/{id}/status
    ModAPI->>Database: update users.status, insert mod_actions
    Database-->>ModAPI: success
    ModAPI-->>ModDashboard: updated user
    ModDashboard-->>Moderator: refresh user list
Loading
sequenceDiagram
    participant User
    participant ReportModal as Report Modal
    participant Decryption as Decryption Engine
    participant ReportAPI as Report API
    User->>ReportModal: open modal for message
    ReportModal->>Decryption: decrypt message content
    Decryption-->>ReportModal: plaintext (or failure)
    ReportModal->>ReportModal: display preview + privacy notice
    User->>ReportModal: submit report
    ReportModal->>ReportModal: confirm via modal
    User->>ReportModal: confirm submission
    ReportModal->>ReportAPI: POST /api/report {messageContent, reason, ...}
    ReportAPI->>ReportAPI: store decrypted content + log
    ReportAPI-->>ReportModal: success
    ReportModal-->>User: close + toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A moderator's burrow now deep and secure,
With statuses tracked and suspensions that lure,
Appeals hop on through, and dashboards glow bright,
While cryptic reports see decrypted light—
The warren is safer, with roles standing tall!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: enabling appeal submission for suspended/banned users by removing the session requirement.
Description check ✅ Passed The PR description is substantially complete, covering the what, why, and how of the fix with technical details and testing results, though it deviates from the template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/test-api-setup-XBHY0

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 10 high

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
ErrorProne 9 high
Security 1 high

View in Codacy

🟢 Metrics 91 complexity · 0 duplication

Metric Results
Complexity 91
Duplication 0

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@codacy-production codacy-production Bot left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This Pull Request introduces a significant amount of logic beyond the stated goal of fixing appeal submissions, implementing a full moderation suite. Codacy analysis indicates the PR is not up to standards due to 10 new quality issues and a substantial increase in complexity without corresponding test coverage.

Critical issues that must be addressed before merging include a broken database schema for fresh installs, lack of rate limiting on the new credential-verification endpoint, and a logic flaw in user status transitions. Furthermore, the implementation of session-less appeals currently prevents banned users from checking their appeal status, which contradicts the functional intent of the feature.

About this PR

  • The 'get_appeal_status' endpoint is currently protected by 'require_user', which blocks banned users. This prevents the target audience from verifying if their appeal has been received or processed.
  • This PR contains significant scope creep. The addition of a complete moderation system should ideally be handled in a separate PR to allow for more focused review and testing.
  • The new reporting flow decrypts message content on the client and sends it to the server. This represents a departure from the platform's zero-knowledge guarantee for these specific interactions.

Test suggestions

  • Submit appeal as a suspended user with correct email and authHash (password) results in 200 OK.
  • Submit appeal as a banned user with correct credentials results in 200 OK.
  • Submit appeal with incorrect authHash results in 401 Unauthorized.
  • Active user attempting to submit an appeal results in 400 Bad Request.
  • Submitting a second appeal while one is already pending results in 400 Bad Request.
  • Moderator approving an appeal updates user status to 'active' and revokes suspension expiry.
  • Verify that banning or suspending a user via the moderator API successfully revokes all their active sessions.
  • Unit tests for user status transition logic in backend/main.py.
  • Automated verification of the session-less credential check in the appeals endpoint.
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Submit appeal as a suspended user with correct email and authHash (password) results in 200 OK.
2. Submit appeal as a banned user with correct credentials results in 200 OK.
3. Submit appeal with incorrect authHash results in 401 Unauthorized.
4. Active user attempting to submit an appeal results in 400 Bad Request.
5. Submitting a second appeal while one is already pending results in 400 Bad Request.
6. Moderator approving an appeal updates user status to 'active' and revokes suspension expiry.
7. Verify that banning or suspending a user via the moderator API successfully revokes all their active sessions.
8. Unit tests for user status transition logic in backend/main.py.
9. Automated verification of the session-less credential check in the appeals endpoint.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread backend/main.py
# Set via CIPHER_MODERATOR_EMAILS environment variable (comma-separated emails).
# Example: export CIPHER_MODERATOR_EMAILS="mod1@example.com,mod2@example.com"
# If not set, all registered users will be regular users (role='user')
MODERATOR_EMAILS = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

This file has increased in complexity (+65) due to new moderation and audit logic but lacks any automated test coverage. This represents a high regression risk for critical security paths.

See Complexity in Codacy

Comment thread backend/main.py


@app.post("/api/appeals")
def create_appeal(body: AppealSubmitIn, request: Request):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

The /api/appeals endpoint performs password verification (authHash) without session cookies. This endpoint must be rate-limited to prevent brute-force attacks and potential database exhaustion.

Comment thread backend/main.py
Comment on lines +254 to +263
CREATE TABLE IF NOT EXISTS appeals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending',
reason TEXT NOT NULL,
response TEXT,
created_at INTEGER NOT NULL,
reviewed_at INTEGER,
reviewed_by INTEGER REFERENCES users(id) ON DELETE SET NULL
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

The 'appeals' table definition in the SCHEMA string is missing the 'appeal_type' column, which is required by the 'create_appeal' function and migrations. Add this column to the CREATE TABLE statement to ensure fresh installations function correctly.

Comment thread backend/main.py
salt_bytes = bytes.fromhex(body.authSalt)
auth_hash_stored = hasher.hash(body.authHash.lower())
now = int(time.time())
role = "moderator" if email in MODERATOR_EMAILS else "user"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

The system requires a 'super_moderator' role for role management, but no mechanism exists to assign this role. Consider updating the MODERATOR_EMAILS logic to support bootstrapping super-moderators.

Comment thread backend/main.py
elif body.action == "ban":
new_status = "banned"
suspended_until = None
else: # restore
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

The moderation logic defaults to restoring a user (setting status to 'active') for any action input that isn't explicitly 'suspend' or 'ban'. Actions like 'approve' or 'reject' sent to this endpoint will unintentionally reactivate users. Explicitly validate the 'restore' action.

Comment thread static/js/app.js
const reason = $("appeal-reason").value.trim();

if (!email || !type || reason.length < 50) {
return $("appeal-error").textContent = "All fields required (reason: min 50 characters)", void $("appeal-error").classList.remove("hidden");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: Move DOM updates out of the return expression. Assignments and side-effects inside a return statement are error-prone and reduce readability.

See Issue in Codacy

Comment thread static/js/app.js
$("report-preview-text").textContent = decrypted.text.slice(0, 200) + (decrypted.text.length > 200 ? "…" : "");
$("report-preview").style.display = "block";
}
} catch (err) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Nitpick: The captured error object 'err' is unused. Simplify by using an optional catch binding: 'catch { ... }'.

See Issue in Codacy

Comment thread static/index.html
Comment on lines +738 to +753
<div class="modal hidden" id="submit-appeal-modal">
<div class="modal-card sm">
<header class="modal-head"><h3>Appeal your restriction</h3></header>
<form id="submit-appeal-form" class="form">
<p class="muted small" style="margin-bottom:12px;">Explain why you believe your restriction should be lifted.</p>
<label class="field">
<span>Appeal reason</span>
<textarea id="appeal-reason-input" required placeholder="Provide details about your appeal..." maxlength="1000"></textarea>
</label>
</form>
<div class="modal-foot">
<button class="btn ghost" id="appeal-submit-cancel" type="button">Cancel</button>
<button class="btn primary" id="appeal-submit-ok" type="button">Submit appeal</button>
</div>
</div>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Nitpick: This modal is defined but never referenced in app.js. Remove this dead code to simplify the DOM.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
static/js/app.js (1)

233-246: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle mod/* hashes inside setRoute().

auth and unlock both call setRoute(window.location.hash.slice(1)). If the initial hash is mod/users or similar, this code treats it as a normal app route, hides every pane, and hashchange never rescues it because the hash did not change.

Suggested fix
 function setRoute(r) {
   if (r === "moderation") { enterModMode(); return; }
+  if (r.startsWith("mod/")) {
+    const sub = r.slice(4);
+    if (["dashboard", "reports", "users", "appeals", "audit"].includes(sub)) {
+      state.modRoute = sub;
+      enterModMode();
+      return;
+    }
+  }
   if (state.modMode) return;
   state.route = r;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/js/app.js` around lines 233 - 246, setRoute currently treats hashes
like "mod/users" as unknown routes and returns early when state.modMode is true,
so when auth/unlock call setRoute(window.location.hash.slice(1)) the mod/* path
is ignored; update setRoute(r) to detect r.startsWith("mod/") (or r ===
"moderation") and route into enterModMode() with the subpath (e.g., pass or set
state.route to the "mod/..." value) instead of returning early on state.modMode;
specifically modify setRoute, keep the enterModMode() call path, remove or
adjust the early return that blocks processing when state.modMode is true, and
ensure location.hash is preserved/updated for mod/* so the UI shows the correct
moderation pane.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/main.py`:
- Around line 1635-1655: The mod_user_status function accepts actions beyond the
intended set because ModUserActionIn can be approve/reject; update
mod_user_status to explicitly validate body.action against the allowed values
("suspend", "ban", "restore") before applying changes (use an early
HTTPException for invalid actions) or replace ModUserActionIn with a dedicated
request model/enumeration that only permits those three values; reference the
function name mod_user_status and the request field body.action when adding the
check and ensure the suspend/ban/restore logic only runs after validation.
- Around line 1763-1780: After fetching user_row from db() in the appeals flow,
apply the same expired-suspension normalization used by the login path: if
user_row["status"] == "suspended" and user_row.get("suspended_until") is present
and datetime.utcnow() is past that timestamp, treat the account as active
(update user_row["status"] = "active" and persist the change via an UPDATE on
the users table or clear suspended_until so future checks see the account as
active). Keep this normalization before the hasher.verify and the subsequent
"Only suspended or banned users can appeal" check so expired suspensions are
correctly handled.
- Around line 1757-1776: Add the same throttling used by the login endpoint to
the session-less credential check in create_appeal: before calling hasher.verify
in create_appeal, invoke the existing rate-limiter used by your auth/login
handler (e.g. the login endpoint's limiter function or middleware) with a key
that includes the lowercased email and request.client IP (or
request.remote_addr) so repeated guesses are limited, and if the limiter rejects
the attempt return HTTP 429 rather than proceeding; ensure you import/ reuse the
same limiter symbol and error handling used by the POST /api/auth/login path so
behavior and configuration remain consistent.
- Around line 1784-1797: The SELECT-then-INSERT is vulnerable to TOCTOU races;
enforce the "one pending appeal" rule at the database level and handle
conflicts: add a unique partial index on appeals(user_id) WHERE status='pending'
(so the DB rejects concurrent INSERTs), then remove the pre-check or keep it for
fast-fail and wrap the INSERT executed via conn.execute(...) in an exception
handler catching the DB integrity constraint error
(IntegrityError/OperationalError) and translate it to raise HTTPException(400,
"You already have a pending appeal"). Ensure the unique index creation runs as
part of migrations or startup and keep references to db(), conn, the INSERT
statement and the HTTPException mapping when implementing the change.

In `@static/index.html`:
- Line 163: Two different buttons share the id "appeal-cancel", causing
document.getElementById("appeal-cancel") to resolve to the wrong element; give
the modal's cancel button a unique id (e.g., "appeal-modal-cancel") and update
all corresponding selectors in the JS (search for "appeal-cancel" usage in
static/js/app.js and change to the new id where the modal's handler is
attached), ensuring the page-level back button keeps its original id or is
likewise renamed if needed so each getElementById call targets the intended
element.

In `@static/js/app.js`:
- Around line 357-364: After a restricted-login path stores credentials
(state.userEmail, state.authHash, state.loginRestriction) and calls
showSuspensionScreen, clear sensitive secrets immediately: set state.authHash =
null (and clear any password input value/placeholder and hide the show-password
control) before showing the suspension/appeal UI. Also ensure any "back"/cancel
handlers that return from the suspension view explicitly clear state.authHash
and reset the password input and show-password toggle so credentials cannot be
revealed later; update the suspension-entry code and the suspension-exit/back
handlers to perform these clears.
- Around line 1091-1092: The issue is that persistent elements (e.g.
reportsSearchInput) get new event listeners each time
loadModeration()/renderReports() runs, causing duplicated handlers; to fix, stop
rebinding on refresh by adding a one-time guard and only attach listeners once
(for example introduce a boolean flag like moderationListenersBound and in
loadModeration() or the module init check if false before calling
reportsSearchInput.addEventListener("input", renderReports) and the other
handlers referenced around lines 1183-1189, then set the flag true), or
alternatively move those addEventListener calls out of the refresh path into the
module initialization so renderReports()/loadModeration() only update UI state
and do not re-register listeners.

---

Outside diff comments:
In `@static/js/app.js`:
- Around line 233-246: setRoute currently treats hashes like "mod/users" as
unknown routes and returns early when state.modMode is true, so when auth/unlock
call setRoute(window.location.hash.slice(1)) the mod/* path is ignored; update
setRoute(r) to detect r.startsWith("mod/") (or r === "moderation") and route
into enterModMode() with the subpath (e.g., pass or set state.route to the
"mod/..." value) instead of returning early on state.modMode; specifically
modify setRoute, keep the enterModMode() call path, remove or adjust the early
return that blocks processing when state.modMode is true, and ensure
location.hash is preserved/updated for mod/* so the UI shows the correct
moderation pane.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44829296-8439-41da-a425-5f14073c2012

📥 Commits

Reviewing files that changed from the base of the PR and between fe21c4f and 4a6ebd2.

📒 Files selected for processing (4)
  • backend/main.py
  • static/index.html
  • static/js/app.js
  • static/js/sw-register.js

Comment thread backend/main.py
Comment on lines +1635 to +1655
def mod_user_status(user_id: int, body: ModUserActionIn, user = Depends(require_moderator)):
if user_id == user["id"]:
raise HTTPException(400, "Cannot modify your own account")

now = int(time.time())
with db() as conn:
target = conn.execute("SELECT id, role FROM users WHERE id = ?", (user_id,)).fetchone()
if not target:
raise HTTPException(404, "User not found")

# Determine new status and suspended_until
if body.action == "suspend":
new_status = "suspended"
suspended_until = now + (body.duration_days * 86400) if body.duration_days else now + (7 * 86400)
elif body.action == "ban":
new_status = "banned"
suspended_until = None
else: # restore
new_status = "active"
suspended_until = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unsupported actions in mod_user_status.

ModUserActionIn also allows approve and reject, and those values currently fall through the else branch on Line 1652 and restore the target account. Validate this endpoint against only suspend|ban|restore, or give it a dedicated request model.

Suggested fix
 `@app.post`("/api/mod/users/{user_id}/status")
 def mod_user_status(user_id: int, body: ModUserActionIn, user = Depends(require_moderator)):
+    if body.action not in ("suspend", "ban", "restore"):
+        raise HTTPException(400, "Action must be suspend, ban, or restore")
     if user_id == user["id"]:
         raise HTTPException(400, "Cannot modify your own account")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def mod_user_status(user_id: int, body: ModUserActionIn, user = Depends(require_moderator)):
if user_id == user["id"]:
raise HTTPException(400, "Cannot modify your own account")
now = int(time.time())
with db() as conn:
target = conn.execute("SELECT id, role FROM users WHERE id = ?", (user_id,)).fetchone()
if not target:
raise HTTPException(404, "User not found")
# Determine new status and suspended_until
if body.action == "suspend":
new_status = "suspended"
suspended_until = now + (body.duration_days * 86400) if body.duration_days else now + (7 * 86400)
elif body.action == "ban":
new_status = "banned"
suspended_until = None
else: # restore
new_status = "active"
suspended_until = None
def mod_user_status(user_id: int, body: ModUserActionIn, user = Depends(require_moderator)):
if body.action not in ("suspend", "ban", "restore"):
raise HTTPException(400, "Action must be suspend, ban, or restore")
if user_id == user["id"]:
raise HTTPException(400, "Cannot modify your own account")
now = int(time.time())
with db() as conn:
target = conn.execute("SELECT id, role FROM users WHERE id = ?", (user_id,)).fetchone()
if not target:
raise HTTPException(404, "User not found")
# Determine new status and suspended_until
if body.action == "suspend":
new_status = "suspended"
suspended_until = now + (body.duration_days * 86400) if body.duration_days else now + (7 * 86400)
elif body.action == "ban":
new_status = "banned"
suspended_until = None
else: # restore
new_status = "active"
suspended_until = None
🧰 Tools
🪛 Ruff (0.15.13)

[warning] 1635-1635: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 1635 - 1655, The mod_user_status function
accepts actions beyond the intended set because ModUserActionIn can be
approve/reject; update mod_user_status to explicitly validate body.action
against the allowed values ("suspend", "ban", "restore") before applying changes
(use an early HTTPException for invalid actions) or replace ModUserActionIn with
a dedicated request model/enumeration that only permits those three values;
reference the function name mod_user_status and the request field body.action
when adding the check and ensure the suspend/ban/restore logic only runs after
validation.

Comment thread backend/main.py
Comment on lines +1757 to +1776
def create_appeal(body: AppealSubmitIn, request: Request):
"""Allow suspended/banned users to appeal their status."""
email = body.email.lower().strip()
now = int(time.time())

# Look up user and verify credentials
with db() as conn:
user_row = conn.execute(
"SELECT id, auth_hash, status FROM users WHERE email = ?",
(email,)
).fetchone()

if not user_row:
raise HTTPException(401, "Invalid credentials")

# Verify the authHash
try:
hasher.verify(user_row["auth_hash"], body.authHash.lower())
except (VerifyMismatchError, InvalidHash) as err:
raise HTTPException(401, "Invalid credentials") from err
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rate-limit the session-less credential check.

POST /api/appeals now does password verification for unauthenticated callers, but it no longer has the throttling that POST /api/auth/login applies. That reintroduces an online guessing path for suspended/banned accounts.

Suggested fix
 `@app.post`("/api/appeals")
 def create_appeal(body: AppealSubmitIn, request: Request):
     """Allow suspended/banned users to appeal their status."""
     email = body.email.lower().strip()
+    rate_limit(f"appeal:{client_ip(request)}", 10, 60)
     now = int(time.time())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 1757 - 1776, Add the same throttling used by
the login endpoint to the session-less credential check in create_appeal: before
calling hasher.verify in create_appeal, invoke the existing rate-limiter used by
your auth/login handler (e.g. the login endpoint's limiter function or
middleware) with a key that includes the lowercased email and request.client IP
(or request.remote_addr) so repeated guesses are limited, and if the limiter
rejects the attempt return HTTP 429 rather than proceeding; ensure you import/
reuse the same limiter symbol and error handling used by the POST
/api/auth/login path so behavior and configuration remain consistent.

Comment thread backend/main.py
Comment on lines +1763 to +1780
with db() as conn:
user_row = conn.execute(
"SELECT id, auth_hash, status FROM users WHERE email = ?",
(email,)
).fetchone()

if not user_row:
raise HTTPException(401, "Invalid credentials")

# Verify the authHash
try:
hasher.verify(user_row["auth_hash"], body.authHash.lower())
except (VerifyMismatchError, InvalidHash) as err:
raise HTTPException(401, "Invalid credentials") from err

# User must be suspended or banned to appeal
if user_row["status"] == "active":
raise HTTPException(400, "Only suspended or banned users can appeal")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the same expired-suspension normalization as login.

/api/auth/login auto-restores expired suspensions before deciding whether the account is restricted, but /api/appeals only checks the stored status. A user whose suspended_until has already passed can still submit an appeal until they log in again, even though they should now be treated as active.

Suggested fix
     with db() as conn:
         user_row = conn.execute(
-            "SELECT id, auth_hash, status FROM users WHERE email = ?",
+            "SELECT id, auth_hash, status, suspended_until FROM users WHERE email = ?",
             (email,)
         ).fetchone()

     if not user_row:
         raise HTTPException(401, "Invalid credentials")
+
+    if (
+        user_row["status"] == "suspended"
+        and user_row["suspended_until"]
+        and user_row["suspended_until"] <= now
+    ):
+        with db() as conn:
+            conn.execute(
+                "UPDATE users SET status = 'active', suspended_until = NULL WHERE id = ?",
+                (user_row["id"],),
+            )
+        user_row = dict(user_row)
+        user_row["status"] = "active"

     # Verify the authHash
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 1763 - 1780, After fetching user_row from db()
in the appeals flow, apply the same expired-suspension normalization used by the
login path: if user_row["status"] == "suspended" and
user_row.get("suspended_until") is present and datetime.utcnow() is past that
timestamp, treat the account as active (update user_row["status"] = "active" and
persist the change via an UPDATE on the users table or clear suspended_until so
future checks see the account as active). Keep this normalization before the
hasher.verify and the subsequent "Only suspended or banned users can appeal"
check so expired suspensions are correctly handled.

Comment thread backend/main.py
Comment on lines +1784 to +1797
# Check if user already has a pending appeal
with db() as conn:
existing = conn.execute(
"SELECT id FROM appeals WHERE user_id = ? AND status = 'pending'",
(user_id,)
).fetchone()
if existing:
raise HTTPException(400, "You already have a pending appeal")

# Create appeal
conn.execute(
"INSERT INTO appeals (user_id, status, appeal_type, reason, created_at) VALUES (?, ?, ?, ?, ?)",
(user_id, "pending", body.appeal_type, body.reason, now)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce the “one pending appeal” rule in the database.

The SELECT-then-INSERT sequence is a TOCTOU race. Two concurrent submissions can both miss existing and create duplicate pending appeals, which breaks the contract this endpoint is meant to enforce.

Suggested fix
 CREATE TABLE IF NOT EXISTS appeals (
   id          INTEGER PRIMARY KEY AUTOINCREMENT,
   user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
   status      TEXT NOT NULL DEFAULT 'pending',
   reason      TEXT NOT NULL,
   response    TEXT,
   created_at  INTEGER NOT NULL,
   reviewed_at INTEGER,
   reviewed_by INTEGER REFERENCES users(id) ON DELETE SET NULL
 );
 CREATE INDEX IF NOT EXISTS idx_appeals_user ON appeals(user_id);
 CREATE INDEX IF NOT EXISTS idx_appeals_status ON appeals(status, created_at DESC);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_appeals_one_pending_per_user
+ON appeals(user_id) WHERE status = 'pending';
-        existing = conn.execute(
-            "SELECT id FROM appeals WHERE user_id = ? AND status = 'pending'",
-            (user_id,)
-        ).fetchone()
-        if existing:
-            raise HTTPException(400, "You already have a pending appeal")
-
-        # Create appeal
-        conn.execute(
-            "INSERT INTO appeals (user_id, status, appeal_type, reason, created_at) VALUES (?, ?, ?, ?, ?)",
-            (user_id, "pending", body.appeal_type, body.reason, now)
-        )
+        try:
+            conn.execute(
+                "INSERT INTO appeals (user_id, status, appeal_type, reason, created_at) VALUES (?, ?, ?, ?, ?)",
+                (user_id, "pending", body.appeal_type, body.reason, now)
+            )
+        except sqlite3.IntegrityError as err:
+            raise HTTPException(400, "You already have a pending appeal") from err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 1784 - 1797, The SELECT-then-INSERT is
vulnerable to TOCTOU races; enforce the "one pending appeal" rule at the
database level and handle conflicts: add a unique partial index on
appeals(user_id) WHERE status='pending' (so the DB rejects concurrent INSERTs),
then remove the pre-check or keep it for fast-fail and wrap the INSERT executed
via conn.execute(...) in an exception handler catching the DB integrity
constraint error (IntegrityError/OperationalError) and translate it to raise
HTTPException(400, "You already have a pending appeal"). Ensure the unique index
creation runs as part of migrations or startup and keep references to db(),
conn, the INSERT statement and the HTTPException mapping when implementing the
change.

Comment thread static/index.html

<p id="appeal-error" class="form-error hidden"></p>
<div class="modal-foot" style="margin-top:20px;">
<button class="btn ghost" type="button" id="appeal-cancel">Back</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the appeal-cancel IDs unique.

These two buttons share the same id, so getElementById("appeal-cancel") resolves to the page-level back button first. The moderator appeal-review modal then binds its cancel handler to the wrong element, leaving the modal's own Cancel button inert.

Suggested fix
-          <button class="btn ghost" type="button" id="appeal-cancel">Back</button>
+          <button class="btn ghost" type="button" id="appeal-page-cancel">Back</button>
...
-      <button class="btn ghost" id="appeal-cancel" type="button">Cancel</button>
+      <button class="btn ghost" id="appeal-review-cancel" type="button">Cancel</button>

Then update the corresponding selectors in static/js/app.js.

Also applies to: 684-684

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/index.html` at line 163, Two different buttons share the id
"appeal-cancel", causing document.getElementById("appeal-cancel") to resolve to
the wrong element; give the modal's cancel button a unique id (e.g.,
"appeal-modal-cancel") and update all corresponding selectors in the JS (search
for "appeal-cancel" usage in static/js/app.js and change to the new id where the
modal's handler is attached), ensuring the page-level back button keeps its
original id or is likewise renamed if needed so each getElementById call targets
the intended element.

Comment thread static/js/app.js
Comment on lines +357 to +364
const loginRes = await api.post("/api/auth/login", { email, authHash });

if (loginRes.restricted) {
state.userEmail = email;
state.authHash = authHash;
state.loginRestriction = { type: loginRes.restrictionType, timeRemaining: loginRes.timeRemaining };
showSuspensionScreen(loginRes.restrictionType, loginRes.timeRemaining);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear restricted-login secrets when leaving the auth flow.

After a restricted login, the entered password remains in the auth form and authHash stays in memory. The suspension/appeal “back” actions only swap views, so on a shared device the previous credentials are still sitting in the page and can be revealed via the existing show-password control.

Suggested fix
+function clearRestrictedAuthState() {
+  state.authHash = null;
+  state.userEmail = null;
+  state.loginRestriction = null;
+  $("auth-form").reset();
+}
+
       if (loginRes.restricted) {
         state.userEmail = email;
         state.authHash = authHash;
         state.loginRestriction = { type: loginRes.restrictionType, timeRemaining: loginRes.timeRemaining };
+        $("auth-password").value = "";
+        $("auth-confirm").value = "";
         showSuspensionScreen(loginRes.restrictionType, loginRes.timeRemaining);
         return;
       }
...
-$("susp-logout-btn").addEventListener("click", () => { setView("auth"); });
+$("susp-logout-btn").addEventListener("click", () => {
+  clearRestrictedAuthState();
+  setView("auth");
+});
...
-$("appeal-back-btn").addEventListener("click", () => { setView("auth"); });
+$("appeal-back-btn").addEventListener("click", () => {
+  clearRestrictedAuthState();
+  setView("auth");
+});

Also applies to: 431-467

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/js/app.js` around lines 357 - 364, After a restricted-login path
stores credentials (state.userEmail, state.authHash, state.loginRestriction) and
calls showSuspensionScreen, clear sensitive secrets immediately: set
state.authHash = null (and clear any password input value/placeholder and hide
the show-password control) before showing the suspension/appeal UI. Also ensure
any "back"/cancel handlers that return from the suspension view explicitly clear
state.authHash and reset the password input and show-password toggle so
credentials cannot be revealed later; update the suspension-entry code and the
suspension-exit/back handlers to perform these clears.

Comment thread static/js/app.js
Comment on lines +1091 to +1092
renderReports();
reportsSearchInput.addEventListener("input", renderReports);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't rebind moderation listeners on every refresh.

loadModeration() runs on mode entry and again after suspend/ban/restore/review actions. These addEventListener calls target persistent elements, so each refresh stacks another handler and later clicks can open multiple dialogs or submit duplicate status changes.

Suggested fix
-    reportsSearchInput.addEventListener("input", renderReports);
+    reportsSearchInput.oninput = renderReports;
...
-  searchInput.addEventListener("input", renderUsersList);
-  filterSelect.addEventListener("change", renderUsersList);
+  searchInput.oninput = renderUsersList;
+  filterSelect.onchange = renderUsersList;
...
-  usersList.addEventListener("click", async (e) => {
+  usersList.onclick = async (e) => {
     const btn = e.target.closest("[data-mod-action]");
     if (!btn) return;
     ...
-  });
+  };

Also applies to: 1183-1189

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/js/app.js` around lines 1091 - 1092, The issue is that persistent
elements (e.g. reportsSearchInput) get new event listeners each time
loadModeration()/renderReports() runs, causing duplicated handlers; to fix, stop
rebinding on refresh by adding a one-time guard and only attach listeners once
(for example introduce a boolean flag like moderationListenersBound and in
loadModeration() or the module init check if false before calling
reportsSearchInput.addEventListener("input", renderReports) and the other
handlers referenced around lines 1183-1189, then set the flag true), or
alternatively move those addEventListener calls out of the refresh path into the
module initialization so renderReports()/loadModeration() only update UI state
and do not re-register listeners.

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

Labels

area: backend area: frontend app/, components/, styles

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants