Skip to content

fix: prevent duplicate submissions and harden join idempotency#35

Open
Flashl3opard wants to merge 2 commits intoalphaonelabs:mainfrom
Flashl3opard:fix/duplication-submissions
Open

fix: prevent duplicate submissions and harden join idempotency#35
Flashl3opard wants to merge 2 commits intoalphaonelabs:mainfrom
Flashl3opard:fix/duplication-submissions

Conversation

@Flashl3opard
Copy link
Copy Markdown

@Flashl3opard Flashl3opard commented Apr 8, 2026

Summary

Prevent duplicate user actions and make join operations idempotent.

Changes

  • Added frontend in-flight guards to block repeated submits/clicks.
  • Added stale-response protection for hosted activity refresh flow.
  • Hardened [/api/join] with idempotent handling on backend.

Problem

Rapid clicks / repeated submits could trigger duplicate requests and inconsistent UI states.

Testing

  • Verified login/register cannot be submitted repeatedly while request is in progress.
  • Verified activity/session creation actions are locked during in-flight requests.
  • Verified repeated join attempts return a safe, already-joined outcome without duplicate enrollment.

Purpose

Prevent duplicate user submissions and enforce idempotent join behavior by adding frontend in-flight request guards and backend idempotent handling for join operations.

Key Modifications

Frontend

  • Introduced a global inFlight Set and withActionLock(key, action) helper used across pages to block concurrent actions for the same key.
  • course.html: Wrapped joinActivity in withActionLock('join-activity:' + actId); preserves button state ("Joining...", disabled), alerts on error, reloads on success.
  • login.html: Wrapped login and register submit handlers with withActionLock('login'/'register') and moved main script to end of body; prevents repeated form submissions.
  • teach.html: Wrapped activity/session creation submits with withActionLock; added hostedLoadVersion to ignore stale /api/dashboard responses; improved error messaging and UI reset on dashboard load failure.
  • Minor HTML/CSS formatting and accessibility tweaks (label for/id associations, whitespace/indentation changes).

Backend

  • src/worker.py: /api/join now checks for an existing enrollment (activity_id, user_id) before insert; uses INSERT OR IGNORE and re-queries the row to return a safe "Already joined activity" outcome when appropriate, making join idempotent and avoiding duplicate enrollment records.

Functional Impact

  • UX: Rapid clicks or repeated submits no longer trigger duplicate requests; buttons/forms provide clear in-flight feedback and are locked while requests are pending.
  • Data integrity: Duplicate enrollments are prevented; repeated join attempts return a safe, already-joined result rather than producing duplicate rows or errors.
  • Consistency: Stale dashboard responses are ignored, reducing the chance of the UI being overwritten by out-of-order fetches.

Copilot AI review requested due to automatic review settings April 8, 2026 17:10
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Walkthrough

Adds client-side action locks to prevent concurrent submissions across three frontend pages (course, login, teach), introduces a "My Hosted Activities" section with versioned dashboard loading on teach.html, and hardens the backend join handler to detect and short-circuit duplicate enrollments before/after insertion. Also includes HTML/CSS reformatting and minor markup adjustments.

Changes

