From ef48c16bd53787196f031eb74da5adb40f43a269 Mon Sep 17 00:00:00 2001 From: FlashL3opard <69573060+Flashl3opard@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:36:52 +0530 Subject: [PATCH 1/2] fix: prevent duplicate submissions and harden join idempotency --- public/course.html | 428 +++++++++++++++++---------------- public/login.html | 288 +++++++++++++---------- public/teach.html | 574 ++++++++++++++++++++++++--------------------- src/worker.py | 10 +- 4 files changed, 714 insertions(+), 586 deletions(-) diff --git a/public/course.html b/public/course.html index fd3cbb6..47ab49e 100644 --- a/public/course.html +++ b/public/course.html @@ -1,233 +1,267 @@ + Activity - Alpha One Labs - + + - + -
-
-
- ActivitiesLoading... +
+
+
+ ActivitiesLoading... +
+

Loading...

+

+
+
-

Loading...

-

-
-
-
- -
-
- - - - - -
- - - - - + +
+
+

Sessions

+ +
+
    +
  • Loading...
  • +
+
+ + + +
+ + + + + + - -
-

Welcome!

-

Select an activity and join to see full details including session locations and descriptions.

+ +
+

Welcome!

+

Select an activity and join to see full + details including session locations and descriptions.

+
+
+
-
- - - - - + + if (!actId) { + document.getElementById('act-title').textContent = 'No activity selected'; + } else { + loadActivity().catch(e => { document.getElementById('act-title').textContent = 'Error: ' + e.message; }); + } + + \ No newline at end of file diff --git a/public/login.html b/public/login.html index b7d8c23..5031096 100644 --- a/public/login.html +++ b/public/login.html @@ -1,5 +1,6 @@ + @@ -8,140 +9,177 @@ - -
-
- - Alpha One Labs - Alpha One Labs - -

All user data encrypted at rest - zero plaintext PII stored

-
+ -
-
- - +
+
+ + Alpha One Labs + Alpha One Labs + +

All user data encrypted at rest - zero plaintext PII stored

-
-

Welcome back!

-
-
- - -
-
- - -
- - -
-

- Demo: alice / password123 - or charlie / password123 -

-
+
+
+ + +
- + +

+ Back to activities +

-

- Back to activities -

-
- - + + \ No newline at end of file diff --git a/public/teach.html b/public/teach.html index 8c00bff..646edfb 100644 --- a/public/teach.html +++ b/public/teach.html @@ -1,319 +1,367 @@ + Host Hub - Alpha One Labs - + + - + - - diff --git a/public/login.html b/public/login.html index 5031096..cac2b24 100644 --- a/public/login.html +++ b/public/login.html @@ -32,9 +32,9 @@
- -
@@ -42,13 +42,13 @@

Welcome back!

- +
- + @@ -68,25 +68,25 @@

Welcome back!

Create your account

- +
- +
- +
- + diff --git a/public/teach.html b/public/teach.html index 646edfb..650a750 100644 --- a/public/teach.html +++ b/public/teach.html @@ -69,20 +69,20 @@

🌟 Create New Activity

- +
- +
- +
- +
- + @@ -136,38 +136,40 @@

📅 Add Session

- +
- +
- +
- +
- +
- + @@ -248,44 +250,61 @@

My Hosted Activities

async function loadHostedActivities() { const loadVersion = ++hostedLoadVersion; - const res = await fetch('/api/dashboard', { headers: { Authorization: 'Bearer ' + token } }); - const data = await res.json(); - if (loadVersion !== hostedLoadVersion) return; - hostedActivities = data.hosted_activities || []; - document.getElementById('hosted-count').textContent = hostedActivities.length + ' activities'; + try { + const res = await fetch('/api/dashboard', { headers: { Authorization: 'Bearer ' + token } }); + let data = null; + try { + data = await res.json(); + } catch { + data = null; + } + if (!res.ok) { + const msg = (data && data.error) ? data.error : 'Failed to load hosted activities'; + throw new Error(msg); + } + if (loadVersion !== hostedLoadVersion) return; + hostedActivities = data.hosted_activities || []; + document.getElementById('hosted-count').textContent = hostedActivities.length + ' activities'; - const sel = document.getElementById('s-activity'); - sel.innerHTML = '' + - hostedActivities.map(a => - '' - ).join(''); + const sel = document.getElementById('s-activity'); + sel.innerHTML = '' + + hostedActivities.map(a => + '' + ).join(''); - const list = document.getElementById('hosted-list'); - if (!hostedActivities.length) { - list.innerHTML = '

No activities yet. Create one above!

'; - return; + const list = document.getElementById('hosted-list'); + if (!hostedActivities.length) { + list.innerHTML = '

No activities yet. Create one above!

'; + return; + } + list.innerHTML = hostedActivities.map(a => { + const tc = typeColor[a.type] || 'bg-slate-100 text-slate-600'; + const ic = typeIcon[a.type] || 'โœจ'; + const tags = (a.tags || []).slice(0, 4).map(t => + '' + esc(t) + '').join(''); + return '
' + + '
' + + '

' + ic + ' ' + esc(a.title) + '

' + + '
' + + '' + esc(a.type) + '' + + '' + (fmtLabel[a.format] || a.format) + '' + + '๐Ÿ‘ฅ ' + a.participant_count + '' + + '๐Ÿ—“ ' + a.session_count + ' sessions' + + tags + + '
' + + '
' + + '
' + + 'View' + + '
' + + '
'; + }).join(''); + } catch (err) { + if (loadVersion !== hostedLoadVersion) return; + hostedActivities = []; + document.getElementById('hosted-count').textContent = 'Unable to load'; + document.getElementById('s-activity').innerHTML = ''; + document.getElementById('hosted-list').innerHTML = '

' + esc(err.message || 'Failed to load hosted activities') + '

'; } - list.innerHTML = hostedActivities.map(a => { - const tc = typeColor[a.type] || 'bg-slate-100 text-slate-600'; - const ic = typeIcon[a.type] || 'โœจ'; - const tags = (a.tags || []).slice(0, 4).map(t => - '' + esc(t) + '').join(''); - return '
' + - '
' + - '

' + ic + ' ' + esc(a.title) + '

' + - '
' + - '' + esc(a.type) + '' + - '' + (fmtLabel[a.format] || a.format) + '' + - '๐Ÿ‘ฅ ' + a.participant_count + '' + - '๐Ÿ—“ ' + a.session_count + ' sessions' + - tags + - '
' + - '
' + - '
' + - 'View' + - '
' + - '
'; - }).join(''); } function showMsg(id, msg, isErr) { diff --git a/src/worker.py b/src/worker.py index c99b9f2..83adc8f 100644 --- a/src/worker.py +++ b/src/worker.py @@ -985,15 +985,20 @@ async def api_join(req, env): enr_id = new_id() try: await env.DB.prepare( - "INSERT INTO enrollments (id,activity_id,user_id,role)" + "INSERT OR IGNORE INTO enrollments (id,activity_id,user_id,role)" " VALUES (?,?,?,?)" ).bind(enr_id, act_id, user["id"], role).run() except Exception as e: - if "UNIQUE" in str(e): - return ok(None, "Already joined activity") capture_exception(e, req, env, "api_join.insert_enrollment") return err("Failed to join activity โ€” please try again", 500) + # Distinguish fresh join vs concurrent duplicate using the row that now exists. + current = await env.DB.prepare( + "SELECT id FROM enrollments WHERE activity_id=? AND user_id=?" + ).bind(act_id, user["id"]).first() + if current and current.id != enr_id: + return ok(None, "Already joined activity") + return ok(None, "Joined activity successfully")