From e2a818633f406fc1f079113e602a4059816a41f1 Mon Sep 17 00:00:00 2001 From: Paulo Anes Date: Tue, 10 Feb 2026 15:26:01 +0000 Subject: [PATCH 01/16] Add extracurricular activities and signup validation to the API --- src/app.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/app.py b/src/app.py index 4ebb1d9..f2ab1b1 100644 --- a/src/app.py +++ b/src/app.py @@ -21,6 +21,42 @@ # In-memory activity database activities = { + "Basketball Team": { + "description": "Join the school basketball team for training and competitions", + "schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM", + "max_participants": 15, + "participants": [] + }, + "Soccer Club": { + "description": "Practice soccer skills and play friendly matches", + "schedule": "Wednesdays, 3:30 PM - 5:30 PM", + "max_participants": 18, + "participants": [] + }, + "Drama Club": { + "description": "Participate in acting, stage production, and school plays", + "schedule": "Mondays, 4:00 PM - 5:30 PM", + "max_participants": 25, + "participants": [] + }, + "Art Workshop": { + "description": "Explore painting, drawing, and other visual arts", + "schedule": "Fridays, 2:30 PM - 4:00 PM", + "max_participants": 20, + "participants": [] + }, + "Math Olympiad": { + "description": "Prepare for math competitions and solve challenging problems", + "schedule": "Thursdays, 3:30 PM - 5:00 PM", + "max_participants": 10, + "participants": [] + }, + "Science Club": { + "description": "Conduct experiments and explore scientific concepts", + "schedule": "Wednesdays, 4:00 PM - 5:00 PM", + "max_participants": 15, + "participants": [] + }, "Chess Club": { "description": "Learn strategies and compete in chess tournaments", "schedule": "Fridays, 3:30 PM - 5:00 PM", @@ -62,6 +98,11 @@ def signup_for_activity(activity_name: str, email: str): # Get the specific activity activity = activities[activity_name] + # Validate student is not already signed up + if email in activity["participants"]: + raise HTTPException(status_code=400, detail="Student already signed up for this activity") + + # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} From 8b30a065dbf009fdc1640b20d504b8fc67e29e86 Mon Sep 17 00:00:00 2001 From: Paulo Anes Date: Tue, 10 Feb 2026 15:45:19 +0000 Subject: [PATCH 02/16] Add participants list and styling to activity cards --- src/static/app.js | 26 ++++++++++++++++++++++++++ src/static/styles.css | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..1909bd7 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -20,11 +20,37 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; + // Create participants list + let participantsHTML = ""; + if (details.participants.length > 0) { + participantsHTML = ` +
+ Participants: +
    + ${details.participants + .map( + (email) => + `
  • ${email}
  • ` + ) + .join("")} +
+
+ `; + } else { + participantsHTML = ` +
+ Participants: +

No participants yet.

+
+ `; + } + activityCard.innerHTML = `

${name}

${details.description}

Schedule: ${details.schedule}

Availability: ${spotsLeft} spots left

