diff --git a/api/blueprints/mail/oauth_callback.py b/api/blueprints/mail/oauth_callback.py index d1bcb4b..635a5cc 100644 --- a/api/blueprints/mail/oauth_callback.py +++ b/api/blueprints/mail/oauth_callback.py @@ -40,7 +40,7 @@ sys.path.append(".") sys.path.append("..") # For using Quart: -from quart import Blueprint, current_app, request, render_template +from quart import Blueprint, current_app, g, request, render_template # My utils: from utils_v2.string import json @@ -108,6 +108,81 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- +async def handle_gmail_callback(): + + """ + To handle the callbacks from GMail specifically. Refer to the individual comments to check what hap[pens at each + step of the process. + :return: A rendered template of the final status of the authorization. + """ + + # Start by assuming failure: + tokens_saved = False + + # In case the user cancelled halfway through (on Google's screen): + if g.inbound_data.get("error") == "access_denied": return await render_template( + "/mail/oauth/oauth_cancelled_v2.html", + mail_client = g.mail_client.title() + ) + + # Generate the tokens from the callback. Google sends all the needed params in the callback as the URL's query + # params. We can simply use the exact URL that was hit to generate the tokens. In Quart (and Flask) this can be + # achieved by 'request.url' like this: + tokens = await current_app.gmail_client.get_authorization_tokens( + redirect_url = request.url, + scopes = g.inbound_data["scope"] + ) + + if tokens: + + # Get the e-mail id that granted authorization. We will be comparing this to the e-mail id that had been given + # to us when the authorization was initiated. We don't mind any e-mail id being used, but we need them to be the + # same at both ends: + user_profile = await current_app.gmail_client.get_user_profile(tokens = tokens) + tokens.email = user_profile.data["emailAddress"] if user_profile.success else None + + # Here's where we do the checking of the e-mails, + # if they don't match, we reject the authorization: + placeholder_token = await current_app.mail_oauth_model.get_token( + mongo_conn = current_app.data_mongo, + account_identifier = g.inbound_data["state"] + ) + if ( + (not placeholder_token) or + placeholder_token["clientUserId"] != str(tokens.email) + ): return await render_template( + "/mail/oauth/oauth_failure_v2.html", + mail_client = g.mail_client.title(), + failure_hint = f"We were expecting authorization from '{placeholder_token['clientUserId']['email']}' but got authorization from '{tokens.email}' instead." + ) + + # Now that we have passed the check, + # we save the tokens to the database: + tokens_saved = await current_app.mail_oauth_model.set_token( + db_conn = current_app.sql_writer, + mongo_conn = current_app.data_mongo, + session_token = g.inbound_headers.get("X-Session-Token"), + account_identifier = g.inbound_data["state"], + token = tokens.model_dump() + ) + + # Return an HTML response for success: + if tokens_saved: return await render_template( + "/mail/oauth/oauth_success_v2.html", + mail_client = g.mail_client.title() + ) + + # Return an HTML response for failure: + else: return await render_template( + "/mail/oauth/oauth_failure_v2.html", + mail_client = g.mail_client.title(), + failure_hint = f"Unknown error. Please use log-id '{g.log_id}' to check with the support team." + ) + + +# --------------------------------------------------------------------------------------------------------------------- + + @mail_callback_bp.route("/callback/", methods = ["POST", "GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @@ -141,87 +216,31 @@ async def mail_callback( :return: A standard response structure. """ - # Start by assuming failure: - tokens_saved = False + # ┓┏ ┓┓ ┓┏ • ┓ ┓ + # ┣┫┏┓┏┓┏┫┃┏┓ ┃┃┏┓┏┓┓┏┓┣┓┃┏┓┏ + # ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┛ ┗┗┻┗┛┗┗ ┛ - # ┏┓ ┏┓┳┳┓ •┓ - # ┣ ┏┓┏┓ ┃┓┃┃┃┏┓┓┃ - # ┻ ┗┛┛ ┗┛┛ ┗┗┻┗┗ + # Here we make various variables available in the scope of the current request through 'g': + g.log_id = kwargs.get("log_id") + g.inbound_headers = inbound_headers + g.inbound_data = inbound_data + g.mail_client = mail_client - if mail_client == "gmail": + # ┓┏ ┓┓ ┏┓ ┓┓┓ ┓ + # ┣┫┏┓┏┓┏┫┃┏┓ ┃ ┏┓┃┃┣┓┏┓┏┃┏┏ + # ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┗┗┗┛┗┻┗┛┗┛ - # In case the user cancelled halfway through (on Google's screen): - if inbound_data.get("error") == "access_denied": return await render_template( - "/mail/oauth/oauth_cancelled_v2.html", - mail_client = mail_client.title() - ) + if mail_client == "gmail": return await handle_gmail_callback() - # Generate the tokens from the callback: - tokens = await current_app.gmail_client.get_authorization_tokens( - redirect_url = request.url, - scopes = inbound_data["scope"] - ) + # ┓┏ ┓┓ ┳ ┓• ┓ ┏┓┓• + # ┣┫┏┓┏┓┏┫┃┏┓ ┃┏┓┓┏┏┓┃┓┏┫ ┃ ┃┓┏┓┏┓╋ + # ┛┗┗┻┛┗┗┻┗┗ ┻┛┗┗┛┗┻┗┗┗┻ ┗┛┗┗┗ ┛┗┗ - if tokens: - - # Get the e-mail id that granted authorization: - user_profile = await current_app.gmail_client.get_user_profile(tokens = tokens) - tokens.email = user_profile.data["emailAddress"] if user_profile.success else None - - # The e-mail id that we requested access to and the one that granted us access should be the same: - placeholder_token = await current_app.mail_oauth_model.get_token( - mongo_conn = current_app.data_mongo, - account_identifier = inbound_data["state"] - ) - if ( - (not placeholder_token) or - placeholder_token["clientUserId"] != str(tokens.email) - ): return await render_template( - "/mail/oauth/oauth_failure_v2.html", - mail_client = mail_client.title(), - failure_hint = f"We were expecting authorization from '{placeholder_token['clientUserId']}' but got authorization from '{tokens.email}' instead." - ) - - # Save the tokens to the database - tokens_saved = await current_app.mail_oauth_model.set_token( - db_conn = current_app.sql_writer, - mongo_conn = current_app.data_mongo, - session_token = inbound_headers.get("X-Session-Token"), - account_identifier = inbound_data["state"], - token = tokens.model_dump() - ) - - # tokens_saved = False - - # ┳┓ - # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ - # ┛┗┗ ┛┣┛┗┛┛┗┛┗ - # ┛ - - # # Return a JSON response: - # return ResponseModel( - # status_code = StatusCodes.OK if tokens_saved else StatusCodes.FAILED, - # http_code = HttpCodes.SUCCESS if tokens_saved else HttpCodes.INTERNAL_SERVER_ERROR, - # data = { - # "mailClient": mail_client, - # "authorized": True - # } - # ) - - # Return an HTML response for success: - if tokens_saved: - return await render_template( - "/mail/oauth/oauth_success_v2.html", - mail_client = mail_client.title() - ) - - # Return an HTML response for failure: - else: - return await render_template( - "/mail/oauth/oauth_failure_v2.html", - mail_client = mail_client.title(), - failure_hint = f"Unknown error. Please use log-id '{kwargs['logId']}' to check with the support team." - ) + return await render_template( + "/mail/oauth/oauth_failure_v2.html", + mail_client = mail_client.title(), + failure_hint = f"Invalid client '{mail_client}' selected. Please use log-id '{g.log_id}' to check with the support team." + ) # *****************************************************************************************************************