(20250220) Bug fix in reminders for work.

This commit is contained in:
2025-02-20 13:07:41 +05:30
parent 1985d8c6a4
commit 2435760ea3
10 changed files with 846 additions and 3 deletions
+292
View File
@@ -0,0 +1,292 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Tuesday, 10th Sept., 2024
OBJECTIVE:
To be able to send SMS messages and get reports and balance through Nimbus IT's SMS sending service.
The core logic is implemented elsewhere, this module aims to build an API layer around it.
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
# Common:
from shared import constants
# My utils:
from utils_v2.string import json
from utils_v2.date_time import date_time
from utils_v2.security.otp import HashedOTP
from utils_v2.api.response import ResponseModel
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.async_quart import (
log_request_to_mongo,
only_whitelisted_ips,
should_not_be_under_maintenance,
read_input,
limit_rate, validate_input
)
# Data models:
from models.common.otp.timed_otp import GenerateTimedOTP, VerifyTimedOTP
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Related to Quart:
timed_otp_bp = Blueprint("timed_otp", __name__)
api_version = "2.0.0"
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
@timed_otp_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
# ---------------------------------------------------------------------------------------------------------------------
@timed_otp_bp.route("/generate", methods = ["POST", "GET"])
@read_input(sanitize_headers = True, sanitize_data = True)
@log_request_to_mongo(
attr_name = "mongo",
log_type = "otp",
operation = "generateOtp",
api_version = api_version,
log_input = True,
log_output = True
)
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
# @only_whitelisted_ips(attr_name = "whitelisted_ips")
@validate_input(data_validator = lambda x: GenerateTimedOTP(**x))
# @limit_rate(
# attr_name = "redis_cache",
# rate_limit = 1,
# seconds = 2,
# data_keys = ["id"],
# allow_if_exception = False
# )
async def generate_otp(
inbound_headers: dict = None,
inbound_data: dict | GenerateTimedOTP = None,
inbound_files: dict = None,
log_id: str = None
):
"""
Generates one OTP for the amount of time specified in the request.
:param inbound_headers: auto-extracted by the decorators from 'async_quart.py'.
:param inbound_data: auto-extracted by the decorators from 'async_quart.py'.
:param inbound_files: auto-extracted by the decorators from 'async_quart.py'.
:param log_id: An identifier for the logs (if logging is enabled).
:return: A standard response structure from the function in 'async_quart.py'.
"""
try:
# Generate the OTP:
otp_key = current_app.redis_cache.make_key(str(inbound_data.id))
otp_client = HashedOTP(secret = HashedOTP.generate_secret())
otp = otp_client.generate_otp(count = 0)
# Delete any existing OTP with the same identifiers:
await current_app.redis_cache.delete(key = otp_key)
# Store the OTP in Redis:
otp_stored = await current_app.redis_cache.set(
key = otp_key,
value = {
"otp": otp,
"att": inbound_data.attempts,
"iat": date_time.get_current_utc_date_time().timestamp(),
"sec": inbound_data.seconds
},
expiry = inbound_data.seconds
)
# Done here:
return ResponseModel(
api_version = api_version,
status_code = StatusCodes.OK if otp_stored else StatusCodes.FAILED,
data = {"otp": otp} if otp_stored else None
)
# In case the client terminates the connection prematurely:
except asyncio.CancelledError as exception:
current_app.printer(exception)
return ResponseModel(api_version = api_version, status_code = StatusCodes.CLIENT_CLOSED_REQUEST)
# ---------------------------------------------------------------------------------------------------------------------
@timed_otp_bp.route("/verify", methods = ["POST", "GET"])
@read_input(sanitize_headers = True, sanitize_data = True)
@log_request_to_mongo(
attr_name = "mongo",
log_type = "otp",
operation = "verifyOtp",
api_version = api_version,
log_input = True,
log_output = True
)
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
# @only_whitelisted_ips(attr_name = "whitelisted_ips")
@validate_input(data_validator = lambda x: VerifyTimedOTP(**x))
# @limit_rate(
# attr_name = "redis_cache",
# rate_limit = 1,
# seconds = 2,
# data_keys = ["id"],
# allow_if_exception = False,
# count_for_http_codes = [200]
# )
async def verify_otp(
inbound_headers: dict = None,
inbound_data: dict | VerifyTimedOTP = None,
inbound_files: dict = None,
log_id: str = None
):
"""
Verifies the claimed OTP against the stored OTP.
:param inbound_headers: auto-extracted by the decorators from 'async_quart.py'.
:param inbound_data: auto-extracted by the decorators from 'async_quart.py'.
:param inbound_files: auto-extracted by the decorators from 'async_quart.py'.
:param log_id: An identifier for the logs (if logging is enabled).
:return: A standard response structure from the function in 'async_quart.py'.
"""
try:
# Start by assuming failure:
is_valid = False
attempts_left = None
# Fetch the OTP from Redis:
otp_key = current_app.redis_cache.make_key(str(inbound_data.id))
stored_otp = await current_app.redis_cache.get(key = otp_key)
# Test the validity of the OTP:
if stored_otp is not None:
# Delete the record from the cache.:
await current_app.redis_cache.delete(key = otp_key)
# Make note of the attempts left.
# We reduce the count by one straightaway:
attempts_left = stored_otp["att"] - 1
# If the stored OTP matches the claimed OTP,
# we note down the acceptance and delete the record from the cache:
if inbound_data.otp == stored_otp["otp"]:
is_valid = True
attempts_left = 0
# If the stored OTP and the claimed OTP don't match,
# we reduce the attempt count and :
elif attempts_left > 0:
# Update the OTP info in the cache:
otp_updated = await current_app.redis_cache.set(
key = otp_key,
value = {
"otp": stored_otp["otp"],
"att": attempts_left,
"iat": stored_otp["iat"],
"sec": stored_otp["sec"]
},
expiry = (stored_otp["iat"] + stored_otp["sec"]) - date_time.get_current_utc_date_time().timestamp()
)
# If the update failed:
if not otp_updated: attempts_left = None
# Done here:
return ResponseModel(
api_version = api_version,
status_code = StatusCodes.OK if is_valid else StatusCodes.FAILED,
data = {
"isValid": is_valid,
"attemptsLeft": attempts_left
},
http_code = HttpCodes.UNAUTHORIZED
)
# In case the client terminates the connection prematurely:
except asyncio.CancelledError as exception:
current_app.printer(exception)
return ResponseModel(api_version = api_version, status_code = StatusCodes.CLIENT_CLOSED_REQUEST)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass