diff --git a/.gitignore b/.gitignore index 7e14a5b..76f90e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ __pycache__/ .env .envrc .DS_Store -ca-certificate.crt \ No newline at end of file +ca-certificate.crt +firebase-service-account-key.json \ No newline at end of file diff --git a/app.py b/app.py index 9720900..a1fa4df 100644 --- a/app.py +++ b/app.py @@ -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() + app = Flask(__name__) # CORS: allow frontend (different origin) to call this API diff --git a/requirements.txt b/requirements.txt index 6ef629f..fb220c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ Flask-APScheduler python-dotenv pytz gunicorn +firebase-admin \ No newline at end of file diff --git a/src/mutations/login_user.py b/src/mutations/login_user.py index b606738..b3bece6 100644 --- a/src/mutations/login_user.py +++ b/src/mutations/login_user.py @@ -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"] + user = db["users"].find_one({"firebase_uid": firebase_uid}) if not user: raise GraphQLError("User not found.") identity = str(user["_id"]) diff --git a/src/mutations/signup_user.py b/src/mutations/signup_user.py index eb6f6ae..4ff60b6 100644 --- a/src/mutations/signup_user.py +++ b/src/mutations/signup_user.py @@ -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"] + 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) identity = str(result.inserted_id) return SignupUser( diff --git a/src/schema.py b/src/schema.py index 70b5473..3bda2d0 100644 --- a/src/schema.py +++ b/src/schema.py @@ -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.",