Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ __pycache__/
.env
.envrc
.DS_Store
ca-certificate.crt
ca-certificate.crt
firebase-service-account-key.json
21 changes: 21 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@
from src.utils.team_loader import TeamLoader
from src.database import db

import os
import firebase_admin
from firebase_admin import credentials, auth

SERVICE_ACCOUNT_PATH = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")


def initialize_firebase():
if not firebase_admin._apps:
if not SERVICE_ACCOUNT_PATH:
raise ValueError(
"GOOGLE_APPLICATION_CREDENTIALS is not set. Set it to your firebase-service-account-key.json path."
)
cred = credentials.Certificate(SERVICE_ACCOUNT_PATH)
firebase_admin.initialize_app(cred)
logging.info("Firebase app initialized.")
return firebase_admin.get_app()


initialize_firebase()
Comment on lines +26 to +45
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 25, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if .env_template includes the Firebase credential variable
echo "=== Checking .env_template ==="
cat .env_template 2>/dev/null || echo ".env_template not found"

echo ""
echo "=== Checking docker-compose.yml for volume mounts ==="
cat docker-compose.yml 2>/dev/null | grep -A5 "volumes:" || echo "No volumes section found"

Repository: cuappdev/score-backend

Length of output: 498


🏁 Script executed:

# Check file size and read app.py to verify all claims
wc -l app.py
echo "=== Full app.py contents ===" 
cat -n app.py

Repository: cuappdev/score-backend

Length of output: 8592


🏁 Script executed:

# Check if Dockerfile exists and examine it
if [ -f Dockerfile ]; then
  echo "=== Dockerfile found ==="
  cat -n Dockerfile
else
  echo "Dockerfile not found"
fi

Repository: cuappdev/score-backend

Length of output: 391


Deployment configuration incomplete for Firebase.

The Firebase initialization requires GOOGLE_APPLICATION_CREDENTIALS but the deployment setup is missing key configurations:

  1. .env_template doesn't document this required variable
  2. docker-compose.yml doesn't mount the Firebase key file into containers
  3. Dockerfile doesn't configure the environment variable

This will cause startup failures in containerized deployments.

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

In `@app.py` around lines 26 - 45, The deployment is missing configuration for the
Firebase service account used by initialize_firebase: ensure
SERVICE_ACCOUNT_PATH (GOOGLE_APPLICATION_CREDENTIALS) is documented in
.env_template, make the Firebase key file available to containers by mounting it
in docker-compose.yml (add a volume mapping and set the container env
GOOGLE_APPLICATION_CREDENTIALS to the mounted path), and update the Dockerfile
or the docker-compose service env to set the GOOGLE_APPLICATION_CREDENTIALS
environment variable (or use Docker secrets) so that SERVICE_ACCOUNT_PATH is
present when initialize_firebase() runs; keep references to SERVICE_ACCOUNT_PATH
and initialize_firebase in the changes so reviewers can find the initialization
logic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these set in both dev and prod servers @claiireyu ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


app = Flask(__name__)

# CORS: allow frontend (different origin) to call this API
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Flask-APScheduler
python-dotenv
pytz
gunicorn
firebase-admin
3 changes: 3 additions & 0 deletions src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def setup_database_indexes():
game_collection.create_index([("date", -1)], background=True)

try:
# Ensure doubleheaders on the same day remain distinct by including `time`.
game_collection.create_index(
[
("sport", 1),
Expand All @@ -79,8 +80,10 @@ def setup_database_indexes():
("city", 1),
("state", 1),
("location", 1),
("time", 1),
],
unique=True,
name="uniq_game_key_with_time",
background=True
)
except (DuplicateKeyError, OperationFailure) as e:
Expand Down
15 changes: 11 additions & 4 deletions src/mutations/login_user.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
from graphql import GraphQLError
from graphene import Mutation, String, Field
from graphene import Mutation, String

from firebase_admin import auth as firebase_auth
from flask_jwt_extended import create_access_token, create_refresh_token
from src.database import db


class LoginUser(Mutation):
class Arguments:
net_id = String(required=True, description="User's net ID (e.g. Cornell netid).")
id_token = String(required=True, description="Firebase ID token from the client.")

access_token = String()
refresh_token = String()

