Compare commits
3 Commits
5e085ae67e
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 18c9c1e01a | |||
| 4fc74f404f | |||
| e208792738 |
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from src.application.domain.entities import UserEntity
|
from src.application.domain.entities import UserEntity
|
||||||
|
from src.application.domain.entities.organization import PartySearchEntity
|
||||||
|
|
||||||
|
|
||||||
class IUserRepository(ABC):
|
class IUserRepository(ABC):
|
||||||
@@ -25,3 +26,19 @@ class IUserRepository(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def exists_by_email(self, email: str) -> bool:
|
async def exists_by_email(self, email: str) -> bool:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def search_individuals(self, *, query: str, limit: int, offset: int) -> list[PartySearchEntity]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def count_individuals(self, *, query: str) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_password_hash(self, user_id: str) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
from src.application.commands.admin_login import AdminLoginCommand
|
|
||||||
from src.application.commands.admin_logout import AdminLogoutCommand
|
|
||||||
from src.application.commands.admin_jwt_refresh import AdminJwtRefreshCommand
|
from src.application.commands.admin_jwt_refresh import AdminJwtRefreshCommand
|
||||||
from src.application.commands.get_admin_me import GetAdminMeCommand
|
from src.application.commands.get_admin_me import GetAdminMeCommand
|
||||||
from src.application.commands.create_organization import CreateOrganizationCommand
|
from src.application.commands.create_organization import CreateOrganizationCommand
|
||||||
from src.application.commands.create_organization_wallets import CreateOrganizationWalletsCommand
|
from src.application.commands.create_organization_wallets import CreateOrganizationWalletsCommand
|
||||||
from src.application.commands.upload_organization_document import UploadOrganizationDocumentCommand
|
from src.application.commands.put_organization_document import PutOrganizationDocumentCommand
|
||||||
from src.application.commands.organization_commands import (
|
from src.application.commands.organization_commands import (
|
||||||
ListOrganizationsCommand,
|
ListOrganizationsCommand,
|
||||||
GetOrganizationCommand,
|
GetOrganizationCommand,
|
||||||
|
SearchPartiesCommand,
|
||||||
UpdateOrganizationCommand,
|
UpdateOrganizationCommand,
|
||||||
)
|
)
|
||||||
from src.application.commands.organization_document_commands import (
|
from src.application.commands.organization_document_commands import (
|
||||||
ListOrganizationDocumentsCommand,
|
ListOrganizationDocumentsCommand,
|
||||||
GetOrganizationDocumentCommand,
|
GetOrganizationDocumentCommand,
|
||||||
)
|
)
|
||||||
|
from src.application.commands.organization_wallet_commands import (
|
||||||
|
GetOrganizationMnemonicCommand,
|
||||||
|
GetOrganizationSecretKeysCommand,
|
||||||
|
ListOrganizationWalletsCommand,
|
||||||
|
)
|
||||||
from src.application.commands.purchase_request_commands import (
|
from src.application.commands.purchase_request_commands import (
|
||||||
ListPurchaseRequestsCommand,
|
ListPurchaseRequestsCommand,
|
||||||
GetPurchaseRequestCommand,
|
GetPurchaseRequestCommand,
|
||||||
|
UpdatePurchaseRequestCommand,
|
||||||
UpdatePurchaseRequestStatusCommand,
|
UpdatePurchaseRequestStatusCommand,
|
||||||
SetPurchaseRequestQuoteCommand,
|
SetPurchaseRequestQuoteCommand,
|
||||||
)
|
)
|
||||||
@@ -28,14 +33,19 @@ __all__ = [
|
|||||||
'GetAdminMeCommand',
|
'GetAdminMeCommand',
|
||||||
'CreateOrganizationCommand',
|
'CreateOrganizationCommand',
|
||||||
'CreateOrganizationWalletsCommand',
|
'CreateOrganizationWalletsCommand',
|
||||||
'UploadOrganizationDocumentCommand',
|
'PutOrganizationDocumentCommand',
|
||||||
'ListOrganizationsCommand',
|
'ListOrganizationsCommand',
|
||||||
'GetOrganizationCommand',
|
'GetOrganizationCommand',
|
||||||
|
'SearchPartiesCommand',
|
||||||
'UpdateOrganizationCommand',
|
'UpdateOrganizationCommand',
|
||||||
'ListPurchaseRequestsCommand',
|
'ListPurchaseRequestsCommand',
|
||||||
'GetPurchaseRequestCommand',
|
'GetPurchaseRequestCommand',
|
||||||
|
'UpdatePurchaseRequestCommand',
|
||||||
'UpdatePurchaseRequestStatusCommand',
|
'UpdatePurchaseRequestStatusCommand',
|
||||||
'SetPurchaseRequestQuoteCommand',
|
'SetPurchaseRequestQuoteCommand',
|
||||||
'ListOrganizationDocumentsCommand',
|
'ListOrganizationDocumentsCommand',
|
||||||
'GetOrganizationDocumentCommand',
|
'GetOrganizationDocumentCommand',
|
||||||
|
'ListOrganizationWalletsCommand',
|
||||||
|
'GetOrganizationMnemonicCommand',
|
||||||
|
'GetOrganizationSecretKeysCommand',
|
||||||
]
|
]
|
||||||
31
src/application/commands/set_password.py
Normal file
31
src/application/commands/set_password.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from src.application.abstractions import IUnitOfWork
|
||||||
|
from src.application.contracts import IHashService, ILogger
|
||||||
|
from src.application.domain.exceptions import ApplicationException
|
||||||
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
|
|
||||||
|
class SetPasswordCommand:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
unit_of_work: IUnitOfWork,
|
||||||
|
hash_service: IHashService,
|
||||||
|
logger: ILogger
|
||||||
|
):
|
||||||
|
self._unit_of_work = unit_of_work
|
||||||
|
self._hash_service = hash_service
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
@transactional
|
||||||
|
async def __call__(self, email: str, password: str) -> bool:
|
||||||
|
try:
|
||||||
|
user = await self._unit_of_work.user_repository.get_user_by_email(email)
|
||||||
|
|
||||||
|
password_hash = self._hash_service.hash_password(password)
|
||||||
|
await self._unit_of_work.user_repository.set_password(
|
||||||
|
user_id=user.id,
|
||||||
|
password_hash=password_hash,
|
||||||
|
)
|
||||||
|
self._logger.info(f'Set password for user {user.id}')
|
||||||
|
return True
|
||||||
|
except ApplicationException:
|
||||||
|
raise
|
||||||
21
src/application/domain/password_policy.py
Normal file
21
src/application/domain/password_policy.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
SPECIAL_CHARS = '!@#$%^&*()_+-=.,:;?/[]{}<>'
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password_strength(password: str) -> str:
|
||||||
|
if re.search(r'\s', password):
|
||||||
|
raise ValueError('Password must not contain whitespace')
|
||||||
|
if len(password) < 12:
|
||||||
|
raise ValueError('Password must be at least 12 characters')
|
||||||
|
if not re.search(r'[a-z]', password):
|
||||||
|
raise ValueError('Password must contain at least one lowercase letter')
|
||||||
|
if not re.search(r'[A-Z]', password):
|
||||||
|
raise ValueError('Password must contain at least one uppercase letter')
|
||||||
|
if not re.search(r'\d', password):
|
||||||
|
raise ValueError('Password must contain at least one digit')
|
||||||
|
if not any(c in SPECIAL_CHARS for c in password):
|
||||||
|
raise ValueError(
|
||||||
|
'Password must contain at least one special character from: !@#$%^&*()_+-=.,:;?/[]{}<>'
|
||||||
|
)
|
||||||
|
return password
|
||||||
@@ -11,7 +11,7 @@ from src.application.abstractions.repositories import IUserRepository
|
|||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.entities import UserEntity
|
from src.application.domain.entities import UserEntity
|
||||||
from src.application.domain.enums.account_type import AccountType
|
from src.application.domain.enums.account_type import AccountType
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import InternalServerException, ApplicationException
|
||||||
from src.infrastructure.database.models import UserModel
|
from src.infrastructure.database.models import UserModel
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +74,23 @@ class UserRepository(IUserRepository):
|
|||||||
self._logger.exception(str(exc))
|
self._logger.exception(str(exc))
|
||||||
raise ApplicationException(status_code=500, message='Database error')
|
raise ApplicationException(status_code=500, message='Database error')
|
||||||
|
|
||||||
|
async def _update_field(self, user_id: str, **fields: object) -> UserEntity:
|
||||||
|
try:
|
||||||
|
user = await self._get_active_user(user_id)
|
||||||
|
for key, value in fields.items():
|
||||||
|
setattr(user, key, value)
|
||||||
|
await self._session.flush()
|
||||||
|
await self._session.refresh(user)
|
||||||
|
return self._to_entity(user)
|
||||||
|
except ApplicationException:
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as exception:
|
||||||
|
self._logger.exception(str(exception))
|
||||||
|
raise InternalServerException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
|
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
||||||
|
return await self._update_field(user_id, password_hash=password_hash)
|
||||||
|
|
||||||
async def get_user_by_email(self, email: str) -> UserEntity:
|
async def get_user_by_email(self, email: str) -> UserEntity:
|
||||||
try:
|
try:
|
||||||
stmt = select(UserModel).where(UserModel.email == email, UserModel.is_deleted.is_(False))
|
stmt = select(UserModel).where(UserModel.email == email, UserModel.is_deleted.is_(False))
|
||||||
|
|||||||
@@ -10,16 +10,21 @@ from src.application.commands import (
|
|||||||
GetAdminMeCommand,
|
GetAdminMeCommand,
|
||||||
CreateOrganizationCommand,
|
CreateOrganizationCommand,
|
||||||
CreateOrganizationWalletsCommand,
|
CreateOrganizationWalletsCommand,
|
||||||
|
GetOrganizationMnemonicCommand,
|
||||||
|
GetOrganizationSecretKeysCommand,
|
||||||
GetOrganizationCommand,
|
GetOrganizationCommand,
|
||||||
|
ListOrganizationWalletsCommand,
|
||||||
GetPurchaseRequestCommand,
|
GetPurchaseRequestCommand,
|
||||||
ListOrganizationsCommand,
|
ListOrganizationsCommand,
|
||||||
GetOrganizationDocumentCommand,
|
GetOrganizationDocumentCommand,
|
||||||
ListOrganizationDocumentsCommand,
|
ListOrganizationDocumentsCommand,
|
||||||
ListPurchaseRequestsCommand,
|
ListPurchaseRequestsCommand,
|
||||||
|
SearchPartiesCommand,
|
||||||
SetPurchaseRequestQuoteCommand,
|
SetPurchaseRequestQuoteCommand,
|
||||||
UpdateOrganizationCommand,
|
UpdateOrganizationCommand,
|
||||||
|
UpdatePurchaseRequestCommand,
|
||||||
UpdatePurchaseRequestStatusCommand,
|
UpdatePurchaseRequestStatusCommand,
|
||||||
UploadOrganizationDocumentCommand,
|
PutOrganizationDocumentCommand,
|
||||||
)
|
)
|
||||||
from src.application.contracts import IHashService, IJwtService, ILogger
|
from src.application.contracts import IHashService, IJwtService, ILogger
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
@@ -91,11 +96,32 @@ def get_create_organization_wallets_command(
|
|||||||
return CreateOrganizationWalletsCommand(uow, logger)
|
return CreateOrganizationWalletsCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
def get_upload_organization_document_command(
|
def get_list_organization_wallets_command(
|
||||||
uow: IUnitOfWork = Depends(get_unit_of_work),
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
) -> UploadOrganizationDocumentCommand:
|
) -> ListOrganizationWalletsCommand:
|
||||||
return UploadOrganizationDocumentCommand(uow, get_s3_documents_service(), logger)
|
return ListOrganizationWalletsCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_get_organization_mnemonic_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> GetOrganizationMnemonicCommand:
|
||||||
|
return GetOrganizationMnemonicCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_get_organization_secret_keys_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> GetOrganizationSecretKeysCommand:
|
||||||
|
return GetOrganizationSecretKeysCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_put_organization_document_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> PutOrganizationDocumentCommand:
|
||||||
|
return PutOrganizationDocumentCommand(uow, get_s3_documents_service(), logger)
|
||||||
|
|
||||||
|
|
||||||
def get_list_organizations_command(
|
def get_list_organizations_command(
|
||||||
@@ -105,6 +131,13 @@ def get_list_organizations_command(
|
|||||||
return ListOrganizationsCommand(uow, logger)
|
return ListOrganizationsCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_parties_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> SearchPartiesCommand:
|
||||||
|
return SearchPartiesCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
def get_get_organization_command(
|
def get_get_organization_command(
|
||||||
uow: IUnitOfWork = Depends(get_unit_of_work),
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
@@ -154,6 +187,13 @@ def get_update_purchase_request_status_command(
|
|||||||
return UpdatePurchaseRequestStatusCommand(uow, logger)
|
return UpdatePurchaseRequestStatusCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_update_purchase_request_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> UpdatePurchaseRequestCommand:
|
||||||
|
return UpdatePurchaseRequestCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
def get_set_purchase_request_quote_command(
|
def get_set_purchase_request_quote_command(
|
||||||
uow: IUnitOfWork = Depends(get_unit_of_work),
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from src.presentation.routing.documents import documents_router
|
|||||||
from src.presentation.routing.jwt import jwt_router
|
from src.presentation.routing.jwt import jwt_router
|
||||||
from src.presentation.routing.organizations import organizations_router
|
from src.presentation.routing.organizations import organizations_router
|
||||||
from src.presentation.routing.purchase_requests import purchase_requests_router
|
from src.presentation.routing.purchase_requests import purchase_requests_router
|
||||||
|
from src.presentation.routing.users import users_router
|
||||||
|
|
||||||
v1_router = APIRouter(prefix='/v1')
|
v1_router = APIRouter(prefix='/v1')
|
||||||
v1_router.include_router(auth_router)
|
v1_router.include_router(auth_router)
|
||||||
@@ -12,3 +13,4 @@ v1_router.include_router(jwt_router)
|
|||||||
v1_router.include_router(organizations_router)
|
v1_router.include_router(organizations_router)
|
||||||
v1_router.include_router(documents_router)
|
v1_router.include_router(documents_router)
|
||||||
v1_router.include_router(purchase_requests_router)
|
v1_router.include_router(purchase_requests_router)
|
||||||
|
v1_router.include_router(users_router)
|
||||||
17
src/presentation/routing/users.py
Normal file
17
src/presentation/routing/users.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
|
from starlette import status
|
||||||
|
from src.presentation.dependencies.commands import get_set_password_command
|
||||||
|
from src.application.commands.set_password import SetPasswordCommand
|
||||||
|
from presentation.schemas.password import SetPasswordRequest
|
||||||
|
|
||||||
|
users_router = APIRouter(prefix='/users', tags=['users'])
|
||||||
|
|
||||||
|
@users_router.patch(path='/password', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
|
||||||
|
async def set_password(
|
||||||
|
request: Request,
|
||||||
|
body: SetPasswordRequest,
|
||||||
|
command: SetPasswordCommand = Depends(get_set_password_command),
|
||||||
|
):
|
||||||
|
await command(email=body.email, password=body.password)
|
||||||
|
return ORJSONResponse(content={'message': 'Password updated successfully'})
|
||||||
13
src/presentation/schemas/password.py
Normal file
13
src/presentation/schemas/password.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import re
|
||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
from src.application.domain.password_policy import validate_password_strength
|
||||||
|
|
||||||
|
|
||||||
|
class SetPasswordRequest(BaseModel):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
@field_validator('password')
|
||||||
|
@classmethod
|
||||||
|
def validate_password(cls, v: str) -> str:
|
||||||
|
return validate_password_strength(v)
|
||||||
Reference in New Issue
Block a user