From c2801858c52104ed7f33dedf093edec31ef9742b Mon Sep 17 00:00:00 2001 From: Jeroen Vijgen Date: Wed, 16 Jul 2025 18:10:10 +0000 Subject: [PATCH] Add updateable route for my user --- api/asset_manager/src/modules/auth/schemas.py | 11 +--- api/asset_manager/src/modules/users/models.py | 6 +- api/asset_manager/src/modules/users/router.py | 23 +++++++- .../src/modules/users/schemas.py | 18 ++++++ api/asset_manager/src/modules/users/utils.py | 11 +--- .../src/tests/test_users_routes/test_users.py | 56 ++++++++++++++++++- 6 files changed, 101 insertions(+), 24 deletions(-) diff --git a/api/asset_manager/src/modules/auth/schemas.py b/api/asset_manager/src/modules/auth/schemas.py index 08d312bc..a7c680d4 100644 --- a/api/asset_manager/src/modules/auth/schemas.py +++ b/api/asset_manager/src/modules/auth/schemas.py @@ -1,14 +1,5 @@ -from pydantic import BaseModel, EmailStr from tortoise.contrib.pydantic import pydantic_model_creator from modules.auth.models import Token -token_model = pydantic_model_creator(Token) - -class register_model(BaseModel): - email: EmailStr - username: str - name: str - surname: str - password: str - validate_password: str \ No newline at end of file +token_model = pydantic_model_creator(Token) \ No newline at end of file diff --git a/api/asset_manager/src/modules/users/models.py b/api/asset_manager/src/modules/users/models.py index be2c70dc..0fa03330 100644 --- a/api/asset_manager/src/modules/users/models.py +++ b/api/asset_manager/src/modules/users/models.py @@ -1,5 +1,6 @@ from datetime import datetime import uuid +from fastapi import HTTPException, status from pydantic import EmailStr import pytz from tortoise.models import Model @@ -41,9 +42,10 @@ class User(Model): def __str__(self) -> str: return f"{self.id} - {self.name} {self.surname}" - async def set_password(self, password: str) -> None: + async def set_password(self, password: str) -> bool: self.password = crypt.hash(password) await self.save() # Make sure to save the model in DB + return True def check_against_password(self, password: str) -> bool: return crypt.verify(password, self.password) @@ -55,7 +57,7 @@ class User(Model): return False if new_password is not verify_new_password: return False - await self.set_password(new_password) + return await self.set_password(new_password) async def delete(self, force: bool = False) -> None: if force: diff --git a/api/asset_manager/src/modules/users/router.py b/api/asset_manager/src/modules/users/router.py index 6717653b..ed19541b 100644 --- a/api/asset_manager/src/modules/users/router.py +++ b/api/asset_manager/src/modules/users/router.py @@ -6,7 +6,7 @@ from fastapi import HTTPException, status from tortoise.expressions import Q from modules.users.utils import get_current_active_user -from modules.auth.schemas import register_model +from modules.users.schemas import register_model, update_user_model from modules.users.models import User from modules.users.schemas import user_model @@ -24,12 +24,13 @@ password_failed: str = "Password validation failed, please try again." @router.post("/", status_code=status.HTTP_201_CREATED, response_model=user_model) async def create_user(user: register_model): # Prevent existing users from reapplying for our system. - existing_user: User | None = await User.filter( + existing_user: User | None = await User.get_or_none( Q(email=user.email) & Q(username=user.username) & Q(name=user.name) & Q(surname=user.surname) - ).get_or_none() + & Q(disabled=False) + ) if existing_user is not None: raise HTTPException( @@ -52,6 +53,22 @@ async def create_user(user: register_model): ) +@router.put("/me", status_code=status.HTTP_204_NO_CONTENT) +async def update_user(user: Annotated[User, Depends(get_current_active_user)], + updated_user: update_user_model): + if updated_user.email: + user.email = updated_user.email + if updated_user.name: + user.name = updated_user.name + if updated_user.surname: + user.surname = updated_user.surname + + if updated_user.old_password and updated_user.password and updated_user.validate_password: + user.update_password(updated_user.old_password, updated_user.password, updated_user.validate_password) + + await user.save() + + @router.get("/me", response_model=user_model) async def get_user(user: Annotated[User, Depends(get_current_active_user)]): return user diff --git a/api/asset_manager/src/modules/users/schemas.py b/api/asset_manager/src/modules/users/schemas.py index 07504195..50134f37 100644 --- a/api/asset_manager/src/modules/users/schemas.py +++ b/api/asset_manager/src/modules/users/schemas.py @@ -1,5 +1,23 @@ +from pydantic import BaseModel, EmailStr from tortoise.contrib.pydantic import pydantic_model_creator from modules.users.models import User user_model = pydantic_model_creator(User, exclude=["password"]) + +class register_model(BaseModel): + email: EmailStr + username: str + name: str + surname: str + password: str + validate_password: str + + +class update_user_model(BaseModel): + email: EmailStr | None + name: str | None + surname: str | None + old_password: str | None + password: str | None + validate_password: str | None diff --git a/api/asset_manager/src/modules/users/utils.py b/api/asset_manager/src/modules/users/utils.py index 5af250df..a700730e 100644 --- a/api/asset_manager/src/modules/users/utils.py +++ b/api/asset_manager/src/modules/users/utils.py @@ -10,7 +10,7 @@ from config import settings async def get_user_from_token( - token: Annotated[str, Depends(settings.OAUTH2_SCHEME)] + token: Annotated[str, Depends(settings.OAUTH2_SCHEME)], ) -> User | None: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -28,7 +28,7 @@ async def get_user_from_token( except: raise credentials_exception - return await User.filter(Q(id=user_id)).first() + return await User.filter(Q(id=user_id) & Q(disabled=False)).first() async def get_current_active_user( @@ -37,11 +37,6 @@ async def get_current_active_user( if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="User is not found or active", - ) - if user.disabled: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User is not found or active", + detail="The requested token does not exist or you are not logged in.", ) return user diff --git a/api/asset_manager/src/tests/test_users_routes/test_users.py b/api/asset_manager/src/tests/test_users_routes/test_users.py index 7f5e9f08..12a0808b 100644 --- a/api/asset_manager/src/tests/test_users_routes/test_users.py +++ b/api/asset_manager/src/tests/test_users_routes/test_users.py @@ -40,7 +40,6 @@ class TestAccounts(Test): } async def test_me_route(self, client: AsyncClient, create_user_with_org): - # Ensure account is never available. Prevents account already being available. _, _, _, tokens = await create_user_with_org() @@ -60,4 +59,59 @@ class TestAccounts(Test): "name": "awesome", "surname": "user", "username": "user", + } + + async def test_update_me_route(self, client: AsyncClient, create_user_with_org): + _, _, _, tokens = await create_user_with_org() + + + account = await client.get( + "https://localhost/api/v1/users/me", + headers={"Authorization": f"Bearer {tokens.access_token}"}, + ) + + assert account.status_code == 200 + assert account.json() == { + "created_at": ANY, + "disabled": False, + "disabled_at": None, + "email": "user@localhost.com", + "id": ANY, + "modified_at": ANY, + "name": "awesome", + "surname": "user", + "username": "user", + } + + account = await client.put( + "https://localhost/api/v1/users/me", + json={ + "email": None, + "name": None, + "surname": "bluey", + "old_password": None, + "password": None, + "validate_password": None, + }, + headers={"Authorization": f"Bearer {tokens.access_token}"}, + ) + + assert account.status_code == 204 + + account = await client.get( + "https://localhost/api/v1/users/me", + headers={"Authorization": f"Bearer {tokens.access_token}"}, + ) + + assert account.status_code == 200 + assert account.json() == { + "created_at": ANY, + "disabled": False, + "disabled_at": None, + "email": "user@localhost.com", + "id": ANY, + "modified_at": ANY, + "name": "awesome", + "surname": "bluey", + "username": "user", } \ No newline at end of file