def mutate(self, info, net_id):
user = db["users"].find_one({"net_id": net_id})
def mutate(self, info, id_token):
try:
decoded = firebase_auth.verify_id_token(id_token)
except Exception:
raise GraphQLError("Invalid or expired token.")

firebase_uid = decoded["uid"]
Comment on lines +17 to +22
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 | 🟡 Minor

Improve exception handling for token verification.

Two issues flagged by static analysis:

  1. Catching bare Exception obscures the root cause and catches unrelated errors (e.g., KeyboardInterrupt in Python 2, programming bugs)
  2. Missing exception chaining (from err or from None) loses debugging context

Additionally, accessing decoded["uid"] can raise KeyError if the token structure is unexpected.

Proposed fix
     def mutate(self, info, id_token):
         try:
             decoded = firebase_auth.verify_id_token(id_token)
-        except Exception:
-            raise GraphQLError("Invalid or expired token.")
-
-        firebase_uid = decoded["uid"]
+            firebase_uid = decoded["uid"]
+        except (ValueError, KeyError) as err:
+            raise GraphQLError("Invalid or expired token.") from None
+        except Exception as err:
+            raise GraphQLError("Invalid or expired token.") from None
+
         user = db["users"].find_one({"firebase_uid": firebase_uid})

Alternatively, catch the specific firebase_admin.auth.InvalidIdTokenError and related exceptions for more precise error handling.

📝 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
try:
decoded = firebase_auth.verify_id_token(id_token)
except Exception:
raise GraphQLError("Invalid or expired token.")
firebase_uid = decoded["uid"]
try:
decoded = firebase_auth.verify_id_token(id_token)
firebase_uid = decoded["uid"]
except (ValueError, KeyError) as err:
raise GraphQLError("Invalid or expired token.") from None
except Exception as err:
raise GraphQLError("Invalid or expired token.") from None
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 19-19: Do not catch blind exception: Exception

(BLE001)


[warning] 20-20: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

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

