Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 78 additions & 28 deletions functions/auth/oauth_callback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from dataclasses import asdict
import json
import os
import time
import threading
import requests
from firebase_functions import https_fn
from firebase_functions.params import SecretParam, StringParam
Expand Down Expand Up @@ -92,6 +94,63 @@
)


# ---------------------------------------------------------------------------
# In-memory spreadsheet cache – avoids hitting Google Sheets on every login.
# Cloud Functions reuse warm instances, so the cache survives across
# invocations on the same instance. A TTL (default 5 min) keeps it fresh.
# ---------------------------------------------------------------------------

SHEET_CACHE_TTL_SECONDS = 300 # 5 minutes

_sheet_cache_lock = threading.Lock()
_sheet_cache: dict | None = (
None # {"ts": float, "reg_headers": [...], "reg_rows": [...], "checkin_headers": [...], "checkin_rows": [...]}
)


def _fetch_sheets_data() -> dict:
"""Fetch both worksheets from Google Sheets and return them as a dict."""
import gspread

creds, _ = google.auth.default(
scopes=[
"https://www.googleapis.com/auth/spreadsheets.readonly",
"https://www.googleapis.com/auth/drive",
]
)
gc = gspread.authorize(creds)
spreadsheet = gc.open_by_url(SPREADSHEET_URL.value)

reg_sheet = spreadsheet.worksheet("Registration Submissions")
checkin_sheet = spreadsheet.worksheet("Check-in")

# Batch-fetch all values in two calls (instead of per-row later)
reg_all = reg_sheet.get_all_values()
checkin_all = checkin_sheet.get_all_values()

return {
"ts": time.monotonic(),
"reg_headers": reg_all[0] if reg_all else [],
"reg_rows": reg_all[1:] if len(reg_all) > 1 else [],
"checkin_headers": checkin_all[0] if checkin_all else [],
"checkin_rows": checkin_all[1:] if len(checkin_all) > 1 else [],
}


def _get_cached_sheets() -> dict:
"""Return cached sheet data, refreshing if stale or missing."""
global _sheet_cache
with _sheet_cache_lock:
now = time.monotonic()
if _sheet_cache is None or (now - _sheet_cache["ts"]) > SHEET_CACHE_TTL_SECONDS:
print("[sheet-cache] cache miss – fetching from Google Sheets")
_sheet_cache = _fetch_sheets_data()
else:
age = round(now - _sheet_cache["ts"], 1)
print(f"[sheet-cache] cache hit (age {age}s)")
return _sheet_cache


def _get_col_index_from_headers(headers: list[str], name: str) -> int:
"""Return 1-based column index for a given header name.

Expand All @@ -116,49 +175,40 @@ def _get_registration(uid: str, username: str) -> User:
Organizers will not need to be present on the spreadsheet
"""

import gspread

is_organizer = uid in [
o.strip() for o in ORGANIZERS_LIST.value.split(",") if o.strip()
]

if is_organizer:
return User(id=uid, role="organizer", username=username)
else:
creds, _ = google.auth.default(
scopes=[
"https://www.googleapis.com/auth/spreadsheets.readonly",
"https://www.googleapis.com/auth/drive",
]
)
gc = gspread.authorize(creds)

spreadsheet = gc.open_by_url(
SPREADSHEET_URL.value,
)

print(spreadsheet.worksheets())

reg_sheet = spreadsheet.worksheet("Registration Submissions")
checkin_sheet = spreadsheet.worksheet("Check-in")

reg_headers = reg_sheet.row_values(1)
checkin_headers = checkin_sheet.row_values(1)
sheets = _get_cached_sheets()
reg_headers = sheets["reg_headers"]
checkin_headers = sheets["checkin_headers"]

username_col_idx = _get_col_index_from_headers(
reg_headers, USERNAME_COL_R.value
)

cell = reg_sheet.find(
username, in_column=username_col_idx, case_sensitive=False
)
# Search for the user in cached registration rows
username_lower = username.lower()
matched_row_idx: int | None = None
for i, row in enumerate(sheets["reg_rows"]):
cell_val = _get_cell_from_row(row, username_col_idx)
if cell_val.strip().lower() == username_lower:
matched_row_idx = i
break

# participant not registered
if not cell:
if matched_row_idx is None:
raise ValueError("participant_not_found")

reg_row = reg_sheet.row_values(cell.row)
checkin_row = checkin_sheet.row_values(cell.row)
reg_row = sheets["reg_rows"][matched_row_idx]
checkin_row = (
sheets["checkin_rows"][matched_row_idx]
if matched_row_idx < len(sheets["checkin_rows"])
else []
)

