From e5de9dace1b06ebc9f7047faa2684e12472fa709 Mon Sep 17 00:00:00 2001 From: khushal Date: Thu, 16 Jan 2025 19:10:22 +0530 Subject: [PATCH] (20250116) Day-end push. --- api/blueprints/message/mail/oauth/callback.py | 326 ------------------ api/blueprints/message/mail/oauth/request.py | 241 ------------- 2 files changed, 567 deletions(-) delete mode 100644 api/blueprints/message/mail/oauth/callback.py delete mode 100644 api/blueprints/message/mail/oauth/request.py diff --git a/api/blueprints/message/mail/oauth/callback.py b/api/blueprints/message/mail/oauth/callback.py deleted file mode 100644 index 721d963..0000000 --- a/api/blueprints/message/mail/oauth/callback.py +++ /dev/null @@ -1,326 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - Monday, 25th Nov., 2024 - - OBJECTIVE: - - To receive callbacks (webhooks). - - REFERENCES: - - N/A - - DOWNLOADS: - - N/A - - NOTES: - - N/A - -""" - - -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys -sys.path.append(".") -sys.path.append("..") - -# For using Quart: -from quart import Blueprint, current_app, g, request, render_template - -# My utils: -from utils_v2.string import json -from utils_v2.logging.context import AsyncLoggerContext -from utils_v2.api.codes import StatusCodes, HttpCodes -from utils_v2.api.response import ResponseModel -from utils_v2.api.async_quart import ( - set_api_version, - read_input, - get_session_info, - log_request_to_mongo, - log_chain_to_mongo, - should_not_be_under_maintenance, - only_whitelisted_ips, - limit_rate, - validate_input, - handle_cancelled_request -) - -# GMail-related utils: -from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT - -# Data Models: -from models.core.auth_token import CoreAuthTokenModel - -# Common: -from shared import constants - -# For asynchronous activities: -import asyncio - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -# Related to Quart: -mail_oauth_callback_bp = Blueprint("mail_cb", __name__) - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -@mail_oauth_callback_bp.record_once -def init(blueprint_setup_state): - - # This gets called when the blueprint is registered. - # Consider this to be a one-time setup for the whole blueprint: - pass - - -# --------------------------------------------------------------------------------------------------------------------- - - -@AsyncLoggerContext.log_it( - api_version = "1.0.0", - project = constants.PROJECT_NAME, - log_type = constants.MODULE_NAME, - operation = "gmailClbk", - log_input = 2, - log_output = 1, - sensitive_keys = ["sessionToken", "X-Session-Token"] -) -async def handle_gmail_callback() -> render_template: - - """ - 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 (HTML) 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( - "/message/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: - google_tokens = await current_app.gmail_client.get_authorization_tokens( - redirect_url = request.url, - scopes = None - ) - - if google_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 = google_tokens) - if user_profile.success: - google_tokens.email = user_profile.data["emailAddress"] - google_tokens.displayName = user_profile.data["displayName"] - google_tokens.displayPictureUrl = user_profile.data["displayPictureUrl"] - - # Here's where we do the checking of the e-mails, - # if they don't match, we reject the authorization: - auth_token = await current_app.mail_controller.get_token_from_key( - mongo_conn = current_app.data_mongo, - token_key = g.inbound_data["state"] - ) - if ( - (not auth_token) or - auth_token.clientUserId["email"] != str(google_tokens.email) - ): return await render_template( - "/message/mail/oauth/oauth_failure_v2.html", - mail_client = g.mail_client.title(), - failure_hint = ( - f"We were expecting authorization from '{auth_token.clientUserId['email']}', " - f"but got authorization from '{google_tokens.email}' instead." - ) - ) - - # We create standard labels that we will use: - labels = [ - { - "name": "TCAOFF", - "textColor": "#434343", - "backgroundColor": "#e7e7e7" - }, - { - "name": "CA-Doc", - "textColor": "#434343", - "backgroundColor": "#e7e7e7" - }, - { - "name": "CA-AI", - "textColor": "#434343", - "backgroundColor": "#e7e7e7" - } - ] - tasks = [ - current_app.gmail_client.create_label( - tokens = google_tokens, - label_name = label["name"], - label_visibility = "labelShow", - message_visibility = "show", - label_text_color = label["textColor"], - label_background_color = label["backgroundColor"] - ) for label in labels - ] - client_responses = await asyncio.gather(*tasks) - - # Add the labels to the tokens data: - client_response = await current_app.gmail_client.list_labels(tokens = google_tokens) - google_tokens.labels = client_response.data if client_response.success else None - - # Now that we have passed the check, - # we save the tokens to the database: - auth_token.clientUserId = google_tokens.client_user_id - auth_token.token = google_tokens.model_dump() - auth_token.status = "active" - tokens_saved = await current_app.mail_controller.set_token( - db_conn = current_app.sql_writer, - mongo_conn = current_app.data_mongo, - session_token = g.inbound_headers.get("X-Session-Token"), - token_key = g.inbound_data["state"], - auth_token = auth_token - ) - - # Return an HTML response for success: - if tokens_saved: return await render_template( - "/message/mail/oauth/oauth_success_v2.html", - mail_client = g.mail_client.title() - ) - - # Return an HTML response for failure: - else: return await render_template( - "/message/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_oauth_callback_bp.route("/callback/", methods = ["POST", "GET"]) -@set_api_version(api_version = "1.0.0") -@read_input(sanitize_headers = False, sanitize_data = False) -@log_request_to_mongo( - attr_name = "logs_mongo", - project = constants.PROJECT_NAME, - log_type = constants.MODULE_NAME, - operation = "gmailCllBckApi", - log_input = True, - log_output = True, - sensitive_keys = None -) -@log_chain_to_mongo(attr_name = "logs_mongo") -@should_not_be_under_maintenance(attr_name = "is_under_maintenance") -@handle_cancelled_request() -async def mail_auth_callback( - mail_client: str = None, - inbound_headers: dict = None, - inbound_data: dict = None, - inbound_files: dict = None, - **kwargs -): - - """ - Use this when authorizing access to someone's GMail account. This can be used to capture the authentication token. - :param mail_client: The mail company/brand that you want the authorization from. - :param inbound_headers: auto-extracted by the decorators. - :param inbound_data: auto-extracted by the decorators. - :param inbound_files: auto-extracted by the decorators. - :param kwargs: Any number of extra inputs supplied by the decorators. - :return: A standard response structure. - """ - - # ┓┏ ┓┓ ┓┏ • ┓ ┓ - # ┣┫┏┓┏┓┏┫┃┏┓ ┃┃┏┓┏┓┓┏┓┣┓┃┏┓┏ - # ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┛ ┗┗┻┗┛┗┗ ┛ - - # 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 - - # ┓┏ ┓┓ ┏┓ ┓┓┓ ┓ - # ┣┫┏┓┏┓┏┫┃┏┓ ┃ ┏┓┃┃┣┓┏┓┏┃┏┏ - # ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┗┗┗┛┗┻┗┛┗┛ - - try: - - if mail_client == "gmail": return await handle_gmail_callback() - - # If something goes wrong: - except Exception as exception: return await render_template( - "/message/mail/oauth/oauth_failure_v2.html", - mail_client = mail_client.title(), - failure_hint = ( - f"An internal server error occurred. " - f"Please use log-id '{g.log_id}' to check with the support team." - ) - ) - - # ┓┏ ┓┓ ┳ ┓• ┓ ┏┓┓• - # ┣┫┏┓┏┓┏┫┃┏┓ ┃┏┓┓┏┏┓┃┓┏┫ ┃ ┃┓┏┓┏┓╋ - # ┛┗┗┻┛┗┗┻┗┗ ┻┛┗┗┛┗┻┗┗┗┻ ┗┛┗┗┗ ┛┗┗ - - return await render_template( - "/message/mail/oauth/oauth_failure_v2.html", - mail_client = mail_client.title(), - failure_hint = ( - f"Invalid client '{mail_client}' selected. " - f"Please use log-id '{g.log_id}' to check with the support team." - ) - ) - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - pass diff --git a/api/blueprints/message/mail/oauth/request.py b/api/blueprints/message/mail/oauth/request.py deleted file mode 100644 index aaf8e67..0000000 --- a/api/blueprints/message/mail/oauth/request.py +++ /dev/null @@ -1,241 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - Wednesday, 27th Nov., 2024 - - OBJECTIVE: - - To receive authorization requests (OAuth2.0) for various mail providers and accordingly respond with the - authorization request URLs. - - REFERENCES: - - N/A - - DOWNLOADS: - - N/A - - NOTES: - - N/A - -""" - - -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys -sys.path.append(".") -sys.path.append("..") - -# For using Quart: -from quart import Blueprint, current_app, request - -# My utils: -from utils_v2.string import json -from utils_v2.api.codes import StatusCodes, HttpCodes -from utils_v2.api.response import ResponseModel -from utils_v2.api.async_quart import ( - set_api_version, - read_input, - get_session_info, - log_request_to_mongo, - log_chain_to_mongo, - should_not_be_under_maintenance, - only_whitelisted_ips, - limit_rate, - validate_input, - handle_cancelled_request -) - -# GMail-related utils: -from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT - -# Common: -from shared import constants - -# Data Models: -from models.api.message.mail.oauth import ( - OAuthMailAuthorizationRequestHeaders, - OAuthMailAuthorizationRequestData -) -from models.core.auth_token import CoreAuthTokenModel - -# For asynchronous activities: -import asyncio - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -# Related to Quart: -mail_oauth_request_bp = Blueprint("mail_oauth", __name__) - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -@mail_oauth_request_bp.record_once -def init(blueprint_setup_state): - - # This gets called when the blueprint is registered. - # Consider this to be a one-time setup for the whole blueprint: - pass - - -# --------------------------------------------------------------------------------------------------------------------- - - -@mail_oauth_request_bp.route("/oauth", methods = ["GET"]) -@set_api_version(api_version = "1.0.0") -@read_input(sanitize_headers = False, sanitize_data = False) -@get_session_info(key = "X-Session-Token", session_coro = "get_session") -@log_request_to_mongo( - attr_name = "logs_mongo", - project = constants.PROJECT_NAME, - log_type = constants.MODULE_NAME, - operation = "mailOAuthUrlReqApi", - log_input = True, - log_output = True, - sensitive_keys = ["sessionToken", "X-Session-Token"] -) -@log_chain_to_mongo(attr_name = "logs_mongo") -@should_not_be_under_maintenance(attr_name = "is_under_maintenance") -@validate_input( - header_validator = lambda x: OAuthMailAuthorizationRequestHeaders(**x).model_dump(), - data_validator = lambda x: OAuthMailAuthorizationRequestData(**x) -) -@handle_cancelled_request() -async def request_oauth_authorization_url( - inbound_headers: dict | OAuthMailAuthorizationRequestHeaders = None, - inbound_data: dict | OAuthMailAuthorizationRequestData = None, - inbound_files: dict = None, - **kwargs -): - - """ - Use this when requesting access to someone's GMail account. This API should be used from the UI. A button click - "Connect to GMail" should hit this API, which will generate a request to gain access to the user's GMail account. - When the URL is hit, it opens Google's own UI, and, when the user clicks "Continue", Google hits your 'redirect_url' - to inform you about the user's action. - :param inbound_headers: auto-extracted by the decorators. - :param inbound_data: auto-extracted by the decorators. - :param inbound_files: auto-extracted by the decorators. - :param kwargs: Any number of extra inputs supplied by the decorators. - :return: A standard response structure. - """ - - # ┏┓ - # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ - # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ - # ┛ - - # If the session token is invalid/expired: - if kwargs.get("session_info") is None: - return ResponseModel( - status_code = StatusCodes.FAILED, - http_code = HttpCodes.UNAUTHORIZED - ) - - # Start by assuming failure: - auth_url = None - - # ┏┓ ┏┳┓ ┓ ┓┏┓ - # ┃┓┏┓┏┓┏┓┏┓┏┓╋┏┓ ┃ ┏┓┃┏┏┓┏┓ ┃┫ ┏┓┓┏ - # ┗┛┗ ┛┗┗ ┛ ┗┻┗┗ ┻ ┗┛┛┗┗ ┛┗ ┛┗┛┗ ┗┫ - # ┛ - - # Make a user identifier from the session info: - token_key = await current_app.mail_controller.generate_token_key( - db_conn = current_app.sql_writer, - mongo_conn = current_app.data_mongo, - auth_token = CoreAuthTokenModel( - serviceType = "email", - client = inbound_data.mailClient, - authType = "oauth", - user = kwargs["session_info"], - clientUserId = {"email": inbound_data.mailId}, - status = "pending", - syncFreq = inbound_data.syncFreq, - ), - session_token = inbound_headers["X-Session-Token"] - ) - if token_key is None: - return ResponseModel( - status_code = StatusCodes.FAILED, - http_code = HttpCodes.INTERNAL_SERVER_ERROR, - message = "Failed to generate token key." - ) - - # ┏┓ ┏┓┳┳┓ •┓ - # ┣ ┏┓┏┓ ┃┓┃┃┃┏┓┓┃ - # ┻ ┗┛┛ ┗┛┛ ┗┗┻┗┗ - - if inbound_data.mailClient == "gmail": - - # Get the authorization URL: - auth_url = await current_app.gmail_client.get_authorization_url( - scopes = SCOPES_GMAIL_MAIL_MANAGEMENT, - state = str(token_key), - access_type = "offline", - approval_prompt = "force", - include_granted_scopes = "true", - user_email = inbound_data.mailId - ) - - # ┳┓ - # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ - # ┛┗┗ ┛┣┛┗┛┛┗┛┗ - # ┛ - - # Done here: - return ResponseModel( - status_code = StatusCodes.OK if auth_url else StatusCodes.FAILED, - http_code = HttpCodes.SUCCESS if auth_url else HttpCodes.INTERNAL_SERVER_ERROR, - data = { - "client": inbound_data.mailClient, - "authorizationUrl": auth_url - } - ) - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - pass