+ ${participantsHTML} `; activitiesList.appendChild(activityCard); diff --git a/src/static/styles.css b/src/static/styles.css index a533b32..2fc4f73 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -142,3 +142,35 @@ footer { padding: 20px; color: #666; } + +.participants-section { + margin-top: 12px; + padding: 10px; + background-color: #e3f2fd; + border-radius: 4px; + border: 1px solid #bbdefb; +} + +.participants-section strong { + color: #1976d2; + font-size: 15px; +} + +.participants-list { + margin-top: 6px; + margin-left: 18px; + list-style-type: disc; +} + +.participant-item { + color: #333; + font-size: 14px; + padding: 2px 0; +} + +.participants-none { + color: #757575; + font-style: italic; + font-size: 14px; + margin-top: 6px; +} From b93df53f5fe7b6e40489ac7d12faa48bac1d0e8c Mon Sep 17 00:00:00 2001 From: Paulo Anes Date: Tue, 10 Feb 2026 16:09:10 +0000 Subject: [PATCH 03/16] Add unregister functionality and update participant list display --- requirements.txt | 3 +++ src/app.py | 16 +++++++++++-- src/static/app.js | 37 +++++++++++++++++++++++++---- tests/test_app.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 tests/test_app.py diff --git a/requirements.txt b/requirements.txt index 97dc7cd..9c42c4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ +pytest +httpx +pytest-cov fastapi uvicorn diff --git a/src/app.py b/src/app.py index f2ab1b1..599a053 100644 --- a/src/app.py +++ b/src/app.py @@ -1,3 +1,6 @@ +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse +# ...existing code... """ High School Management System API @@ -16,8 +19,17 @@ # Mount the static files directory current_dir = Path(__file__).parent -app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent, - "static")), name="static") +app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent, "static")), name="static") +# Unregister endpoint +@app.post("/activities/{activity_name}/unregister") +async def unregister_from_activity(activity_name: str, email: str): + if activity_name not in activities: + raise HTTPException(status_code=404, detail="Activity not found") + activity = activities[activity_name] + if email not in activity["participants"]: + raise HTTPException(status_code=400, detail="Participant not found") + activity["participants"].remove(email) + return {"message": f"{email} removed from {activity_name}"} # In-memory activity database activities = { diff --git a/src/static/app.js b/src/static/app.js index 1909bd7..af1915b 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -20,20 +20,23 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; - // Create participants list + // Create participants list with delete icon let participantsHTML = ""; if (details.participants.length > 0) { participantsHTML = `
Participants: -
    +
    ${details.participants .map( (email) => - `
  • ${email}
  • ` + `
    + ${email} + 🗑 +
    ` ) .join("")} -
