feat: sns webhook

feat: text content type handler
feat: email blocking table
This commit is contained in:
deepvasoya 2025-05-16 18:19:14 +05:30
parent 30f51618fe
commit 205e423b56
11 changed files with 383 additions and 16 deletions

View File

@ -2,10 +2,12 @@ from fastapi import APIRouter, Depends, Security
from middleware.auth_dependency import auth_required
from fastapi.security import HTTPBearer
from apis.endpoints import sns
# Import the security scheme
bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication")
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts, notifications
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts, notifications,sns
api_router = APIRouter()
# api_router.include_router(twilio.router, prefix="/twilio")
@ -19,6 +21,8 @@ api_router.include_router(appointments.router, prefix="/appointments", tags=["ap
api_router.include_router(patients.router, prefix="/patients", tags=["patients"])
api_router.include_router(sns.router, prefix="/sns", tags=["sns"], include_in_schema=False)
api_router.include_router(
admin.router,
prefix="/admin",

14
apis/endpoints/sns.py Normal file
View File

@ -0,0 +1,14 @@
from typing import Optional
from fastapi import APIRouter, Body, Header
from fastapi import Request
from services.authService import AuthService
from schemas.BaseSchemas import SNSBase
import json
router = APIRouter()
@router.post("/")
async def send_sms(request: Request):
body = await request.body()
body = json.loads(body)
AuthService().blockEmailSNS(body)
return "OK"

18
main.py
View File

@ -1,9 +1,9 @@
import dotenv
from fastapi import FastAPI, Security
from fastapi import FastAPI
from contextlib import asynccontextmanager
import logging
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.security import HTTPBearer
# db
from database import Base, engine
@ -13,6 +13,7 @@ from apis import api_router
# middleware
from middleware.ErrorHandlerMiddleware import ErrorHandlerMiddleware, configure_exception_handlers
from middleware.CustomRequestTypeMiddleware import TextPlainMiddleware
dotenv.load_dotenv()
@ -45,16 +46,6 @@ app = FastAPI(
title="Twillio Voice API",
description="API for Twillio Voice application",
version="1.0.0",
# Define security scheme for Swagger UI
openapi_tags=[
{"name": "admin", "description": "Admin operations requiring authentication"},
{"name": "auth", "description": "Authentication operations"},
{"name": "clinics", "description": "Operations with clinics"},
{"name": "doctors", "description": "Operations with doctors"},
{"name": "calender", "description": "Calendar operations"},
{"name": "appointments", "description": "Operations with appointments"},
{"name": "patients", "description": "Operations with patients"}
],
swagger_ui_parameters={"defaultModelsExpandDepth": -1}
)
@ -67,6 +58,9 @@ app.add_middleware(
allow_headers=["*"], # Allows all headers
)
# Custom request type middleware
app.add_middleware(TextPlainMiddleware)
# Error handler middleware
app.add_middleware(ErrorHandlerMiddleware)

View File

@ -0,0 +1,41 @@
from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
class TextPlainMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp):
super().__init__(app)
async def dispatch(self, request: Request, call_next):
# Check if content type is text/*
content_type = request.headers.get("content-type", "")
if content_type and content_type.startswith("text/"):
# Store the original receive method
original_receive = request._receive
# Create a modified receive that will store the body
body = b""
async def receive():
nonlocal body
message = await original_receive()
if message["type"] == "http.request":
body += message.get("body", b"")
# Update body to be empty so it won't be processed by other middleware
# message["body"] = b""
return message
# Replace the receive method
request._receive = receive
# Process the request
response = await call_next(request)
# After the response is generated, we can access the full body
# and attach it to the request state for the route to access
request.state.text_body = body.decode("utf-8")
return response
else:
# For other content types, proceed as normal
return await call_next(request)

13
models/BlockedEmail.py Normal file
View File

@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String
from database import Base
from .CustomBase import CustomBase
class BlockedEmail(Base, CustomBase):
__tablename__ = "blocked_emails"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True)
reason = Column(String)
severity = Column(String)

View File

@ -10,6 +10,7 @@ from .ClinicDoctors import ClinicDoctors
from .Notifications import Notifications
from .CallTranscripts import CallTranscripts
from .Fcm import Fcm
from .BlockedEmail import BlockedEmail
__all__ = [
"Users",
@ -24,4 +25,5 @@ __all__ = [
"Notifications",
"CallTranscripts",
"Fcm",
"BlockedEmail",
]

View File

@ -4,6 +4,15 @@ from typing import List, Optional
from pydantic import BaseModel, EmailStr
from enums.enums import AppointmentStatus, ClinicDoctorStatus, ClinicDoctorType, ClinicUserRoles, UserType, Integration
class SNSBase(BaseModel):
Type: str
MessageId: str
Token: str
TopicArn: str
SubscribeURL: str
Message: str
class AuthBase(BaseModel):
email: EmailStr

View File

@ -1,15 +1,23 @@
from database import get_db
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 utils.password_utils import verify_password
from schemas.CreateSchemas import UserCreate
from schemas.BaseSchemas import AuthBase
from exceptions.unauthorized_exception import UnauthorizedException
from database import get_db
class AuthService:
def __init__(self):
self.user_service = UserServices()
self.db = next(get_db())
def login(self, data: AuthBase) -> str:
# get user
@ -39,4 +47,30 @@ class AuthService:
"clinicId": response.created_clinics[0].id
}
token = create_jwt_token(user)
return token
return token
def blockEmailSNS(self, body: str):
# confirm subscription
if body["Type"] == "SubscriptionConfirmation":
urllib.request.urlopen(body["SubscribeURL"])
# disable automatic unsubscribe confirmation by activating subscription again
elif body["Type"] == "UnsubscribeConfirmation":
urllib.request.urlopen(body["SubscribeURL"])
# handle bounce notifications only
elif body["Type"] == "Notification":
msg = json.loads(body["Message"])
# check if msg contains notificationType
if "notificationType" not in msg:
return
recepients = msg["bounce"]["bouncedRecipients"]
for recipient in recepients:
blockEmail = BlockedEmail(email=recipient["emailAddress"], reason=msg["notificationType"], severity=msg["bounce"]["bounceType"])
self.db.add(blockEmail)
self.db.commit()
return "OK"

103
services/emailService.py Normal file
View File

@ -0,0 +1,103 @@
import dotenv
import json
dotenv.load_dotenv()
import os
import boto3
from logging import getLogger
logger = getLogger(__name__)
class EmailService:
def __init__(self):
self.client = boto3.client(
"ses",
region_name=os.getenv("AWS_REGION"),
aws_access_key_id=os.getenv("AWS_ACCESS_KEY"),
aws_secret_access_key=os.getenv("AWS_SECRET_KEY"),
)
self.senderEmail = os.getenv("AWS_SENDER_EMAIL")
def createTemplate(self):
"""Create or update email templates"""
try:
otp_template = {
"TemplateName": "sendOTP",
"SubjectPart": "Your OTP for login is {{otp}}",
"HtmlPart": """
<p>Dear User,</p>
<p>Thank you for using our service. To complete your authentication, please use the following one-time password (OTP):</p>
<p>OTP: {{otp}}</p>
<p>This OTP is valid for 15 minutes. Do not share this OTP with anyone for security reasons. If you did not request this OTP, please ignore this email.</p>
<p>Thank you,<br/>Team 24x7 AIHR</p>
""",
"TextPart": "Dear User, Thank you for using our service. To complete your authentication, please use the following one-time password (OTP): OTP: {{otp}} This OTP is valid for 15 minutes. Do not share this OTP with anyone for security reasons. If you did not request this OTP, please ignore this email. Thank you, Team 24x7 AIHR"
}
new_clinic_template = {
"TemplateName": "newClinic",
"SubjectPart": "New Clinic Added",
"HtmlPart": """
<p>Dear Admin,</p>
<p>A new clinic has been added to the system.</p>
<p>Thank you,<br/>Team 24x7 AIHR</p>
""",
"TextPart": "Dear Admin, A new clinic has been added to the system. Thank you, Team 24x7 AIHR"
}
reject_clinic_template = {
"TemplateName": "rejectClinic",
"SubjectPart": "Clinic Rejected",
"HtmlPart": """
<p>Dear User,</p>
<p>Your clinic {{name}} has been rejected.</p>
<p>Thank you,<br/>Team 24x7 AIHR</p>
""",
"TextPart": "Dear User, Your clinic {{name}} has been rejected. Thank you, Team 24x7 AIHR"
}
apporve_clinic_template = {
"TemplateName": "apporveClinic",
"SubjectPart": "Clinic Approved",
"HtmlPart": """
<p>Dear User,</p>
<p>Congratulations! Your clinic {{name}} has been approved.</p>
<p>Thank you,<br/>Team 24x7 AIHR</p>
""",
"TextPart": "Dear User, Congratulations! Your clinic {{name}} has been approved. Thank you, Team 24x7 AIHR"
}
self.client.create_template(Template=otp_template)
self.client.create_template(Template=new_clinic_template)
self.client.create_template(Template=reject_clinic_template)
self.client.create_template(Template=apporve_clinic_template)
except Exception as e:
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):
"""Send an email using a template"""
try:
response = self.client.send_templated_email(
Source=self.senderEmail,
Destination={
'ToAddresses': [to_address]
},
Template=template_name,
TemplateData=json.dumps(template_data),
ReplyToAddresses=[self.senderEmail]
)
logger.info(f"Email sent to {to_address} successfully. MessageId: {response['MessageId']}")
return response
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",
to_address=email,
template_data={"otp": otp, "email": email}
)