In `@src/mutations/login_user.py` around lines 17 - 22, Replace the bare except
around firebase_auth.verify_id_token with targeted exception handling: catch the
firebase_admin.auth exceptions (e.g., InvalidIdTokenError, ExpiredIdTokenError,
RevokedIdTokenError) and other decode-related errors (ValueError) as err and
re-raise GraphQLError using exception chaining (raise GraphQLError("Invalid or
expired token.") from err); after a successful verify_id_token call, validate
the returned payload before indexing (use decoded.get("uid") or check "uid" in
decoded) and raise a clear GraphQLError if uid is missing to avoid KeyError.
Ensure you reference firebase_auth.verify_id_token and the decoded["uid"] access
points when applying these changes.

user = db["users"].find_one({"firebase_uid": firebase_uid})
if not user:
raise GraphQLError("User not found.")
identity = str(user["_id"])
Expand Down
24 changes: 16 additions & 8 deletions src/mutations/signup_user.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
from graphql import GraphQLError
from graphene import Mutation, String

from firebase_admin import auth as firebase_auth
from flask_jwt_extended import create_access_token, create_refresh_token
from src.database import db


class SignupUser(Mutation):
class Arguments:
net_id = String(required=True, description="User's net ID (e.g. Cornell netid).")
id_token = String(required=True, description="Firebase ID token from the client.")
name = String(required=False, description="Display name.")
email = String(required=False, description="Email address.")
email = String(required=False, description="Email (overrides token email if provided).")

access_token = String()
refresh_token = String()

def mutate(self, info, net_id, name=None, email=None):
if db["users"].find_one({"net_id": net_id}):
raise GraphQLError("Net ID already exists.")
def mutate(self, info, id_token, name=None, email=None):
try:
decoded = firebase_auth.verify_id_token(id_token)
except Exception:
raise GraphQLError("Invalid or expired token.")

firebase_uid = decoded["uid"]
Comment on lines +19 to +24
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 | 🟡 Minor

Improve exception handling for token verification.

Same issues as in login_user.py:

  1. Catching bare Exception is too broad
  2. Missing exception chaining loses debugging context
  3. decoded["uid"] access can raise KeyError
Proposed fix
     def mutate(self, info, id_token, name=None, email=None):
         try:
             decoded = firebase_auth.verify_id_token(id_token)
-        except Exception:
-            raise GraphQLError("Invalid or expired token.")
-
-        firebase_uid = decoded["uid"]
+            firebase_uid = decoded["uid"]
+        except (ValueError, KeyError) as err:
+            raise GraphQLError("Invalid or expired token.") from None
+        except Exception as err:
+            raise GraphQLError("Invalid or expired token.") from None
+
         if db["users"].find_one({"firebase_uid": firebase_uid}):
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 21-21: Do not catch blind exception: Exception

(BLE001)


[warning] 22-22: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

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

In `@src/mutations/signup_user.py` around lines 19 - 24, Replace the broad
try/except around firebase_auth.verify_id_token and the direct decoded["uid"]
access with targeted exception handling: catch the specific firebase_auth token
errors (e.g., firebase_auth.InvalidIdTokenError,
firebase_auth.ExpiredIdTokenError, firebase_auth.RevokedIdTokenError) and
ValueError as separate except clauses, re-raising GraphQLError with the original
exception chained (use "from e"); after verify_id_token succeeds, use
decoded.get("uid") and if it is None raise a GraphQLError("Token missing uid")
chained to a KeyError to preserve context. Ensure you update the verify_id_token
and decoded["uid"] usages accordingly.

if db["users"].find_one({"firebase_uid": firebase_uid}):
raise GraphQLError("User already exists.")

email = email or decoded.get("email")
user_doc = {
"net_id": net_id,
"firebase_uid": firebase_uid,
"email": email,
"favorite_game_ids": [],
}
if name is not None:
user_doc["name"] = name
if email is not None:
user_doc["email"] = email
result = db["users"].insert_one(user_doc)
Comment on lines +25 to 36
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

Potential race condition in user creation.

The check-then-insert pattern (find_one followed by insert_one) is vulnerable to race conditions under concurrent signup requests with the same Firebase UID. Two requests could pass the existence check simultaneously and both insert.

Consider using a unique index on firebase_uid and handling the duplicate key error, or using find_one_and_update with upsert=True.

Proposed fix using unique index + exception handling

First, ensure a unique index exists on firebase_uid in your database setup:

db["users"].create_index("firebase_uid", unique=True)

Then handle the duplicate key error:

-        if db["users"].find_one({"firebase_uid": firebase_uid}):
-            raise GraphQLError("User already exists.")
-
         email = email or decoded.get("email")
         user_doc = {
             "firebase_uid": firebase_uid,
             "email": email,
             "favorite_game_ids": [],
         }
         if name is not None:
             user_doc["name"] = name
-        result = db["users"].insert_one(user_doc)
+        try:
+            result = db["users"].insert_one(user_doc)
+        except pymongo.errors.DuplicateKeyError:
+            raise GraphQLError("User already exists.") from None
         identity = str(result.inserted_id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mutations/signup_user.py` around lines 25 - 36, The current
check-then-insert in signup_user.py (the find_one on db["users"] followed by
insert_one creating user_doc with firebase_uid/email/name) can race under
concurrent requests; add a unique index on firebase_uid in your DB setup and
change the insertion flow to either use find_one_and_update(..., upsert=True) to
atomically create-if-not-exists or keep insert_one but catch the duplicate key
error (e.g., DuplicateKeyError) around db["users"].insert_one to convert it into
the GraphQLError("User already exists.") so concurrent inserts are handled
safely.

identity = str(result.inserted_id)
return SignupUser(
Expand Down
6 changes: 4 additions & 2 deletions src/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ class Mutation(ObjectType):
create_team = CreateTeam.Field(description="Creates a new team.")
create_youtube_video = CreateYoutubeVideo.Field(description="Creates a new youtube video.")
create_article = CreateArticle.Field(description="Creates a new article.")
login_user = LoginUser.Field(description="Login by net_id; returns access_token and refresh_token.")
login_user = LoginUser.Field(
description="Login with Firebase ID token; returns access_token and refresh_token.",
)
signup_user = SignupUser.Field(
description="Create a new user by net_id; returns access_token and refresh_token (no separate login needed).",
description="Create a new user with Firebase ID token; returns access_token and refresh_token.",
)
refresh_access_token = RefreshAccessToken.Field(
description="Exchange a valid refresh token (in Authorization header) for a new access_token.",
Expand Down
32 changes: 30 additions & 2 deletions src/scrapers/game_details_scrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,34 @@ def extract_teams_and_scores(box_score_section, sport):

return team_names, period_scores

def softball_summary(box_score_section):
summary = []
scoring_section = box_score_section.find(TAG_SECTION, {ATTR_ARIA_LABEL: LABEL_SCORING_SUMMARY})
if scoring_section:
scoring_rows = scoring_section.find(TAG_TBODY)
if scoring_rows:
for row in scoring_rows.find_all(TAG_TR):
team = row.find_all(TAG_TD)[0].find(TAG_IMG)[ATTR_ALT]
inning = row.find_all(TAG_TD)[3].text.strip()
desc_cell = row.find_all(TAG_TD)[4]
span = desc_cell.find(TAG_SPAN)
if span:
span.extract()
desc = desc_cell.get_text(strip=True)
cornell_score = int(row.find_all(TAG_TD)[5].get_text(strip=True) or 0)
opp_score = int(row.find_all(TAG_TD)[6].get_text(strip=True) or 0)
summary.append({
'team': team,
'inning': inning,
'description': desc,
'cor_score': cornell_score,
'opp_score': opp_score
})
if not summary:
summary = [{"message": "No scoring events in this game."}]
return summary


def soccer_summary(box_score_section):
summary = []
scoring_section = box_score_section.find(TAG_SECTION, {ATTR_ARIA_LABEL: LABEL_SCORING_SUMMARY})
Expand Down Expand Up @@ -124,14 +152,13 @@ def hockey_summary(box_score_section):
scorer = row.find_all(TAG_TD)[4].text.strip()
assist = row.find_all(TAG_TD)[5].text.strip()

if team == "COR" or team == "CU" or team == "Cornell":
if team == "COR" or team == "CU" or team == "Cornell" or team == "CORNELL":
cornell_score += 1
else:
opp_score += 1

summary.append({
'team': team,
'period': period,
'time': time,
'scorer': scorer,
'assist': assist,
Expand Down Expand Up @@ -272,6 +299,7 @@ def scrape_game(url, sport):
'field hockey': (lambda: extract_teams_and_scores(box_score_section, 'field hockey'), field_hockey_summary),
'lacrosse': (lambda: extract_teams_and_scores(box_score_section, 'lacrosse'), lacrosse_summary),
'baseball': (lambda: extract_teams_and_scores(box_score_section, 'baseball'), baseball_summary),
'softball': (lambda: extract_teams_and_scores(box_score_section, 'softball'), softball_summary),
'basketball': (lambda: extract_teams_and_scores(box_score_section, 'basketball'), lambda _: []),
}

Expand Down
13 changes: 7 additions & 6 deletions src/scrapers/games_scraper.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ def parse_schedule_page(url, sport, gender):

result_tag = game_item.select_one(RESULT_TAG)
if result_tag:
game_data["result"] = result_tag.text.strip().replace("\n", "")
#game_data["result"] = result_tag.get_text(" ", strip=True)
game_data["result"] = result_tag.text.strip().replace("\n", " ")
else:
game_data["result"] = None

Expand Down Expand Up @@ -241,17 +242,16 @@ def process_game_data(game_data):
if str(final_box_cor_score) != str(cor_final) or str(final_box_opp_score) != str(opp_final):
game_data["score_breakdown"] = game_data["score_breakdown"][::-1]

# Try to find by tournament key fields to handle placeholder teams
# Try to find an existing game record to update.
curr_game = GameService.get_game_by_tournament_key_fields(
city,
game_data["date"],
game_data["gender"],
location,
game_data["sport"],
state
state,
)

# If no tournament game found, try the regular lookup with opponent_id

if not curr_game:
curr_game = GameService.get_game_by_key_fields(
city,
Expand All @@ -260,14 +260,15 @@ def process_game_data(game_data):
location,
team.id,
game_data["sport"],
state
state,
)

if isinstance(curr_game, list):
if curr_game:
curr_game = curr_game[0]
else:
curr_game = None

if curr_game:
updates = {
"time": game_time,
Expand Down
1 change: 1 addition & 0 deletions src/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class BoxScoreEntryType(ObjectType):

team = String(required=False)
period = String(required=False)
inning = String(required=False)
time = String(required=False)
description = String(required=False)
scorer = String(required=False)
Expand Down
Loading