diff --git a/app/notification/Dockerfile b/app/notification/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/app/notification/backend/application/notification_hub.py b/app/notification/backend/application/notification_hub.py new file mode 100644 index 0000000..1323e4e --- /dev/null +++ b/app/notification/backend/application/notification_hub.py @@ -0,0 +1,11 @@ +from app.notification.backend.business.notification_manager import ( + NotificationManager, +) + + +class NotificationHub: + def __init__( + self, + ): + self.notification_manager = NotificationManager() + return diff --git a/app/notification/backend/business/notification_manager.py b/app/notification/backend/business/notification_manager.py new file mode 100644 index 0000000..57e663b --- /dev/null +++ b/app/notification/backend/business/notification_manager.py @@ -0,0 +1,10 @@ +from app.notification.backend.services.sms_service import SmsService +from app.notification.backend.services.in_app_notif_service import InAppNotifService +from app.notification.backend.services.email_service import EmailService + + +class NotificationManager: + def __init__(self) -> None: + self.sms_service = SmsService() + self.in_app_notif_service = InAppNotifService() + self.email_service = EmailService() diff --git a/app/notification/backend/infra/email_handler.py b/app/notification/backend/infra/email_handler.py new file mode 100644 index 0000000..a91276c --- /dev/null +++ b/app/notification/backend/infra/email_handler.py @@ -0,0 +1,2 @@ +class EmailHandler: + pass diff --git a/app/notification/backend/infra/in_app_notif_handler.py b/app/notification/backend/infra/in_app_notif_handler.py new file mode 100644 index 0000000..3d36c86 --- /dev/null +++ b/app/notification/backend/infra/in_app_notif_handler.py @@ -0,0 +1,2 @@ +class InAppNotifHandler: + pass diff --git a/app/notification/backend/infra/sms_handler.py b/app/notification/backend/infra/sms_handler.py new file mode 100644 index 0000000..95c84dc --- /dev/null +++ b/app/notification/backend/infra/sms_handler.py @@ -0,0 +1,2 @@ +class SmsHandler: + pass diff --git a/app/notification/backend/models/models.py b/app/notification/backend/models/models.py new file mode 100644 index 0000000..e69de29 diff --git a/app/notification/backend/readme.md b/app/notification/backend/readme.md new file mode 100644 index 0000000..f6f0396 --- /dev/null +++ b/app/notification/backend/readme.md @@ -0,0 +1 @@ +You can put code for backend logic here. \ No newline at end of file diff --git a/app/notification/backend/services/email_service.py b/app/notification/backend/services/email_service.py new file mode 100644 index 0000000..89270ee --- /dev/null +++ b/app/notification/backend/services/email_service.py @@ -0,0 +1,2 @@ +class EmailService: + pass diff --git a/app/notification/backend/services/in_app_notif_service.py b/app/notification/backend/services/in_app_notif_service.py new file mode 100644 index 0000000..c9f03b2 --- /dev/null +++ b/app/notification/backend/services/in_app_notif_service.py @@ -0,0 +1,2 @@ +class InAppNotifService: + pass diff --git a/app/notification/backend/services/sms_service.py b/app/notification/backend/services/sms_service.py new file mode 100644 index 0000000..ed18408 --- /dev/null +++ b/app/notification/backend/services/sms_service.py @@ -0,0 +1,2 @@ +class SmsService: + pass diff --git a/app/notification/common/config/app_settings.py b/app/notification/common/config/app_settings.py new file mode 100644 index 0000000..a0ec4dd --- /dev/null +++ b/app/notification/common/config/app_settings.py @@ -0,0 +1,18 @@ +import os +from pydantic_settings import BaseSettings + + +class AppSettings(BaseSettings): + NAME: str = "notification" + + AZURE_STORAGE_DOCUMENT_API_ENDPOINT: str = ( + "https://freeleaps1document.blob.core.windows.net/" + ) + AZURE_STORAGE_DOCUMENT_API_KEY: str = "" + + class Config: + env_file = ".myapp.env" + env_file_encoding = "utf-8" + + +app_settings = AppSettings() diff --git a/app/notification/common/config/log_settings.py b/app/notification/common/config/log_settings.py new file mode 100644 index 0000000..55a36da --- /dev/null +++ b/app/notification/common/config/log_settings.py @@ -0,0 +1,14 @@ +## This would be the app specific log setting file +class LogSettings: + LOG_LEVEL: str = "DEBUG" + LOG_PATH_BASE: str = "./logs" + LOG_PATH: str = LOG_PATH_BASE + "/" + "app" + ".log" + LOG_RETENTION: str = "14 days" + LOG_ROTATION: str = "00:00" # mid night + + class Config: + env_file = ".log.env" + env_file_encoding = "utf-8" + + +log_settings = LogSettings() diff --git a/app/notification/requirements.txt b/app/notification/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/app/notification/webapi/bootstrap/application.py b/app/notification/webapi/bootstrap/application.py new file mode 100644 index 0000000..7a38aa7 --- /dev/null +++ b/app/notification/webapi/bootstrap/application.py @@ -0,0 +1,75 @@ +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 .freeleaps_app import FreeleapsApp + + +def create_app() -> FastAPI: + logging.info("App initializing") + + app = FreeleapsApp() + + register(app, exception_handler) + register(app, database) + register(app, logger) + register(app, router) + register(app, scheduler) + register(app, common) + + # Call the custom_openapi function to change the OpenAPI version + customize_openapi_security(app) + return app + + +# This function overrides the OpenAPI schema version to 3.0.0 +def customize_openapi_security(app: FastAPI) -> None: + + def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + + # Generate OpenAPI schema + openapi_schema = get_openapi( + title="FreeLeaps API", + version="3.1.0", + description="FreeLeaps API Documentation", + routes=app.routes, + ) + + # Ensure the components section exists in the OpenAPI schema + if "components" not in openapi_schema: + openapi_schema["components"] = {} + + # Add security scheme to components + openapi_schema["components"]["securitySchemes"] = { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + + # Add security requirement globally + openapi_schema["security"] = [{"bearerAuth": []}] + + app.openapi_schema = openapi_schema + return app.openapi_schema + + app.openapi = custom_openapi + + +def register(app, provider): + logging.info(provider.__name__ + " registering") + provider.register(app) + + +def boot(app, provider): + logging.info(provider.__name__ + " booting") + provider.boot(app) diff --git a/app/notification/webapi/bootstrap/freeleaps_app.py b/app/notification/webapi/bootstrap/freeleaps_app.py new file mode 100644 index 0000000..496633a --- /dev/null +++ b/app/notification/webapi/bootstrap/freeleaps_app.py @@ -0,0 +1,6 @@ +from fastapi import FastAPI + + +class FreeleapsApp(FastAPI): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) diff --git a/app/notification/webapi/config/site_settings.py b/app/notification/webapi/config/site_settings.py new file mode 100644 index 0000000..43d05d6 --- /dev/null +++ b/app/notification/webapi/config/site_settings.py @@ -0,0 +1,25 @@ +import os + +from pydantic_settings import BaseSettings + + +class SiteSettings(BaseSettings): + NAME: str = "appname" + DEBUG: bool = True + + ENV: str = "dev" + + SERVER_HOST: str = "0.0.0.0" + SERVER_PORT: int = 8103 + + URL: str = "http://localhost" + TIME_ZONE: str = "UTC" + + BASE_PATH: str = os.path.dirname(os.path.dirname((os.path.abspath(__file__)))) + + class Config: + env_file = ".devbase-webapi.env" + env_file_encoding = "utf-8" + + +site_settings = SiteSettings() diff --git a/app/notification/webapi/main.py b/app/notification/webapi/main.py new file mode 100755 index 0000000..df0c604 --- /dev/null +++ b/app/notification/webapi/main.py @@ -0,0 +1,30 @@ +from webapi.bootstrap.application import create_app +from 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(): + """ + TODO: redirect client to /doc# + """ + return RedirectResponse("docs") + + +if __name__ == "__main__": + 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 {} \ No newline at end of file diff --git a/app/notification/webapi/providers/common.py b/app/notification/webapi/providers/common.py new file mode 100644 index 0000000..1dd849f --- /dev/null +++ b/app/notification/webapi/providers/common.py @@ -0,0 +1,31 @@ +from fastapi.middleware.cors import CORSMiddleware +from webapi.config.site_settings import site_settings + + +def register(app): + app.debug = site_settings.DEBUG + app.title = site_settings.NAME + + add_global_middleware(app) + + # This hook ensures that a connection is opened to handle any queries + # generated by the request. + @app.on_event("startup") + def startup(): + pass + + # This hook ensures that the connection is closed when we've finished + # processing the request. + @app.on_event("shutdown") + def shutdown(): + pass + + +def add_global_middleware(app): + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) diff --git a/app/notification/webapi/providers/database.py b/app/notification/webapi/providers/database.py new file mode 100644 index 0000000..59ed3ab --- /dev/null +++ b/app/notification/webapi/providers/database.py @@ -0,0 +1,17 @@ +from webapi.config.site_settings import site_settings +from beanie import init_beanie +from motor.motor_asyncio import AsyncIOMotorClient + + +def register(app): + app.debug = site_settings.DEBUG + app.title = site_settings.NAME + + @app.on_event("startup") + async def start_database(): + await initiate_database() + + +async def initiate_database(): + #init your database here + pass diff --git a/app/notification/webapi/providers/exception_handler.py b/app/notification/webapi/providers/exception_handler.py new file mode 100644 index 0000000..21117a5 --- /dev/null +++ b/app/notification/webapi/providers/exception_handler.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI, HTTPException +from fastapi.exceptions import RequestValidationError +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.status import ( + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_403_FORBIDDEN, + HTTP_404_NOT_FOUND, + HTTP_422_UNPROCESSABLE_ENTITY, + HTTP_500_INTERNAL_SERVER_ERROR, +) + + +async def custom_http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"error": exc.detail}, + ) + + + +async def validation_exception_handler(request: Request, exc: RequestValidationError): + return JSONResponse( + status_code=HTTP_400_BAD_REQUEST, + content={"error": str(exc)}, + ) + +async def exception_handler(request: Request, exc: Exception): + return JSONResponse( + status_code=HTTP_500_INTERNAL_SERVER_ERROR, + content={"error": str(exc)}, + ) + + +def register(app: FastAPI): + app.add_exception_handler(HTTPException, custom_http_exception_handler) + app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(Exception, exception_handler) diff --git a/app/notification/webapi/providers/logger.py b/app/notification/webapi/providers/logger.py new file mode 100644 index 0000000..4a3f1e7 --- /dev/null +++ b/app/notification/webapi/providers/logger.py @@ -0,0 +1,60 @@ +import logging +import sys +from loguru import logger +from common.config.log_settings import log_settings + + +def register(app=None): + level = log_settings.LOG_LEVEL + file_path = log_settings.LOG_PATH + retention = log_settings.LOG_RETENTION + rotation = log_settings.LOG_ROTATION + + # intercept everything at the root logger + logging.root.handlers = [InterceptHandler()] + logging.root.setLevel(level) + + # remove every other logger's handlers + # and propagate to root logger + for name in logging.root.manager.loggerDict.keys(): + logging.getLogger(name).handlers = [] + logging.getLogger(name).propagate = True + + # configure loguru + logger.add( + sink=sys.stdout + ) + logger.add( + sink=file_path, + level=level, + retention=retention, + rotation=rotation + ) + + logger.disable("pika.adapters") + logger.disable("pika.connection") + logger.disable("pika.channel") + logger.disable("pika.callback") + logger.disable("pika.frame") + logger.disable("pika.spec") + logger.disable("aiormq.connection") + logger.disable("urllib3.connectionpool") + + +class InterceptHandler(logging.Handler): + def emit(self, record): + # Get corresponding Loguru level if it exists + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + # Find caller from where originated the logged message + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log( + level, record.getMessage() + ) diff --git a/app/notification/webapi/providers/router.py b/app/notification/webapi/providers/router.py new file mode 100644 index 0000000..3ad11ae --- /dev/null +++ b/app/notification/webapi/providers/router.py @@ -0,0 +1,34 @@ +from webapi.routes import api_router + +from starlette import routing + + +def register(app): + app.include_router( + api_router, + prefix="/api", + tags=["api"], + dependencies=[], + responses={404: {"description": "no page found"}}, + ) + + if app.debug: + for route in app.routes: + if not isinstance(route, routing.WebSocketRoute): + print( + { + "path": route.path, + "endpoint": route.endpoint, + "name": route.name, + "methods": route.methods, + } + ) + else: + print( + { + "path": route.path, + "endpoint": route.endpoint, + "name": route.name, + "type": "web socket route", + } + ) diff --git a/app/notification/webapi/providers/scheduler.py b/app/notification/webapi/providers/scheduler.py new file mode 100644 index 0000000..7ea8d6c --- /dev/null +++ b/app/notification/webapi/providers/scheduler.py @@ -0,0 +1,8 @@ +import asyncio + + +def register(app): + @app.on_event("startup") + async def start_scheduler(): + #create your scheduler here + pass diff --git a/app/notification/webapi/routes/__init__.py b/app/notification/webapi/routes/__init__.py new file mode 100644 index 0000000..3237813 --- /dev/null +++ b/app/notification/webapi/routes/__init__.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter + +api_router = APIRouter() + +websocket_router = APIRouter() diff --git a/app/notification/webapi/routes/api.py b/app/notification/webapi/routes/api.py new file mode 100644 index 0000000..6545552 --- /dev/null +++ b/app/notification/webapi/routes/api.py @@ -0,0 +1,15 @@ +from fastapi.routing import APIRoute +from starlette import routing + + +def post_process_router(app) -> None: + """ + Simplify operation IDs so that generated API clients have simpler function + names. + + Should be called only after all routes have been added. + """ + for route in app.routes: + if isinstance(route, APIRoute): + if hasattr(route, "operation_id"): + route.operation_id = route.name # in this case, 'read_items' diff --git a/sites/authentication/.env b/sites/authentication/.env index d8a6620..4df0fdd 100644 --- a/sites/authentication/.env +++ b/sites/authentication/.env @@ -1 +1,2 @@ -export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ== \ No newline at end of file +export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ== +export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0 \ No newline at end of file diff --git a/sites/central_storage/.env b/sites/central_storage/.env index d8a6620..4df0fdd 100644 --- a/sites/central_storage/.env +++ b/sites/central_storage/.env @@ -1 +1,2 @@ -export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ== \ No newline at end of file +export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ== +export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0 \ No newline at end of file diff --git a/sites/notification/.env b/sites/notification/.env new file mode 100644 index 0000000..7a825c1 --- /dev/null +++ b/sites/notification/.env @@ -0,0 +1,5 @@ +export SENDGRID_API_KEY='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' +export EMAIL_FROM=freeleaps@freeleaps.com +export TWILIO_ACCOUNT_SID=ACf8c9283a6acda060258eadb29be58bc8 +export TWILIO_AUTH_TOKEN=ef160748cc22c8b7195b49df4b8eca7e +export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0 \ No newline at end of file