checked_in_idx = _get_col_index_from_headers(
checkin_headers, CHECKED_IN_COL_C.value
Expand Down Expand Up @@ -259,7 +309,7 @@ def _generate_login_token(uid: str, username: str) -> str:
user = _get_registration(uid, username)

# If login is disabled for participants, reject them
if user.role != "organizer" and os.getenv("FUNCTIONS_EMULATOR") == "false":
if user.role != "organizer" and os.getenv("FUNCTIONS_EMULATOR") != "true":
db = firestore.client()
event_doc = db.document("event/main").get()
if event_doc.exists:
Expand Down
36 changes: 21 additions & 15 deletions src/pages/ParticipantManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ export default function ParticipantManager() {

return (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{`${user.firstName} ${user.lastName}`}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.university || "N/A"}</TableCell>
<TableCell className="break-words">{user.username}</TableCell>
<TableCell className="break-words">{`${user.firstName} ${user.lastName}`}</TableCell>
<TableCell className="break-words">{user.email}</TableCell>
<TableCell className="break-words">
{user.university || "N/A"}
</TableCell>
<TableCell>{user.shirtSize}</TableCell>
<TableCell>{user.dietaryRestrictions || "None"}</TableCell>
<TableCell>{user.attendedEvents.join(", ") || "None"}</TableCell>
<TableCell className="break-words">
{user.dietaryRestrictions || "None"}
</TableCell>
<TableCell className="break-words">
{user.attendedEvents.join(", ") || "None"}
</TableCell>
<TableCell className="justify-end flex">
{resumeURL && (
<Button
Expand Down Expand Up @@ -86,17 +92,17 @@ export default function ParticipantManager() {
</header>

<main>
<Table>
<Table className="table-fixed w-full">
<TableHeader>
<TableRow>
<TableHead>Username</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>University</TableHead>
<TableHead>Shirt Size</TableHead>
<TableHead>Dietary Restrictions</TableHead>
<TableHead>Events Attended</TableHead>
<TableHead className="w-0">Actions</TableHead>
<TableHead className="w-[10%]">Username</TableHead>
<TableHead className="w-[12%]">Name</TableHead>
<TableHead className="w-[15%]">Email</TableHead>
<TableHead className="w-[12%]">University</TableHead>
<TableHead className="w-[8%]">Shirt Size</TableHead>
<TableHead className="w-[13%]">Dietary Restrictions</TableHead>
<TableHead className="w-[18%]">Events Attended</TableHead>
<TableHead className="w-[12%]">Actions</TableHead>
</TableRow>
</TableHeader>

Expand Down
5 changes: 2 additions & 3 deletions src/pages/RFIDReader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { activitiesAtom } from "@/atoms/event/activities";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { cn } from "@/lib/utils";
import { firestoreService } from "@/services/firestore.service";
import { rfidService } from "@/services/rfid.service";
Expand Down Expand Up @@ -174,7 +173,7 @@ export default function RFIDReader() {
<br />* means this activity is not eligible for the raffle.
</p>

<ButtonGroup orientation="horizontal" className="mt-2">
<div className="mt-2 flex flex-wrap gap-2">
<Button
variant={!selectedActivity ? "default" : "outline"}
onClick={() => setSelectedActivity(null)}
Expand All @@ -196,7 +195,7 @@ export default function RFIDReader() {
{!activity.eligibleForRaffle && "*"}
</Button>
))}
</ButtonGroup>
</div>

{showActivityAssignedToast === "success" && (
<p className="mt-2 text-green-500">
Expand Down
26 changes: 13 additions & 13 deletions src/pages/TeamManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,15 @@ export default function TeamManager() {
key={team.id}
className={isUnverified ? "bg-yellow-50 dark:bg-yellow-900/20" : ""}
>
<TableCell>{team.name}</TableCell>
<TableCell>{team.track}</TableCell>
<TableCell>
<TableCell className="break-words">{team.name}</TableCell>
<TableCell className="break-words">{team.track}</TableCell>
<TableCell className="break-words">
{team.challenges.length > 0 ? team.challenges.join(", ") : "-"}
</TableCell>
<TableCell>
<TableCell className="break-words">
<TeamMembersList team={team} />
</TableCell>
<TableCell className="whitespace-pre-wrap">
<TableCell className="whitespace-pre-wrap break-words">
{team.mentoringHelp}
</TableCell>
<TableCell>{team.status}</TableCell>
Expand Down Expand Up @@ -268,16 +268,16 @@ export default function TeamManager() {
</header>

<main>
<Table>
<Table className="table-fixed w-full">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Track</TableHead>
<TableHead>Challenge</TableHead>
<TableHead>Members</TableHead>
<TableHead>Mentoring Help</TableHead>
<TableHead className="w-0">Status</TableHead>
<TableHead className="w-0">Actions</TableHead>
<TableHead className="w-[12%]">Name</TableHead>
<TableHead className="w-[10%]">Track</TableHead>
<TableHead className="w-[15%]">Challenge</TableHead>
<TableHead className="w-[18%]">Members</TableHead>
<TableHead className="w-[20%]">Mentoring Help</TableHead>
<TableHead className="w-[8%]">Status</TableHead>
<TableHead className="w-[17%]">Actions</TableHead>
</TableRow>
</TableHeader>

Expand Down