feat: otp table
This commit is contained in:
parent
727d979145
commit
eaa7519303
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -74,3 +80,30 @@ class AuthService:
|
|||
self.db.commit()
|
||||
|
||||
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
|
||||
|
|
@ -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 = ""):
|
||||
|
||||
|
|
@ -43,9 +46,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
def send_otp_email(self, email: str, otp: str) -> None:
|
||||
try:
|
||||
"""Send OTP email"""
|
||||
return self.send_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={"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
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -235,3 +241,18 @@ class UserServices:
|
|||
|
||||
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)}")
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import dotenv
|
||||
import os
|
||||
import random
|
||||
dotenv.load_dotenv()
|
||||
|
||||
DEFAULT_SKIP = 0
|
||||
|
|
@ -12,3 +13,22 @@ DEFAULT_ORDER = "desc"
|
|||
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")
|
||||
JWT_SECRET = os.getenv("JWT_SECRET")
|
||||
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))
|
||||
Loading…
Reference in New Issue