Compare commits

3 Commits

Author SHA1 Message Date
18c9c1e01a fix: merge conflict 2 2026-06-10 18:42:06 +03:00
4fc74f404f fix: merge conflict 2026-06-10 18:35:26 +03:00
e208792738 feat: add change password 2026-06-08 11:55:51 +03:00
9 changed files with 178 additions and 10 deletions

View File

@@ -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

View File

@@ -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',
]

View 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

View 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

View File

@@ -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
@@ -73,6 +73,23 @@ class UserRepository(IUserRepository):
except SQLAlchemyError as exc: except SQLAlchemyError as exc:
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:

View File

@@ -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),

View File

@@ -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)

View 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'})

View 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)