diff --git a/api/blueprints/software/auth.py b/api/blueprints/software/auth.py index ab415c7..3bd2e48 100644 --- a/api/blueprints/software/auth.py +++ b/api/blueprints/software/auth.py @@ -259,6 +259,29 @@ async def authorize_software_client( else: auth_url_failed = f"https://api.thecaoffice.com/shopify/auth/template?status=0&storeName={inbound_data.auth.storeName}&storeUrl={inbound_data.auth.storeUrl}" + elif inbound_data.softwareClient == "googlePlaces": + # Make the client controller test and save the auth: + response = await current_app.places_controller.save_auth( + sql_conn=current_app.sql_writer, + mongo_data_conn=current_app.data_mongo, + auth=inbound_data.auth, + user=kwargs.get("session_info"), + session_token=inbound_headers.get("X-Session-Token") + ) + + # Note down the results: + success = response.success + message = response.message + token_id = response.token_id + + auth_url_success = "" + auth_url_failed = "" + + if token_id is not None: + auth_url_success = f"https://api.thecaoffice.com/user/auth/template?status=1&client=googlePlaces" + else: + auth_url_failed = f"https://api.thecaoffice.com/user/auth/template?status=0&client=googlePlaces&message={message}" + # ┳┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ # ┛┗┗ ┛┣┛┗┛┛┗┛┗ diff --git a/api/blueprints/software/google_places/get_place.py b/api/blueprints/software/google_places/get_place.py index 4a5416b..a172608 100644 --- a/api/blueprints/software/google_places/get_place.py +++ b/api/blueprints/software/google_places/get_place.py @@ -71,11 +71,10 @@ from utils_v2.goog.models.auth_tokens import GoogleAuthTokens from shared import constants # Data Models: -# from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData -from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData +from models.api.software.places.places import PlacesRequestHeaders, PlacesTextSearchRequestData, PlacesNearbyRequestData + from models.message.mail.send import MailSendOneResult from models.core.user import CoreUserInfoModel -from models.core.auth_token import CoreAuthTokenModel # To work with datatypes: from typing import Literal @@ -129,7 +128,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@google_places_bp.route("/", methods = ["POST"]) +@google_places_bp.route("/nearby", methods = ["POST"]) @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") @@ -145,13 +144,13 @@ 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: MailSendRequestHeaders(**x).model_dump(), - data_validator = lambda x: MailSendRequestData(**x) + header_validator = lambda x: PlacesRequestHeaders(**x).model_dump(), + data_validator = lambda x: PlacesNearbyRequestData(**x) ) @handle_cancelled_request() async def find_nearby( - inbound_headers: dict | MailSendRequestHeaders = None, - inbound_data: dict | MailSendRequestData = None, + inbound_headers: dict | PlacesRequestHeaders = None, + inbound_data: dict | PlacesNearbyRequestData = None, inbound_files: dict = None, **kwargs ): @@ -267,13 +266,13 @@ async def find_nearby( @log_chain_to_mongo(attr_name = "logs_mongo") @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @validate_input( - header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(), - data_validator = lambda x: MailSendRequestData(**x) + header_validator = lambda x: PlacesRequestHeaders(**x).model_dump(), + data_validator = lambda x: PlacesTextSearchRequestData(**x) ) @handle_cancelled_request() async def text_search( - inbound_headers: dict = None, - inbound_data: dict = None, + inbound_headers: dict | PlacesRequestHeaders = None, + inbound_data: dict | PlacesTextSearchRequestData = None, inbound_files: dict = None, **kwargs ): @@ -303,11 +302,11 @@ async def text_search( # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ # ┛ - + print(inbound_data) # Get the token based on the key: auth_token = await current_app.places_controller.get_token_from_key( mongo_data_conn = current_app.data_mongo, - token_key = inbound_data["tokenKey"], + token_key = inbound_data.tokenKey, must_be_active = True ) print("AUTH:", auth_token) @@ -334,10 +333,10 @@ async def text_search( # If the controller and connector were matched: if client_controller is not None and client_connector is not None: - send_result = await client_controller.text_query_search( + send_result = await client_connector.text_query_search( tokens=auth_token, - text_query=inbound_data["textQuery"], - max_count=inbound_data["maxCount"] + text_query=inbound_data.textQuery, + max_count=inbound_data.maxCount ) # ┳┓ diff --git a/controllers_v2/software/google_places/base.py b/controllers_v2/software/google_places/base.py index 22c9bf0..0abfae6 100644 --- a/controllers_v2/software/google_places/base.py +++ b/controllers_v2/software/google_places/base.py @@ -52,20 +52,12 @@ from utils_v2.rest.models.api_call import ApiResponse from models.core.user import CoreUserInfoModel from models.core.auth_token import CoreAuthTokenModel from models.core.user import CoreUserInfoModel -from models.api.message.mail.oauth import ( - OAuthMailAuthorizationRequestHeaders, - OAuthMailAuthorizationRequestData -) -from models.message.mail.oauth import OAuthMailGetAuthorizationURLResponse, OAuthMailHandleCallbackResponse -from models.core.message import CoreMessageModel +from models.api.software.places.oauth import PlacesAuthResponse + # To make HTTP requests: import httpx -# to work with MongoDB: -from bson.objectid import ObjectId -# Mail Client(s): -from utils_v2.goog.controllers.places.places_client import AsyncPlacesClient # To make abstract classes: from abc import ABC, abstractmethod @@ -173,80 +165,22 @@ class GooglePlacesController(CoreSoftwareController, ABC): debug_only_errors = debug_only_errors ) - # ┏┓┏┓ ┓ ┏┓ ┏┓ - # ┃┃┣┫┓┏╋┣┓┏┛ ┃┫ - # ┗┛┛┗┗┻┗┛┗┗━•┗┛ - @abstractmethod - async def get_authorization_url( + async def save_auth( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, - mail_client: AsyncPlacesClient, - user_info: CoreUserInfoModel, - inbound_data: OAuthMailAuthorizationRequestData, + auth: CoreAuthTokenModel, + user: CoreUserInfoModel, session_token: str - ) -> OAuthMailGetAuthorizationURLResponse: + ) -> PlacesAuthResponse: """ - To accept an incoming request for mail integration and provide a URL that the user can use to authorize your - service to access his mail inbox. + Checks if a particular set of incoming credentials give access to a valid server and then stores the + credentials. :param sql_conn: The database connection to use to perform this task. :param mongo_data_conn: The database connection to use to perform this task. - :param mail_client: The instance of the third-party mail client that will be used to get the URL. - :param user_info: The information about your user who is trying to use this system. - :param inbound_data: The data that came in with the request (API call). - :param session_token: The session token of the user. - :return: A structure response with details about the URL generation process. - """ - - pass - - @abstractmethod - async def handle_authorization_callback( - self, - sql_conn: AsyncMySQL, - mongo_data_conn: AsyncMongo, - mail_client: AsyncPlacesClient, - request_url: str, - inbound_data: dict, - session_token: str = None - ) -> OAuthMailHandleCallbackResponse: - """ - To handle the authorization callback for the mail client. The user may grant or deny authorization. - :param sql_conn: The database connection to use to perform this task. - :param mongo_data_conn: The database connection to use to perform this task. - :param mail_client: The instance of the third-party mail client that will be used to get the URL. - :param request_url: The full callback URL invoked by the third-party client. - :param inbound_data: The data that came in with the request (API call). - :param session_token: The session token of the user. It is expected that this will be null in all cases. - :return: A structured response of the process of handling the mail callback. - """ - - pass - - @abstractmethod - async def refresh_authorization( - self, - sql_conn: AsyncMySQL, - mongo_data_conn: AsyncMongo, - mail_client: AsyncPlacesClient, - http_client: httpx.AsyncClient, - auth_token: CoreAuthTokenModel, - force_refresh: bool = False, - session_token: str = None - ) -> CoreAuthTokenModel: - """ - To refresh the third-party client's access/authorization token(s) before use. - :param sql_conn: The database connection to use when storing the refreshed tokens. - :param mongo_data_conn: The database connection to use when storing the refreshed tokens. - :param mail_client: The connection of the third-party mail client. - :param http_client: The HTTP client to use to make the token refresh request. - :param auth_token: The auth-token model of the existing integration. This may get updated if a refresh is needed - (or forced). - :param force_refresh: Whether, or not, you would like to force a refresh even if the token hasn't expired yet. - :param session_token: The session token of the user. This will be null if this method is invoked by a cron - script in the background. Needed only to identify the user in case of a failure to send a timely alert. - :return: The same auth-token model instance, but maybe with updated tokens. + :param auth: The set of credentials as received from the UI/API. + :return: A structured response to indicate what happened during authorization. """ pass diff --git a/controllers_v2/software/google_places/google_places.py b/controllers_v2/software/google_places/google_places.py index 210276f..e6cbbfb 100644 --- a/controllers_v2/software/google_places/google_places.py +++ b/controllers_v2/software/google_places/google_places.py @@ -34,7 +34,7 @@ import sys sys.path.append(".") sys.path.append("..") - +import aiohttp # My async utils: from utils_v2.string import json from utils_v2.mail import mail_parser @@ -53,7 +53,8 @@ from models.core.auth_token import CoreAuthTokenModel from models.core.message import CoreMessageModel from models.api.software.places.oauth import ( OAuthPlacesAuthorizationRequestHeaders, - OAuthPlacesAuthorizationRequestData + OAuthPlacesAuthorizationRequestData, + PlacesAuthResponse ) from models.software.places.oauth import OAuthPlacesGetAuthorizationURLResponse, OAuthPlacesHandleCallbackResponse @@ -168,6 +169,81 @@ class PlacesController(GooglePlacesController): # Init a variable in a parent: self._client = self.CLIENT_NAME + async def is_google_places_api_key_valid(api_key: str) -> bool: + url = "https://places.googleapis.com/v1/places:searchText" + headers = { + "Content-Type": "application/json", + "X-Goog-Api-Key": api_key, + "X-Goog-FieldMask": "places.displayName" # minimal required field + } + payload = { + "textQuery": "Google Sydney" # dummy search text + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, json=payload, timeout=10) as response: + if response.status != 200: + return False + data = await response.json() + # If key invalid, response will contain "error" + if "error" in data: + return False + return True + except Exception: + return False + + async def save_auth( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + auth: CoreAuthTokenModel , + user: CoreUserInfoModel, + session_token: str + ) -> PlacesAuthResponse: + + # CHECK -- API KEY IS VALID - + valid_api = await PlacesController.is_google_places_api_key_valid(api_key=auth.apiKey) + if valid_api: + success, object_id = await self.set_token_direct_with_return_id( + sql_conn=sql_conn, + mongo_data_conn=mongo_data_conn, + auth_token=CoreAuthTokenModel( + serviceType=self.SERVICE_TYPE, + client=self.CLIENT_NAME, + authType="auth", + auth=auth.model_dump(), + user=user, + clientUserId={ + "email": auth.email, + "apiKey": auth.apiKey + }, + status="active", + syncFreq=60 + ), + token_notes={ + "email": auth.email, + "apiKey": auth.apiKey + }, + display_name=auth.displayName, + display_picture=None, + session_token=session_token + ) + + # Done here: + return PlacesAuthResponse( + success=success, + token_id=str(object_id), + message="Places Account Added successfully." if success else "Places Account Added failed." + ) + else: + return PlacesAuthResponse( + success=False, + token_id="", + message="Invalid Google Places API KEY, please provide valid API KEY. Thank You" + ) + + # ┏┓┏┓ ┓ ┏┓ ┏┓ # ┃┃┣┫┓┏╋┣┓┏┛ ┃┫ # ┗┛┛┗┗┻┗┛┗┗━•┗┛ diff --git a/models/api/software/auth.py b/models/api/software/auth.py index 90a71c3..b08099f 100644 --- a/models/api/software/auth.py +++ b/models/api/software/auth.py @@ -43,6 +43,7 @@ from typing import Optional, Literal, Union from models.software.tcaoff_ai.auth import TheCAOfficeAIAuth from models.software.mikrotik.auth import MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth from models.software.ecommerce.auth import ShopifyAuth +from models.software.places.oauth import GooglePlacesAuth # My utils: from utils_v2.string import regex from utils_v2.date_time import date_time @@ -105,8 +106,8 @@ class SoftwareAuthRequestHeaders(BaseModel): class SoftwareAuthRequestData(BaseModel): - softwareClient: Literal["theCaOfficeAi", "mikrotikPPPoE1000", "mikrotikHotspot1000", "shopify"] = Field(alias = "client") - auth: Union[TheCAOfficeAIAuth, MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth, ShopifyAuth] + softwareClient: Literal["theCaOfficeAi", "mikrotikPPPoE1000", "mikrotikHotspot1000", "shopify", "googlePlaces"] = Field(alias = "client") + auth: Union[TheCAOfficeAIAuth, MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth, ShopifyAuth, GooglePlacesAuth] # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ @@ -128,7 +129,8 @@ class SoftwareAuthRequestData(BaseModel): "theCaOfficeAi": TheCAOfficeAIAuth, "mikrotikPPPoE1000": MikroTikPPPoE1000Auth, "mikrotikHotspot1000": MikroTikHotspot1000Auth, - "shopify": ShopifyAuth + "shopify": ShopifyAuth, + "googlePlaces": GooglePlacesAuth } if not isinstance(auth, harmony_map[client]): raise ValueError(f"incorrect 'auth' for selected client '{client}'") diff --git a/models/api/software/places/oauth.py b/models/api/software/places/oauth.py index bdd0949..de268a6 100644 --- a/models/api/software/places/oauth.py +++ b/models/api/software/places/oauth.py @@ -38,7 +38,7 @@ sys.path.append("..") # For making data behaviour_models: from pydantic import BaseModel, Field, field_validator -from typing import Optional, Literal +from typing import Optional, Literal, Any # My utils: from utils_v2.string import regex @@ -123,6 +123,56 @@ class OAuthPlacesAuthorizationRequestData(BaseModel): pass +# ---------------------------------------------------------------------------------------------------------------------- + + + +class PlacesAuthResponse(BaseModel): + + success: bool = Field( + description = "To indicate whether or not, the action was a success", + frozen = False, + default = False + ) + + token_id : str = Field( + description="MongoDb object id", + frozen=False, + default=False + ) + + message: str = Field( + description = "To explain what happened in the process of handling the OAuth callback.", + frozen = False, + default = "ERR: Message not captured." + ) + + exception: Any = Field( + description = "To pass on any exception that occurred in the process.", + frozen = False, + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + pass + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + pass + # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** diff --git a/models/api/software/places/places.py b/models/api/software/places/places.py index cb55277..5d70a77 100644 --- a/models/api/software/places/places.py +++ b/models/api/software/places/places.py @@ -345,15 +345,15 @@ class PlacesTextSearchRequestData(BaseModel): frozen = True ) - textQuery: float = Field( + textQuery: str = Field( description= "Text Query for find places", frozen= True ) - max_count: int = Field( + maxCount: int = Field( description= "Max count for get result counts", frozen=True, - default=20 + default=5 ) diff --git a/models/software/places/oauth.py b/models/software/places/oauth.py index 51c61ba..a4215dd 100644 --- a/models/software/places/oauth.py +++ b/models/software/places/oauth.py @@ -173,6 +173,41 @@ class OAuthPlacesHandleCallbackResponse(BaseModel): pass +class GooglePlacesAuth(BaseModel): + email: str = Field( + description="Email for google places API", + frozen=False + ) + + apiKey: str = Field( + description="API KEY of Places API", + frozen=True, + ) + + displayName: str = Field( + description="Display Name", + frozen=True, + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + pass + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + pass # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM ***