From e2ec8ea684de4867042036165c239bf6ccbb7980 Mon Sep 17 00:00:00 2001 From: flavien-hugs Date: Tue, 17 Dec 2024 21:54:18 +0000 Subject: [PATCH] update: remove unusing feature --- .isort.cfg | 2 +- src/__init__.py | 4 +- src/config/settings.py | 1 - src/config/swaggers.py | 3 +- src/models/__init__.py | 6 +- src/models/users.py | 25 +---- src/routers/auth.py | 4 +- src/routers/users.py | 69 +------------ src/schemas/__init__.py | 5 +- src/services/auth/auth.py | 7 +- src/services/auth/email.py | 2 +- src/services/files.py | 191 ------------------------------------ src/services/tracker.py | 62 ------------ tests/services/test_auth.py | 14 +-- 14 files changed, 28 insertions(+), 367 deletions(-) delete mode 100644 src/services/files.py delete mode 100644 src/services/tracker.py diff --git a/.isort.cfg b/.isort.cfg index 15ccd58..a9a9a28 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,2 @@ [settings] -known_third_party = beanie,email_validator,fastapi,fastapi_cache,fastapi_jwt,fastapi_pagination,httpx,jinja2,jose,mongomock_motor,motor,pwdlib,pydantic,pydantic_settings,pymongo,pyotp,pytest,pytest_asyncio,slugify,starlette,typer,user_agents,uvicorn,yaml +known_third_party = beanie,email_validator,fastapi,fastapi_cache,fastapi_jwt,fastapi_pagination,httpx,jinja2,jose,mongomock_motor,motor,pwdlib,pydantic,pydantic_settings,pymongo,pyotp,pytest,pytest_asyncio,slugify,starlette,typer,uvicorn,yaml diff --git a/src/__init__.py b/src/__init__.py index a1fb31f..e3cb5d2 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -19,7 +19,7 @@ from src.common.helpers.error_codes import AppErrorCode from src.common.helpers.exceptions import setup_exception_handlers from src.config import settings, shutdown_db, startup_db -from src.models import LoginLog, Params, Role, User +from src.models import Params, Role, User from src.routers import auth_router, param_router, perm_router, role_router, user_router from src.services import roles, users from src.shared import blacklist_token @@ -33,7 +33,7 @@ class State(TypedDict): @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[State]: - await startup_db(app=app, models=[User, Role, Params, LoginLog]) + await startup_db(app=app, models=[User, Role, Params]) await load_app_description(mongodb_client=app.mongo_db_client) await load_permissions(mongodb_client=app.mongo_db_client) diff --git a/src/config/settings.py b/src/config/settings.py index ea139f1..b351da6 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -31,7 +31,6 @@ class AuthBaseConfig(BaseSettings): USER_MODEL_NAME: str = Field(..., alias="USER_MODEL_NAME") ROLE_MODEL_NAME: str = Field(..., alias="ROLE_MODEL_NAME") PARAM_MODEL_NAME: str = Field(..., alias="PARAM_MODEL_NAME") - LOGIN_LOG_MODEL_NAME: str = Field(..., alias="LOGIN_LOG_MODEL_NAME") # FRONTEND URL CONFIG FRONTEND_URL: Optional[str] = Field(..., alias="FRONTEND_URL") diff --git a/src/config/swaggers.py b/src/config/swaggers.py index 3b264b7..f63295c 100644 --- a/src/config/swaggers.py +++ b/src/config/swaggers.py @@ -1,5 +1,6 @@ from functools import lru_cache from typing import Optional + from pydantic import Field from pydantic_settings import BaseSettings @@ -9,6 +10,6 @@ class EnableEndpointSettings(BaseSettings): SHOW_CHECK_USER_ATTRIBUTE_ENDPOINT: Optional[bool] = Field(default=1, alias="SHOW_CHECK_USER_ATTRIBUTE_ENDPOINT") -@lru_cache() +@lru_cache def enable_endpoint() -> EnableEndpointSettings: return EnableEndpointSettings() diff --git a/src/models/__init__.py b/src/models/__init__.py index 3e2115d..32266cd 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,5 +1,5 @@ -from .roles import Role -from .users import User, UserOut, LoginLog from .params import Params +from .roles import Role +from .users import User, UserOut -__all__ = ["User", "Role", "Params", "UserOut", "LoginLog"] +__all__ = ["User", "Role", "Params", "UserOut"] diff --git a/src/models/users.py b/src/models/users.py index d120dfb..87c7c59 100644 --- a/src/models/users.py +++ b/src/models/users.py @@ -1,8 +1,7 @@ from typing import Any, Dict, Optional import pymongo -from beanie import Document, PydanticObjectId -from pydantic import StrictBool +from beanie import Document from src.config import settings from src.schemas import CreateUser @@ -10,8 +9,8 @@ class User(CreateUser, DatetimeTimestamp, Document): - is_active: Optional[StrictBool] = False - is_primary: Optional[StrictBool] = False + is_active: Optional[bool] = False + is_primary: Optional[bool] = False class Settings: name = settings.USER_MODEL_NAME @@ -19,23 +18,5 @@ class Settings: indexes = [pymongo.IndexModel(keys=[("fullname", pymongo.TEXT)])] -class LoginLog(DatetimeTimestamp, Document): - user_id: PydanticObjectId - ip_address: str - device: Optional[str] = None - os: Optional[str] = None - browser: Optional[str] = None - is_tablet: Optional[bool] = False - is_mobile: Optional[bool] = False - is_pc: Optional[bool] = False - is_bot: Optional[bool] = False - is_touch_capable: Optional[bool] = False - is_email_client: Optional[bool] = False - - class Settings: - name = settings.LOGIN_LOG_MODEL_NAME - use_state_management = True - - class UserOut(User): extras: Dict[str, Any] = {} diff --git a/src/routers/auth.py b/src/routers/auth.py index 717e342..71aca18 100644 --- a/src/routers/auth.py +++ b/src/routers/auth.py @@ -52,8 +52,8 @@ async def register_completed(token: str, bg: BackgroundTasks, payload: UserBaseS @auth_router.post("/login", summary="Login", status_code=status.HTTP_200_OK) -async def login(bg: BackgroundTasks, request: Request, payload: LoginUser = Body(...)): - return await auth.login(bg, request, payload) +async def login(request: Request, payload: LoginUser = Body(...)): + return await auth.login(request, payload) if not bool(settings.REGISTER_WITH_EMAIL): diff --git a/src/routers/users.py b/src/routers/users.py index 75dbca4..dba78a3 100644 --- a/src/routers/users.py +++ b/src/routers/users.py @@ -1,17 +1,16 @@ from typing import Optional from beanie import PydanticObjectId -from fastapi import APIRouter, Body, Depends, File, Path, Query, Request, status, UploadFile +from fastapi import APIRouter, Body, Depends, Query, Request, status from fastapi_pagination.async_paginator import paginate -from motor.motor_asyncio import AsyncIOMotorGridFSBucket from pymongo import ASCENDING, DESCENDING from src.config import settings from src.middleware import AuthorizedHTTPBearer, CheckPermissionsHandler, CheckUserAccessHandler from src.models import User, UserOut from src.schemas import CreateUser, UpdatePassword, UpdateUser -from src.services import files, roles, users -from src.shared.utils import AccountAction, customize_page, get_fs, SortEnum +from src.services import roles, users +from src.shared.utils import AccountAction, customize_page, SortEnum user_router = APIRouter(prefix="/users", tags=["USERS"], redirect_slashes=False) @@ -200,65 +199,3 @@ async def activate_user_account(id: PydanticObjectId, action: AccountAction): ) async def delete_user(id: PydanticObjectId): return await users.delete_user_account(user_id=PydanticObjectId(id)) - - -if settings.USE_GRIDFS_STORAGE: - user_router.tags = ["USERS: PICTURES"] - user_router.prefix = "/pictures" - - @user_router.put( - "", - dependencies=[ - Depends(AuthorizedHTTPBearer), - Depends(CheckPermissionsHandler(required_permissions={"auth:can-update-user"})), - ], - summary="Upload user picture", - status_code=status.HTTP_200_OK, - ) - async def upload( - request: Request, - user_id: PydanticObjectId, - file: UploadFile = File(...), - description: Optional[str] = Body(None, description="Description of the file"), - fs: AsyncIOMotorGridFSBucket = Depends(get_fs), - ): - result = await files.upload_file(request=request, user_id=user_id, description=description, file=file, fs=fs) - return result - - @user_router.get( - "/{id}", - dependencies=[ - Depends(AuthorizedHTTPBearer), - Depends(CheckPermissionsHandler(required_permissions={"auth:can-read-user"})), - ], - summary="Download user picture", - status_code=status.HTTP_200_OK, - ) - @user_router.get( - "/{id}/view", - summary="Show user picture", - status_code=status.HTTP_200_OK, - ) - async def download( - request: Request, id: str = Path(..., description="File ID"), fs: AsyncIOMotorGridFSBucket = Depends(get_fs) - ): - if request.url.path.endswith("/show"): - return await files.get_file(id=id, fs=fs) - else: - return await files.download_file(id=id, fs=fs) - - @user_router.delete( - "/{id}", - dependencies=[ - Depends(AuthorizedHTTPBearer), - Depends(CheckPermissionsHandler(required_permissions={"auth:can-update-user"})), - ], - summary="Delete user picture", - status_code=status.HTTP_204_NO_CONTENT, - ) - async def delete_picture( - user_id: PydanticObjectId, - id: str = Path(..., description="File ID"), - fs: AsyncIOMotorGridFSBucket = Depends(get_fs), - ): - return await files.delete_file(id=id, user_id=user_id, fs=fs) diff --git a/src/schemas/__init__.py b/src/schemas/__init__.py index 5ac846a..e496b34 100644 --- a/src/schemas/__init__.py +++ b/src/schemas/__init__.py @@ -1,18 +1,18 @@ from .auth import ( ChangePassword, - UpdatePassword, ChangePasswordWithOTPCode, EmailModelMixin, LoginUser, ManageAccount, RequestChangePassword, + UpdatePassword, VerifyOTP, ) from .mixins import FilterParams, SendEmailMessage, SendSmsMessage from .params import ParamsModel from .response import ResponseModelData from .roles import RoleModel -from .users import CreateUser, Metadata, PhonenumberModel, UpdateUser, UserBaseSchema +from .users import CreateUser, PhonenumberModel, UpdateUser, UserBaseSchema __all__ = [ "UserBaseSchema", @@ -32,6 +32,5 @@ "FilterParams", "SendEmailMessage", "SendSmsMessage", - "Metadata", "ChangePasswordWithOTPCode", ] diff --git a/src/services/auth/auth.py b/src/services/auth/auth.py index 60e8f15..43975f0 100644 --- a/src/services/auth/auth.py +++ b/src/services/auth/auth.py @@ -2,7 +2,7 @@ from typing import Optional from beanie import PydanticObjectId -from fastapi import BackgroundTasks, Request, status +from fastapi import Request, status from fastapi.encoders import jsonable_encoder from starlette.responses import JSONResponse @@ -13,7 +13,6 @@ from src.models import User from src.schemas import ChangePassword, LoginUser from src.services.roles import get_one_role -from src.services.tracker import tracking from src.services.users import get_one_user from src.shared import blacklist_token from src.shared.error_codes import AuthErrorCode, UserErrorCode @@ -40,7 +39,7 @@ async def _validate_user_status(user: User) -> None: ) -async def login(bg: BackgroundTasks, request: Request, payload: LoginUser) -> JSONResponse: +async def login(request: Request, payload: LoginUser) -> JSONResponse: is_email = settings.REGISTER_WITH_EMAIL identifier: Optional[str] = payload.email if is_email else payload.phonenumber @@ -74,8 +73,6 @@ async def login(bg: BackgroundTasks, request: Request, payload: LoginUser) -> JS "user": user_data, } - await tracking.insert_log(task=bg, request=request, user_id=user.id) - return JSONResponse(content=jsonable_encoder(response_data), status_code=status.HTTP_200_OK) diff --git a/src/services/auth/email.py b/src/services/auth/email.py index 85ea2ad..5c98adb 100644 --- a/src/services/auth/email.py +++ b/src/services/auth/email.py @@ -15,10 +15,10 @@ ChangePassword, UserBaseSchema, ) +from src.services.users import check_if_email_exist from src.shared import mail_service from src.shared.error_codes import UserErrorCode from src.shared.utils import password_hash -from src.services.users import check_if_email_exist template_loader = PackageLoader("src", "templates") template_env = Environment(loader=template_loader, autoescape=select_autoescape(["html", "txt"])) diff --git a/src/services/files.py b/src/services/files.py deleted file mode 100644 index 3e1ae29..0000000 --- a/src/services/files.py +++ /dev/null @@ -1,191 +0,0 @@ -from datetime import datetime, UTC -from io import BytesIO -from pathlib import Path -from typing import Optional - -from beanie import PydanticObjectId -from fastapi import Depends, File, Request, status, UploadFile -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse, StreamingResponse -from motor.motor_asyncio import AsyncIOMotorGridFSBucket -from slugify import slugify - -from src.common.helpers.exceptions import CustomHTTException -from src.config import settings -from src.schemas import Metadata -from src.shared.error_codes import UserErrorCode -from src.shared.utils import get_fs -from .users import get_one_user - -ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png"} - - -async def upload_file( - request: Request, - user_id: PydanticObjectId, - description: Optional[str], - file: UploadFile = File(...), - fs: AsyncIOMotorGridFSBucket = Depends(get_fs), -): - user = await get_one_user(user_id=user_id) - - try: - contents = await file.read() - - filepath = Path(file.filename) - original_filename = filepath.stem - file_suffixes = filepath.suffixes - - if not file_suffixes: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error="File extension not allowed", - status_code=status.HTTP_400_BAD_REQUEST, - ) - - file_extension = "".join(file_suffixes).lower() - - if file_extension not in ALLOWED_EXTENSIONS: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error="File extension not allowed", - status_code=status.HTTP_400_BAD_REQUEST, - ) - - timestamp = datetime.now(tz=UTC).strftime("%Y%m%d%H%M") - sanitized_filename = slugify(original_filename) - unique_filename = f"{sanitized_filename}-{timestamp}{file_extension}" - - file_id = await fs.upload_from_stream( - unique_filename, BytesIO(contents), metadata={"content_type": file.content_type} - ) - - metadata_doc = Metadata( - file_id=str(file_id), filename=unique_filename, content_type=file.content_type, description=description - ) - await user.set({"attributes": {**user.attributes, "picture": metadata_doc}}) - - base_url = str(request.base_url) if request else f"http://127.0.0.1:{settings.APP_DEFAULT_PORT}" - download_url = f"{base_url}pictures/{file_id}" - - response_data = jsonable_encoder( - {"filename": str(unique_filename), "file_id": str(file_id), "download_url": download_url} - ) - except Exception as e: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error=f"Error while uploading file: {e}", - status_code=status.HTTP_400_BAD_REQUEST, - ) from e - - return JSONResponse(content=response_data, status_code=status.HTTP_200_OK) - - -async def _get_stream_file(id: str, fs: AsyncIOMotorGridFSBucket = Depends(get_fs)): - try: - oid = PydanticObjectId(id) - except Exception as e: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error=f"Error while downloading file: {e}", - status_code=status.HTTP_400_BAD_REQUEST, - ) from e - - cursor = fs.find({"_id": oid}) - files = await cursor.to_list(length=1) - - if not files: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error="File not found", - status_code=status.HTTP_404_NOT_FOUND, - ) - - file = files[0] - - if not file: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error="File not found", - status_code=status.HTTP_404_NOT_FOUND, - ) - - try: - stream = await fs.open_download_stream(file_id=oid) - except Exception as e: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error=f"Error while reading file: {e}", - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) from e - - return stream - - -async def download_file(id: str, fs: AsyncIOMotorGridFSBucket = Depends(get_fs)): - stream = await _get_stream_file(id=id, fs=fs) - - async def file_iterator(): - while True: - chunk = await stream.readchunk() - if not chunk: - break - yield chunk - - return StreamingResponse( - content=file_iterator(), - media_type=stream.metadata.get("content_type", "application/octet-stream"), - headers={ - "Content-Disposition": f'attachment; filename="{stream.filename}"', - "Content-Length": str(stream.length), - }, - ) - - -async def get_file(id: str, fs: AsyncIOMotorGridFSBucket = Depends(get_fs)): - stream = await _get_stream_file(id=id, fs=fs) - - async def file_iterator(): - while True: - chunk = await stream.readchunk() - if not chunk: - break - yield chunk - - return StreamingResponse( - content=file_iterator(), - media_type=stream.metadata.get("content_type", "application/octet-stream"), - headers={ - "Content-Disposition": f'inline; filename="{stream.filename}"', - "Content-Length": str(stream.length), - }, - ) - - -async def delete_file(id: str, user_id: PydanticObjectId, fs: AsyncIOMotorGridFSBucket = Depends(get_fs)): - try: - oid = PydanticObjectId(id) - except Exception as e: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error=f"Error while deleting file: {e}", - status_code=status.HTTP_400_BAD_REQUEST, - ) from e - - cursor = fs.find({"_id": oid}) - files = await cursor.to_list(length=1) - file = files[0] - - if not file: - raise CustomHTTException( - code_error=UserErrorCode.UPLOAD_FILE_ERROR, - message_error="File not found", - status_code=status.HTTP_404_NOT_FOUND, - ) - - await fs.delete(file_id=oid) - - user = await get_one_user(user_id=user_id) - await user.set({"attributes": {**user.attributes, "picture": None}}) - - return JSONResponse(content={"message": "File deleted successfully"}, status_code=status.HTTP_200_OK) diff --git a/src/services/tracker.py b/src/services/tracker.py deleted file mode 100644 index 8c8370e..0000000 --- a/src/services/tracker.py +++ /dev/null @@ -1,62 +0,0 @@ -from datetime import datetime, timedelta, UTC - -from beanie import PydanticObjectId -from fastapi import BackgroundTasks, Request -from user_agents import parse - -from src.models import LoginLog -from src.shared.scripts.utils import make_request - - -class TrackingUser: - - def __init__(self): - self.ip_base_url = "https://api64.ipify.org?format=json" - - async def __call__(self, request: Request, user_id: PydanticObjectId): - - response = make_request(method="get", url=self.ip_base_url) - data = response.json() if response.is_success else {} - ip_address = data.get("ip", "Unknown") if data else "Unknown" - - user_agent_str = request.headers.get("User-Agent", "") - user_agent = parse(user_agent_str) - if not user_agent: - return - - last_24_hours = datetime.now(tz=UTC) - timedelta(hours=24) - if await LoginLog.find_one( - { - "user_id": user_id, - "ip_address": ip_address, - "device": user_agent.get_device(), - "os": user_agent.get_os(), - "browser": user_agent.get_browser(), - "created_at": {"$gte": last_24_hours}, - } - ).exists(): - return - - login_log = LoginLog( - user_id=user_id, - ip_address=ip_address, - device=user_agent.get_device(), - os=user_agent.get_os(), - browser=user_agent.get_browser(), - is_tablet=user_agent.is_tablet, - is_mobile=user_agent.is_mobile, - is_pc=user_agent.is_pc, - is_bot=user_agent.is_bot, - is_touch_capable=user_agent.is_touch_capable, - is_email_client=user_agent.is_email_client, - ) - await login_log.create() - - async def insert_log(self, task: BackgroundTasks, request: Request, user_id: PydanticObjectId): - """ - Insère un log d'utilisateur en arrière-plan en utilisant les tâches de fond. - """ - task.add_task(self.__call__, request=request, user_id=user_id) - - -tracking = TrackingUser() diff --git a/tests/services/test_auth.py b/tests/services/test_auth.py index 4141aa0..31c3684 100644 --- a/tests/services/test_auth.py +++ b/tests/services/test_auth.py @@ -59,7 +59,7 @@ async def test_login_success( return_value={"_id": "66e85363aa07cb1e95d3e3d0", "name": "admin"} ) - response = await auth.login(bg=mock_task, request=mock_request, payload=payload) + response = await auth.login(request=mock_request, payload=payload) assert isinstance(response, JSONResponse) assert response.status_code == status.HTTP_200_OK @@ -72,7 +72,7 @@ async def test_login_success( @pytest.mark.asyncio @mock.patch("src.services.auth.auth.User.find_one", new_callable=mock.AsyncMock) -async def test_login_inactive_user(mock_find_one, fixture_models, mock_request, mock_task): +async def test_login_inactive_user(mock_find_one, fixture_models, mock_request): for register_with_email in [True, False]: settings.REGISTER_WITH_EMAIL = register_with_email @@ -99,7 +99,7 @@ async def test_login_inactive_user(mock_find_one, fixture_models, mock_request, mock_find_one.return_value = fake_user with pytest.raises(CustomHTTException) as excinfo: - await auth.login(bg=mock_task, request=mock_request, payload=payload) + await auth.login(request=mock_request, payload=payload) assert excinfo.typename == "CustomHTTException" assert excinfo.value.status_code == status.HTTP_403_FORBIDDEN @@ -114,7 +114,7 @@ async def test_login_inactive_user(mock_find_one, fixture_models, mock_request, @pytest.mark.asyncio @mock.patch("src.services.auth.auth.User.find_one", new_callable=mock.AsyncMock) @mock.patch("src.services.auth.auth.verify_password", return_value=False) -async def test_login_invalid_password(mock_verify_password, mock_find_one, mock_task, mock_request, fixture_models): +async def test_login_invalid_password(mock_verify_password, mock_find_one, mock_request, fixture_models): settings.REGISTER_WITH_EMAIL = True fake_user = fixture_models.users.User( @@ -129,7 +129,7 @@ async def test_login_invalid_password(mock_verify_password, mock_find_one, mock_ payload = LoginUser(email="test@example.com", password="wrongpassword") with pytest.raises(CustomHTTException) as excinfo: - await auth.login(mock_task, mock_request, payload) + await auth.login(mock_request, payload) assert excinfo.value.status_code == status.HTTP_400_BAD_REQUEST assert excinfo.value.code_error == AuthErrorCode.AUTH_INVALID_PASSWORD @@ -140,7 +140,7 @@ async def test_login_invalid_password(mock_verify_password, mock_find_one, mock_ @pytest.mark.asyncio @mock.patch("src.services.auth.auth.User.find_one", new_callable=mock.AsyncMock) -async def test_login_invalid_identifier_not_found(mock_find_one, mock_task, mock_request, fixture_models): +async def test_login_invalid_identifier_not_found(mock_find_one, mock_request, fixture_models): for register_with_email in [True, False]: settings.REGISTER_WITH_EMAIL = register_with_email @@ -152,7 +152,7 @@ async def test_login_invalid_identifier_not_found(mock_find_one, mock_task, mock mock_find_one.return_value = None with pytest.raises(CustomHTTException) as excinfo: - await auth.login(bg=mock_task, request=mock_request, payload=payload) + await auth.login(request=mock_request, payload=payload) expected_message = "User does not exist." assert excinfo.value.status_code == status.HTTP_400_BAD_REQUEST