Add some more authentication methods along with rest of notification method

This commit is contained in:
jetli 2024-10-25 17:14:18 +00:00
parent 1f551ad161
commit 9c4baae49e
26 changed files with 273 additions and 55 deletions

View File

@ -67,6 +67,16 @@ class SignInHub:
user_id=user_id, password=password user_id=user_id, password=password
) )
@log_entry_exit_async
async def send_email_code(self, sender_id: str, email: str) -> dict[str, any]:
result = await self.signin_manager.send_email_code(sender_id, email)
return {"succeeded": result}
@log_entry_exit_async
async def send_mobile_code(self, sender_id: str, mobile: str) -> dict[str, any]:
result = await self.signin_manager.send_mobile_code(sender_id, mobile)
return {"succeeded": result}
@log_entry_exit_async @log_entry_exit_async
async def sign_out(self, identity: str) -> bool: async def sign_out(self, identity: str) -> bool:
# TODO: to be implemented # TODO: to be implemented

View File

@ -16,6 +16,12 @@ from app.authentication.backend.services.code_depot.code_depot_service import (
from infra.log.module_logger import ModuleLogger from infra.log.module_logger import ModuleLogger
from infra.utils.string import check_password_complexity from infra.utils.string import check_password_complexity
from infra.exception.exceptions import InvalidDataError from infra.exception.exceptions import InvalidDataError
from app.authentication.backend.services.notification.notification_service import (
NotificationService,
)
from app.authentication.backend.models.user.constants import (
AuthType,
)
class SignInManager: class SignInManager:
@ -25,6 +31,7 @@ class SignInManager:
self.user_management_service = UserManagementService() self.user_management_service = UserManagementService()
self.module_logger = ModuleLogger(sender_id=SignInManager) self.module_logger = ModuleLogger(sender_id=SignInManager)
self.code_depot_service = CodeDepotService() self.code_depot_service = CodeDepotService()
self.notification_service = NotificationService()
# TODO: Dax: notification service # TODO: Dax: notification service
# self.event_service = EventService() # self.event_service = EventService()
@ -319,3 +326,31 @@ class SignInManager:
user_id, user_flid, password user_id, user_flid, password
) )
return {"succeeded": True} return {"succeeded": True}
async def send_email_code(self, sender_id: str, email: str) -> bool:
mail_code = await self.user_auth_service.generate_auth_code_for_object(
email, AuthType.EMAIL
)
success = await self.notification_service.send_notification(
sender_id=sender_id,
channels=["email"],
receiver_id=email,
subject="email",
event="authentication",
properties={"auth_code": mail_code},
)
return success
async def send_mobile_code(self, sender_id: str, mobile: str) -> bool:
mail_code = await self.user_auth_service.generate_auth_code_for_object(
mobile, AuthType.MOBILE
)
success = await self.notification_service.send_notification(
sender_id=sender_id,
channels=["email"],
receiver_id=mobile,
subject="mobile",
event="authentication",
properties={"auth_code": mail_code},
)
return success

View File

@ -6,8 +6,6 @@ from infra.utils.string import generate_auth_code
from app.authentication.backend.infra.code_management.depot_handler import ( from app.authentication.backend.infra.code_management.depot_handler import (
CodeDepotHandler, CodeDepotHandler,
) )
from app.authentication.backend.models.user.constants import ( from app.authentication.backend.models.user.constants import (
AuthType, AuthType,
) )
@ -331,18 +329,19 @@ class UserAuthHandler:
else: else:
return False return False
async def generate_auth_code(self, email: str) -> str: async def generate_auth_code(self, deliver_object: str, auth_type: AuthType) -> str:
"""send auth code to email address """send auth code to email address
Args: Args:
email (str): email address deliver_object (str): email address, mobile, etc
auth_type (str): authentication type
""" """
auth_code = generate_auth_code() auth_code = generate_auth_code()
expiry = datetime.now(timezone.utc) + timedelta(minutes=5) expiry = datetime.now(timezone.utc) + timedelta(minutes=5)
auth_code_doc = AuthCodeDoc( auth_code_doc = AuthCodeDoc(
auth_code=auth_code, auth_code=auth_code,
method=email.lower(), method=deliver_object.lower(),
method_type=AuthType.EMAIL, method_type=auth_type,
expiry=expiry, expiry=expiry,
) )

View File

