diff --git a/api/blueprints/mail/callback.py b/api/blueprints/mail/callback.py index ea76abc..d8265fa 100644 --- a/api/blueprints/mail/callback.py +++ b/api/blueprints/mail/callback.py @@ -62,6 +62,9 @@ from utils_v2.api.async_quart import ( # Common: from shared import constants +# Behaviour Models: +from models.behaviour.mail.oauth import MailOAuthModel + # For asynchronous activities: import asyncio @@ -105,7 +108,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@mail_callback_bp.route("/callback/gmail", methods = ["POST", "GET"]) +@mail_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( @@ -121,6 +124,7 @@ def init(blueprint_setup_state): @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @handle_cancelled_request() async def mail_callback( + mail_client: str = None, inbound_headers: dict = None, inbound_data: dict = None, inbound_files: dict = None, @@ -136,39 +140,46 @@ async def mail_callback( :return: A standard response structure. """ - # Construct a message: - message = "🪝 *WEBHOOK/CALLBACK ALERT!* 🪝\n\n" - message += f"Method: *{request.method}*\nLog Id.: `{kwargs.get('log_id')}`\n\n" - message += "*Headers:*\n```json\n" - message += json.to_string({k: v for k, v in request.headers.items()}) - message += "\n```\n" - message += "*Query Args:*\n```json\n" - message += json.to_string(request.args.to_dict()) - message += "\n```\n" - message += "*JSON:*\n```json\n" - message += json.to_string(await request.get_json()) - message += "\n```\n" - message += "*Form-Data:*\n```json\n" - message += json.to_string((await request.form).to_dict()) - message += "\n```\n" - message += "*Form-Files:*\n```json\n" - message += json.to_string(inbound_files, default = str) - message += "\n```\n" + # Start by assuming failure: + tokens_saved = False - # Send a message on Telegram: - api_response = await current_app.http_client.post( - url = current_app.script_data["alerts"]["url"], - json = { - "message": message, - "type": "info", - "chatId": "1275560043" # ... KPS - } - ) + # ┏┓ ┏┓┳┳┓ •┓ + # ┣ ┏┓┏┓ ┃┓┃┃┃┏┓┓┃ + # ┻ ┗┛┛ ┗┛┛ ┗┗┻┗┗ + + if mail_client == "gmail": + + # Generate the tokens from the callback: + tokens = await current_app.gmail_client.get_authorization_tokens( + redirect_url = request.url + ) + + if tokens: + + # Add information and : + user_profile = await current_app.gmail_client.get_user_profile(tokens = tokens) + tokens.email = user_profile.data["emailAddress"] if user_profile.success else None + + # Save the tokens to the database + tokens_saved = await MailOAuthModel.set_token( + db_conn = current_app.data_mongo, + user_identifier = inbound_data["state"], + token = tokens.model_dump() + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ # Return a success response: return ResponseModel( - status_code = StatusCodes.OK, - data = {"accepted": True} + 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 + } ) diff --git a/api/blueprints/mail/oauth.py b/api/blueprints/mail/oauth.py index ea76abc..0c77ef4 100644 --- a/api/blueprints/mail/oauth.py +++ b/api/blueprints/mail/oauth.py @@ -6,11 +6,12 @@ DATE: - Monday, 25th Nov., 2024 + Wednesday, 27th Nov., 2024 OBJECTIVE: - To receive callbacks (webhooks). + To receive authorization requests (OAuth2.0) for various mail providers and accordingly respond with the + authorization request URLs. REFERENCES: @@ -62,6 +63,15 @@ from utils_v2.api.async_quart import ( # Common: from shared import constants +# Behaviour Models: +from models.behaviour.mail.oauth import MailOAuthModel + +# Data Models: +from models.data.mail.oauth import ( + OAuthMailAuthorizationRequestHeaders, + OAuthMailAuthorizationRequestData +) + # For asynchronous activities: import asyncio @@ -74,7 +84,7 @@ import asyncio # Related to Quart: -mail_callback_bp = Blueprint("mail_cb", __name__) +mail_oauth_bp = Blueprint("mail_oauth", __name__) # ***************************************************************************************************************** @@ -94,7 +104,7 @@ mail_callback_bp = Blueprint("mail_cb", __name__) # ***************************************************************************************************************** -@mail_callback_bp.record_once +@mail_oauth_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. @@ -105,30 +115,38 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@mail_callback_bp.route("/callback/gmail", methods = ["POST", "GET"]) +@mail_oauth_bp.route("/auth/oauth/url/request", 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 = "gmailCllBckApi", + operation = "mailOAuthUrlReqApi", log_input = True, log_output = True, - sensitive_keys = None + sensitive_keys = ["sessionToken"] ) @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 mail_callback( - inbound_headers: dict = None, - inbound_data: dict = None, +async def request_oauth_authorization_url( + inbound_headers: dict | OAuthMailAuthorizationRequestHeaders = None, + inbound_data: dict | OAuthMailAuthorizationRequestData = 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. + 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. @@ -136,39 +154,70 @@ async def mail_callback( :return: A standard response structure. """ - # Construct a message: - message = "🪝 *WEBHOOK/CALLBACK ALERT!* 🪝\n\n" - message += f"Method: *{request.method}*\nLog Id.: `{kwargs.get('log_id')}`\n\n" - message += "*Headers:*\n```json\n" - message += json.to_string({k: v for k, v in request.headers.items()}) - message += "\n```\n" - message += "*Query Args:*\n```json\n" - message += json.to_string(request.args.to_dict()) - message += "\n```\n" - message += "*JSON:*\n```json\n" - message += json.to_string(await request.get_json()) - message += "\n```\n" - message += "*Form-Data:*\n```json\n" - message += json.to_string((await request.form).to_dict()) - message += "\n```\n" - message += "*Form-Files:*\n```json\n" - message += json.to_string(inbound_files, default = str) - message += "\n```\n" + # ┏┓ + # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ + # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ + # ┛ - # Send a message on Telegram: - api_response = await current_app.http_client.post( - url = current_app.script_data["alerts"]["url"], - json = { - "message": message, - "type": "info", - "chatId": "1275560043" # ... KPS - } + # 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: + user_identifier = await MailOAuthModel.get_id( + db_conn = current_app.data_mongo, + user_info = kwargs["session_info"], + service_type = "email", + service_client = inbound_data.mailClient, + auth_type = "oauth" ) + if user_identifier is None: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.INTERNAL_SERVER_ERROR, + message = "failed to generate user identifier" + ) + user_identifier = str(user_identifier) - # Return a success response: + # ┏┓ ┏┓┳┳┓ •┓ + # ┣ ┏┓┏┓ ┃┓┃┃┃┏┓┓┃ + # ┻ ┗┛┛ ┗┛┛ ┗┗┻┗┗ + + if inbound_data.mailClient == "gmail": + + # Get the authorization URL: + auth_url = await current_app.gmail_client.get_authorization_url( + state = user_identifier, + access_type = "offline", + approval_prompt = "force", + include_granted_scopes = "true", + user_email = None + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: return ResponseModel( - status_code = StatusCodes.OK, - data = {"accepted": True} + status_code = StatusCodes.OK if auth_url else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if auth_url else HttpCodes.INTERNAL_SERVER_ERROR, + data = { + "mailClient": inbound_data.mailClient, + "authorizationUrl": auth_url + } ) diff --git a/api/helpers/user/session.py b/api/helpers/user/session.py index 1dfbd0d..3362a8a 100644 --- a/api/helpers/user/session.py +++ b/api/helpers/user/session.py @@ -6,7 +6,7 @@ DATE: - Tuesday, 12th Nov., 2024 + Wednesday, 27th Nov., 2024 OBJECTIVE: @@ -37,11 +37,6 @@ sys.path.append("..") # To use Quart: from quart import current_app -# The data models: -from models.data.user import ( - IsSessionRequest -) - # ***************************************************************************************************************** # ***** **** @@ -88,14 +83,19 @@ async def get_session(session_token): :return: The info or None. """ - session_info = await current_app.user.is_session( - db_conn = current_app.sql_reader, - request = IsSessionRequest(sessionToken = session_token), - cache = current_app.module_cache, - ) - - if isinstance(session_info, dict): return session_info.get("data") - else: return None + try: + raw_info = await current_app.module_cache.get(key = session_token) + session_info = { + "fullName": raw_info["value"]["full_name"], + "entityId": raw_info["value"]["entity_id"], + "billingAccountId": raw_info["value"]["billing_account_id"], + "departmentId": raw_info["value"]["department_id"], + "branchId": raw_info["value"]["branch_id"], + "industry": raw_info["value"]["industry"] + } + return session_info + except Exception as exception: + return None # ***************************************************************************************************************** diff --git a/api/main.py b/api/main.py index b37468b..5567d4c 100644 --- a/api/main.py +++ b/api/main.py @@ -54,6 +54,7 @@ from utils_v2.api import async_quart from utils_v2.date_time import date_time from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache +from utils_v2.serialization.json_serializer import JSONSerializer from utils_v2.api.async_quart import ( set_api_version, read_input, @@ -65,6 +66,9 @@ from utils_v2.api.async_quart import ( handle_cancelled_request ) +# GMail-related utils: +from utils_v2.goog.gmail.gmail_client import AsyncGMailClient, SCOPES_GMAIL_MAIL_MANAGEMENT + # To make REST API calls: import httpx @@ -72,9 +76,13 @@ import httpx from icecream import IceCreamDebugger # All the blueprints: +from api.blueprints.mail.oauth import mail_oauth_bp from api.blueprints.mail.callback import mail_callback_bp from api.blueprints.test.callback import test_callback_bp +# All the helpers: +from api.helpers.user import session + # ***************************************************************************************************************** # ***** **** @@ -98,6 +106,7 @@ APP_VERSION = constants.APP_VERSION # The Quart app: app = Quart(__name__) app = cors(app) +app.register_blueprint(mail_oauth_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_callback_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(test_callback_bp, url_prefix = f"/{MODULE_BASE}/test") @@ -163,9 +172,15 @@ async def app_startup(**kwargs): # Make an instance of an HTTP client to use to make API calls: current_app.http_client = httpx.AsyncClient( + limits = httpx.Limits( + max_connections = 100, # ............ Maximum number of connections allowed in the pool. + max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive. + ), timeout = httpx.Timeout( - 10.0, - read = 5.0 + connect = 2.5, # ... Shorter connection timeout. + read = 2.5, # ...... Like what EasyEcom gives. + write = 10.0, # .... Time to wait for sending data. + pool = 120.0 # ..... Time to wait for a free connection from the pool. ) ) @@ -194,6 +209,7 @@ async def app_startup(**kwargs): ) current_app.module_cache = AsyncRedisCache( connection_string = script_cred["redisCache"]["funcReturn"]["connectionString"], + serializer = JSONSerializer(), debug = enable_debugging, debug_prefix = "User Cache | " ) @@ -206,10 +222,32 @@ async def app_startup(**kwargs): debug = enable_debugging ) await current_app.logs_mongo.connect() + current_app.data_mongo = AsyncMongo( + connection_string = script_cred["mongoDb"]["data"]["connectionString"], + database_name = script_cred["mongoDb"]["data"]["dbName"], + max_connections = script_cred["mongoDb"]["data"]["poolSize"], + debug = enable_debugging + ) + await current_app.data_mongo.connect() + + # Create an instance to handle GMail-related activities: + current_app.gmail_client = AsyncGMailClient( + service_name = "gmail", + oauth_json = script_cred["google"]["oauth"]["tcaoff"], + http_client = current_app.http_client, + redirect_url = r"https://api.thecaoffice.com/converse/mail/callback/gmail", + scopes = SCOPES_GMAIL_MAIL_MANAGEMENT, + debug = True, + debug_prefix = "GMail (M) | ", + debug_only_errors = False + ) # Pick the important stuff: current_app.whitelisted_ips = current_app.script_data["whitelistedIps"] + # Register helpers: + current_app.get_session = session.get_session + # Remove unwanted/sensitive variables from RAM: del script_cred gc.collect() diff --git a/creds/goog/user/test_user_tokens_20241125.json b/creds/goog/user/test_user_tokens_20241125.json index e69de29..0a8d96b 100644 --- a/creds/goog/user/test_user_tokens_20241125.json +++ b/creds/goog/user/test_user_tokens_20241125.json @@ -0,0 +1,10 @@ +{ + "accessToken": "ya29.a0AeDClZAYoo85BXRId_n-hwo_amKshzi46c33GaJcsZZvGB7A7OGU2RFYcWBM_BleNBfAFUSJP2NHAvmd7Nsp_U5Kg68hXSy0iO99PNTm3pvKrJSzbkA-rXsVLsCnBIfPUMyNt2nOOVJmGwm17DNN0jAELkm1fPNTju7SZzmuaCgYKAZwSARMSFQHGX2Mis8TZui2rZT1gKySVds-N0w0175", + "refreshToken": "1//0gnqzjMf9YT19CgYIARAAGBASNgF-L9Ir3rcY37nGrV45XyOUBRllEH7Txui7T1JbwevlmDoNw7PuMu149cCWQSwsScuKaZusUQ", + "expiresIn": 3539, + "expiresAt": "2024-11-25 10:40:40.833699+00:00", + "scopes": [ + "https://www.googleapis.com/auth/gmail.labels", + "https://www.googleapis.com/auth/gmail.modify" + ] +} \ No newline at end of file diff --git a/models/behaviour/mail/oauth.py b/models/behaviour/mail/oauth.py index b3d2788..98ad909 100644 --- a/models/behaviour/mail/oauth.py +++ b/models/behaviour/mail/oauth.py @@ -6,12 +6,12 @@ DATE: - Thursday, 24th Oct., 2024 + Wednesday, 27th Nov., 2024 OBJECTIVE: - To define the interaction between the UI layer and the database connectivity in one place. Here we will handle - all user-related interactions. + To define the interaction between the UI layer and the database connectivity in one place. Here we shall handle + all the activities for OAuth2.0 authorization requests for all the users of our service. REFERENCES: @@ -35,18 +35,19 @@ import sys sys.path.append(".") sys.path.append("..") -# The base model: -from models.behaviour.base import BaseModel - # My async utils: from utils_v2.string import json -from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache -from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.date_time import date_time +from utils_v2.database.async_mongo_v2 import AsyncMongo -# The data models: -from models.data.user.user import ( - IsSessionRequest -) +# To work with MongoDB: +from bson import ObjectId + +# To work with datatypes: +from typing import Literal + +# To make deep-copies: +import copy # ***************************************************************************************************************** @@ -86,35 +87,106 @@ from models.data.user.user import ( # ***************************************************************************************************************** -class UserModel(BaseModel): +class MailOAuthModel: - async def is_session( - self, - db_conn: AsyncMySQL, - request: IsSessionRequest, - cache: AsyncRedisCache, - cache_expiry: int = 3_600 - ): + AUTH_COLLECTION = "_authTokens" + + def __init__(self): + pass + + @staticmethod + async def get_id( + db_conn: AsyncMongo, + user_info: dict, + service_type: Literal["email", "chat"], + service_client: Literal["gmail"], + auth_type: Literal["oauth"] + ) -> ObjectId: """ - Fetches information about the current user from his session. + 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 to use to perform the action. - :param request: The instance of the data model that defines the structure of the request. - :param cache: The caching object to use to set the session in cache memory. - :param cache_expiry: The no. of seconds after which this information will be deleted from the cache. - :return: The raw response from the database call (SQL). + :param user_info: The dictionary that has the user's session information. + :param service_type: The type of service being provided. + :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. + :return: An ObjectId to later store the granted tokens. """ - return await self.call_cached_procedure( - cache = cache, - cache_key = "usr_is_" + str(request.sessionToken), - cache_expiry = cache_expiry, - db_conn = db_conn, - proc_name = "isSession", - proc_args = (request.sessionToken,), - session_token = request.sessionToken + # 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: + db_json = await db_conn.find_one_and_update( + collection = MailOAuthModel.AUTH_COLLECTION, + filter = { + "serviceType": service_type, + "client": service_client, + "authType": auth_type, + "user": user_info, + }, + update = { + "$set": { + "lastRequestTs": request_ts + }, + "$setOnInsert": { + "version": "1.0.0", + "serviceType": service_type, + "client": service_client, + "authType": auth_type, + "user": user_info, + "token": None, + "firstRefreshTs": None, + "lastRefreshTs": None, + "firstRequestTs": request_ts, + } + }, + projection = { + "_id": True + }, + upsert = True, + return_updated = True ) + # Done here: + return db_json["_id"] if db_json else None + + @staticmethod + async def set_token( + db_conn: AsyncMongo, + user_identifier: ObjectId | str, + token: dict + ) -> bool: + + """ + This method is to be called when the end user authorizes your service to connect to his third-party account. For + example, when the end user allows you to access his GMail account. + :param db_conn: The database connection to use to perform the action. + :param user_identifier: The identifier granted by the 'get_id' method. + :param token: The token granted by the third-party service. + :return: + """ + + # Note down the timestamp at which this event occurred: + request_ts = date_time.get_current_utc_date_time(as_string = False) + + # Save the token to the database: + token_saved = await db_conn.update_one( + collection = MailOAuthModel.AUTH_COLLECTION, + filter = {"_id": ObjectId(user_identifier)}, + update = { + "$set": { + "token": token, + "firstRefreshTs": request_ts, + } + } + ) + + # Done here: + return token_saved + # ***************************************************************************************************************** # ***** **** diff --git a/models/data/mail/oauth.py b/models/data/mail/oauth.py index af59578..ce3d5a1 100644 --- a/models/data/mail/oauth.py +++ b/models/data/mail/oauth.py @@ -51,7 +51,8 @@ from utils_v2.string import regex # ***************************************************************************************************************** -# --- Nothing Yet +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" # ***************************************************************************************************************** @@ -71,20 +72,35 @@ from utils_v2.string import regex # ***************************************************************************************************************** -class LoginRequest(BaseModel): +class OAuthMailAuthorizationRequestHeaders(BaseModel): - username: str = Field(description = "the username of the user") - - password: str = Field(description = "the password of the user") - - mode: Optional[str] = Field( - description = "the mode through which this request came in", - default = "N/A" + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" ) - remoteIp: Optional[str] = Field( - description = "the ip addr of the client", - default = "N/A" + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class OAuthMailAuthorizationRequestData(BaseModel): + + mailClient: Literal["gmail"] = Field( + description = "the e-mail provider like 'gmail'", + frozen = True ) # ┏┓ ┏• @@ -95,6 +111,15 @@ class LoginRequest(BaseModel): class Config: extra = "forbid" + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("mailClient", mode = "before") + def to_lowercase(cls, value): + if isinstance(value, str): value = value.strip().lower() + return value + # ***************************************************************************************************************** # ***** **** diff --git a/models/data/shared/__init__.py b/models/data/shared/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/models/data/shared/message.py b/models/data/shared/message.py deleted file mode 100644 index 30405d3..0000000 --- a/models/data/shared/message.py +++ /dev/null @@ -1,107 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - Friday, 22nd Nov., 2024. - - OBJECTIVE: - - To provide the data model for the structure of each message. - - REFERENCES: - - N/A - - DOWNLOADS: - - N/A - -""" - - -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys -sys.path.append(".") -sys.path.append("..") - -# For making data behaviour_models: -from pydantic import BaseModel, Field, field_validator -from typing import Optional, Literal - -# My utils: -from utils_v2.string import regex - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -class LoginRequest(BaseModel): - - username: str = Field(description = "the username of the user") - - password: str = Field(description = "the password of the user") - - mode: Optional[str] = Field( - description = "the mode through which this request came in", - default = "N/A" - ) - - remoteIp: Optional[str] = Field( - description = "the ip addr of the client", - default = "N/A" - ) - - # ┏┓ ┏• - # ┃ ┏┓┏┓╋┓┏┓ - # ┗┛┗┛┛┗┛┗┗┫ - # ┛ - - class Config: - extra = "forbid" - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - pass