feat: otp table

This commit is contained in:
deepvasoya 2025-05-20 18:20:36 +05:30
parent 727d979145
commit eaa7519303
11 changed files with 180 additions and 32 deletions

View File

@ -1,8 +1,10 @@
from fastapi import APIRouter from fastapi import APIRouter, BackgroundTasks
from services.authService import AuthService from services.authService import AuthService
from schemas.CreateSchemas import UserCreate from schemas.CreateSchemas import UserCreate
from schemas.ApiResponse import ApiResponse from schemas.ApiResponse import ApiResponse
from schemas.BaseSchemas import AuthBase from schemas.BaseSchemas import AuthBase, AuthOTP
from services.clinicServices import ClinicServices
from http import HTTPStatus
router = APIRouter() router = APIRouter()
@ -16,9 +18,31 @@ def login(data: AuthBase):
@router.post("/register") @router.post("/register")
def register(user_data: UserCreate): def register(user_data: UserCreate, background_tasks: BackgroundTasks):
token = AuthService().register(user_data) token = AuthService().register(user_data, background_tasks)
return ApiResponse( return ApiResponse(
data=token, data=token,
message="User registered successfully" message="User registered successfully"
) )
@router.get("/clinic/latest-id")
def get_latest_clinic_id():
clinic_id = ClinicServices().get_latest_clinic_id()
return ApiResponse(
data=clinic_id,
message="Latest clinic ID retrieved successfully"
)
@router.post("/send-otp")
def send_otp(email: str):
AuthService().send_otp(email)
return HTTPStatus.OK
@router.post("/verify-otp")
def verify_otp(data: AuthOTP):
AuthService().verify_otp(data)
return ApiResponse(
data="OK",
message="OTP verified successfully"
)

View File

@ -30,12 +30,6 @@ async def get_clinics(
clinics = ClinicServices().get_clinics(req.state.user, limit, offset, filter_type, search) clinics = ClinicServices().get_clinics(req.state.user, limit, offset, filter_type, search)
return ApiResponse(data=clinics, message="Clinics retrieved successfully" ) return ApiResponse(data=clinics, message="Clinics retrieved successfully" )
@router.get("/latest-id")
async def get_latest_clinic_id():
clinic_id = ClinicServices().get_latest_clinic_id()
return ApiResponse(data=clinic_id, message="Latest clinic ID retrieved successfully")
@router.get("/verified-files/{clinic_id}") @router.get("/verified-files/{clinic_id}")
async def get_verified_files(clinic_id: int): async def get_verified_files(clinic_id: int):
clinic = ClinicServices().get_clinic_by_id(clinic_id) clinic = ClinicServices().get_clinic_by_id(clinic_id)

11
models/OTP.py Normal file
View File

@ -0,0 +1,11 @@
from sqlalchemy import Column, Integer, String, DateTime
from database import Base
from .CustomBase import CustomBase
class OTP(Base, CustomBase):
__tablename__ = "otp"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), nullable=False)
otp = Column(String(6), nullable=False)
expireAt = Column(DateTime, nullable=False)

View File