@ -1,6 +1,9 @@
from app.authentication.backend.infra.auth.user_auth_handler import ( from app.authentication.backend.infra.auth.user_auth_handler import (
UserAuthHandler, UserAuthHandler,
) )
from app.authentication.backend.models.user.constants import (
AuthType,
)
from typing import Optional from typing import Optional
@ -29,8 +32,12 @@ class UserAuthService:
async def update_flid(self, user_id: str, user_flid: str) -> str: async def update_flid(self, user_id: str, user_flid: str) -> str:
return await self.user_auth_handler.update_flid(user_id, user_flid) return await self.user_auth_handler.update_flid(user_id, user_flid)
async def generate_auth_code_for_email(self, email: str) -> str: async def generate_auth_code_for_object(
return await self.user_auth_handler.generate_auth_code(email) self, deliver_object: str, auth_type: AuthType
) -> str:
return await self.user_auth_handler.generate_auth_code(
deliver_object, auth_type
)
async def verify_user_with_password(self, user_id: str, password: str) -> bool: async def verify_user_with_password(self, user_id: str, password: str) -> bool:
return await self.user_auth_handler.verify_user_with_password(user_id, password) return await self.user_auth_handler.verify_user_with_password(user_id, password)

View File

@ -0,0 +1,35 @@
import httpx
from pydantic import BaseModel
from typing import Dict, List
class NotificationService:
def __init__(self, base_url: str = "http://localhost:8003"):
self.base_url = base_url
async def send_notification(
self,
sender_id: str,
channels: List[str],
receiver_id: str,
subject: str,
event: str,
properties: Dict,
) -> bool:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/send_notification",
json={
"sender_id": sender_id,
"channels": channels,
"receiver_id": receiver_id,
"subject": subject,
"event": event,
"properties": properties,
},
)
if response.status_code == 200:
return True
else:
# Optionally log or handle errors here
return False

View File

@ -8,6 +8,7 @@ uvicorn==0.23.2
beanie==1.21.0 beanie==1.21.0
jieba==0.42.1 jieba==0.42.1
aio-pika aio-pika
httpx
pydantic-settings pydantic-settings
python-jose python-jose
passlib[bcrypt] passlib[bcrypt]

View File

@ -1,8 +1,10 @@
from fastapi import APIRouter from fastapi import APIRouter
from .signin import router as signin_router from .signin import router as signin_router
from .tokens import router as token_router from .tokens import router as token_router
from .auth import router as auth_router
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(signin_router, tags=["user"]) api_router.include_router(signin_router, tags=["user"])
api_router.include_router(token_router, tags=["auth"]) api_router.include_router(token_router, tags=["token"])
api_router.include_router(auth_router, tags=["auth"])
websocket_router = APIRouter() websocket_router = APIRouter()

View File

@ -0,0 +1,9 @@
from fastapi import APIRouter
from .send_email_code import router as sec_router
from .send_mobile_code import router as smc_router
router = APIRouter(prefix="/auth")
router.include_router(sec_router, tags=["authentication"])
router.include_router(smc_router, tags=["authentication"])

View File

@ -0,0 +1,41 @@
from app.authentication.backend.application.signin_hub import SignInHub
from pydantic import BaseModel
from infra.token.token_manager import TokenManager
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
router = APIRouter()
token_manager = TokenManager()
# Web API
# send_email_code
#
class RequestIn(BaseModel):
email: str
sender_id: str
@router.post(
"/send-email-code",
operation_id="send-email-code",
summary="Send a verification code to the specified email address",
description="This API requires an authenticated user and will send a code to the specified email. \
The code can be used later to verify the email address.",
response_description="Indicates success or failure of the email code send operation",
)
async def send_email_code(
item: RequestIn,
current_user: dict = Depends(token_manager.get_current_user),
):
user_id = current_user.get("id")
if not user_id:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)
result = await SignInHub(user_id).send_email_code(item.sender_id, item.email)
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,40 @@
from app.authentication.backend.application.signin_hub import SignInHub
from pydantic import BaseModel
from infra.token.token_manager import TokenManager
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
router = APIRouter()
token_manager = TokenManager()
# Web API
# send_email_code
#
class RequestIn(BaseModel):
email: str
@router.post(
"/send-mobile-code",
operation_id="send-mobile-code",
summary="Send a verification code to the specified mobile number",
description="This API requires an authenticated user and will send a code to the specified mobile. \
The code can be used later to verify the mobile.",
response_description="Indicates success or failure of the mobile code send operation",
)
async def send_email_code(
item: RequestIn,
current_user: dict = Depends(token_manager.get_current_user),
):
user_id = current_user.get("id")
if not user_id:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)
result = await SignInHub(user_id).send_mobile_code(item.email)
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,19 @@
# Dockerfile for Python Service
FROM python:3.10-slim
# Set the working directory inside the container
WORKDIR /app
# Copy the requirements.txt to the working directory and install dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code to the working directory
COPY . /app
# Expose the port used by the FastAPI app
EXPOSE 8003
# Run the application using the start script
CMD ["uvicorn", "app.notification.webapi.main:app", "--reload", "--port=8003", "--host=0.0.0.0"]