+
`; } else { @@ -52,6 +55,31 @@ document.addEventListener("DOMContentLoaded", () => {

Availability: ${spotsLeft} spots left

${participantsHTML} `; + // Add delete icon event listeners + setTimeout(() => { + const deleteIcons = activityCard.querySelectorAll(".delete-icon"); + deleteIcons.forEach((icon) => { + icon.addEventListener("click", async (e) => { + const activityName = icon.getAttribute("data-activity"); + const email = icon.getAttribute("data-email"); + if (!activityName || !email) return; + if (!confirm(`Remove ${email} from ${activityName}?`)) return; + try { + const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`, { + method: "POST", + }); + if (response.ok) { + fetchActivities(); + } else { + const result = await response.json(); + alert(result.detail || "Failed to remove participant."); + } + } catch (error) { + alert("Error removing participant."); + } + }); + }); + }, 0); activitiesList.appendChild(activityCard); @@ -88,6 +116,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + fetchActivities(); // Refresh activities list } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..b62b6d8 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,59 @@ +import pytest +from fastapi.testclient import TestClient +from src.app import app + +client = TestClient(app) + +def test_get_activities(): + response = client.get("/activities") + assert response.status_code == 200 + assert isinstance(response.json(), dict) + +def test_signup_and_unregister(): + activity = "Basketball Team" + email = "testuser@example.com" + # Signup + signup_resp = client.post(f"/activities/{activity}/signup?email={email}") + assert signup_resp.status_code == 200 or signup_resp.status_code == 400 + # Unregister + unregister_resp = client.post(f"/activities/{activity}/unregister?email={email}") + assert unregister_resp.status_code == 200 or unregister_resp.status_code == 400 + +def test_signup_twice(): + def test_signup_nonexistent_activity(): + email = "ghost@example.com" + resp = client.post("/activities/Nonexistent Activity/signup?email=" + email) + assert resp.status_code == 404 + assert resp.json()["detail"] == "Activity not found" + + def test_unregister_non_participant(): + activity = "Drama Club" + email = "not_signed_up@example.com" + resp = client.post(f"/activities/{activity}/unregister?email={email}") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Participant not found" + + def test_max_participants_limit(): + activity = "Art Workshop" + emails = [f"user{i}@example.com" for i in range(1, 22)] + # Fill up the activity + for email in emails[:20]: + resp = client.post(f"/activities/{activity}/signup?email={email}") + assert resp.status_code == 200 or resp.status_code == 400 + # Try to add one more + resp = client.post(f"/activities/{activity}/signup?email={emails[20]}") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Activity is full" + + def test_static_index(): + resp = client.get("/static/index.html") + assert resp.status_code == 200 + activity = "Soccer Club" + email = "twiceuser@example.com" + # First signup + resp1 = client.post(f"/activities/{activity}/signup?email={email}") + assert resp1.status_code == 200 + # Second signup should fail + resp2 = client.post(f"/activities/{activity}/signup?email={email}") + assert resp2.status_code == 400 + assert resp2.json()["detail"] == "Student already signed up for this activity" From 43d8aa3deef44d44cd9735ded1b037b01b17d9a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:57:07 +0000 Subject: [PATCH 04/16] Initial plan From c0aae0ce2cb5af7387b96a9756f399afd01a270f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:57:41 +0000 Subject: [PATCH 05/16] Initial plan From 7ec80ae3ed850a4c438d027e7298821e85192c23 Mon Sep 17 00:00:00 2001 From: Paulo Anes Date: Tue, 10 Feb 2026 16:58:10 +0000 Subject: [PATCH 06/16] Update tests/test_app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_app.py | 61 +++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index b62b6d8..adc1ffc 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -19,35 +19,40 @@ def test_signup_and_unregister(): unregister_resp = client.post(f"/activities/{activity}/unregister?email={email}") assert unregister_resp.status_code == 200 or unregister_resp.status_code == 400 +def test_signup_nonexistent_activity(): + email = "ghost@example.com" + resp = client.post("/activities/Nonexistent Activity/signup?email=" + email) + assert resp.status_code == 404 + assert resp.json()["detail"] == "Activity not found" + + +def test_unregister_non_participant(): + activity = "Drama Club" + email = "not_signed_up@example.com" + resp = client.post(f"/activities/{activity}/unregister?email={email}") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Participant not found" + + +def test_max_participants_limit(): + activity = "Art Workshop" + emails = [f"user{i}@example.com" for i in range(1, 22)] + # Fill up the activity + for email in emails[:20]: + resp = client.post(f"/activities/{activity}/signup?email={email}") + assert resp.status_code == 200 or resp.status_code == 400 + # Try to add one more + resp = client.post(f"/activities/{activity}/signup?email={emails[20]}") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Activity is full" + + +def test_static_index(): + resp = client.get("/static/index.html") + assert resp.status_code == 200 + + def test_signup_twice(): - def test_signup_nonexistent_activity(): - email = "ghost@example.com" - resp = client.post("/activities/Nonexistent Activity/signup?email=" + email) - assert resp.status_code == 404 - assert resp.json()["detail"] == "Activity not found" - - def test_unregister_non_participant(): - activity = "Drama Club" - email = "not_signed_up@example.com" - resp = client.post(f"/activities/{activity}/unregister?email={email}") - assert resp.status_code == 400 - assert resp.json()["detail"] == "Participant not found" - - def test_max_participants_limit(): - activity = "Art Workshop" - emails = [f"user{i}@example.com" for i in range(1, 22)] - # Fill up the activity - for email in emails[:20]: - resp = client.post(f"/activities/{activity}/signup?email={email}") - assert resp.status_code == 200 or resp.status_code == 400 - # Try to add one more - resp = client.post(f"/activities/{activity}/signup?email={emails[20]}") - assert resp.status_code == 400 - assert resp.json()["detail"] == "Activity is full" - - def test_static_index(): - resp = client.get("/static/index.html") - assert resp.status_code == 200 activity = "Soccer Club" email = "twiceuser@example.com" # First signup From 6859f1363f5fe82113893e498ffa3eac0a9e57fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:58:43 +0000 Subject: [PATCH 07/16] Initial plan From e45d7f088b290bc0e2a6dc7042e1b8221c7300e5 Mon Sep 17 00:00:00 2001 From: Paulo Anes Date: Tue, 10 Feb 2026 16:58:58 +0000 Subject: [PATCH 08/16] Update tests/test_app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index adc1ffc..17692fe 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,4 +1,3 @@ -import pytest from fastapi.testclient import TestClient from src.app import app From 64f5317262a203951b14c74fa3a632d2cbef65be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:59:14 +0000 Subject: [PATCH 09/16] Fix XSS vulnerability by using DOM methods instead of innerHTML Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/static/app.js | 140 +++++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 57 deletions(-) diff --git a/src/static/app.js b/src/static/app.js index af1915b..5a74e15 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -20,67 +20,93 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; - // Create participants list with delete icon - let participantsHTML = ""; - if (details.participants.length > 0) { - participantsHTML = ` -
- Participants: -
- ${details.participants - .map( - (email) => - `
- ${email} - 🗑 -
` - ) - .join("")} -
-
- `; - } else { - participantsHTML = ` -
- Participants: -

