-
Notifications
You must be signed in to change notification settings - Fork 0
Claire/sport scrapers #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
cdd7281
f19c92e
f2c5a56
5805267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,4 +4,5 @@ __pycache__/ | |
| .env | ||
| .envrc | ||
| .DS_Store | ||
| ca-certificate.crt | ||
| ca-certificate.crt | ||
| firebase-service-account-key.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,3 +11,4 @@ Flask-APScheduler | |
| python-dotenv | ||
| pytz | ||
| gunicorn | ||
| firebase-admin==7.3.0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,36 @@ | ||
| 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 | ||
|
|
||
| _TOKEN_ERRORS = ( | ||
| firebase_auth.InvalidIdTokenError, | ||
| firebase_auth.ExpiredIdTokenError, | ||
| firebase_auth.RevokedIdTokenError, | ||
| ) | ||
|
|
||
|
|
||
| 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 _TOKEN_ERRORS as err: | ||
| raise GraphQLError("Invalid or expired token.") from err | ||
| except ValueError as err: | ||
| raise GraphQLError("Invalid or expired token.") from err | ||
|
|
||
| firebase_uid = decoded.get("uid") | ||
| if not firebase_uid: | ||
| raise GraphQLError("Invalid or expired token.") | ||
| user = db["users"].find_one({"firebase_uid": firebase_uid}) | ||
|
Comment on lines
+8
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In the Firebase Admin Python SDK (firebase_admin.auth), the verify_id_token method includes a check_revoked parameter (named check_revoked, boolean, default False). Usage: from firebase_admin import auth try: decoded_token = auth.verify_id_token(id_token, check_revoked=True) uid = decoded_token['uid'] # Token valid and not revoked except auth.RevokedIdTokenError: # Token revoked - force reauthentication pass except auth.UserDisabledError: # User disabled pass except auth.InvalidIdTokenError: # Invalid token pass When check_revoked=True, it fetches the UserRecord for the token's uid and compares the token's iat (issued at) against user.tokens_valid_after_timestamp (milliseconds since epoch; None if no revocation). If iat * 1000 < tokens_valid_after_timestamp, raises RevokedIdTokenError. Also checks if user is disabled (raises UserDisabledError). This requires an extra network call to Firebase Auth backend, making it slower. Basic verification (check_revoked=False) only checks signature, expiration, issuer, audience. Official docs confirm this behavior with Python examples using check_revoked=True. Note: Early bug fixed in 2018 where None handling caused TypeError. Citations:
🏁 Script executed: # First, let's check the actual code in the file
cat -n src/mutations/login_user.py | head -50Repository: cuappdev/score-backend Length of output: 1702 🌐 Web query:
💡 Result: The firebase_admin.auth.CertificateFetchError is a specific exception raised by the Firebase Admin Python SDK when it fails to fetch public key certificates needed to verify Firebase ID tokens or session cookies. This occurs during calls to auth.verify_id_token or auth.verify_session_cookie. Key details: - Bases: UnknownError (subtype of FirebaseError) - Trigger: Network or connectivity issues preventing access to Google's certificate endpoint (typically https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com). - Common causes: Firewalls blocking outbound HTTPS to Google APIs, proxy misconfigurations (especially on macOS where system proxies intercept localhost but also affect external calls), network timeouts, ISP outages, SSL/TLS issues (outdated OpenSSL/LibreSSL), or corporate network restrictions. - The SDK caches certificates after successful fetch to reduce repeated calls. Solutions and workarounds: - Verify network connectivity: Test direct access to the certs URL using requests.get('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'). - Check proxies: Set NO_PROXY='www.googleapis.com,.googleapis.com' or os.environ['NO_PROXY'] = 'localhost,127.0.0.1,.googleapis.com'. - Update environment: Ensure recent Python (3.9+), OpenSSL, and firebase-admin SDK versions. Check ssl.OPENSSL_VERSION. - Disable proxy trust if using requests: session.trust_env = False (though SDK manages this internally). - Firewall/VPN: Temporarily disable to test; configure exceptions for Google domains. - Retry logic: Implement exponential backoff around verify_id_token calls. - Ensure latest SDK: Uses HTTP/2, improved timeouts, and caching. Handle in code: try: claims = auth.verify_id_token(id_token) except auth.CertificateFetchError as exc: # Log cause: exc.cause may contain underlying requests/urllib error raise # or handle with retry Citations:
🌐 Web query:
💡 Result: The complete list of exceptions raised by firebase_admin.auth.verify_id_token(id_token, check_revoked=False, clock_skew_seconds=0) is: - ValueError: If id_token is not a string or is empty. - InvalidIdTokenError: If id_token is not a valid Firebase ID token. - ExpiredIdTokenError: If the specified ID token has expired. - RevokedIdTokenError: If check_revoked is True and the ID token has been revoked. - TenantIdMismatchError: If id_token belongs to a tenant that is different than this Client instance. - CertificateFetchError: If an error occurs while fetching the public key certificates required to verify the ID token. - UserDisabledError: If check_revoked is True and the corresponding user record is disabled. These are the specific exceptions documented for this method. Note that some older versions raised more generic ValueError for token issues, but specific auth exceptions were added later. Citations:
Remove
Additionally, handle 🤖 Prompt for AI Agents |
||
| if not user: | ||
| raise GraphQLError("User not found.") | ||
| identity = str(user["_id"]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,31 +1,51 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 pymongo.errors import DuplicateKeyError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from src.database import db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _TOKEN_ERRORS = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firebase_auth.InvalidIdTokenError, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firebase_auth.ExpiredIdTokenError, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firebase_auth.RevokedIdTokenError, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 _TOKEN_ERRORS as err: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise GraphQLError("Invalid or expired token.") from err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except ValueError as err: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise GraphQLError("Invalid or expired token.") from err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firebase_uid = decoded.get("uid") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if firebase_uid is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise GraphQLError("Token missing uid") from KeyError("uid") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = db["users"].insert_one(user_doc) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except DuplicateKeyError as err: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise GraphQLError("User already exists.") from err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: inconsistency with Two small nits:
💡 Proposed tweak- firebase_uid = decoded.get("uid")
- if firebase_uid is None:
- raise GraphQLError("Token missing uid") from KeyError("uid")
+ firebase_uid = decoded.get("uid")
+ if not firebase_uid:
+ raise GraphQLError("Invalid or expired token.")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identity = str(result.inserted_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return SignupUser( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| access_token=create_access_token(identity=identity), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
firebase-admin Python 7.3.0 recommended way to check if default app is initialized💡 Result:
The recommended way to check if the default Firebase app is initialized in firebase-admin Python SDK version 7.3.0 is to attempt to retrieve it using firebase_admin.get_app and catch the ValueError exception if it does not exist. This is the idiomatic approach since the official documentation states that get_app raises ValueError if the specified app (default is '[DEFAULT]') does not exist, while initialize_app raises ValueError if it already exists. Example code: import firebase_admin from firebase_admin import credentials try: app = firebase_admin.get_app # Checks if default app exists except ValueError: cred = credentials.Certificate('path/to/serviceAccountKey.json') app = firebase_admin.initialize_app(cred) Alternative (not officially recommended, uses internal state): if not firebase_admin._apps: firebase_admin.initialize_app(cred) This checks the internal _apps dict, but _apps is an implementation detail (protected member). No public is_app_initialized function exists in v7.3.0; a PR for it remains open. Official docs emphasize initializing once and using get_app thereafter.
Citations:
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 74
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 86
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 48
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 8210
🏁 Script executed:
# Check docker-compose.yml cat -n ./docker-compose.ymlRepository: cuappdev/score-backend
Length of output: 843
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 48
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 48
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 48
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 98
🏁 Script executed:
# Check what happens when scraper.py runs without GOOGLE_APPLICATION_CREDENTIALS head -50 ./scraper.pyRepository: cuappdev/score-backend
Length of output: 1441
🏁 Script executed:
Repository: cuappdev/score-backend
Length of output: 48
Firebase initialization at import time will hard-fail any process that imports
app.The code captures
SERVICE_ACCOUNT_PATHat module load (line 30) and invokesinitialize_firebase()unconditionally at line 45, beforelogging.basicConfig()(line 115). This creates multiple failure modes:docker-compose.yml(line 18) does not setGOOGLE_APPLICATION_CREDENTIALS, so starting it will crash withValueError, taking down the container.credentials.Certificate()also fails with the same blast radius.logging.info("Firebase app initialized.")call at line 41 executes beforelogging.basicConfig(), so the message goes to the root logger with default configuration and is effectively swallowed.Move initialization out of module scope — either into an app factory, a request context, or behind an explicit initialization call. When credentials are absent, degrade gracefully (log a warning, skip init) rather than crashing. Only the mutations that actually require auth need Firebase.
Also, relying on the private
firebase_admin._appsattribute is brittle. Use the documented approach:🤖 Prompt for AI Agents