View File

@ -1,5 +1,5 @@
from app.notification.backend.models.constants import NotificationChannel from app.notification.backend.models.constants import NotificationChannel
from backend.infra.rabbitmq.async_publisher import AsyncMQPublisher from app.notification.backend.infra.rabbitmq.async_publisher import AsyncMQPublisher
class NotificationPublisherService: class NotificationPublisherService:

View File

@ -0,0 +1,14 @@
fastapi==0.114.0
fastapi-jwt==0.2.0
pika==1.3.2
pydantic==2.9.2
uvicorn==0.23.2
beanie==1.21.0
httpx==0.24.1
loguru==0.7.2
sendgrid
aio-pika
twilio
pydantic-settings
python-multipart
python-jose

View File

@ -2,12 +2,12 @@ import logging
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from webapi.providers import common from app.notification.webapi.providers import common
from webapi.providers import logger from app.notification.webapi.providers import logger
from webapi.providers import router from app.notification.webapi.providers import router
from webapi.providers import database from app.notification.webapi.providers import database
from webapi.providers import scheduler from app.notification.webapi.providers import scheduler
from webapi.providers import exception_handler from app.notification.webapi.providers import exception_handler
from .freeleaps_app import FreeleapsApp from .freeleaps_app import FreeleapsApp
@ -49,11 +49,7 @@ def customize_openapi_security(app: FastAPI) -> None:
# Add security scheme to components # Add security scheme to components
openapi_schema["components"]["securitySchemes"] = { openapi_schema["components"]["securitySchemes"] = {
"bearerAuth": { "bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
} }
# Add security requirement globally # Add security requirement globally

View File

@ -1,8 +1,8 @@
from fastapi import FastAPI from fastapi import FastAPI
from backend.infra.rabbitmq.async_subscriber import AsyncMQSubscriber from app.notification.backend.infra.rabbitmq.async_subscriber import AsyncMQSubscriber
from backend.models.constants import NotificationChannel from app.notification.backend.models.constants import NotificationChannel
from webapi.utils.email_consumer import EmailMQConsumer from app.notification.webapi.utils.email_consumer import EmailMQConsumer
from webapi.utils.sms_consumer import SmsMQConsumer from app.notification.webapi.utils.sms_consumer import SmsMQConsumer
class FreeleapsApp(FastAPI): class FreeleapsApp(FastAPI):

View File

@ -1,15 +1,13 @@
from webapi.bootstrap.application import create_app from app.notification.webapi.bootstrap.application import create_app
from webapi.config.site_settings import site_settings from app.notification.webapi.config.site_settings import site_settings
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from strawberry.fastapi import GraphQLRouter
from strawberry.fastapi.handlers import GraphQLTransportWSHandler, GraphQLWSHandler
import uvicorn import uvicorn
from typing import Any from typing import Any
app = create_app() app = create_app()
@app.get("/", status_code=301) @app.get("/", status_code=301)
async def root(): async def root():
""" """
@ -19,12 +17,16 @@ async def root():
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run(app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT) uvicorn.run(
app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT
)
def get_context() -> Any: def get_context() -> Any:
# Define your context function. This is where you can set up authentication, database connections, etc. # Define your context function. This is where you can set up authentication, database connections, etc.
return {} return {}
def get_root_value() -> Any: def get_root_value() -> Any:
# Define your root value function. This can be used to customize the root value for GraphQL operations. # Define your root value function. This can be used to customize the root value for GraphQL operations.
return {} return {}

View File

@ -1,5 +1,5 @@
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from webapi.config.site_settings import site_settings from app.notification.webapi.config.site_settings import site_settings
def register(app): def register(app):

View File

@ -1,4 +1,4 @@
from webapi.config.site_settings import site_settings from app.notification.webapi.config.site_settings import site_settings
from beanie import init_beanie from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient from motor.motor_asyncio import AsyncIOMotorClient
@ -13,5 +13,5 @@ def register(app):
async def initiate_database(): async def initiate_database():
#init your database here # init your database here
pass pass

View File

@ -1,7 +1,7 @@
import logging import logging
import sys import sys
from loguru import logger from loguru import logger
from common.config.log_settings import log_settings from infra.config.log_settings import log_settings
def register(app=None): def register(app=None):
@ -21,15 +21,8 @@ def register(app=None):
logging.getLogger(name).propagate = True logging.getLogger(name).propagate = True
# configure loguru # configure loguru
logger.add( logger.add(sink=sys.stdout)
sink=sys.stdout logger.add(sink=file_path, level=level, retention=retention, rotation=rotation)
)
logger.add(
sink=file_path,
level=level,
retention=retention,
rotation=rotation
)
logger.disable("pika.adapters") logger.disable("pika.adapters")
logger.disable("pika.connection") logger.disable("pika.connection")

View File

@ -1,4 +1,4 @@
from webapi.routes import api_router from app.notification.webapi.routes import api_router
from starlette import routing from starlette import routing

View File

@ -4,5 +4,5 @@ import asyncio
def register(app): def register(app):
@app.on_event("startup") @app.on_event("startup")
async def start_scheduler(): async def start_scheduler():
#create your scheduler here # create your scheduler here
pass pass

View File

@ -1,5 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
from .send_notification import router as sn_router
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(sn_router, tags=["notification"])
websocket_router = APIRouter() websocket_router = APIRouter()

View File

@ -22,14 +22,14 @@ class NotificationRequest(BaseModel):
# Web API # Web API
# Send notification to user for a certain channel # Send notification to user for a certain channel
@router.post( @router.post(
"/enqueue_notification", "/send_notification",
operation_id="enqueue_notification", operation_id="send_notification",
summary="Send notification to user for a certain channel", summary="Send notification to user for a certain channel",
description="Send notification to user for a channel (e.g., sms, email, in-app, etc.)", description="Send notification to user for a channel (e.g., sms, email, in-app, etc.)",
response_description="Success/failure response in processing the notification send request", response_description="Success/failure response in processing the notification send request",
) )
# API route to enqueue notifications # API route to enqueue notifications
@router.post("/enqueue_notification") @router.post("/send_notification")
async def enqueue_notification(request: NotificationRequest): async def enqueue_notification(request: NotificationRequest):
try: try:
notification_hub = NotificationHub() notification_hub = NotificationHub()

View File

@ -1,6 +1,6 @@
from app.notification.common.config.app_settings import app_settings from app.notification.common.config.app_settings import app_settings
from backend.infra.rabbitmq.async_subscriber import AsyncMQSubscriber from app.notification.backend.infra.rabbitmq.async_subscriber import AsyncMQSubscriber
from backend.infra.email_handler import EmailHandler from app.notification.backend.infra.email_handler import EmailHandler
class EmailMQConsumer: class EmailMQConsumer:

View File

@ -1,6 +1,6 @@
from app.notification.common.config.app_settings import app_settings from app.notification.common.config.app_settings import app_settings
from backend.infra.rabbitmq.async_subscriber import AsyncMQSubscriber from app.notification.backend.infra.rabbitmq.async_subscriber import AsyncMQSubscriber
from backend.infra.sms_handler import SmsHandler from app.notification.backend.infra.sms_handler import SmsHandler
from infra.log.module_logger import ModuleLogger from infra.log.module_logger import ModuleLogger

View File

@ -43,6 +43,20 @@ services:
volumes: volumes:
- .:/app # Mount the current directory to /app in the container - .:/app # Mount the current directory to /app in the container
notification:
build:
context: app/notification
dockerfile: Dockerfile
restart: always
ports:
- "8003:8003" # Map the central_storage service port
networks:
- freeleaps_service_hub_network
env_file:
- sites/notification/.env
volumes:
- .:/app # Mount the current directory to /app in the container
networks: networks:
freeleaps_service_hub_network: freeleaps_service_hub_network:
driver: bridge driver: bridge