No participants yet.

-
- `; - } + // Create title + const title = document.createElement("h4"); + title.textContent = name; + activityCard.appendChild(title); + + // Create description + const description = document.createElement("p"); + description.textContent = details.description; + activityCard.appendChild(description); + + // Create schedule + const schedule = document.createElement("p"); + const scheduleLabel = document.createElement("strong"); + scheduleLabel.textContent = "Schedule: "; + schedule.appendChild(scheduleLabel); + schedule.appendChild(document.createTextNode(details.schedule)); + activityCard.appendChild(schedule); + + // Create availability + const availability = document.createElement("p"); + const availabilityLabel = document.createElement("strong"); + availabilityLabel.textContent = "Availability: "; + availability.appendChild(availabilityLabel); + availability.appendChild(document.createTextNode(`${spotsLeft} spots left`)); + activityCard.appendChild(availability); + + // Create participants section + const participantsSection = document.createElement("div"); + participantsSection.className = "participants-section"; + + const participantsLabel = document.createElement("strong"); + participantsLabel.textContent = "Participants:"; + participantsSection.appendChild(participantsLabel); - activityCard.innerHTML = ` -

${name}

-

${details.description}

-

Schedule: ${details.schedule}

-

Availability: ${spotsLeft} spots left

- ${participantsHTML} - `; - // Add delete icon event listeners - setTimeout(() => { - const deleteIcons = activityCard.querySelectorAll(".delete-icon"); - deleteIcons.forEach((icon) => { - icon.addEventListener("click", async (e) => { - const activityName = icon.getAttribute("data-activity"); - const email = icon.getAttribute("data-email"); - if (!activityName || !email) return; - if (!confirm(`Remove ${email} from ${activityName}?`)) return; - try { - const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(email)}`, { - method: "POST", - }); - if (response.ok) { - fetchActivities(); - } else { - const result = await response.json(); - alert(result.detail || "Failed to remove participant."); + if (details.participants.length > 0) { + const participantsList = document.createElement("div"); + participantsList.className = "participants-list"; + + details.participants.forEach((email) => { + const participantItem = document.createElement("div"); + participantItem.className = "participant-item"; + + const emailSpan = document.createElement("span"); + emailSpan.className = "participant-email"; + emailSpan.textContent = email; + participantItem.appendChild(emailSpan); + + const deleteIcon = document.createElement("span"); + deleteIcon.className = "delete-icon"; + deleteIcon.title = "Remove participant"; + deleteIcon.textContent = "🗑️"; + deleteIcon.setAttribute("data-activity", name); + deleteIcon.setAttribute("data-email", email); + + deleteIcon.addEventListener("click", async () => { + const activityName = deleteIcon.getAttribute("data-activity"); + const participantEmail = deleteIcon.getAttribute("data-email"); + if (!activityName || !participantEmail) return; + if (!confirm(`Remove ${participantEmail} from ${activityName}?`)) return; + try { + const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(participantEmail)}`, { + method: "POST", + }); + if (response.ok) { + fetchActivities(); + } else { + const result = await response.json(); + alert(result.detail || "Failed to remove participant."); + } + } catch (error) { + alert("Error removing participant."); } - } catch (error) { - alert("Error removing participant."); - } + }); + + participantItem.appendChild(deleteIcon); + participantsList.appendChild(participantItem); }); - }); - }, 0); + participantsSection.appendChild(participantsList); + } else { + const noParticipants = document.createElement("p"); + noParticipants.className = "participants-none"; + noParticipants.textContent = "No participants yet."; + participantsSection.appendChild(noParticipants); + } + + activityCard.appendChild(participantsSection); activitiesList.appendChild(activityCard); // Add option to select dropdown From a8c3b5cdde73427879ea6ef86ab6b7b257f735f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:59:37 +0000 Subject: [PATCH 10/16] Fix: Move unregister endpoint after activities dictionary definition Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/app.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app.py b/src/app.py index 599a053..787a054 100644 --- a/src/app.py +++ b/src/app.py @@ -20,16 +20,6 @@ # Mount the static files directory current_dir = Path(__file__).parent app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent, "static")), name="static") -# Unregister endpoint -@app.post("/activities/{activity_name}/unregister") -async def unregister_from_activity(activity_name: str, email: str): - if activity_name not in activities: - raise HTTPException(status_code=404, detail="Activity not found") - activity = activities[activity_name] - if email not in activity["participants"]: - raise HTTPException(status_code=400, detail="Participant not found") - activity["participants"].remove(email) - return {"message": f"{email} removed from {activity_name}"} # In-memory activity database activities = { @@ -118,3 +108,15 @@ def signup_for_activity(activity_name: str, email: str): # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} + + +@app.post("/activities/{activity_name}/unregister") +async def unregister_from_activity(activity_name: str, email: str): + """Unregister a student from an activity""" + if activity_name not in activities: + raise HTTPException(status_code=404, detail="Activity not found") + activity = activities[activity_name] + if email not in activity["participants"]: + raise HTTPException(status_code=400, detail="Participant not found") + activity["participants"].remove(email) + return {"message": f"{email} removed from {activity_name}"} From ce4844f52db4f7a0d713668b2c9599ee697a36b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:59:56 +0000 Subject: [PATCH 11/16] Remove data attributes and use closures for better security Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/static/app.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/static/app.js b/src/static/app.js index 5a74e15..5eacaa5 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -71,16 +71,14 @@ document.addEventListener("DOMContentLoaded", () => { deleteIcon.className = "delete-icon"; deleteIcon.title = "Remove participant"; deleteIcon.textContent = "🗑️"; - deleteIcon.setAttribute("data-activity", name); - deleteIcon.setAttribute("data-email", email); + // Store data in closure instead of data attributes for better security deleteIcon.addEventListener("click", async () => { - const activityName = deleteIcon.getAttribute("data-activity"); - const participantEmail = deleteIcon.getAttribute("data-email"); - if (!activityName || !participantEmail) return; - if (!confirm(`Remove ${participantEmail} from ${activityName}?`)) return; + // Create a safe confirmation message + const confirmMsg = `Remove participant from activity?\n\nParticipant: ${email}\nActivity: ${name}`; + if (!confirm(confirmMsg)) return; try { - const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(participantEmail)}`, { + const response = await fetch(`/activities/${encodeURIComponent(name)}/unregister?email=${encodeURIComponent(email)}`, { method: "POST", }); if (response.ok) { From 5ea4b0ff8c1a98f52a64d63e376aa0aefbe3b021 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:00:30 +0000 Subject: [PATCH 12/16] Add accessibility improvements to delete icon Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/static/app.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/static/app.js b/src/static/app.js index 5eacaa5..e9fed8b 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -71,9 +71,11 @@ document.addEventListener("DOMContentLoaded", () => { deleteIcon.className = "delete-icon"; deleteIcon.title = "Remove participant"; deleteIcon.textContent = "🗑️"; + deleteIcon.setAttribute("role", "button"); + deleteIcon.setAttribute("aria-label", "Remove participant"); + deleteIcon.setAttribute("tabindex", "0"); - // Store data in closure instead of data attributes for better security - deleteIcon.addEventListener("click", async () => { + const handleDelete = async () => { // Create a safe confirmation message const confirmMsg = `Remove participant from activity?\n\nParticipant: ${email}\nActivity: ${name}`; if (!confirm(confirmMsg)) return; @@ -90,6 +92,15 @@ document.addEventListener("DOMContentLoaded", () => { } catch (error) { alert("Error removing participant."); } + }; + + // Store data in closure instead of data attributes for better security + deleteIcon.addEventListener("click", handleDelete); + deleteIcon.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleDelete(); + } }); participantItem.appendChild(deleteIcon); From 96fd9f51e5b488b31091e8db1889b4f2622999e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:00:42 +0000 Subject: [PATCH 13/16] Add email validation for empty and whitespace-only values Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/app.py | 8 ++++++++ tests/test_app.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/app.py b/src/app.py index 599a053..43de867 100644 --- a/src/app.py +++ b/src/app.py @@ -23,6 +23,10 @@ # Unregister endpoint @app.post("/activities/{activity_name}/unregister") async def unregister_from_activity(activity_name: str, email: str): + # Validate email is not empty or whitespace-only + if not email or not email.strip(): + raise HTTPException(status_code=400, detail="Email cannot be empty or whitespace-only") + if activity_name not in activities: raise HTTPException(status_code=404, detail="Activity not found") activity = activities[activity_name] @@ -103,6 +107,10 @@ def get_activities(): @app.post("/activities/{activity_name}/signup") def signup_for_activity(activity_name: str, email: str): """Sign up a student for an activity""" + # Validate email is not empty or whitespace-only + if not email or not email.strip(): + raise HTTPException(status_code=400, detail="Email cannot be empty or whitespace-only") + # Validate activity exists if activity_name not in activities: raise HTTPException(status_code=404, detail="Activity not found") diff --git a/tests/test_app.py b/tests/test_app.py index adc1ffc..c6fe2fc 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -62,3 +62,35 @@ def test_signup_twice(): resp2 = client.post(f"/activities/{activity}/signup?email={email}") assert resp2.status_code == 400 assert resp2.json()["detail"] == "Student already signed up for this activity" + + +def test_signup_empty_email(): + activity = "Basketball Team" + # Test with empty email + resp = client.post(f"/activities/{activity}/signup?email=") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Email cannot be empty or whitespace-only" + + +def test_signup_whitespace_email(): + activity = "Basketball Team" + # Test with whitespace-only email + resp = client.post(f"/activities/{activity}/signup?email= ") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Email cannot be empty or whitespace-only" + + +def test_unregister_empty_email(): + activity = "Drama Club" + # Test with empty email + resp = client.post(f"/activities/{activity}/unregister?email=") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Email cannot be empty or whitespace-only" + + +def test_unregister_whitespace_email(): + activity = "Drama Club" + # Test with whitespace-only email + resp = client.post(f"/activities/{activity}/unregister?email= ") + assert resp.status_code == 400 + assert resp.json()["detail"] == "Email cannot be empty or whitespace-only" From 95ff5b77c76ef64d45bac9c14aa99621f92c6df6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:01:34 +0000 Subject: [PATCH 14/16] Simplify email validation logic per code review feedback Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index 43de867..b62eb14 100644 --- a/src/app.py +++ b/src/app.py @@ -24,7 +24,7 @@ @app.post("/activities/{activity_name}/unregister") async def unregister_from_activity(activity_name: str, email: str): # Validate email is not empty or whitespace-only - if not email or not email.strip(): + if not email.strip(): raise HTTPException(status_code=400, detail="Email cannot be empty or whitespace-only") if activity_name not in activities: @@ -108,7 +108,7 @@ def get_activities(): def signup_for_activity(activity_name: str, email: str): """Sign up a student for an activity""" # Validate email is not empty or whitespace-only - if not email or not email.strip(): + if not email.strip(): raise HTTPException(status_code=400, detail="Email cannot be empty or whitespace-only") # Validate activity exists From 1c31d8a7ccf6aba7fe8533388be5857c201f4bc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:34:53 +0000 Subject: [PATCH 15/16] Initial plan From f886c5950634f8b6d623e2b48e27f12fe8be7270 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:36:28 +0000 Subject: [PATCH 16/16] Change unregister_from_activity from async to sync function Co-authored-by: pmca31 <6774003+pmca31@users.noreply.github.com> --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 31cb1e9..c23ac90 100644 --- a/src/app.py +++ b/src/app.py @@ -115,7 +115,7 @@ def signup_for_activity(activity_name: str, email: str): @app.post("/activities/{activity_name}/unregister") -async def unregister_from_activity(activity_name: str, email: str): +def unregister_from_activity(activity_name: str, email: str): """Unregister a student from an activity""" # Validate email is not empty or whitespace-only if not email.strip():