From 378ae41b39b445a868c29add2f18b89951192d20 Mon Sep 17 00:00:00 2001 From: Jet Li Date: Sat, 19 Oct 2024 20:56:19 +0000 Subject: [PATCH] Add API support for get_document_by_id --- .gitignore | 5 ++ app/central_storage/Dockerfile | 4 +- .../infra/azure_storage/blob_manager.py | 3 + app/central_storage/requirements.txt | 14 ++++- .../webapi/bootstrap/application.py | 18 +++--- app/central_storage/webapi/main.py | 15 +++-- .../webapi/providers/common.py | 2 +- .../webapi/providers/database.py | 18 ++++-- .../webapi/providers/logger.py | 13 +--- .../webapi/providers/router.py | 2 +- app/central_storage/webapi/routes/__init__.py | 2 + .../webapi/routes/get_document_by_id.py | 33 +++++++++-- infra/config/app_settings.py | 15 +++-- infra/token/token_manager.py | 59 +++++++++++++++++++ 14 files changed, 154 insertions(+), 49 deletions(-) create mode 100644 infra/token/token_manager.py diff --git a/.gitignore b/.gitignore index 21d0b89..dee16c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ .venv/ +*__pycache__* +.vscode +/deploy/.* +*.log +*.pyc \ No newline at end of file diff --git a/app/central_storage/Dockerfile b/app/central_storage/Dockerfile index 1ae22ec..9aeaf6c 100644 --- a/app/central_storage/Dockerfile +++ b/app/central_storage/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile for Python Service -FROM python:3.11-slim +FROM python:3.10-slim # Set the working directory inside the container WORKDIR /app @@ -16,4 +16,4 @@ EXPOSE 8005 # Run the application using the start script -CMD ["uvicorn", "webapi.main:app", "--reload", "--port=8003", "--host=0.0.0.0"] +CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0"] diff --git a/app/central_storage/backend/infra/azure_storage/blob_manager.py b/app/central_storage/backend/infra/azure_storage/blob_manager.py index 943f2c0..614baf4 100644 --- a/app/central_storage/backend/infra/azure_storage/blob_manager.py +++ b/app/central_storage/backend/infra/azure_storage/blob_manager.py @@ -92,6 +92,9 @@ class AzureBlobManager: app_settings.AZURE_STORAGE_DOCUMENT_API_ENDPOINT, credential=app_settings.AZURE_STORAGE_DOCUMENT_API_KEY, ) as blob_service_client: + # user_delegation_key = await blob_service_client.get_user_delegation_key( + # key_start_time=key_start_time, key_expiry_time=key_expiry_time + # ) blob_client = blob_service_client.get_blob_client( container=container_name, blob=file_name ) diff --git a/app/central_storage/requirements.txt b/app/central_storage/requirements.txt index 5301cc4..d9b553f 100644 --- a/app/central_storage/requirements.txt +++ b/app/central_storage/requirements.txt @@ -1,3 +1,13 @@ +fastapi==0.114.0 +fastapi-jwt==0.2.0 pika==1.3.2 -fastapi -uvicorn \ No newline at end of file +pydantic==2.9.2 +loguru==0.7.2 +uvicorn==0.23.2 +beanie==1.21.0 +aio-pika +pydantic-settings +python-jose +azure-storage-blob==12.22.0 +azure-identity +azure-core[aio] \ No newline at end of file diff --git a/app/central_storage/webapi/bootstrap/application.py b/app/central_storage/webapi/bootstrap/application.py index 7a38aa7..cf20628 100644 --- a/app/central_storage/webapi/bootstrap/application.py +++ b/app/central_storage/webapi/bootstrap/application.py @@ -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.central_storage.webapi.providers import common +from app.central_storage.webapi.providers import logger +from app.central_storage.webapi.providers import router +from app.central_storage.webapi.providers import database +from app.central_storage.webapi.providers import scheduler +from app.central_storage.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 diff --git a/app/central_storage/webapi/main.py b/app/central_storage/webapi/main.py index df0c604..2f1dad7 100755 --- a/app/central_storage/webapi/main.py +++ b/app/central_storage/webapi/main.py @@ -1,15 +1,14 @@ -from webapi.bootstrap.application import create_app -from webapi.config.site_settings import site_settings +from app.central_storage.webapi.bootstrap.application import create_app +from app.central_storage.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 +18,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 {} \ No newline at end of file + return {} diff --git a/app/central_storage/webapi/providers/common.py b/app/central_storage/webapi/providers/common.py index 1dd849f..f61954d 100644 --- a/app/central_storage/webapi/providers/common.py +++ b/app/central_storage/webapi/providers/common.py @@ -1,5 +1,5 @@ from fastapi.middleware.cors import CORSMiddleware -from webapi.config.site_settings import site_settings +from app.central_storage.webapi.config.site_settings import site_settings def register(app): diff --git a/app/central_storage/webapi/providers/database.py b/app/central_storage/webapi/providers/database.py index 59ed3ab..4d43966 100644 --- a/app/central_storage/webapi/providers/database.py +++ b/app/central_storage/webapi/providers/database.py @@ -1,11 +1,12 @@ -from webapi.config.site_settings import site_settings +from infra.config.app_settings import app_settings from beanie import init_beanie from motor.motor_asyncio import AsyncIOMotorClient +from app.central_storage.backend.models.models import DocumentDoc def register(app): - app.debug = site_settings.DEBUG - app.title = site_settings.NAME + app.debug = "mongo_debug" + app.title = "mongo_name" @app.on_event("startup") async def start_database(): @@ -13,5 +14,12 @@ def register(app): async def initiate_database(): - #init your database here - pass + client = AsyncIOMotorClient( + app_settings.MONGODB_URI, + serverSelectionTimeoutMS=60000, + minPoolSize=5, # Minimum number of connections in the pool + maxPoolSize=20, # Maximum number of connections in the pool + ) + await init_beanie( + database=client[app_settings.MONGODB_NAME], document_models=[DocumentDoc] + ) diff --git a/app/central_storage/webapi/providers/logger.py b/app/central_storage/webapi/providers/logger.py index 4a3f1e7..c53e52d 100644 --- a/app/central_storage/webapi/providers/logger.py +++ b/app/central_storage/webapi/providers/logger.py @@ -1,7 +1,7 @@ import logging import sys from loguru import logger -from common.config.log_settings import log_settings +from app.central_storage.common.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") diff --git a/app/central_storage/webapi/providers/router.py b/app/central_storage/webapi/providers/router.py index 3ad11ae..276b944 100644 --- a/app/central_storage/webapi/providers/router.py +++ b/app/central_storage/webapi/providers/router.py @@ -1,4 +1,4 @@ -from webapi.routes import api_router +from app.central_storage.webapi.routes import api_router from starlette import routing diff --git a/app/central_storage/webapi/routes/__init__.py b/app/central_storage/webapi/routes/__init__.py index 3237813..67eb039 100644 --- a/app/central_storage/webapi/routes/__init__.py +++ b/app/central_storage/webapi/routes/__init__.py @@ -1,5 +1,7 @@ from fastapi import APIRouter +from .get_document_by_id import router as doc_router api_router = APIRouter() +api_router.include_router(doc_router, tags=["attachment"]) websocket_router = APIRouter() diff --git a/app/central_storage/webapi/routes/get_document_by_id.py b/app/central_storage/webapi/routes/get_document_by_id.py index 1aaad08..3201aed 100644 --- a/app/central_storage/webapi/routes/get_document_by_id.py +++ b/app/central_storage/webapi/routes/get_document_by_id.py @@ -1,11 +1,11 @@ -from fastapi import APIRouter, Security, HTTPException +from fastapi import APIRouter, HTTPException, Request from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from fastapi_jwt import JwtAuthorizationCredentials -from backend.infra.authentication.auth import access_security +from infra.token.token_manager import TokenManager from app.central_storage.backend.application.document_app import DocumentHub router = APIRouter() +token_manager = TokenManager() # Web API @@ -19,9 +19,32 @@ router = APIRouter() ) async def get_document_by_id( document_id: str, - credentials: JwtAuthorizationCredentials = Security(access_security), + request: Request, ): - user_id = credentials["id"] + # Extract the Authorization header + auth_header = request.headers.get("Authorization") + + if not auth_header: + raise HTTPException(status_code=401, detail="Authorization header missing") + + # Ensure the header starts with 'Bearer' + if not auth_header.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Invalid authorization header") + + token = auth_header.split(" ")[1] + + try: + # Decode the token using the TokenManager + credentials = token_manager.decode_token(token) + except Exception as e: + raise HTTPException( + status_code=401, detail=f"Invalid or expired token: {str(e)}" + ) + + # Get the user_id from the decoded token + user_id = credentials.get("id") + if not user_id: + raise HTTPException(status_code=401, detail="Invalid token payload") # Fetch the document using DocumentHub document = await DocumentHub(user_id).get_document_by_id(document_id) diff --git a/infra/config/app_settings.py b/infra/config/app_settings.py index 70bf94e..d61e3f6 100644 --- a/infra/config/app_settings.py +++ b/infra/config/app_settings.py @@ -1,15 +1,18 @@ -import os from pydantic_settings import BaseSettings -class AppSettings(): - NAME: str = "myapp" + +class AppSettings(BaseSettings): + JWT_SECRET_KEY: str = "" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + REFRESH_TOKEN_EXPIRE_DAYS: int = 30 + MONGODB_NAME: str = "freeleaps2" + MONGODB_URI: str = ( + "mongodb+srv://freeadmin:0eMV0bt8oyaknA0m@freeleaps2.zmsmpos.mongodb.net/?retryWrites=true&w=majority" + ) class Config: env_file = ".myapp.env" env_file_encoding = "utf-8" - APPLICATION_ACTIVITY_LOG: str = "myapp-application-activity" - USER_ACTIVITY_LOG: str = "myapp-user-activity" - BUSINESS_METRIC_LOG: str = "myapp-business-metrics" app_settings = AppSettings() diff --git a/infra/token/token_manager.py b/infra/token/token_manager.py new file mode 100644 index 0000000..16f142e --- /dev/null +++ b/infra/token/token_manager.py @@ -0,0 +1,59 @@ +# application/auth/token/token_manager.py +from datetime import datetime, timedelta, timezone +from typing import Dict +from jose import jwt +from infra.config.app_settings import app_settings + + +class TokenManager: + def __init__(self): + self.secret_key = app_settings.JWT_SECRET_KEY + self.algorithm = "HS256" + self.access_token_expire_minutes = app_settings.ACCESS_TOKEN_EXPIRE_MINUTES + self.refresh_token_expire_days = app_settings.REFRESH_TOKEN_EXPIRE_DAYS + + def create_access_token(self, subject: Dict[str, str]) -> str: + """ + Generates an access token with a short expiration time. + Args: + subject (Dict[str, str]): A dictionary containing user information like 'id' and 'role'. + + Returns: + str: Encoded JWT access token. + """ + expire = datetime.now(timezone.utc) + timedelta( + minutes=self.access_token_expire_minutes + ) + to_encode = subject.copy() + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm) + + def create_refresh_token(self, subject: Dict[str, str]) -> str: + """ + Generates a refresh token with a longer expiration time. + Args: + subject (Dict[str, str]): A dictionary containing user information like 'id' and 'role'. + + Returns: + str: Encoded JWT refresh token. + """ + expire = datetime.now(timezone.utc) + timedelta( + days=self.refresh_token_expire_days + ) + to_encode = subject.copy() + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm) + + def decode_token(self, token: str) -> Dict: + """ + Decodes a JWT token and returns the payload. + Args: + token (str): Encoded JWT token. + + Returns: + Dict: Decoded token payload. + + Raises: + JWTError: If the token is invalid or expired. + """ + return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])