From 24ad86eb60b9e8266cc0e469ff63c61fb57f0ada Mon Sep 17 00:00:00 2001 From: Greg V Date: Sat, 30 May 2026 14:25:11 -0700 Subject: [PATCH 1/3] [Teams] Fix team creation crash on free-text/Slack members queue_team's member loop used `if "id" in member`, a substring test against the mixed string/object teamMembers array (freeSolo picker): free-text names containing "id" matched then raised TypeError, others were silently dropped. Guard with isinstance(member, dict) + .get("id"). Also replaces the undefined SLACK_USER_ID_PREFIX with normalize_slack_user_id() (NameError that broke the whole flow). Co-Authored-By: Claude Opus 4.7 --- api/teams/teams_service.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/api/teams/teams_service.py b/api/teams/teams_service.py index 1c57053..3f809b5 100644 --- a/api/teams/teams_service.py +++ b/api/teams/teams_service.py @@ -14,7 +14,7 @@ from common.utils.github import create_github_repo, validate_github_username from common.utils.slack import create_slack_channel, invite_user_to_channel, send_slack, send_slack_audit from common.utils.firebase import get_hackathon_by_event_id -from common.utils.oauth_providers import extract_slack_user_id, is_oauth_user_id +from common.utils.oauth_providers import extract_slack_user_id, is_oauth_user_id, normalize_slack_user_id from common.utils.slack import add_bot_to_channel @@ -421,11 +421,18 @@ def queue_team(propel_user_id, json): send_slack_audit(action="queue_team", message=f"Queueing {len(teamMembers)} team members", payload=json) for member in teamMembers: - if "id" in member and member["id"] != root_slack_user_id: - logger.info("Inviting user %s to slack channel %s", member["id"], slack_channel) - invite_user_to_channel(member["id"], slack_channel) - # Lookup the user in the database by user_id which is their slack user id - full_user_id_with_slack_prefix = f"{SLACK_USER_ID_PREFIX}{member['id']}" + # teamMembers is a mixed list: Slack-user objects ({id, name, real_name}) + # picked from the autocomplete, plus any free-text names the user typed + # (the picker is freeSolo). Only objects with a Slack id can be invited to + # the channel and linked to the team. Guard on dict explicitly — `"id" in + # member` against a raw string is a substring test (e.g. "Sidney" matches), + # and member["id"] would then raise TypeError on that string. + member_slack_id = member.get("id") if isinstance(member, dict) else None + if member_slack_id and member_slack_id != root_slack_user_id: + logger.info("Inviting user %s to slack channel %s", member_slack_id, slack_channel) + invite_user_to_channel(member_slack_id, slack_channel) + # Lookup the user in the database by user_id which is their slack user id + full_user_id_with_slack_prefix = normalize_slack_user_id(member_slack_id) user_db_check = get_user_from_slack_id(full_user_id_with_slack_prefix) if not user_db_check: new_user = save_user( @@ -433,18 +440,18 @@ def queue_team(propel_user_id, json): email="", last_login="", profile_image="", - name=member["real_name"], - nickname=member["name"] - ) + name=member.get("real_name", ""), + nickname=member.get("name", "") + ) users_list.append(get_user_doc_reference(new_user.user_id)) else: in_db_user = get_user_doc_reference(user_db_check.user_id) users_list.append(in_db_user) logger.info("User %s already exists in the database", user_db_check) - + else: - logger.info("Skipping user: %s", member) + logger.info("Skipping non-Slack team member (free-text or self): %s", member) # Add Slack admins From 7f7492ee4379642a2c6d01c1056b89884d7c2ca4 Mon Sep 17 00:00:00 2001 From: Greg V Date: Sat, 30 May 2026 14:46:35 -0700 Subject: [PATCH 2/3] [bugfix] handle hackathon serialization --- api/users/users_views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/users/users_views.py b/api/users/users_views.py index 40b9b67..fd090c9 100644 --- a/api/users/users_views.py +++ b/api/users/users_views.py @@ -23,9 +23,16 @@ def getOrgId(req): @auth.require_user def profile(): # user_id is a uuid from Propel Auth - if auth_user and auth_user.user_id: + if auth_user and auth_user.user_id: u: User | None = users_service.get_profile_metadata(auth_user.user_id) - return vars(u) if u is not None else None + if u is None: + return None + # vars(u) exposes u.hackathons as raw Hackathon objects, which Flask + # can't JSON-encode (TypeError: Object of type Hackathon is not JSON + # serializable). Shallow-copy and replace it with serialized dicts. + result = dict(vars(u)) + result["hackathons"] = u.serialize_hackathons() + return result else: return None From ad64a45d539bc1de27661e70315d381ae973e19e Mon Sep 17 00:00:00 2001 From: Greg V Date: Sat, 30 May 2026 14:47:54 -0700 Subject: [PATCH 3/3] [bugfix] userid fallback --- api/volunteers/volunteers_views.py | 34 +++++++++++++++++++++++++----- common/utils/validators.py | 9 ++++++++ services/hackathons_service.py | 8 +++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/api/volunteers/volunteers_views.py b/api/volunteers/volunteers_views.py index 77b31e7..7da1401 100644 --- a/api/volunteers/volunteers_views.py +++ b/api/volunteers/volunteers_views.py @@ -7,6 +7,7 @@ from common.utils.slack import send_slack_audit from services.volunteers_service import ( get_volunteer_by_user_id, + get_volunteer_by_email, get_volunteers_by_event, create_or_update_volunteer, update_volunteer_selection, @@ -140,12 +141,35 @@ def handle_get(user, event_id: str, volunteer_type: str) -> Tuple[Dict[str, Any] logger.info(f"Getting {volunteer_type} application for event {event_id}") try: - volunteer = get_volunteer_by_user_id(user.user_id, event_id, volunteer_type) - + uid = getattr(user, "user_id", None) + + # 1. Direct match on the id we were handed. For self-submitted apps this + # is the PropelAuth UUID (handle_submit stores auth_user.user_id), and + # for the ?userId= query-param path it's whatever the caller passed. + volunteer = get_volunteer_by_user_id(uid, event_id, volunteer_type) if uid else None + + # 2. Fall back to email / OAuth user_id. Volunteer docs created through + # flows that stored the OAuth identity (oauth2|slack|...) rather than + # the PropelAuth UUID won't match step 1, so resolve the caller the + # same way the mentor self-check does: email first, then OAuth user_id. + if volunteer is None and uid: + email = oauth_user_id = None + try: + from services.users_service import get_propel_user_details_by_id + details = get_propel_user_details_by_id(uid) or () + email = details[0] if len(details) > 0 else None + oauth_user_id = details[1] if len(details) > 1 else None + except Exception as resolve_err: + logger.warning(f"handle_get: could not resolve caller {uid}: {resolve_err}") + + if email: + volunteer = get_volunteer_by_email(email, event_id, volunteer_type) + if volunteer is None and oauth_user_id and oauth_user_id != uid: + volunteer = get_volunteer_by_user_id(oauth_user_id, event_id, volunteer_type) + if volunteer: - result = _success_response(volunteer, "Application retrieved successfully") - logger.info(f"Retrieved {volunteer_type} application: {result}") - return result + logger.info(f"Retrieved {volunteer_type} application for event {event_id}") + return _success_response(volunteer, "Application retrieved successfully") else: return _success_response(None, "No application found") except Exception as e: diff --git a/common/utils/validators.py b/common/utils/validators.py index 1c50e27..36f4174 100644 --- a/common/utils/validators.py +++ b/common/utils/validators.py @@ -306,6 +306,15 @@ def _skip(field, reason): _skip("mentor_slack_channel", "must be a string <= 80 chars (Slack channel name)") cleaned.pop("mentor_slack_channel") + # github_org — optional GitHub organization slug used to link to the org's + # GitHub page and to scope team repo lookups. Loose validation: a string + # within GitHub's org-name length bounds. + if "github_org" in cleaned and cleaned["github_org"] is not None: + go = cleaned["github_org"] + if not isinstance(go, str) or len(go) > 100: + _skip("github_org", "must be a string <= 100 chars (GitHub org slug)") + cleaned.pop("github_org") + return cleaned, skipped diff --git a/services/hackathons_service.py b/services/hackathons_service.py index 8a4e304..790ab46 100644 --- a/services/hackathons_service.py +++ b/services/hackathons_service.py @@ -970,6 +970,14 @@ def save_hackathon(json_data, propel_id): if "visible_problem_statements" in data: hackathon_data["visible_problem_statements"] = data["visible_problem_statements"] + # Optional top-level fields that pass straight through when present. + # (Validated in validate_hackathon_data_partial; not part of the core + # required set, so they need an explicit copy here or merge=True drops + # them.) + for optional_key in ("github_org", "mentor_slack_channel"): + if optional_key in data: + hackathon_data[optional_key] = data[optional_key] + @firestore.transactional def update_hackathon(transaction): hackathon_ref = db.collection('hackathons').document(doc_id)