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 schemas.CreateSchemas import UserCreate
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()
@ -16,9 +18,31 @@ def login(data: AuthBase):
@router.post("/register")
def register(user_data: UserCreate):
token = AuthService().register(user_data)
def register(user_data: UserCreate, background_tasks: BackgroundTasks):
token = AuthService().register(user_data, background_tasks)
return ApiResponse(
data=token,
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)
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}")
async def get_verified_files(clinic_id: int):
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 .SignupPricingMaster import SignupPricingMaster
from .ClinicFileVerifications import ClinicFileVerifications
from .OTP import OTP
__all__ = [
"Users",
@ -29,5 +30,6 @@ __all__ = [
"Fcm",
"BlockedEmail",
"SignupPricingMaster",
"ClinicFileVerifications"
"ClinicFileVerifications",
"OTP"
]

View File

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

View File

@ -1,12 +1,17 @@
import datetime
import json
import urllib.request
from sqlalchemy.orm import Session
from services.jwtService import create_jwt_token
from services.userServices import UserServices
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 schemas.CreateSchemas import UserCreate
from schemas.BaseSchemas import AuthBase
from schemas.BaseSchemas import AuthBase, AuthOTP
from exceptions.unauthorized_exception import UnauthorizedException
from database import get_db
@ -17,6 +22,7 @@ class AuthService:
def __init__(self):
self.user_service = UserServices()
self.db = next(get_db())
self.email_service = EmailService()
def login(self, data: AuthBase) -> str:
@ -35,8 +41,8 @@ class AuthService:
token = create_jwt_token(user_dict)
return token
def register(self, user_data: UserCreate):
response = self.user_service.create_user(user_data)
def register(self, user_data: UserCreate, background_tasks=None):
response = self.user_service.create_user(user_data, background_tasks)
user = {
"id": response.id,
"username": response.username,
@ -73,4 +79,31 @@ class AuthService:
self.db.add(blockEmail)
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 interface.common_response import CommonResponse
from sqlalchemy import or_,func, case
from sqlalchemy import text
from services.s3Service import get_signed_url
from models import ClinicFileVerifications
from schemas.BaseSchemas import ClinicFileVerificationBase
from services.emailService import EmailService
class ClinicServices:
def __init__(self):
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 = ""):
@ -42,10 +45,7 @@ class ClinicServices:
)
clinics = clinics_query.limit(limit).offset(offset).all()
# Get all counts in a single optimized query
from sqlalchemy import text
count_query = text("""
SELECT
COUNT(*) as total,
@ -216,6 +216,9 @@ class ClinicServices:
self.db.add(clinic_file_verification)
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:
clinic_file_verification = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first()
@ -238,6 +241,10 @@ class ClinicServices:
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 clinic.status == ClinicStatus.REJECTED or clinic.status == ClinicStatus.UNDER_REVIEW:
pass

View File

@ -76,7 +76,7 @@ class EmailService:
logger.error(f"Failed to create template: {e}")
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"""
try:
response = self.client.send_templated_email(
@ -89,15 +89,47 @@ class EmailService:
ReplyToAddresses=[self.senderEmail]
)
logger.info(f"Email sent to {to_address} successfully. MessageId: {response['MessageId']}")
return response
return
except Exception as e:
logger.error(f"Error sending email to {to_address}: {str(e)}")
raise
def send_otp_email(self, email: str, otp: str):
"""Send OTP email"""
return self.send_email(
template_name="sendOTP",
def send_otp_email(self, email: str, otp: str) -> None:
try:
"""Send OTP email"""
self.send_email(
template_name="sendOTP",
to_address=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={"otp": otp, "email": 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.db_exceptions import DBExceptionHandler
from sqlalchemy.orm import joinedload
from services.emailService import EmailService
class UserServices:
def __init__(self):
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
try:
user = user_data.user
@ -116,6 +117,11 @@ class UserServices:
# Now commit both user and clinic in a single transaction
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
except Exception as e:
logger.error(f"Error creating user: {str(e)}")
@ -234,4 +240,19 @@ class UserServices:
user.soft_delete(self.db)
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,3 +1,3 @@
# Database pagination constants
DEFAULT_SKIP = 0
DEFAULT_LIMIT = 10
DEFAULT_LIMIT = 10

View File

@ -1,5 +1,6 @@
import dotenv
import os
import random
dotenv.load_dotenv()
DEFAULT_SKIP = 0
@ -11,4 +12,23 @@ DEFAULT_ORDER = "desc"
# jwt
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")
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))