(20250620) SMS sending module also ported for internal use.
This commit is contained in:
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
DATE:
|
DATE:
|
||||||
|
|
||||||
Wednesday, 19th Jun., 2025
|
Friday, 20th Jun., 2025
|
||||||
|
|
||||||
OBJECTIVE:
|
OBJECTIVE:
|
||||||
|
|
||||||
To generate and verify OTPs that expire.
|
To send out SMS messages for internal use.
|
||||||
|
|
||||||
REFERENCES:
|
REFERENCES:
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ from shared import constants
|
|||||||
from utils_v2.string import json
|
from utils_v2.string import json
|
||||||
from utils_v2.date_time import date_time
|
from utils_v2.date_time import date_time
|
||||||
from utils_v2.security.otp import HashedOTP
|
from utils_v2.security.otp import HashedOTP
|
||||||
|
from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS
|
||||||
from utils_v2.api.response import ResponseModel
|
from utils_v2.api.response import ResponseModel
|
||||||
from utils_v2.api.codes import StatusCodes, HttpCodes
|
from utils_v2.api.codes import StatusCodes, HttpCodes
|
||||||
from utils_v2.api.async_quart import (
|
from utils_v2.api.async_quart import (
|
||||||
@@ -64,7 +65,7 @@ from utils_v2.api.async_quart import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Data models:
|
# Data models:
|
||||||
from models.otp.timed_otp import GenerateTimedOTP, VerifyTimedOTP
|
from models.sms.nimbus_sms import NimbusSMSSendRequest
|
||||||
|
|
||||||
# For asynchronous activities:
|
# For asynchronous activities:
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -78,7 +79,7 @@ import asyncio
|
|||||||
|
|
||||||
|
|
||||||
# Related to Quart:
|
# Related to Quart:
|
||||||
otp_bp = Blueprint("otp", __name__)
|
sms_bp = Blueprint("sms_bp", __name__)
|
||||||
|
|
||||||
|
|
||||||
# *****************************************************************************************************************
|
# *****************************************************************************************************************
|
||||||
@@ -98,7 +99,7 @@ otp_bp = Blueprint("otp", __name__)
|
|||||||
# *****************************************************************************************************************
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
@otp_bp.record_once
|
@sms_bp.record_once
|
||||||
def init(blueprint_setup_state):
|
def init(blueprint_setup_state):
|
||||||
|
|
||||||
# This gets called when the blueprint is registered.
|
# This gets called when the blueprint is registered.
|
||||||
@@ -109,7 +110,8 @@ def init(blueprint_setup_state):
|
|||||||
# ---------------------------------------------------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@otp_bp.route("/timed/generate", methods = ["POST", "GET"])
|
@sms_bp.route("/nimbus", methods = ["POST"])
|
||||||
|
@sms_bp.route("/nimbus/send", methods = ["POST"])
|
||||||
@set_api_version(api_version = "1.0.0")
|
@set_api_version(api_version = "1.0.0")
|
||||||
@read_input(sanitize_headers = True, sanitize_data = True)
|
@read_input(sanitize_headers = True, sanitize_data = True)
|
||||||
@log_request_to_mongo(
|
@log_request_to_mongo(
|
||||||
@@ -123,156 +125,68 @@ def init(blueprint_setup_state):
|
|||||||
)
|
)
|
||||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||||
# @only_whitelisted_ips(attr_name = "whitelisted_ips")
|
@only_whitelisted_ips(attr_name = "whitelisted_ips")
|
||||||
@validate_input(data_validator = lambda x: GenerateTimedOTP(**x))
|
@validate_input(data_validator = lambda x: NimbusSMSSendRequest(**x))
|
||||||
# @limit_rate(
|
@limit_rate(
|
||||||
# attr_name = "otp_redis",
|
attr_name = "rate_limit_cache",
|
||||||
# rate_limit = 1,
|
rate_limit = 1,
|
||||||
# seconds = 2,
|
seconds = 30,
|
||||||
# data_keys = ["id"],
|
data_keys = ["recipientNo"],
|
||||||
# allow_if_exception = False
|
allow_if_exception = True,
|
||||||
# )
|
count_for_http_codes = [200]
|
||||||
@handle_cancelled_request()
|
)
|
||||||
async def generate_otp(
|
@limit_rate(
|
||||||
inbound_headers: dict = None,
|
attr_name = "rate_limit_cache",
|
||||||
inbound_data: dict | GenerateTimedOTP = None,
|
rate_limit = 5,
|
||||||
inbound_files: dict = None,
|
seconds = 86_400,
|
||||||
log_id: str = None,
|
data_keys = ["recipientNo"],
|
||||||
**kwargs
|
allow_if_exception = False,
|
||||||
):
|
count_for_http_codes = [200]
|
||||||
|
|
||||||
"""
|
|
||||||
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'.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Generate the OTP:
|
|
||||||
otp_key = current_app.otp_redis.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.otp_redis.delete(key = otp_key)
|
|
||||||
|
|
||||||
# Store the OTP in Redis:
|
|
||||||
otp_stored = await current_app.otp_redis.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(
|
|
||||||
status_code = StatusCodes.OK if otp_stored else StatusCodes.FAILED,
|
|
||||||
data = {"otp": otp} if otp_stored else None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@otp_bp.route("/timed/verify", methods = ["POST", "GET"])
|
|
||||||
@set_api_version(api_version = "1.0.0")
|
|
||||||
@read_input(sanitize_headers = True, sanitize_data = True)
|
|
||||||
@log_request_to_mongo(
|
|
||||||
attr_name = "logs_mongo",
|
|
||||||
project = constants.PROJECT_NAME,
|
|
||||||
log_type = "timedOTP",
|
|
||||||
operation = "verify",
|
|
||||||
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")
|
|
||||||
# @only_whitelisted_ips(attr_name = "whitelisted_ips")
|
|
||||||
@validate_input(data_validator = lambda x: VerifyTimedOTP(**x))
|
|
||||||
# @limit_rate(
|
|
||||||
# attr_name = "otp_redis",
|
|
||||||
# rate_limit = 1,
|
|
||||||
# seconds = 2,
|
|
||||||
# data_keys = ["id"],
|
|
||||||
# allow_if_exception = False,
|
|
||||||
# count_for_http_codes = [200]
|
|
||||||
# )
|
|
||||||
@handle_cancelled_request()
|
@handle_cancelled_request()
|
||||||
async def verify_otp(
|
async def nimbus_send_sms(
|
||||||
inbound_headers: dict = None,
|
inbound_headers: dict = None,
|
||||||
inbound_data: dict | VerifyTimedOTP = None,
|
inbound_data: dict | NimbusSMSSendRequest = None,
|
||||||
inbound_files: dict = None,
|
inbound_files: dict = None,
|
||||||
log_id: str = None,
|
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Verifies the claimed OTP against the stored OTP.
|
Sends one SMS out through Nimbus IT's system.
|
||||||
:param inbound_headers: auto-extracted by the decorators from 'async_quart.py'.
|
: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_data: auto-extracted by the decorators from 'async_quart.py'.
|
||||||
:param inbound_files: 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'.
|
:return: A standard response structure from the function in 'async_quart.py'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Start by assuming failure:
|
# Get the default credentials ready:
|
||||||
is_valid = False
|
default_nimbus_creds = current_app.script_data["sms"]["nimbus"]["tcaoff"]
|
||||||
attempts_left = None
|
|
||||||
|
|
||||||
# Fetch the OTP from Redis:
|
# Initialise an object of Nimbus SMS:
|
||||||
otp_key = current_app.otp_redis.make_key(str(inbound_data.id))
|
sms_client = AsyncNimbusSMS(
|
||||||
stored_otp = await current_app.otp_redis.get(key = otp_key)
|
entity_id = inbound_data.entityId or default_nimbus_creds["entityId"],
|
||||||
|
sender_id = inbound_data.senderId or default_nimbus_creds["senderId"],
|
||||||
# Test the validity of the OTP:
|
user_id = inbound_data.userId or default_nimbus_creds["userId"],
|
||||||
if stored_otp is not None:
|
api_key = inbound_data.apiKey or default_nimbus_creds["apiKey"]
|
||||||
|
|
||||||
# Delete the record from the cache.:
|
|
||||||
await current_app.otp_redis.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.otp_redis.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:
|
# Send the message out:
|
||||||
if not otp_updated: attempts_left = None
|
response = await sms_client.send_sms(
|
||||||
|
template_id = inbound_data.templateId,
|
||||||
|
message = inbound_data.message,
|
||||||
|
recipient_number = inbound_data.recipientNo
|
||||||
|
)
|
||||||
|
|
||||||
# Done here:
|
# Done here:
|
||||||
return ResponseModel(
|
return ResponseModel(
|
||||||
status_code = StatusCodes.OK if is_valid else StatusCodes.FAILED,
|
http_code = HttpCodes.SUCCESS if response.success else HttpCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
status_code = StatusCodes.OK if response.success else StatusCodes.FAILED,
|
||||||
data = {
|
data = {
|
||||||
"isValid": is_valid,
|
"client": response.client,
|
||||||
"attemptsLeft": attempts_left
|
"ts": response.ts.timestamp(),
|
||||||
},
|
"messageId": response.messageId,
|
||||||
http_code = HttpCodes.SUCCESS if is_valid else HttpCodes.UNAUTHORIZED
|
"rawResponse": response.rawResponse,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -75,6 +75,7 @@ from controllers.servers.server import CoreServerController
|
|||||||
from api_v2.blueprints.cred_and_data.blueprint import cred_and_data_bp
|
from api_v2.blueprints.cred_and_data.blueprint import cred_and_data_bp
|
||||||
from api_v2.blueprints.logs.blueprint import logs_bp
|
from api_v2.blueprints.logs.blueprint import logs_bp
|
||||||
from api_v2.blueprints.otp.blueprint import otp_bp
|
from api_v2.blueprints.otp.blueprint import otp_bp
|
||||||
|
from api_v2.blueprints.sms.blueprint import sms_bp
|
||||||
from api_v2.blueprints.servers.blueprint import servers_bp
|
from api_v2.blueprints.servers.blueprint import servers_bp
|
||||||
from api_v2.blueprints.test.blueprint import test_bp
|
from api_v2.blueprints.test.blueprint import test_bp
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ app = cors(app)
|
|||||||
app.register_blueprint(cred_and_data_bp, url_prefix = f"/{MODULE_BASE}")
|
app.register_blueprint(cred_and_data_bp, url_prefix = f"/{MODULE_BASE}")
|
||||||
app.register_blueprint(logs_bp, url_prefix = f"/{MODULE_BASE}/logs")
|
app.register_blueprint(logs_bp, url_prefix = f"/{MODULE_BASE}/logs")
|
||||||
app.register_blueprint(otp_bp, url_prefix = f"/{MODULE_BASE}/otp")
|
app.register_blueprint(otp_bp, url_prefix = f"/{MODULE_BASE}/otp")
|
||||||
|
app.register_blueprint(sms_bp, url_prefix = f"/{MODULE_BASE}/sms")
|
||||||
app.register_blueprint(servers_bp, url_prefix = f"/{MODULE_BASE}/servers")
|
app.register_blueprint(servers_bp, url_prefix = f"/{MODULE_BASE}/servers")
|
||||||
app.register_blueprint(test_bp, url_prefix = f"/{MODULE_BASE}/test")
|
app.register_blueprint(test_bp, url_prefix = f"/{MODULE_BASE}/test")
|
||||||
|
|
||||||
@@ -192,12 +194,15 @@ async def app_startup(**kwargs):
|
|||||||
# ┣┫┏┓┏┫┓┏ ┃ ┏┓┏┣┓┏┓
|
# ┣┫┏┓┏┫┓┏ ┃ ┏┓┏┣┓┏┓
|
||||||
# ┛┗┗ ┗┻┗┛ ┗┛┗┻┗┛┗┗
|
# ┛┗┗ ┗┻┗┛ ┗┛┗┻┗┛┗┗
|
||||||
|
|
||||||
current_app.otp_redis = AsyncRedisCache(
|
current_app.redis_cache = AsyncRedisCache(
|
||||||
connection_string = script_cred["redisCache"]["general"]["sentinelJson"],
|
connection_string = script_cred["redisCache"]["general"]["sentinelJson"],
|
||||||
debug = enable_debugging,
|
debug = enable_debugging,
|
||||||
debug_prefix = "OTP Cache | "
|
debug_prefix = "Redis Cache | "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_app.otp_redis = current_app.redis_cache
|
||||||
|
current_app.rate_limit_cache = current_app.redis_cache
|
||||||
|
|
||||||
current_app.printer("Redis ready.")
|
current_app.printer("Redis ready.")
|
||||||
|
|
||||||
# ┏┓ ┓┓
|
# ┏┓ ┓┓
|
||||||
|
|||||||
+50
-38
@@ -71,25 +71,46 @@ from utils_v2.string import regex
|
|||||||
# *****************************************************************************************************************
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
class NimbusSendSMS(BaseModel):
|
class NimbusSMSSendRequest(BaseModel):
|
||||||
|
|
||||||
"""
|
entityId: str | int | None = Field(
|
||||||
This data model is used when the API call is made to send an SMS message.
|
description = "The id given to you by DLT.",
|
||||||
'entityId': is the id given to you by DLT.
|
default = None,
|
||||||
'templateId' is the id given to you by DLT for a template of a message.
|
frozen = True
|
||||||
'recipientNo' is the phone number of the person you want to send the message to.
|
)
|
||||||
'senderId' 6-char code like "HDFCBK", "NSESMS", "ZRODHA" that you see in your SMS inbox.
|
|
||||||
'userId' is the 6-digit id given to you by Nimbus.
|
|
||||||
'apiKey' is the key generated on Nimbus's portal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
entityId: str | int
|
templateId: str | int = Field(
|
||||||
templateId: str | int
|
description = "The id given to you by DLT for a template of a message.",
|
||||||
recipientNo: str | int
|
frozen = True
|
||||||
message: str
|
)
|
||||||
senderId: str | int
|
|
||||||
userId: str | int
|
recipientNo: str | int = Field(
|
||||||
apiKey: str
|
description = "The phone number of the person you want to send the message to.",
|
||||||
|
frozen = True
|
||||||
|
)
|
||||||
|
|
||||||
|
message: str = Field(
|
||||||
|
description = "The message you want to send.",
|
||||||
|
frozen = True
|
||||||
|
)
|
||||||
|
|
||||||
|
senderId: str | int | None = Field(
|
||||||
|
description = "6-char code like 'HDFCBK', 'NSESMS', 'ZRODHA' that you see in your SMS inbox.",
|
||||||
|
default = None,
|
||||||
|
frozen = True
|
||||||
|
)
|
||||||
|
|
||||||
|
userId: str | int | None = Field(
|
||||||
|
description = "The 6-digit id given to you by Nimbus.",
|
||||||
|
default = None,
|
||||||
|
frozen = True
|
||||||
|
)
|
||||||
|
|
||||||
|
apiKey: str | None = Field(
|
||||||
|
description = "The key generated on Nimbus's portal.",
|
||||||
|
default = None,
|
||||||
|
frozen = True
|
||||||
|
)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "forbid"
|
extra = "forbid"
|
||||||
@@ -118,16 +139,19 @@ class NimbusSendSMS(BaseModel):
|
|||||||
# ---------------------------------------------------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class NimbusGetBalance(BaseModel):
|
class NimbusGetBalanceRequest(BaseModel):
|
||||||
|
|
||||||
"""
|
userId: str | int | None = Field(
|
||||||
This data model is used when the API call is made to check how much balance is remaining in your Nimbus wallet.
|
description = "The 6-digit id given to you by Nimbus.",
|
||||||
'userId' is the 6-digit id given to you by Nimbus.
|
default = None,
|
||||||
'apiKey' is the key generated on Nimbus's portal.
|
frozen = True
|
||||||
"""
|
)
|
||||||
|
|
||||||
userId: str | int
|
apiKey: str | None = Field(
|
||||||
apiKey: str
|
description = "The key generated on Nimbus's portal.",
|
||||||
|
default = None,
|
||||||
|
frozen = True
|
||||||
|
)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "forbid"
|
extra = "forbid"
|
||||||
@@ -151,16 +175,4 @@ class NimbusGetBalance(BaseModel):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
from utils_v2.string import json
|
pass
|
||||||
|
|
||||||
my_msg = NimbusSendSMS(
|
|
||||||
entityId = 123,
|
|
||||||
templateId = 456,
|
|
||||||
recipientNo = "789",
|
|
||||||
message = "Hello, World!",
|
|
||||||
senderId = "TCAOFF",
|
|
||||||
userId = "123456",
|
|
||||||
apiKey = "123@ABC"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(json.to_string(my_msg.model_dump()))
|
|
||||||
|
|||||||
Reference in New Issue
Block a user