diff --git a/api/blueprints/mail/oauth_callback.py b/api/blueprints/mail/oauth_callback.py index 310bbee..9cc1fbc 100644 --- a/api/blueprints/mail/oauth_callback.py +++ b/api/blueprints/mail/oauth_callback.py @@ -211,7 +211,7 @@ async def handle_gmail_callback() -> render_template: mongo_conn = current_app.data_mongo, session_token = g.inbound_headers.get("X-Session-Token"), token_id = g.inbound_data["state"], - email_id = tokens.email, + client_user_id = {"email": tokens.email}, token = tokens.model_dump() ) diff --git a/api/blueprints/mail/oauth_request.py b/api/blueprints/mail/oauth_request.py index 3162e6d..0dca750 100644 --- a/api/blueprints/mail/oauth_request.py +++ b/api/blueprints/mail/oauth_request.py @@ -180,7 +180,7 @@ async def request_oauth_authorization_url( mongo_conn = current_app.data_mongo, session_token = inbound_headers["X-Session-Token"], user_info = kwargs["session_info"], - email_id = inbound_data.mailId, + client_user_id = {"email": inbound_data.mailId}, service_client = inbound_data.mailClient, auth_type = "oauth", sync_freq = inbound_data.syncFreq diff --git a/api/blueprints/sms/auth.py b/api/blueprints/sms/auth.py index 8f5bb28..959e5f7 100644 --- a/api/blueprints/sms/auth.py +++ b/api/blueprints/sms/auth.py @@ -67,10 +67,7 @@ from utils_v2.sms.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS from shared import constants # Data Models: -from models.data.mail.oauth import ( - OAuthMailAuthorizationRequestHeaders, - OAuthMailAuthorizationRequestData -) +from models.data.sms.auth import SMSAuthRequestHeaders, SMSAuthRequestData # For asynchronous activities: import asyncio @@ -131,22 +128,19 @@ def init(blueprint_setup_state): @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) + header_validator = lambda x: SMSAuthRequestHeaders(**x).model_dump(), + data_validator = lambda x: SMSAuthRequestData(**{"client": x}) ) @handle_cancelled_request() async def request_oauth_authorization_url( - inbound_headers: dict | OAuthMailAuthorizationRequestHeaders = None, - inbound_data: dict | OAuthMailAuthorizationRequestData = None, + inbound_headers: dict | SMSAuthRequestHeaders = None, + inbound_data: dict | SMSAuthRequestData = 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. + Use this when a user wants to register a third-party SMS client with your service. :param inbound_headers: auto-extracted by the decorators. :param inbound_data: auto-extracted by the decorators. :param inbound_files: auto-extracted by the decorators. @@ -167,7 +161,7 @@ async def request_oauth_authorization_url( ) # Start by assuming failure: - auth_url = None + auth_success = None # ┳ ┓ •┏ ┳┳ # ┃┏┫┏┓┏┓╋┓╋┓┏ ┃┃┏┏┓┏┓ diff --git a/models/behaviour/mail/oauth_v2.py b/models/behaviour/mail/oauth_v2.py index ad79140..ac3a4df 100644 --- a/models/behaviour/mail/oauth_v2.py +++ b/models/behaviour/mail/oauth_v2.py @@ -100,7 +100,7 @@ class MailOAuthModel(BaseModel): db_conn: AsyncMySQL, mongo_conn: AsyncMongo, user_info: dict, - email_id: str, + client_user_id: dict, service_client: Literal["gmail"], auth_type: Literal["oauth"], sync_freq: Literal[60, 300, 900] = 300, @@ -113,7 +113,7 @@ class MailOAuthModel(BaseModel): :param db_conn: The database connection (MariaDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action. :param user_info: The dictionary that has the user's session information. - :param email_id: The e-mail id that the user wants to connect to your service. + :param client_user_id: The way the third-party client recognizes your user. :param service_client: The name of the company or brand that is providing this service that is being integrated. :param auth_type: To identify the type of authentication being done here. This could indicate simple password authentication, more advance OAuth2.0 authentication, etc. @@ -134,9 +134,7 @@ class MailOAuthModel(BaseModel): "entityId": user_info["entityId"], "billingAccountId": user_info["billingAccountId"] }, - "clientUserId": { - "email": email_id - } + "clientUserId": client_user_id }), update = { "$set": { @@ -148,6 +146,7 @@ class MailOAuthModel(BaseModel): "client": service_client, "authType": auth_type, "user": user_info, + "clientUserId": client_user_id, "token": None, "firstRefreshTs": None, "lastRefreshTs": None, @@ -191,7 +190,7 @@ class MailOAuthModel(BaseModel): db_conn: AsyncMySQL, mongo_conn: AsyncMongo, token_id: ObjectId | str, - email_id: str, + client_user_id: dict, token: dict, session_token: str = None ) -> bool: @@ -203,8 +202,8 @@ class MailOAuthModel(BaseModel): :param db_conn: The database connection (MariaDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action. :param token_id: The identifier granted by the 'get_token_id' method. - :param email_id: The e-mail id that the user tried to connect to your service. This should match the e-mail id - the user claimed he wants to connect when he used 'get_token_identifier'. + :param client_user_id: The way the third-party client recognizes your user. These details should match the + details furnished while requesting the authorization through 'get_token_id' method. :param token: The token granted by the third-party service. :param session_token: The session token of the user who requested this service. :return: True if saved, False if failed. @@ -221,9 +220,7 @@ class MailOAuthModel(BaseModel): collection = MailOAuthModel.AUTH_COLLECTION, filter = mongo_conn.dict_to_dot_notation({ "_id": ObjectId(token_id), - "clientUserId": { - "email": email_id - } + "clientUserId": client_user_id }), update = { "$set": { diff --git a/models/behaviour/sms/__init__.py b/models/behaviour/sms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/behaviour/sms/auth.py b/models/behaviour/sms/auth.py new file mode 100644 index 0000000..9ab75bb --- /dev/null +++ b/models/behaviour/sms/auth.py @@ -0,0 +1,236 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 5th Dec., 2024 + + OBJECTIVE: + + To work with auth details of SMS clients like Nimbus SMS (India) and Savvy Bulk SMS (Kenya). + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.string import json +from utils_v2.date_time import date_time +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo + +# Base model: +from models.behaviour.base import BaseModel + +# To work with MongoDB: +from bson import ObjectId + +# To work with datatypes: +from typing import Literal + +# To make deep-copies: +import copy + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSAuthModel(BaseModel): + + AUTH_COLLECTION = "_authTokens" + + async def set_token( + self, + db_conn: AsyncMySQL, + mongo_conn: AsyncMongo, + user_info: dict, + client_user_id: dict, + service_client: Literal["nimbusSmsIndia", "savvyBulkSmsKenya"], + auth_type: Literal["auth"], + sync_freq: Literal[60, 300, 900] = 300, + session_token: str = None + ) -> ObjectId: + + """ + Stores params from the session info and gives an identifier to use in the authorization URL. Use this when the + user requests an authorization URL to link your service to another service (like GMail). + :param db_conn: The database connection (MariaDB) to use to perform the action. + :param mongo_conn: The database connection (MongoDB) to use to perform the action. + :param user_info: The dictionary that has the user's session information. + :param client_user_id: The way the third-party client recognizes your user. + :param service_client: The name of the company or brand that is providing this service that is being integrated. + :param auth_type: To identify the type of authentication being done here. This could indicate simple password + authentication, more advance OAuth2.0 authentication, etc. + :param sync_freq: The time interval in which mails need to be sync'd. Specify this in seconds. + :param session_token: The session token of the user who requested this service. + :return: An ObjectId to later store the granted tokens. + """ + + # Note down the timestamp at which this event occurred: + request_ts = date_time.get_current_utc_date_time(as_string = False) + + # Get the identifier from the database: + mongo_json = await mongo_conn.find_one_and_update( + collection = self.AUTH_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "serviceType": "email", + "user": { + "entityId": user_info["entityId"], + "billingAccountId": user_info["billingAccountId"] + }, + "clientUserId": client_user_id + }), + update = { + "$set": { + "lastRequestTs": request_ts + }, + "$setOnInsert": { + "version": "1.1.1", + "serviceType": "email", + "client": service_client, + "authType": auth_type, + "user": user_info, + "clientUserId": client_user_id, + "token": None, + "firstRefreshTs": None, + "lastRefreshTs": None, + "firstRequestTs": request_ts, + "status": "active", + "syncFreq": max(sync_freq, 60) + } + }, + projection = { + "_id": True + }, + upsert = True, + return_updated = True + ) + + # Tell MariaDB that an authorization request was initiated: + db_json = {} + if mongo_json is not None: + db_json = await self.call_procedure( + db_conn = db_conn, + proc_name = "entity_integration_save", + proc_args = ( + user_info["entityId"], # ............................................ 'p_entity_id' + service_client, # ................................................... 'p_provider' + "Auth Requested", # ................................................. 'p_current_status' + "Auth URL Generated", # ............................................. 'p_last_action' + None, # ............................................................. 'p_display_name' + None, # ............................................................. 'p_display_picture' + str(mongo_json["_id"]), # ........................................... 'p_token_id' + json.to_string(python_data = {"email": None}, no_space = True), # ... 'p_notes' + user_info["userId"] # ............................................... 'p_created_by' + ), + session_token = session_token + ) + + # Done here: + return mongo_json["_id"] if mongo_json and db_json.get("status") == 1 else None + + async def get_token( + self, + mongo_conn: AsyncMongo, + token_id: ObjectId | str = None, + **kwargs + ) -> dict | None: + + """ + To retrieve stored tokens from the database. + :param mongo_conn: The database connection (MongoDB) to use to perform the action. + :param token_id: The identifier granted providing auth details for the first time in 'set_token'. + :param kwargs: Any set of key-value pairs to build custom search criteria. This could be things like the user + info, the client, the type of authentication used, or even the kind of service. + :return: The retrieved record that has the token, and information about the service and client if found, else + None when there is no matching record. + """ + + # Build the filter: + filter_json = {k: v for k, v in kwargs.items()} + if token_id: filter_json["_id"] = ObjectId(token_id) + + # If there is no search criteria, we exit with failure: + if not filter_json: return None + + # If there is some filtering possible, + # we fetch and return the token: + return await mongo_conn.find_one( + collection = self.AUTH_COLLECTION, + filter = filter_json, + projection = { + "_id": True, + "serviceType": True, + "authType": True, + "client": True, + "clientUserId": True, + "token": True + } + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass