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 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"
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 .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"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""Send OTP email"""
|
try:
|
||||||
return self.send_email(
|
"""Send OTP email"""
|
||||||
template_name="sendOTP",
|
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,
|
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
|
||||||
|
|
@ -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)}")
|
||||||
|
|
@ -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))
|
||||||
Loading…
Reference in New Issue