From da53dbe060d828457ed0738b9e1289408a71a8df Mon Sep 17 00:00:00 2001 From: Mercurio <47455213+NotLugozzi@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:26:42 +0100 Subject: [PATCH] Added endpoints for getting all matches (per-player), calculating new elo. improved authentication flow for mobile app --- calls.py | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- main.py | 36 ++++++++++-- 2 files changed, 195 insertions(+), 7 deletions(-) diff --git a/calls.py b/calls.py index f3e0901..7b53541 100644 --- a/calls.py +++ b/calls.py @@ -1,5 +1,8 @@ import bcrypt from db import get_db_connection +import base64 +import secrets +import time def register_user(email, display_name, password): hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() @@ -27,8 +30,35 @@ def authenticate_user(email, password): cursor.execute("SELECT uid, password_hash FROM users WHERE email = %s;", (email,)) user = cursor.fetchone() if user and bcrypt.checkpw(password.encode(), user["password_hash"].encode()): - return user["uid"] + epoch_timestamp = int(time.time()) + random_int = secrets.randbelow(1000000) + token_data = f"{email}:{password}:{epoch_timestamp}:{random_int}" + encoded_token = base64.b64encode(token_data.encode()).decode() + hashed_token = bcrypt.hashpw(encoded_token.encode(), bcrypt.gensalt()) + cursor.execute("UPDATE users SET session_token = %s WHERE email = %s;", (hashed_token, email)) + conn.commit() + return hashed_token.decode() return None + + finally: + cursor.close() + conn.close() + + +def reauth_user(token): + conn = get_db_connection() + cursor = conn.cursor() + try: + cursor.execute("SELECT email, display_name, current_elo, session_token FROM users WHERE session_token IS = %s;", (token,)) + user = cursor.fetchone() + if user: + user_data = { + "email": user["email"], + "display_name": user["display_name"], + "elo": user["current_elo"] + } + return json.dumps(user_data) + return None finally: cursor.close() conn.close() @@ -88,3 +118,137 @@ def accept_match_invite(match_id, player2_uid): finally: cursor.close() conn.close() + + +def get_all_matches(): + conn = get_db_connection() + cursor = conn.cursor() + try: + query = """ + SELECT + m.match_id, + u1.display_name AS player1_name, + u2.display_name AS player2_name, + m.player1_score, + m.player2_score, + CASE + WHEN m.player1_score IS NULL AND m.player2_score IS NULL THEN 'upcoming' + WHEN m.player1_score IS NOT NULL AND m.player2_score IS NOT NULL THEN 'completed' + ELSE 'ongoing' + END AS status, + m.match_date + FROM matches m + LEFT JOIN users u1 ON m.player1_uid = u1.uid + LEFT JOIN users u2 ON m.player2_uid = u2.uid + ORDER BY + CASE + WHEN m.player1_score IS NULL AND m.player2_score IS NULL THEN 2 -- Upcoming + WHEN m.player1_score IS NOT NULL AND m.player2_score IS NOT NULL THEN 3 -- Completed + ELSE 1 -- Ongoing + END, + m.match_date; + """ + cursor.execute(query) + matches = cursor.fetchall() + return matches + finally: + cursor.close() + conn.close() + + +def get_elo(auth_header): + if not auth_header.startswith('Basic '): + raise ValueError("Invalid Authorization header") + + encoded_credentials = auth_header.split(' ', 1)[1] + decoded_credentials = base64.b64decode(encoded_credentials).decode() + + try: + email, password = decoded_credentials.split(':', 1) + except ValueError: + raise ValueError("Invalid credentials format") + + conn = get_db_connection() + cursor = conn.cursor() + try: + cursor.execute("SELECT email, display_name, password_hash, elo FROM users WHERE email = %s;", (email,)) + user = cursor.fetchone() + + if not user: + raise ValueError("User not found") + + if not bcrypt.checkpw(password.encode(), user["password_hash"].encode()): + raise ValueError("Invalid password") + return { + "email": user["email"], + "elo": user["elo"], + "display_name": user["display_name"] + } + + except Exception as e: + raise e + + finally: + cursor.close() + conn.close() + + +def update_elo(player1_display_name, player2_display_name, player1_score, player2_score, winner): + conn = get_db_connection() + cursor = conn.cursor() + K = 32 + + try: + cursor.execute(""" + SELECT display_name, current_elo + FROM users + WHERE display_name IN (%s, %s); + """, (player1_display_name, player2_display_name)) + + players = cursor.fetchall() + + if len(players) != 2: + return {"error": "Both players must exist in the database."} + + player1_elo = players[0]["current_elo"] + player2_elo = players[1]["current_elo"] + expected_player1 = 1 / (1 + 10 ** ((player2_elo - player1_elo) / 400)) + expected_player2 = 1 / (1 + 10 ** ((player1_elo - player2_elo) / 400)) + + if winner == player1_display_name: + actual_player1 = 1 + actual_player2 = 0 + elif winner == player2_display_name: + actual_player1 = 0 + actual_player2 = 1 + else: + actual_player1 = 0.5 + actual_player2 = 0.5 + + new_player1_elo = player1_elo + K * (actual_player1 - expected_player1) + new_player2_elo = player2_elo + K * (actual_player2 - expected_player2) + + cursor.execute(""" + UPDATE users + SET current_elo = %s + WHERE display_name = %s; + """, (new_player1_elo, player1_display_name)) + + cursor.execute(""" + UPDATE users + SET current_elo = %s + WHERE display_name = %s; + """, (new_player2_elo, player2_display_name)) + + conn.commit() + + return { + "player1_display_name": player1_display_name, + "new_player1_elo": new_player1_elo, + "player2_display_name": player2_display_name, + "new_player2_elo": new_player2_elo + } + + finally: + cursor.close() + conn.close() \ No newline at end of file diff --git a/main.py b/main.py index bb9eb06..0f65fb2 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,16 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel -from calls import register_user, authenticate_user, add_friend, send_match_invite, accept_match_invite +from fastapi.middleware.cors import CORSMiddleware +from calls import register_user, authenticate_user, reauth_user, add_friend, send_match_invite, accept_match_invite, get_all_matches, get_elo, update_elo app = FastAPI() - +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Replace "*" with your allowed origins + allow_credentials=True, + allow_methods=["*"], # Allow all HTTP methods + allow_headers=["*"], # Allow all HTTP headers +) class RegisterRequest(BaseModel): email: str display_name: str @@ -35,9 +42,9 @@ def register(request: RegisterRequest): @app.post("/login") def login(request: LoginRequest): - uid = authenticate_user(request.email, request.password) - if uid: - return {"message": "Login successful", "uid": uid} + sessiontoken = authenticate_user(request.email, request.password) + if sessiontoken: + return {"message": "Login successful", "uid": sessiontoken} else: raise HTTPException(status_code=401, detail="Invalid credentials") @@ -63,4 +70,21 @@ def accept_invite(request: AcceptInviteRequest): success = accept_match_invite(request.match_id, request.player2_uid) return {"message": "Match invite accepted"} if success else HTTPException(400, "Failed to accept invite") except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) \ No newline at end of file + raise HTTPException(status_code=400, detail=str(e)) + +@app.get("/matches") +def get_matches(): + try: + matches = get_all_matches() + return {"matches": matches} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@app.post("/elo") +def get_elo_endpoint(authorization: str): + print(f"Received Authorization header: {authorization}") + try: + result = get_elo(authorization) + return result + except Exception as e: + raise HTTPException(status_code=400, detail=str(e))