diff --git a/app/authentication/backend/business/signin_manager.py b/app/authentication/backend/business/signin_manager.py index 11f8339..3b5a0ab 100644 --- a/app/authentication/backend/business/signin_manager.py +++ b/app/authentication/backend/business/signin_manager.py @@ -6,8 +6,7 @@ from app.authentication.backend.models.user.constants import ( UserLoginAction, NewUserMethod, ) -from infra.exception.exceptions import InvalidAuthCodeException -from app.authentication.backend.models.constants import UserLoginAction +from app.authentication.backend.models.user.constants import UserLoginAction from app.authentication.backend.services.user.user_management_service import ( UserManagementService, ) diff --git a/app/authentication/backend/infra/auth/user_auth_handler.py b/app/authentication/backend/infra/auth/user_auth_handler.py index a6d268d..0b7b1ad 100644 --- a/app/authentication/backend/infra/auth/user_auth_handler.py +++ b/app/authentication/backend/infra/auth/user_auth_handler.py @@ -8,10 +8,10 @@ from app.authentication.backend.infra.code_management.depot_handler import ( ) -from app.authentication.backend.models.constants import ( +from app.authentication.backend.models.user.constants import ( AuthType, ) -from app.authentication.backend.models.models import ( +from app.authentication.backend.models.user.models import ( AuthCodeDoc, UserEmailDoc, UserMobileDoc, diff --git a/app/authentication/backend/models/__init__.py b/app/authentication/backend/models/__init__.py new file mode 100644 index 0000000..ba62d26 --- /dev/null +++ b/app/authentication/backend/models/__init__.py @@ -0,0 +1,8 @@ +from .gitea import code_models +from .user import user_models +from .user_profile import profile_models + +backend_models = [] +backend_models.extend(code_models) +backend_models.extend(user_models) +backend_models.extend(profile_models) diff --git a/app/authentication/backend/models/constants.py b/app/authentication/backend/models/constants.py deleted file mode 100644 index f930cbf..0000000 --- a/app/authentication/backend/models/constants.py +++ /dev/null @@ -1,43 +0,0 @@ -from enum import IntEnum - - -class NewUserMethod(IntEnum): - EMAIL = 1 - MOBILE = 2 - - -class UserAccountProperty(IntEnum): - EMAIL_VERIFIED = 1 - MOBILE_VERIFIED = 2 - PAYMENT_SETUP = 4 - ACCEPT_REQUEST = 8 - READY_PROVIDER = 16 - MANAGE_PROJECT = 32 - - -class UserLoginAction(IntEnum): - VERIFY_EMAIL_WITH_AUTH_CODE = 0 - EXISTING_USER_PASSWORD_REQUIRED = 1 - NEW_USER_SET_PASSWORD = 2 - EMAIL_NOT_ASSOCIATED_WITH_USER = 3 - REVIEW_AND_REVISE_FLID = 4 - USER_SIGNED_IN = 100 - - -class AuthType(IntEnum): - MOBILE = 0 - EMAIL = 1 - PASSWORD = 2 - - -class DepotStatus(IntEnum): - TO_BE_CREATED = 0 - CREATED = 1 - DELETED = 2 - - -class UserAccountStatus(IntEnum): - TO_BE_CREATED = 0 - CREATED = 1 - DELETED = 2 - DEACTIVATED = 3 diff --git a/app/authentication/backend/models/gitea/__init__.py b/app/authentication/backend/models/gitea/__init__.py new file mode 100644 index 0000000..85152b6 --- /dev/null +++ b/app/authentication/backend/models/gitea/__init__.py @@ -0,0 +1,3 @@ +from .models import CodeDepotDoc + +code_models = [CodeDepotDoc] diff --git a/app/authentication/backend/models/models.py b/app/authentication/backend/models/models.py deleted file mode 100644 index 4541868..0000000 --- a/app/authentication/backend/models/models.py +++ /dev/null @@ -1,37 +0,0 @@ -from datetime import datetime -from beanie import Document -from .constants import AuthType - - -class UserPasswordDoc(Document): - user_id: str - password: str - - class Settings: - name = "user_password" - - -class UserEmailDoc(Document): - user_id: str - email: str - - class Settings: - name = "user_email" - - -class UserMobileDoc(Document): - user_id: str - mobile: str - - class Settings: - name = "user_mobile" - - -class AuthCodeDoc(Document): - auth_code: str - method: str - method_type: AuthType - expiry: datetime - - class Settings: - name = "user_auth_code" diff --git a/app/authentication/backend/models/user/__init__.py b/app/authentication/backend/models/user/__init__.py new file mode 100644 index 0000000..031589b --- /dev/null +++ b/app/authentication/backend/models/user/__init__.py @@ -0,0 +1,15 @@ +from .models import ( + UserAccountDoc, + UserPasswordDoc, + UserEmailDoc, + UserMobileDoc, + AuthCodeDoc, +) + +user_models = [ + UserAccountDoc, + UserPasswordDoc, + UserEmailDoc, + UserMobileDoc, + AuthCodeDoc, +] diff --git a/app/authentication/backend/models/user/constants.py b/app/authentication/backend/models/user/constants.py index 8e07233..e7c44ea 100644 --- a/app/authentication/backend/models/user/constants.py +++ b/app/authentication/backend/models/user/constants.py @@ -35,3 +35,45 @@ UserRegionToCurrency = { UserRegion.ZH_CN: Currency.CNY.name, UserRegion.OTHER: Currency.USD.name, } + + +class NewUserMethod(IntEnum): + EMAIL = 1 + MOBILE = 2 + + +class UserAccountProperty(IntEnum): + EMAIL_VERIFIED = 1 + MOBILE_VERIFIED = 2 + PAYMENT_SETUP = 4 + ACCEPT_REQUEST = 8 + READY_PROVIDER = 16 + MANAGE_PROJECT = 32 + + +class UserLoginAction(IntEnum): + VERIFY_EMAIL_WITH_AUTH_CODE = 0 + EXISTING_USER_PASSWORD_REQUIRED = 1 + NEW_USER_SET_PASSWORD = 2 + EMAIL_NOT_ASSOCIATED_WITH_USER = 3 + REVIEW_AND_REVISE_FLID = 4 + USER_SIGNED_IN = 100 + + +class AuthType(IntEnum): + MOBILE = 0 + EMAIL = 1 + PASSWORD = 2 + + +class DepotStatus(IntEnum): + TO_BE_CREATED = 0 + CREATED = 1 + DELETED = 2 + + +class UserAccountStatus(IntEnum): + TO_BE_CREATED = 0 + CREATED = 1 + DELETED = 2 + DEACTIVATED = 3 diff --git a/app/authentication/backend/models/user/models.py b/app/authentication/backend/models/user/models.py index 33a4f8a..da129dc 100644 --- a/app/authentication/backend/models/user/models.py +++ b/app/authentication/backend/models/user/models.py @@ -7,7 +7,9 @@ from app.authentication.backend.models.permission.constants import ( AdministrativeRole, Capability, ) +from datetime import datetime from infra.models.constants import UserRegion +from .constants import AuthType class UserAccountDoc(Document): @@ -21,3 +23,37 @@ class UserAccountDoc(Document): class Settings: name = "user_account" + + +class UserPasswordDoc(Document): + user_id: str + password: str + + class Settings: + name = "user_password" + + +class UserEmailDoc(Document): + user_id: str + email: str + + class Settings: + name = "user_email" + + +class UserMobileDoc(Document): + user_id: str + mobile: str + + class Settings: + name = "user_mobile" + + +class AuthCodeDoc(Document): + auth_code: str + method: str + method_type: AuthType + expiry: datetime + + class Settings: + name = "user_auth_code" diff --git a/app/authentication/backend/models/user_profile/__init__.py b/app/authentication/backend/models/user_profile/__init__.py new file mode 100644 index 0000000..c6b8da5 --- /dev/null +++ b/app/authentication/backend/models/user_profile/__init__.py @@ -0,0 +1,3 @@ +from .models import BasicProfileDoc, ProviderProfileDoc + +profile_models = [BasicProfileDoc, ProviderProfileDoc] diff --git a/app/authentication/webapi/providers/database.py b/app/authentication/webapi/providers/database.py index 448745d..211ea30 100644 --- a/app/authentication/webapi/providers/database.py +++ b/app/authentication/webapi/providers/database.py @@ -1,7 +1,7 @@ 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 +from app.authentication.backend.models import backend_models def register(app): @@ -21,5 +21,5 @@ async def initiate_database(): maxPoolSize=20, # Maximum number of connections in the pool ) await init_beanie( - database=client[app_settings.MONGODB_NAME], document_models=[DocumentDoc] + database=client[app_settings.MONGODB_NAME], document_models=backend_models ) diff --git a/app/central_storage/backend/application/document_app.py b/app/central_storage/backend/application/document_app.py deleted file mode 100644 index 9738a2b..0000000 --- a/app/central_storage/backend/application/document_app.py +++ /dev/null @@ -1,15 +0,0 @@ -from app.central_storage.backend.business.document_manager import ( - DocumentBusinessManager, -) - - -class DocumentHub: - def __init__(self, user_id: str): - self.user_id = user_id - self.document_business_manager = DocumentBusinessManager(self.user_id) - return - - async def get_document_by_id(self, document_id: str): - return await self.document_business_manager.get_document_details_by_id( - document_id - ) diff --git a/app/central_storage/backend/application/document_hub.py b/app/central_storage/backend/application/document_hub.py new file mode 100644 index 0000000..68a4ce7 --- /dev/null +++ b/app/central_storage/backend/application/document_hub.py @@ -0,0 +1,25 @@ +from app.central_storage.backend.business.document_manager import ( + DocumentManager, +) + + +class DocumentHub: + def __init__(self, user_id: str): + self.user_id = user_id + self.document_manager = DocumentManager(self.user_id) + return + + async def get_document_by_id(self, document_id: str): + return await self.document_manager.get_document_details_by_id(document_id) + + async def upload_document_for_object( + self, object_id: str, file_name: str, file_data: bytes + ) -> bool: + """Upload a file + Args: + file_name: the name of the file + file (bytes): the file to be uploaded + """ + return await self.document_manager.upload_file_for_object( + object_id, file_name, file_data + ) diff --git a/app/central_storage/backend/business/document_manager.py b/app/central_storage/backend/business/document_manager.py index 88e78b1..2787945 100644 --- a/app/central_storage/backend/business/document_manager.py +++ b/app/central_storage/backend/business/document_manager.py @@ -1,7 +1,8 @@ from app.central_storage.backend.services.document_service import DocumentService +from app.central_storage.backend.models.models import MediaType, DataFormat -class DocumentBusinessManager: +class DocumentManager: def __init__(self, user_id) -> None: self.user_id = user_id self.document_service = DocumentService() @@ -17,3 +18,20 @@ class DocumentBusinessManager: "file_name": file_name, "file_download_url": download_link, } + + async def upload_file_for_object( + self, object_id: str, file_name: str, file_data: bytes + ) -> bool: + await self.document_service.new_document( + file_name=file_name, + # This 'UNKNOWN' will make the document manager decide the media type from file name + media_type=MediaType.UNKNOWN, + data_format=DataFormat.RAW, + created_by=object_id, + ) + return await self.document_service.save_document_file(file_data) or None + # TODO: This should go to Freeleaps App + # add_doc = await self.attachment_service.add_document_to_request( + # request_id, document_id + # ) + # await self.proposal_store.update_latest_proposal_with_documents(request_id) diff --git a/app/central_storage/backend/infra/azure_storage/blob_manager.py b/app/central_storage/backend/infra/azure_storage/blob_handler.py similarity index 99% rename from app/central_storage/backend/infra/azure_storage/blob_manager.py rename to app/central_storage/backend/infra/azure_storage/blob_handler.py index 614baf4..35a537f 100644 --- a/app/central_storage/backend/infra/azure_storage/blob_manager.py +++ b/app/central_storage/backend/infra/azure_storage/blob_handler.py @@ -16,7 +16,7 @@ from azure.storage.blob import ( from azure.core.exceptions import ResourceExistsError -class AzureBlobManager: +class AzureBlobHandler: def __init__(self) -> None: pass diff --git a/app/central_storage/backend/models/__init__.py b/app/central_storage/backend/models/__init__.py new file mode 100644 index 0000000..0418b05 --- /dev/null +++ b/app/central_storage/backend/models/__init__.py @@ -0,0 +1,4 @@ +from .models import DocumentDoc + +backend_models = [] +backend_models.extend([DocumentDoc]) diff --git a/app/central_storage/backend/services/document_service.py b/app/central_storage/backend/services/document_service.py index 7cd794d..fa8bbe7 100644 --- a/app/central_storage/backend/services/document_service.py +++ b/app/central_storage/backend/services/document_service.py @@ -3,8 +3,8 @@ from app.central_storage.backend.models.models import DocumentDoc from app.central_storage.backend.models.constants import MediaType, DataFormat from infra.exception.exceptions import DoesNotExistError -from app.central_storage.backend.infra.azure_storage.blob_manager import ( - AzureBlobManager, +from app.central_storage.backend.infra.azure_storage.blob_handler import ( + AzureBlobHandler, ) import base64 import os @@ -14,7 +14,7 @@ from re import match class DocumentService: def __init__(self) -> None: self.__document_doc = None - self.blob_manager = AzureBlobManager() + self.blob_manager = AzureBlobHandler() return def __normalize_file_name__(file_name: str) -> str: @@ -86,14 +86,14 @@ class DocumentService: document = DocumentDoc( document_id=None, location=None, - file_name=DocumentManager.__normalize_file_name__(file_name), + file_name=DocumentService.__normalize_file_name__(file_name), created_by=created_by, create_time=dt.now(tz.utc), updated_by=created_by, update_time=dt.now(tz.utc), version_number=1, media_type=( - DocumentManager.__retrieve_media_type_from_file_name__(file_name) + DocumentService.__retrieve_media_type_from_file_name__(file_name) if media_type == MediaType.UNKNOWN else media_type ), diff --git a/app/central_storage/requirements.txt b/app/central_storage/requirements.txt index d9b553f..2b9f8cc 100644 --- a/app/central_storage/requirements.txt +++ b/app/central_storage/requirements.txt @@ -5,8 +5,10 @@ pydantic==2.9.2 loguru==0.7.2 uvicorn==0.23.2 beanie==1.21.0 +httpx aio-pika pydantic-settings +python-multipart python-jose azure-storage-blob==12.22.0 azure-identity diff --git a/app/central_storage/webapi/providers/database.py b/app/central_storage/webapi/providers/database.py index 4d43966..8dbb9e9 100644 --- a/app/central_storage/webapi/providers/database.py +++ b/app/central_storage/webapi/providers/database.py @@ -1,7 +1,7 @@ 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 +from app.central_storage.backend.models import backend_models def register(app): @@ -21,5 +21,5 @@ async def initiate_database(): maxPoolSize=20, # Maximum number of connections in the pool ) await init_beanie( - database=client[app_settings.MONGODB_NAME], document_models=[DocumentDoc] + database=client[app_settings.MONGODB_NAME], document_models=backend_models ) diff --git a/app/central_storage/webapi/routes/__init__.py b/app/central_storage/webapi/routes/__init__.py index 67eb039..4c8c6d5 100644 --- a/app/central_storage/webapi/routes/__init__.py +++ b/app/central_storage/webapi/routes/__init__.py @@ -1,7 +1,9 @@ from fastapi import APIRouter from .get_document_by_id import router as doc_router +from .upload_document import router as ud_router api_router = APIRouter() api_router.include_router(doc_router, tags=["attachment"]) +api_router.include_router(ud_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 3201aed..a1dc665 100644 --- a/app/central_storage/webapi/routes/get_document_by_id.py +++ b/app/central_storage/webapi/routes/get_document_by_id.py @@ -1,8 +1,14 @@ -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter +from infra.token.token_manager import TokenManager +from fastapi import APIRouter, Depends +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from fastapi import Depends, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse from infra.token.token_manager import TokenManager -from app.central_storage.backend.application.document_app import DocumentHub +from app.central_storage.backend.application.document_hub import DocumentHub router = APIRouter() token_manager = TokenManager() @@ -19,32 +25,14 @@ token_manager = TokenManager() ) async def get_document_by_id( document_id: str, - request: Request, + current_user: dict = Depends(token_manager.get_current_user), ): - # Extract the Authorization header - auth_header = request.headers.get("Authorization") + user_id = current_user.get("id") - 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") + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" + ) # Fetch the document using DocumentHub document = await DocumentHub(user_id).get_document_by_id(document_id) diff --git a/app/central_storage/webapi/routes/upload_document.py b/app/central_storage/webapi/routes/upload_document.py new file mode 100644 index 0000000..d625a48 --- /dev/null +++ b/app/central_storage/webapi/routes/upload_document.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +from fastapi import APIRouter, Depends +from infra.token.token_manager import TokenManager +from starlette.status import HTTP_401_UNAUTHORIZED +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from app.central_storage.backend.application.document_hub import DocumentHub + + +router = APIRouter() +token_manager = TokenManager() + + +@router.post( + "/upload-document", + summary="upload a document for a given object.", + description="upload a document. If success, returning the document id", +) +async def attach_document_for_request( + object_id: str = Form(...), + file: UploadFile = File(None), + current_user: dict = Depends(token_manager.get_current_user), +): + print("current user", current_user) + user_id = current_user.get("id") + print("current user id", user_id) + if not user_id: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" + ) + + document_hub = DocumentHub(user_id) + # File processing + try: + file_data = await file.read() # You can use async chunking for larger files + document_id = await document_hub.upload_document_for_object( + object_id, file.filename, file_data + ) + + if document_id: + result = {"document_id": str(document_id), "file_name": file.filename} + return JSONResponse(content=jsonable_encoder(result)) + else: + return JSONResponse( + status_code=500, content={"error": "File upload failed"} + ) + + except Exception as e: + print("this is exception", e) + return JSONResponse(status_code=500, content={"error": "Internal server error"})