0
templates/__init.py Normal file
View File

153
templates/htmlTemplatest.py Normal file
View File

@ -0,0 +1,153 @@
otpTemplate = {
"TemplateName": "sendOTP",
"SubjectPart": "Your OTP for login is {{otp}}",
"HtmlPart": """
<html>
<head>
<meta charset="UTF-8" />
<title>One-Time Passcode</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f5;
color: #333333;
}
.container {
max-width: 600px;
margin: 30px auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
background-color: rgb(70, 45, 24);
color: #ffffff;
padding: 20px;
position: relative;
height: 60px;
}
.logo {
max-height: 40px;
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
}
.header h1 {
margin: 0;
font-size: 20px;
font-weight: normal;
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
}
.content {
padding: 20px 30px;
}
.otp-code {
font-size: 28px;
font-weight: bold;
text-align: center;
letter-spacing: 5px;
margin: 25px 0;
color: rgb(70, 45, 24);
}
.otp-box {
background: #f9fafb;
border-left: 4px solid rgb(70, 45, 24);
padding: 20px;
border-radius: 4px;
margin: 20px 0;
}
.security-notice {
font-size: 13px;
background-color: #fef9c3;
border-left: 4px solid #eab308;
padding: 12px;
margin: 20px 0;
border-radius: 4px;
}
.closing {
margin-top: 25px;
font-size: 14px;
}
.footer {
padding: 15px 30px;
background: #f4f4f5;
font-size: 12px;
color: #666666;
text-align: center;
}
@media screen and (max-width: 480px) {
.content {
padding: 15px;
}
.header,
.footer {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img
src="https://firebasestorage.googleapis.com/v0/b/mansupport-australia.firebasestorage.app/o/Frame%20427320968.png?alt=media&token=9651f57d-aafd-450d-a0b0-1898504f5aac"
alt="Company Logo" class="logo" />
<h1>One-Time Passcode</h1>
</div>
<div class="content">
<p>Dear User,</p>
<p>Thank you for your account verification request. Please use the following one-time passcode to complete the
authentication process:</p>
<div class="otp-box">
<div class="otp-code">{{otp}}</div>
</div>
<div class="security-notice">
<strong>Security Notice:</strong> For your security, please do not share this code with anyone. Team 24x7 AIHR
representatives will never ask for your OTP.
</div>
<p>If you did not request this verification, please disregard this message and consider changing your password.
</p>
<div class="closing">
Regards,<br />
Team 24x7 AIHR
</div>
</div>
<div class="footer">
This is a system-generated email. Please do not reply.<br />
&copy; 2025 Team 24x7 AIHR Pty Ltd. All rights reserved.
</div>
</div>
</body>
</html>
""",
"TextPart": "Dear User, Thank you for using our service. To complete your authentication, please use the following one-time password (OTP): OTP: {{otp}} This OTP is valid for 15 minutes. Do not share this OTP with anyone for security reasons. If you did not request this OTP, please ignore this email. Thank you, Team 24x7 AIHR"
}