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
)
@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
async def sign_out(self, identity: str) -> bool:
# 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.utils.string import check_password_complexity
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:
@ -25,6 +31,7 @@ class SignInManager:
self.user_management_service = UserManagementService()
self.module_logger = ModuleLogger(sender_id=SignInManager)
self.code_depot_service = CodeDepotService()
self.notification_service = NotificationService()
# TODO: Dax: notification service
# self.event_service = EventService()
@ -319,3 +326,31 @@ class SignInManager:
user_id, user_flid, password
)
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 (
CodeDepotHandler,
)
from app.authentication.backend.models.user.constants import (
AuthType,
)
@ -331,18 +329,19 @@ class UserAuthHandler:
else:
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
Args:
email (str): email address
deliver_object (str): email address, mobile, etc
auth_type (str): authentication type
"""
auth_code = generate_auth_code()
expiry = datetime.now(timezone.utc) + timedelta(minutes=5)
auth_code_doc = AuthCodeDoc(
auth_code=auth_code,
method=email.lower(),
method_type=AuthType.EMAIL,
method=deliver_object.lower(),
method_type=auth_type,
expiry=expiry,
)

View File

@ -1,6 +1,9 @@
from app.authentication.backend.infra.auth.user_auth_handler import (
UserAuthHandler,
)
from app.authentication.backend.models.user.constants import (
AuthType,
)
from typing import Optional
@ -29,8 +32,12 @@ class UserAuthService:
async def update_flid(self, user_id: str, user_flid: str) -> str:
return await self.user_auth_handler.update_flid(user_id, user_flid)
async def generate_auth_code_for_email(self, email: str) -> str:
return await self.user_auth_handler.generate_auth_code(email)
async def generate_auth_code_for_object(
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:
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
jieba==0.42.1
aio-pika
httpx
pydantic-settings
python-jose
passlib[bcrypt]

View File

@ -1,8 +1,10 @@
from fastapi import APIRouter
from .signin import router as signin_router
from .tokens import router as token_router
from .auth import router as auth_router
api_router = APIRouter()
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()

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 backend.infra.rabbitmq.async_publisher import AsyncMQPublisher
from app.notification.backend.infra.rabbitmq.async_publisher import AsyncMQPublisher
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.openapi.utils import get_openapi
from webapi.providers import common
from webapi.providers import logger
from webapi.providers import router
from webapi.providers import database
from webapi.providers import scheduler
from webapi.providers import exception_handler
from app.notification.webapi.providers import common
from app.notification.webapi.providers import logger
from app.notification.webapi.providers import router
from app.notification.webapi.providers import database
from app.notification.webapi.providers import scheduler
from app.notification.webapi.providers import exception_handler
from .freeleaps_app import FreeleapsApp
@ -49,11 +49,7 @@ def customize_openapi_security(app: FastAPI) -> None:
# Add security scheme to components
openapi_schema["components"]["securitySchemes"] = {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
}
# Add security requirement globally

View File

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

View File

@ -1,15 +1,13 @@
from webapi.bootstrap.application import create_app
from webapi.config.site_settings import site_settings
from app.notification.webapi.bootstrap.application import create_app
from app.notification.webapi.config.site_settings import site_settings
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
from typing import Any
app = create_app()
@app.get("/", status_code=301)
async def root():
"""
@ -19,12 +17,16 @@ async def root():
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:
# Define your context function. This is where you can set up authentication, database connections, etc.
return {}
def get_root_value() -> Any:
# 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 webapi.config.site_settings import site_settings
from app.notification.webapi.config.site_settings import site_settings
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 motor.motor_asyncio import AsyncIOMotorClient
@ -13,5 +13,5 @@ def register(app):
async def initiate_database():
#init your database here
# init your database here
pass

View File

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

View File

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

View File

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

View File

@ -22,14 +22,14 @@ class NotificationRequest(BaseModel):
# Web API
# Send notification to user for a certain channel
@router.post(
"/enqueue_notification",
operation_id="enqueue_notification",
"/send_notification",
operation_id="send_notification",
summary="Send notification to user for a certain channel",
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",
)
# API route to enqueue notifications
@router.post("/enqueue_notification")
@router.post("/send_notification")
async def enqueue_notification(request: NotificationRequest):
try:
notification_hub = NotificationHub()

View File

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

View File

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

View File

@ -43,6 +43,20 @@ services:
volumes:
- .:/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:
freeleaps_service_hub_network:
driver: bridge