@ -13,6 +13,7 @@ from .Fcm import Fcm
from .BlockedEmail import BlockedEmail from .BlockedEmail import BlockedEmail
from .SignupPricingMaster import SignupPricingMaster from .SignupPricingMaster import SignupPricingMaster
from .ClinicFileVerifications import ClinicFileVerifications from .ClinicFileVerifications import ClinicFileVerifications
from .OTP import OTP
__all__ = [ __all__ = [
"Users", "Users",
@ -29,5 +30,6 @@ __all__ = [
"Fcm", "Fcm",
"BlockedEmail", "BlockedEmail",
"SignupPricingMaster", "SignupPricingMaster",
"ClinicFileVerifications" "ClinicFileVerifications",
"OTP"
] ]

View File

@ -13,6 +13,10 @@ class SNSBase(BaseModel):
Message: str Message: str
class AuthOTP(BaseModel):
email: EmailStr
otp: str
class ClinicFileVerificationBase(BaseModel): class ClinicFileVerificationBase(BaseModel):
abn_doc_is_verified: Optional[bool] = None abn_doc_is_verified: Optional[bool] = None
contract_doc_is_verified: Optional[bool] = None contract_doc_is_verified: Optional[bool] = None

View File

@ -1,12 +1,17 @@
import datetime
import json import json
import urllib.request import urllib.request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from services.jwtService import create_jwt_token from services.jwtService import create_jwt_token
from services.userServices import UserServices from services.userServices import UserServices
from models import BlockedEmail from models import BlockedEmail
from services.emailService import EmailService
from exceptions.validation_exception import ValidationException
from models import OTP
from utils.constants import generateOTP
from utils.password_utils import verify_password from utils.password_utils import verify_password
from schemas.CreateSchemas import UserCreate from schemas.CreateSchemas import UserCreate
from schemas.BaseSchemas import AuthBase from schemas.BaseSchemas import AuthBase, AuthOTP
from exceptions.unauthorized_exception import UnauthorizedException from exceptions.unauthorized_exception import UnauthorizedException
from database import get_db from database import get_db
@ -17,6 +22,7 @@ class AuthService:
def __init__(self): def __init__(self):
self.user_service = UserServices() self.user_service = UserServices()
self.db = next(get_db()) self.db = next(get_db())
self.email_service = EmailService()
def login(self, data: AuthBase) -> str: def login(self, data: AuthBase) -> str:
@ -35,8 +41,8 @@ class AuthService:
token = create_jwt_token(user_dict) token = create_jwt_token(user_dict)
return token return token
def register(self, user_data: UserCreate): def register(self, user_data: UserCreate, background_tasks=None):
response = self.user_service.create_user(user_data) response = self.user_service.create_user(user_data, background_tasks)
user = { user = {
"id": response.id, "id": response.id,
"username": response.username, "username": response.username,
@ -74,3 +80,30 @@ class AuthService:
self.db.commit() self.db.commit()
return "OK" return "OK"
def send_otp(self, email:str):
otp = generateOTP()
self.email_service.send_otp_email(email, otp)
# Create OTP record with proper datetime handling
expire_time = datetime.datetime.now() + datetime.timedelta(minutes=10)
otp_record = OTP(email=email, otp=otp, expireAt=expire_time)
self.db.add(otp_record)
self.db.commit()
return
def verify_otp(self, data: AuthOTP):
db_otp = self.db.query(OTP).filter(OTP.email == data.email, OTP.otp == data.otp).first()
if not db_otp:
raise ValidationException("Invalid OTP")
if db_otp.otp != data.otp:
raise ValidationException("Invalid OTP")
if db_otp.expireAt < datetime.datetime.now():
raise ValidationException("OTP expired")
# OTP is valid, delete it to prevent reuse
# self.db.delete(db_otp)
# self.db.commit()
return

View File

@ -9,14 +9,17 @@ from enums.enums import ClinicStatus, UserType
from exceptions.unauthorized_exception import UnauthorizedException from exceptions.unauthorized_exception import UnauthorizedException
from interface.common_response import CommonResponse from interface.common_response import CommonResponse
from sqlalchemy import or_,func, case from sqlalchemy import or_,func, case
from sqlalchemy import text
from services.s3Service import get_signed_url from services.s3Service import get_signed_url
from models import ClinicFileVerifications from models import ClinicFileVerifications
from schemas.BaseSchemas import ClinicFileVerificationBase from schemas.BaseSchemas import ClinicFileVerificationBase
from services.emailService import EmailService
class ClinicServices: class ClinicServices:
def __init__(self): def __init__(self):
self.db: Session = next(get_db()) self.db: Session = next(get_db())
self.email_service = EmailService()
def get_clinics(self, user, limit:int, offset:int, filter_type: Union[Literal["UNREGISTERED"], Literal["REGISTERED"]] = "UNREGISTERED", search:str = ""): def get_clinics(self, user, limit:int, offset:int, filter_type: Union[Literal["UNREGISTERED"], Literal["REGISTERED"]] = "UNREGISTERED", search:str = ""):
@ -43,9 +46,6 @@ class ClinicServices:
clinics = clinics_query.limit(limit).offset(offset).all() clinics = clinics_query.limit(limit).offset(offset).all()
# Get all counts in a single optimized query
from sqlalchemy import text
count_query = text(""" count_query = text("""
SELECT SELECT
COUNT(*) as total, COUNT(*) as total,
@ -216,6 +216,9 @@ class ClinicServices:
self.db.add(clinic_file_verification) self.db.add(clinic_file_verification)
self.db.commit() self.db.commit()
# send mail to user
self.email_service.send_apporve_clinic_email(clinic.email, clinic.name)
if clinic.status == ClinicStatus.REJECTED or clinic.status == ClinicStatus.UNDER_REVIEW: if clinic.status == ClinicStatus.REJECTED or clinic.status == ClinicStatus.UNDER_REVIEW:
clinic_file_verification = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first() clinic_file_verification = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first()
@ -238,6 +241,10 @@ class ClinicServices:
self.db.commit() self.db.commit()
# send mail to user
self.email_service.send_reject_clinic_email(clinic.email, clinic.name)
# if rejected or under review then email to clinic creator # if rejected or under review then email to clinic creator
if clinic.status == ClinicStatus.REJECTED or clinic.status == ClinicStatus.UNDER_REVIEW: if clinic.status == ClinicStatus.REJECTED or clinic.status == ClinicStatus.UNDER_REVIEW:
pass pass

View File

@ -76,7 +76,7 @@ class EmailService:
logger.error(f"Failed to create template: {e}") logger.error(f"Failed to create template: {e}")
raise Exception("Failed to create template") raise Exception("Failed to create template")
def send_email(self, template_name: str, to_address: str, template_data: dict): def send_email(self, template_name: str, to_address: str, template_data: dict) -> None:
"""Send an email using a template""" """Send an email using a template"""
try: try:
response = self.client.send_templated_email( response = self.client.send_templated_email(
@ -89,15 +89,47 @@ class EmailService:
ReplyToAddresses=[self.senderEmail] ReplyToAddresses=[self.senderEmail]
) )
logger.info(f"Email sent to {to_address} successfully. MessageId: {response['MessageId']}") logger.info(f"Email sent to {to_address} successfully. MessageId: {response['MessageId']}")
return response return
except Exception as e: except Exception as e:
logger.error(f"Error sending email to {to_address}: {str(e)}") logger.error(f"Error sending email to {to_address}: {str(e)}")
raise raise
def send_otp_email(self, email: str, otp: str): def send_otp_email(self, email: str, otp: str) -> None:
try:
"""Send OTP email""" """Send OTP email"""
return self.send_email( self.send_email(
template_name="sendOTP", template_name="sendOTP",
to_address=email, to_address=email,
template_data={"otp": otp, "email": email} template_data={"otp": otp, "email": email}
) )
return
except Exception as e:
logger.error(f"Error sending OTP email to {email}: {str(e)}")
raise
def send_new_clinic_email(self, email: str, clinic_name: str):
"""Send new clinic email"""
self.send_email(
template_name="newClinic",
to_address=email,
template_data={"clinic_name": clinic_name, "email": email}
)
return
def send_reject_clinic_email(self, email: str, clinic_name: str):
"""Send reject clinic email"""
self.send_email(
template_name="rejectClinic",
to_address=email,
template_data={"clinic_name": clinic_name, "email": email}
)
return
def send_apporve_clinic_email(self, email: str, clinic_name: str):
"""Send apporve clinic email"""
self.send_email(
template_name="apporveClinic",
to_address=email,
template_data={"clinic_name": clinic_name, "email": email}
)
return

View File

@ -17,13 +17,14 @@ from schemas.CreateSchemas import UserCreate
from exceptions.resource_not_found_exception import ResourceNotFoundException from exceptions.resource_not_found_exception import ResourceNotFoundException
from exceptions.db_exceptions import DBExceptionHandler from exceptions.db_exceptions import DBExceptionHandler
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from services.emailService import EmailService
class UserServices: class UserServices:
def __init__(self): def __init__(self):
self.db: Session = next(get_db()) self.db: Session = next(get_db())
self.email_service = EmailService()
def create_user(self, user_data: UserCreate): def create_user(self, user_data: UserCreate, background_tasks=None):
# Start a transaction # Start a transaction
try: try:
user = user_data.user user = user_data.user
@ -116,6 +117,11 @@ class UserServices:
# Now commit both user and clinic in a single transaction # Now commit both user and clinic in a single transaction
self.db.commit() self.db.commit()
# Send mail to admin in a non-blocking way using background tasks
if background_tasks:
background_tasks.add_task(self._send_emails_to_admins, clinic.email)
# If no background_tasks provided, we don't send emails
return new_user return new_user
except Exception as e: except Exception as e:
logger.error(f"Error creating user: {str(e)}") logger.error(f"Error creating user: {str(e)}")
@ -235,3 +241,18 @@ class UserServices:
return True return True
def get_super_admins(self):
return self.db.query(Users).filter(Users.userType == UserType.SUPER_ADMIN).all()
def _send_emails_to_admins(self, clinic_name):
"""Helper method to send emails to all super admins"""
try:
admins = self.get_super_admins()
for admin in admins:
self.email_service.send_new_clinic_email(
to_address=admin.email,
clinic_name=clinic_name
)
except Exception as e:
# Log the error but don't interrupt the main flow
logger.error(f"Error sending admin emails: {str(e)}")

View File

@ -1,5 +1,6 @@
import dotenv import dotenv
import os import os
import random
dotenv.load_dotenv() dotenv.load_dotenv()
DEFAULT_SKIP = 0 DEFAULT_SKIP = 0
@ -12,3 +13,22 @@ DEFAULT_ORDER = "desc"
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM") JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")
JWT_SECRET = os.getenv("JWT_SECRET") JWT_SECRET = os.getenv("JWT_SECRET")
JWT_EXPIRE_MINUTES = os.getenv("JWT_EXPIRE_MINUTES") JWT_EXPIRE_MINUTES = os.getenv("JWT_EXPIRE_MINUTES")
def generateOTP(length: int = 6) -> str:
"""
Generate a secure numeric OTP (One-Time Password) of specified length.
Args:
length: Length of the OTP to generate (default: 6)
Returns:
A string containing the generated numeric OTP
"""
if length < 4:
# Ensure minimum security with at least 4 characters
length = 4
# Generate a numeric OTP with exactly 'length' digits
# This ensures we get a number with the correct number of digits
# For example, length=6 gives a number between 100000-999999
return str(random.randint(10**(length-1), 10**length - 1))