From 6e23cb8747bf7ec97bf7002fe423ae50286160bf Mon Sep 17 00:00:00 2001 From: Yubi Mamiya Date: Sun, 26 Oct 2025 14:45:58 -0400 Subject: [PATCH 1/5] Update auth.authenticate and auth.validate to use CAS v3 returning JSON of user information and update app.py to parse JSON user_info correctly for username --- app.py | 82 ++++++++++++++++++++++++++++++++++++------------- src/CAS/auth.py | 80 ++++++++++++++++++++++++++++++----------------- 2 files changed, 112 insertions(+), 50 deletions(-) diff --git a/app.py b/app.py index 3d65cf8..fa9f530 100644 --- a/app.py +++ b/app.py @@ -79,7 +79,14 @@ def index(): # Home page after user logs in through Princeton's CAS @app.route("/menu", methods=["GET"]) def menu(): - username = auth.authenticate() + + # YUBI: update authenticate to return dictionary of user information + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + + # YUBI: update database functions!!! user_insert = user_database.insert_player(username) daily_insert = daily_user_database.insert_player_daily(username) played_date = daily_user_database.get_last_played_date(username) @@ -111,7 +118,11 @@ def menu(): @app.route("/requests", methods=["GET"]) def requests(): username = flask.request.args.get("username") - username_auth = auth.authenticate() + user_info = auth.authenticate() + username_auth = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + last_date = daily_user_database.get_last_versus_date(username_auth) current_date = pictures_database.get_current_date() @@ -157,7 +168,10 @@ def game(): id = pictures_database.pic_of_day() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] user_played = daily_user_database.player_played(username) today_points = daily_user_database.get_daily_points(username) @@ -198,7 +212,10 @@ def game(): @app.route("/submit", methods=["POST"]) def submit(): id = pictures_database.pic_of_day() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] user_played = daily_user_database.player_played(username) today_points = daily_user_database.get_daily_points(username) @@ -262,7 +279,10 @@ def submit(): @app.route("/rules", methods=["GET"]) def rules(): # user must be logged in to access page - auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] html_code = flask.render_template("rules.html") response = flask.make_response(html_code) return response @@ -274,7 +294,10 @@ def rules(): # Congratulations page easter egg @app.route("/congrats", methods=["GET"]) def congrats(): - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] top_player = user_database.get_top_player() check = database_check([top_player]) @@ -309,7 +332,10 @@ def congrats(): @app.route("/team", methods=["GET"]) def team(): # user must be logged in to access page - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] top_player = user_database.get_top_player() check = database_check([top_player]) @@ -333,7 +359,11 @@ def team(): @app.route("/totalboard", methods=["GET"]) def leaderboard(): top_players = user_database.get_top_players() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + points = user_database.get_points(username) daily_points = daily_user_database.get_daily_points(username) rank = user_database.get_rank(username) @@ -368,7 +398,11 @@ def leaderboard(): @app.route("/leaderboard", methods=["GET"]) def totalleaderboard(): top_players = daily_user_database.get_daily_top_players() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + points = user_database.get_points(username) daily_points = daily_user_database.get_daily_points(username) rank = user_database.get_rank(username) @@ -434,7 +468,7 @@ def create_challenge_route(): if ( challengee_id == None or challengee_id not in users - or challengee_id == auth.authenticate() + or challengee_id == (auth.authenticate()["username"]) ): response = { "status": "error", @@ -442,8 +476,10 @@ def create_challenge_route(): } return flask.jsonify(response), 400 # Including a 400 Bad Request status code else: + user_info = auth.authenticate() + username = user_info["username"] result = challenges_database.create_challenge( - auth.authenticate(), challengee_id + username, challengee_id ) check = database_check([result]) @@ -521,7 +557,9 @@ def decline_challenge_route(): @app.route("/play_button", methods=["POST"]) def play_button(): challenge_id = flask.request.form.get("challenge_id") - user = auth.authenticate() + user_info = auth.authenticate() + user = user_info["username"] + status = challenges_database.get_playbutton_status(challenge_id, user) check = database_check([status]) if check is False: @@ -625,7 +663,9 @@ def start_challenge(challenge_id=None, index=None): @app.route("/end_challenge", methods=["POST"]) def end_challenge(): challenge_id = flask.request.form.get("challenge_id") - user = auth.authenticate() + user_info = auth.authenticate() + user = user_info["username"] + finish = challenges_database.update_finish_status(challenge_id, user) check = database_check([finish]) if check is False: @@ -675,7 +715,7 @@ def submit2(): return flask.make_response(html_code) if not currLat or not currLon: pic_status = versus_database.get_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) check = database_check([pic_status]) if check is False: @@ -685,17 +725,17 @@ def submit2(): return flask.redirect(flask.url_for("requests")) if pic_status is False: fin1 = versus_database.update_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) if fin1 is None: return flask.redirect(flask.url_for("requests")) fin2 = versus_database.store_versus_pic_points( - challenge_id, auth.authenticate(), index + 1, points + challenge_id, (auth.authenticate()["username"]), index + 1, points ) if fin2 is None: return flask.redirect(flask.url_for("requests")) fin3 = versus_database.update_versus_points( - challenge_id, auth.authenticate(), points + challenge_id, (auth.authenticate()["username"]), points ) if fin3 is None: return flask.redirect(flask.url_for("requests")) @@ -725,7 +765,7 @@ def submit2(): return flask.redirect(flask.url_for("requests")) distance = round(distance_func.calc_distance(currLat, currLon, coor)) pic_status = versus_database.get_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) check = database_check([pic_status]) if check is False: @@ -736,17 +776,17 @@ def submit2(): if pic_status is False: points = round(versus_database.calculate_versus(distance, time)) fin1 = versus_database.store_versus_pic_points( - challenge_id, auth.authenticate(), index + 1, points + challenge_id, (auth.authenticate()["username"]), index + 1, points ) if fin1 is None: return flask.redirect(flask.url_for("requests")) fin2 = versus_database.update_versus_points( - challenge_id, auth.authenticate(), points + challenge_id, (auth.authenticate()["username"]), points ) if fin2 is None: return flask.redirect(flask.url_for("requests")) fin3 = versus_database.update_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) if fin3 is None: return flask.redirect(flask.url_for("requests")) diff --git a/src/CAS/auth.py b/src/CAS/auth.py index 07f308b..d068a56 100644 --- a/src/CAS/auth.py +++ b/src/CAS/auth.py @@ -32,24 +32,53 @@ def strip_ticket(url): def validate(ticket): + + # YUBI: updated using v3 val_url = ( _CAS_URL - + "validate" + + "p3/serviceValidate" + "?service=" + urllib.parse.quote(strip_ticket(flask.request.url)) + "&ticket=" + urllib.parse.quote(ticket) + + "&format=json" ) - lines = [] - with urllib.request.urlopen(val_url) as flo: - lines = flo.readlines() # Should return 2 lines. - if len(lines) != 2: - return None - first_line = lines[0].decode("utf-8") - second_line = lines[1].decode("utf-8") - if not first_line.startswith("yes"): + + with urllib.request.urlopen(val_url) as response: + data = json.load(response) + + # Check if authentication was successful + service_response = data.get("serviceResponse", {}) + auth_success = service_response.get("authenticationSuccess") + if not auth_success: return None - return second_line + + username = auth_success.get("user", "").strip() + attributes = auth_success.get("attributes", {}) + + # Extract displayName + display_name = "" + if "displayName" in attributes: + # Could be a list + if isinstance(attributes["displayName"], list): + display_name = attributes["displayName"][0] + else: + display_name = attributes["displayName"] + + # Extract class year from grouperGroups + year = "Graduate" + grouper_groups = attributes.get("grouperGroups", []) + if isinstance(grouper_groups, list): + for g in grouper_groups: + if "PU:basis:classyear:" in g: + year = g.split(":")[-1] + break + + return { + "username": username, + "displayName": display_name or username, + "year": year + } # ----------------------------------------------------------------------- @@ -59,35 +88,27 @@ def validate(ticket): def authenticate(): + # If already authenticated, return cached info + if "user_info" in flask.session: + return flask.session.get("user_info") - # If the username is in the session, then the user was - # authenticated previously. So return the username. - if "username" in flask.session: - return flask.session.get("username") - - # If the request does not contain a login ticket, then redirect - # the browser to the login page to get one. + # If no ticket, redirect to CAS login ticket = flask.request.args.get("ticket") if ticket is None: login_url = _CAS_URL + "login?service=" + urllib.parse.quote(flask.request.url) flask.abort(flask.redirect(login_url)) - # If the login ticket is invalid, then redirect the browser - # to the login page to get a new one. - username = validate(ticket) - if username is None: + # Validate ticket + user_info = validate(ticket) + if user_info is None: login_url = ( - _CAS_URL - + "login?service=" - + urllib.parse.quote(strip_ticket(flask.request.url)) + _CAS_URL + "login?service=" + urllib.parse.quote(strip_ticket(flask.request.url)) ) flask.abort(flask.redirect(login_url)) - # The user is authenticated, so store the username in - # the session. - username = username.strip() - flask.session["username"] = username - return username + # Store in session + flask.session["user_info"] = user_info + return user_info # ----------------------------------------------------------------------- @@ -107,6 +128,7 @@ def logoutapp(): def logoutcas(): + # YUBI: ASK, does this correctly logout of cas? # Log out of the CAS session, and then the application. logout_url = ( _CAS_URL From 277efc45d8cd931bb31ece8179b8270e14266d2c Mon Sep 17 00:00:00 2001 From: Yubi Mamiya Date: Sun, 26 Oct 2025 14:57:48 -0400 Subject: [PATCH 2/5] Update users table to include display_name and year, update get_top_players and insert_player function to include display_name and year --- src/Databases/user_database.py | 11 ++++++----- src/models.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Databases/user_database.py b/src/Databases/user_database.py index d594785..f5538ef 100644 --- a/src/Databases/user_database.py +++ b/src/Databases/user_database.py @@ -11,15 +11,15 @@ # Inserts username into users table. - -def insert_player(username): +# YUBI: update function to enable addition of displayName and year as well +def insert_player(username, display_name, year): try: with get_session() as session: # Check if username exists existing = session.query(User).filter_by(username=username).first() if existing is None: - new_user = User(username=username, points=0) + new_user = User(username=username, points=0, display_name=display_name, year=year) session.add(new_user) return "success" @@ -72,7 +72,6 @@ def reset_all_players_total_points(): # Updates username's total points with points. - def update_player(username, points): try: with get_session() as session: @@ -151,9 +150,11 @@ def get_top_players(): .limit(10) .all() ) + # YUBI, ASK: does users return the whole row of information including display name and year? for user in users: - player_stats = {"username": user.username, "points": user.points} + # YUBI: add display name and year to player stats + player_stats = {"Name": user.display_name + " (" + user.year + ")", "points": user.points} top_players.append(player_stats) return top_players diff --git a/src/models.py b/src/models.py index 25930c8..508de17 100644 --- a/src/models.py +++ b/src/models.py @@ -18,9 +18,12 @@ class User(Base): username = Column(String(255), primary_key=True) points = Column(Integer, default=0) + display_name = Column(String(255), default="") + year = Column(String(255), default="") def __repr__(self): - return f"" + # YUBI: add display name and year to user table + return f"" # ----------------------------------------------------------------------- @@ -31,8 +34,11 @@ class UserDaily(Base): __tablename__ = "usersdaily" + # YUBI: add display name and year to user table username = Column(String(255), primary_key=True) points = Column(Integer, default=0) + display_name = Column(String(255), default="") + year = Column(String(255), default="") distance = Column(Integer, default=0) played = Column(Boolean, default=False) last_played = Column(Date, nullable=True) @@ -40,7 +46,7 @@ class UserDaily(Base): current_streak = Column(Integer, default=0) def __repr__(self): - return f"" + return f"" # ----------------------------------------------------------------------- From e8130b8a533f73af5a992d696ef322bc7da2365e Mon Sep 17 00:00:00 2001 From: Joshua Lau Date: Mon, 27 Oct 2025 20:46:41 -0400 Subject: [PATCH 3/5] Create migration --- ...0c8e89_add_name_and_year_to_users_table.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py diff --git a/alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py b/alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py new file mode 100644 index 0000000..21ce25b --- /dev/null +++ b/alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py @@ -0,0 +1,36 @@ +"""add name and year to users table + +Revision ID: 74e63e0c8e89 +Revises: 2df915a8c6ec +Create Date: 2025-10-27 20:46:29.628712 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '74e63e0c8e89' +down_revision: Union[str, None] = '2df915a8c6ec' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('display_name', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('year', sa.String(length=255), nullable=True)) + op.add_column('usersdaily', sa.Column('display_name', sa.String(length=255), nullable=True)) + op.add_column('usersdaily', sa.Column('year', sa.String(length=255), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('usersdaily', 'year') + op.drop_column('usersdaily', 'display_name') + op.drop_column('users', 'year') + op.drop_column('users', 'display_name') + # ### end Alembic commands ### From 1f1c8509cad40ba759786ab2b48103431ea37994 Mon Sep 17 00:00:00 2001 From: Yubi Mamiya Date: Tue, 28 Oct 2025 15:56:42 -0400 Subject: [PATCH 4/5] update get_top_player, get_top_players, and get_daily_top_players functions to return display_name with year instead of username (with the same json name of username) --- app.py | 4 ++-- src/Databases/daily_user_database.py | 8 ++++++-- src/Databases/user_database.py | 11 +++++++---- src/models.py | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index fa9f530..8e03688 100644 --- a/app.py +++ b/app.py @@ -87,8 +87,8 @@ def menu(): year = user_info["year"] # YUBI: update database functions!!! - user_insert = user_database.insert_player(username) - daily_insert = daily_user_database.insert_player_daily(username) + user_insert = user_database.insert_player(username, display_name, year) + daily_insert = daily_user_database.insert_player_daily(username, display_name, year) played_date = daily_user_database.get_last_played_date(username) current_date = pictures_database.get_current_date() diff --git a/src/Databases/daily_user_database.py b/src/Databases/daily_user_database.py index b725b96..3451289 100644 --- a/src/Databases/daily_user_database.py +++ b/src/Databases/daily_user_database.py @@ -12,7 +12,7 @@ # Inserts username into usersDaily table. -def insert_player_daily(username): +def insert_player_daily(username, display_name, year): try: with get_session() as session: existing = session.query(UserDaily).filter_by(username=username).first() @@ -21,6 +21,8 @@ def insert_player_daily(username): new_user = UserDaily( username=username, points=0, + display_name=display_name, + year=year, distance=0, played=False, last_played=None, @@ -287,7 +289,9 @@ def get_daily_top_players(): ) for user in users: - player_stats = {"username": user.username, "points": user.points} + # YUBI: update player_stats to show display_name concatenated with year + # I have replaced username with the name, but kept the json reference as username for now + player_stats = {"username": user.display_name + " (" + user.year + ")", "points": user.points} daily_top_players.append(player_stats) return daily_top_players diff --git a/src/Databases/user_database.py b/src/Databases/user_database.py index f5538ef..3657948 100644 --- a/src/Databases/user_database.py +++ b/src/Databases/user_database.py @@ -71,7 +71,8 @@ def reset_all_players_total_points(): # ----------------------------------------------------------------------- # Updates username's total points with points. - +# YUBI ASK: in this function, do I need to update the full user table or just the points? +# I don't want to lose the other columns in the user table when I update points def update_player(username, points): try: with get_session() as session: @@ -153,8 +154,9 @@ def get_top_players(): # YUBI, ASK: does users return the whole row of information including display name and year? for user in users: - # YUBI: add display name and year to player stats - player_stats = {"Name": user.display_name + " (" + user.year + ")", "points": user.points} + # YUBI: replace username with display name and year + # Keep reference as username, but want to change this to name later + player_stats = {"username": user.display_name + " (" + user.year + ")", "points": user.points} top_players.append(player_stats) return top_players @@ -217,7 +219,8 @@ def get_top_player(): if user is None: return {"username": None, "points": 0} - player_stats = {"username": user.username, "points": user.points} + # YUBI: replace username with display name and year + player_stats = {"username": user.display_name + " (" + user.year + ")", "points": user.points} return player_stats diff --git a/src/models.py b/src/models.py index 508de17..ce42658 100644 --- a/src/models.py +++ b/src/models.py @@ -104,7 +104,7 @@ def __repr__(self): # ----------------------------------------------------------------------- - +# YUBI ASK: do we want to show displayName instead of netID in matches as well? class Match(Base): """Model for matches table - stores completed versus mode match results""" From ff9d38097e9ed99591aa2733493b352764a64bd9 Mon Sep 17 00:00:00 2001 From: Joshua Lau Date: Tue, 4 Nov 2025 18:33:38 -0500 Subject: [PATCH 5/5] Import json --- src/CAS/auth.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/CAS/auth.py b/src/CAS/auth.py index d068a56..933fea3 100644 --- a/src/CAS/auth.py +++ b/src/CAS/auth.py @@ -6,6 +6,7 @@ import urllib.parse import re import flask +import json # ----------------------------------------------------------------------- @@ -32,7 +33,7 @@ def strip_ticket(url): def validate(ticket): - + # YUBI: updated using v3 val_url = ( _CAS_URL @@ -55,7 +56,7 @@ def validate(ticket): username = auth_success.get("user", "").strip() attributes = auth_success.get("attributes", {}) - + # Extract displayName display_name = "" if "displayName" in attributes: @@ -64,7 +65,7 @@ def validate(ticket): display_name = attributes["displayName"][0] else: display_name = attributes["displayName"] - + # Extract class year from grouperGroups year = "Graduate" grouper_groups = attributes.get("grouperGroups", []) @@ -73,12 +74,8 @@ def validate(ticket): if "PU:basis:classyear:" in g: year = g.split(":")[-1] break - - return { - "username": username, - "displayName": display_name or username, - "year": year - } + + return {"username": username, "displayName": display_name or username, "year": year} # ----------------------------------------------------------------------- @@ -102,7 +99,9 @@ def authenticate(): user_info = validate(ticket) if user_info is None: login_url = ( - _CAS_URL + "login?service=" + urllib.parse.quote(strip_ticket(flask.request.url)) + _CAS_URL + + "login?service=" + + urllib.parse.quote(strip_ticket(flask.request.url)) ) flask.abort(flask.redirect(login_url))