Cohort / File(s) Summary
Frontend Concurrency Protection
public/course.html, public/login.html, public/teach.html
Adds an inFlight Set and withActionLock(key, action) helper; wraps join, login/register, create-activity, and add-session submit flows to prevent overlapping requests for the same action.
Hosted Activities / Dashboard
public/teach.html
Adds "My Hosted Activities" UI (#hosted-count, #hosted-list) and implements hostedLoadVersion to ensure only the latest /api/dashboard response updates the activity list and selector.
HTML/CSS Formatting & Markup
public/course.html, public/login.html, public/teach.html
Whitespace/indentation and inline Tailwind config formatting changes, multi-line CSS rule reformatting, minor label for/id associations; no changes to identifiers, endpoints, or visible strings.
Enrollment Deduplication (backend)
src/worker.py
api_join now pre-checks for an existing (activity_id, user_id) row and returns "Already joined activity" if found; after insert attempt it re-queries and returns the same message when a different existing row is observed, avoiding duplicate-success responses.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant Browser
participant Server
participant DB

User->>Browser: Click "Join" (actId)
Browser->>Browser: withActionLock('join-activity:'+actId) acquires lock
Browser->>Browser: update button -> "Joining...", disable
Browser->>Server: POST /api/join { activity_id, user_id }
Server->>DB: SELECT FROM enrollments WHERE activity_id=? AND user_id=?
alt enrollment exists
DB-->>Server: existing row
Server-->>Browser: 200 Ok { message: "Already joined activity" }
else no existing
Server->>DB: INSERT OR IGNORE INTO enrollments (...)
Server->>DB: SELECT FROM enrollments WHERE activity_id=? AND user_id=?
alt row present (maybe inserted by this or concurrent txn)
DB-->>Server: inserted/existing row
Server-->>Browser: 200 Ok { enrollment: {...} } or Already joined
end
end
Browser->>Browser: release lock, re-enable button
Browser->>User: show success or error / reload on success

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the changeset: preventing duplicate submissions through client-side action locks and hardening join idempotency via backend checks.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

Copilot AI 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 PR reduces duplicate user actions in the UI and makes /api/join safe to call repeatedly, preventing duplicate enrollments and inconsistent client states.

Changes:

  • Added client-side in-flight action locks for submit/click handlers (login/register, host actions, join).
  • Added stale-response protection for the hosted-activities refresh flow in Host Hub.
  • Updated /api/join to return an idempotent “Already joined activity” outcome when an enrollment already exists.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/worker.py Adds idempotent join behavior by checking for an existing enrollment and handling unique-constraint conflicts.
public/teach.html Adds in-flight guarding for create-activity/session actions and prevents stale hosted-activities refresh from overwriting newer state.
public/login.html Adds in-flight guarding to prevent duplicate login/register submissions.
public/course.html Adds in-flight guarding for “Join Activity” to prevent duplicate join requests from rapid clicks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@public/course.html`:
- Around line 89-91: The Join CTA button should have an explicit type to prevent
accidental form submission; update the element with id "btn-join" (the button
that calls joinActivity()) to include type="button" so it will not act as a
submit button if moved into a form.

In `@public/login.html`:
- Around line 35-38: Add explicit button types and proper label-input
associations: change the tab buttons (ids "tab-login" and "tab-register") to
include type="button" to prevent form submission, and update each form field so
every <label> has a matching for attribute that points to the corresponding
input's id (ensure inputs like email, password, register-name, etc. have unique
id values and labels reference them). Apply the same type="button" and label-for
fixes to the other tab/button elements and all login/register field label-input
pairs mentioned (lines 45-48, 51-54, 71-74, 77-80, 83-86, 89-92).
- Line 90: The client-side password input (id="r-password") currently enforces
minlength="6" while the backend requires passwords of at least 8 characters;
update the HTML input to require minlength="8" (and adjust any related
client-side validation messages) so the registration form matches the backend
password length check (backend password length >= 8).

In `@public/teach.html`:
- Around line 72-75: The labels are not programmatically associated with their
inputs; update each label to include a for attribute that matches the
corresponding input's id (e.g., label for="a-title" to pair with input
id="a-title") and ensure every input element (e.g., a-title, a-description,
a-language, etc.) has a unique id that the matching label references; apply this
same for/id pairing pattern to all other label/input pairs mentioned (lines
around 78–81, 85–88, 96–99, 105–108, 116–119, 139–143, 146–149, 153–156,
158–161, 164–167, 170–173) so screen readers and click-to-focus work correctly.
- Around line 249-256: The loadHostedActivities function assumes
fetch('/api/dashboard') always succeeds; wrap the fetch/response parsing in a
try/catch, check res.ok before calling res.json(), and handle errors by setting
hostedActivities = [] (or preserving prior state), updating
document.getElementById('hosted-count') with a clear error or zero state, and
optionally surface a user-facing error message (e.g., show an auth
expired/network error banner). Apply the same pattern to the other similar fetch
block at the later section (lines ~263-267) so both network failures and non-OK
responses are handled explicitly using hostedLoadVersion, hostedActivities, and
the 'hosted-count' DOM update.
- Around line 278-279: The code concatenates (fmtLabel[a.format] || a.format)
directly into an HTML string, creating an XSS sink; update the generation to
sanitize that fallback by escaping special characters before inserting into
innerHTML (or, alternatively, build the elements and set span.textContent
instead of using innerHTML). Specifically, ensure the value used for the first
span (where fmtLabel[a.format] || a.format is referenced) is passed through a
safe escapeHtml utility (or created via document.createElement + textContent) so
fmtLabel and the a.format fallback cannot inject HTML.
🪄 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: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 694a6791-4ddb-49aa-af73-cec1fc68096f

📥 Commits

Reviewing files that changed from the base of the PR and between d6e1a9c and ef48c16.

📒 Files selected for processing (4)
  • public/course.html
  • public/login.html
  • public/teach.html
  • src/worker.py

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: 2

♻️ Duplicate comments (2)
public/login.html (1)

90-92: ⚠️ Potential issue | 🟡 Minor

Client-side password validation still misaligned with backend.

The input specifies minlength="6" and the placeholder says "min 6 characters," but src/worker.py (line 656) rejects passwords shorter than 8 characters. This mismatch causes users to submit passwords that pass client validation but fail server-side, leading to a poor user experience.

✨ Suggested fix
-            <input id="r-password" type="password" autocomplete="new-password" required minlength="6"
+            <input id="r-password" type="password" autocomplete="new-password" required minlength="8"
               class="w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-brand/40 text-sm"
-              placeholder="min 6 characters" />
+              placeholder="min 8 characters" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/login.html` around lines 90 - 92, The client-side password field
(input id "r-password" in public/login.html) currently uses minlength="6" and
"min 6 characters" placeholder but the server-side check in src/worker.py
enforces an 8-character minimum; update the frontend to match the backend by
changing the input's minlength to "8" and its placeholder text to "min 8
characters", and also update any client-side JS validation that references a
6-char minimum so all checks align with the server-side rule enforced in
src/worker.py.
public/teach.html (1)

289-290: ⚠️ Potential issue | 🟠 Major

Escape the format fallback to prevent potential XSS.

Line 290 inserts (fmtLabel[a.format] || a.format) directly into the HTML string. If unexpected or malicious data reaches a.format from the backend, this becomes an XSS vector. The fmtLabel lookup is safe, but the fallback to a.format should be escaped.

🛡️ Suggested fix
             '<span class="badge ' + tc + '">' + esc(a.type) + '</span>' +
-            '<span>' + (fmtLabel[a.format] || a.format) + '</span>' +
+            '<span>' + esc(fmtLabel[a.format] || a.format) + '</span>' +
             '<span>👥 ' + a.participant_count + '</span>' +

As per coding guidelines, **/*.html: "Review HTML templates for ... XSS risks from unescaped user content..."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/teach.html` around lines 289 - 290, The fallback output
(fmtLabel[a.format] || a.format) is not escaped and can lead to XSS if a.format
is attacker-controlled; update the template to pass the fallback through the
existing esc(...) sanitizer so it becomes (fmtLabel[a.format] || esc(a.format))
or equivalently esc(fmtLabel[a.format] || a.format), ensuring you use the same
esc function used for a.type; modify the expression near where fmtLabel and
a.format are concatenated into the HTML string so the a.format fallback is
always escaped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@public/course.html`:
- Around line 138-141: The dynamically generated logout button in nav.innerHTML
lacks an explicit type attribute; update the string assigned to nav.innerHTML
(the expression building the greeting, Dashboard link, and Logout button) to
include type="button" on the Logout button so it matches static buttons and
avoids accidental form submissions (reference: nav.innerHTML and logout()).

In `@public/teach.html`:
- Line 45: The Logout button element that calls logout() currently lacks an
explicit type which can cause accidental form submissions later; update the
<button onclick="logout()" class="text-slate-400 text-sm hover:text-red-500">
element to include type="button" so it explicitly behaves as a non-submit button
(i.e., add type="button" to the button that invokes the logout() handler).

---

Duplicate comments:
In `@public/login.html`:
- Around line 90-92: The client-side password field (input id "r-password" in
public/login.html) currently uses minlength="6" and "min 6 characters"
placeholder but the server-side check in src/worker.py enforces an 8-character
minimum; update the frontend to match the backend by changing the input's
minlength to "8" and its placeholder text to "min 8 characters", and also update
any client-side JS validation that references a 6-char minimum so all checks
align with the server-side rule enforced in src/worker.py.

In `@public/teach.html`:
- Around line 289-290: The fallback output (fmtLabel[a.format] || a.format) is
not escaped and can lead to XSS if a.format is attacker-controlled; update the
template to pass the fallback through the existing esc(...) sanitizer so it
becomes (fmtLabel[a.format] || esc(a.format)) or equivalently
esc(fmtLabel[a.format] || a.format), ensuring you use the same esc function used
for a.type; modify the expression near where fmtLabel and a.format are
concatenated into the HTML string so the a.format fallback is always escaped.
🪄 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: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 87bfa8a9-cad0-470b-be9d-8a56117d4a86

📥 Commits

Reviewing files that changed from the base of the PR and between ef48c16 and f3f11e9.

📒 Files selected for processing (4)
  • public/course.html
  • public/login.html
  • public/teach.html
  • src/worker.py

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.

2 participants