add user password hasher and depebdency
This commit is contained in:
@@ -2,7 +2,8 @@ from fastapi import FastAPI
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
|
||||
|
||||
from api.application.abstractions.uow import UnitOfWork
|
||||
from api.application.usecase.create_user import CreateUser
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.application.usecase.user.create_user import CreateUser
|
||||
from api.domain.user.repository import UserRepository
|
||||
from api.infrastructure.dependencies.adapters import (
|
||||
create_engine,
|
||||
@@ -11,6 +12,7 @@ from api.infrastructure.dependencies.adapters import (
|
||||
new_unit_of_work,
|
||||
)
|
||||
from api.infrastructure.dependencies.configs import app_settings
|
||||
from api.infrastructure.dependencies.protocols import get_password_hasher
|
||||
from api.infrastructure.dependencies.repositories import get_user_repository
|
||||
from api.infrastructure.dependencies.usecases import provide_create_user
|
||||
from api.infrastructure.settings import Settings
|
||||
@@ -18,9 +20,15 @@ from api.infrastructure.settings import Settings
|
||||
|
||||
def init_dependencies(app: FastAPI) -> None:
|
||||
app.dependency_overrides[Settings] = app_settings
|
||||
|
||||
app.dependency_overrides[AsyncEngine] = create_engine
|
||||
app.dependency_overrides[async_sessionmaker[AsyncSession]] = create_session_maker
|
||||
app.dependency_overrides[AsyncSession] = new_session
|
||||
app.dependency_overrides[UserRepository] = get_user_repository
|
||||
|
||||
app.dependency_overrides[UnitOfWork] = new_unit_of_work
|
||||
|
||||
app.dependency_overrides[PasswordHasher] = get_password_hasher
|
||||
|
||||
app.dependency_overrides[UserRepository] = get_user_repository
|
||||
|
||||
app.dependency_overrides[CreateUser] = provide_create_user
|
||||
|
@@ -1,4 +1,9 @@
|
||||
from .user_request import UserCreateRequest
|
||||
from .user_request import GetUserByEmailRequest, UserCreateRequest
|
||||
from .user_response import UserDetaledResponse, UserResponse
|
||||
|
||||
__all__ = ("UserResponse", "UserDetaledResponse", "UserCreateRequest")
|
||||
__all__ = (
|
||||
"UserResponse",
|
||||
"UserDetaledResponse",
|
||||
"UserCreateRequest",
|
||||
"GetUserByEmailRequest",
|
||||
)
|
||||
|
@@ -6,3 +6,8 @@ class UserCreateRequest:
|
||||
name: str
|
||||
email: str
|
||||
password: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GetUserByEmailRequest:
|
||||
email: str
|
||||
|
0
api/application/protocols/__init__.py
Normal file
0
api/application/protocols/__init__.py
Normal file
11
api/application/protocols/password_hasher.py
Normal file
11
api/application/protocols/password_hasher.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class PasswordHasher(Protocol):
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def verify_password(password: str, hashed_password: str) -> bool:
|
||||
raise NotImplementedError
|
@@ -1,15 +1,26 @@
|
||||
from api.application.abstractions import UnitOfWork
|
||||
from api.application.contracts.user.user_request import UserCreateRequest
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.domain.user.model import User
|
||||
from api.domain.user.repository import UserRepository
|
||||
|
||||
|
||||
class CreateUser:
|
||||
def __init__(self, uow: UnitOfWork, user_repository: UserRepository) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
uow: UnitOfWork,
|
||||
user_repository: UserRepository,
|
||||
password_hasher: PasswordHasher,
|
||||
) -> None:
|
||||
self.uow = uow
|
||||
self.user_repository = user_repository
|
||||
self.hasher = password_hasher
|
||||
|
||||
async def execute(self, request: UserCreateRequest) -> None:
|
||||
user = User.create(name=request.name, email=request.email, password=request.password)
|
||||
user = User.create(
|
||||
name=request.name,
|
||||
email=request.email,
|
||||
hashed_password=self.hasher.hash_password(request.password),
|
||||
)
|
||||
await self.user_repository.create_user(user=user)
|
||||
await self.uow.commit()
|
15
api/application/usecase/user/get_user_by_email.py
Normal file
15
api/application/usecase/user/get_user_by_email.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from api.application.abstractions import UnitOfWork
|
||||
from api.application.contracts.user import GetUserByEmailRequest, UserResponse
|
||||
from api.domain.user.repository import UserRepository
|
||||
|
||||
|
||||
class GetUserByEmail:
|
||||
def __init__(self, uow: UnitOfWork, user_repository: UserRepository) -> None:
|
||||
self.uow = uow
|
||||
self.user_repository = user_repository
|
||||
|
||||
async def execute(self, request: GetUserByEmailRequest) -> UserResponse | None:
|
||||
user = await self.user_repository.get_user(filter={"email": request.email})
|
||||
if user:
|
||||
return None
|
||||
return None
|
@@ -1,3 +1,6 @@
|
||||
from .error import DomainError
|
||||
from .error import DomainError, DomainValidationError
|
||||
|
||||
__all__ = ("DomainError",)
|
||||
__all__ = (
|
||||
"DomainError",
|
||||
"DomainValidationError",
|
||||
)
|
||||
|
11
api/domain/entity.py
Normal file
11
api/domain/entity.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from api.domain.value_obj import DomainValueObject
|
||||
|
||||
EntityId = TypeVar("EntityId", bound=DomainValueObject)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DomainEntity(Generic[EntityId]):
|
||||
id: EntityId
|
@@ -1,4 +1,8 @@
|
||||
class DomainError(Exception):
|
||||
def __init__(self, message: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.message = message
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class DomainValidationError(DomainError):
|
||||
pass
|
||||
|
@@ -7,3 +7,15 @@ class UserNotFoundError(DomainError):
|
||||
|
||||
class UserValidationError(DomainError):
|
||||
...
|
||||
|
||||
|
||||
class UserInvalidCredentialsError(DomainError):
|
||||
...
|
||||
|
||||
|
||||
class UserAlreadyExistsError(DomainError):
|
||||
...
|
||||
|
||||
|
||||
class UserIsNotAuthorizedError(DomainError):
|
||||
...
|
||||
|
@@ -1,33 +1,65 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from api.domain.user import UserValidationError
|
||||
from api.domain import DomainValidationError
|
||||
from api.domain.entity import DomainEntity
|
||||
from api.domain.value_obj import DomainValueObject
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserEmail(DomainValueObject):
|
||||
value: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
pattern = r"^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$"
|
||||
|
||||
if not re.match(pattern, self.value):
|
||||
raise DomainValidationError("Invalid email format. Email must be in the format 'example@example.com'.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserFirstName(DomainValueObject):
|
||||
value: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if len(self.value) < 1:
|
||||
raise DomainValidationError("First name must be at least 1 character long.")
|
||||
if len(self.value) > 100:
|
||||
raise DomainValidationError("First name must be at most 100 characters long.")
|
||||
if not self.value.isalpha():
|
||||
raise DomainValidationError("First name must only contain letters.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserLastName(DomainValueObject):
|
||||
value: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if len(self.value) < 1:
|
||||
raise DomainValidationError("Last name must be at least 1 character long.")
|
||||
if len(self.value) > 100:
|
||||
raise DomainValidationError("Last name must be at most 100 characters long.")
|
||||
if not self.value.isalpha():
|
||||
raise DomainValidationError("Last name must only contain letters.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserId(DomainValueObject):
|
||||
value: UUID
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: UUID
|
||||
name: str
|
||||
email: str
|
||||
password: str
|
||||
class User(DomainEntity[UserId]):
|
||||
name: UserFirstName
|
||||
email: UserEmail
|
||||
hashed_password: str
|
||||
|
||||
@staticmethod
|
||||
def create(name: str, email: str, password: str) -> "User":
|
||||
if not name:
|
||||
raise UserValidationError("User name cannot be empty")
|
||||
|
||||
if not email:
|
||||
raise UserValidationError("User email cannot be empty")
|
||||
|
||||
if len(name) > 50:
|
||||
raise UserValidationError("User name cannot be longer than 50 characters")
|
||||
|
||||
if len(email) > 30:
|
||||
raise UserValidationError("User email cannot be longer than 30 characters")
|
||||
|
||||
def create(name: str, email: str, hashed_password: str) -> "User":
|
||||
return User(
|
||||
id=uuid4(),
|
||||
name=name,
|
||||
email=email,
|
||||
password=password,
|
||||
id=UserId(uuid4()),
|
||||
name=UserFirstName(name),
|
||||
email=UserEmail(email),
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
|
6
api/domain/value_obj.py
Normal file
6
api/domain/value_obj.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DomainValueObject:
|
||||
pass
|
@@ -30,8 +30,7 @@ def create_engine(
|
||||
def create_session_maker(
|
||||
engine: Annotated[AsyncEngine, Depends(Stub(AsyncEngine))],
|
||||
) -> async_sessionmaker[AsyncSession]:
|
||||
maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
return maker
|
||||
return async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
async def new_session(
|
||||
|
6
api/infrastructure/dependencies/protocols.py
Normal file
6
api/infrastructure/dependencies/protocols.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.infrastructure.security.password_hasher import Pbkdf2PasswordHasher
|
||||
|
||||
|
||||
def get_password_hasher() -> PasswordHasher:
|
||||
return Pbkdf2PasswordHasher()
|
@@ -3,13 +3,15 @@ from typing import Annotated
|
||||
from fastapi import Depends
|
||||
|
||||
from api.application.abstractions.uow import UnitOfWork
|
||||
from api.application.usecase.create_user import CreateUser
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
from api.application.usecase.user.create_user import CreateUser
|
||||
from api.domain.user.repository import UserRepository
|
||||
from api.infrastructure.dependencies.stub import Stub
|
||||
|
||||
|
||||
def provide_create_user(
|
||||
user_repository: Annotated[UserRepository, Depends(Stub(UserRepository))],
|
||||
uow: Annotated[UnitOfWork, Depends()],
|
||||
uow: Annotated[UnitOfWork, Depends(Stub(UnitOfWork))],
|
||||
password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))],
|
||||
) -> CreateUser:
|
||||
return CreateUser(uow=uow, user_repository=user_repository)
|
||||
return CreateUser(uow=uow, user_repository=user_repository, password_hasher=password_hasher)
|
||||
|
@@ -11,10 +11,10 @@ class SqlAlchemyUserRepository(UserRepository):
|
||||
|
||||
async def create_user(self, user: User) -> None:
|
||||
stmt = insert(UserModel).values(
|
||||
id=user.id,
|
||||
name=user.name,
|
||||
email=user.email,
|
||||
hashed_password=user.password,
|
||||
id=user.id.value,
|
||||
name=user.name.value,
|
||||
email=user.email.value,
|
||||
hashed_password=user.hashed_password,
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
|
||||
|
0
api/infrastructure/security/__init__.py
Normal file
0
api/infrastructure/security/__init__.py
Normal file
13
api/infrastructure/security/password_hasher.py
Normal file
13
api/infrastructure/security/password_hasher.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from passlib.handlers.pbkdf2 import pbkdf2_sha256
|
||||
|
||||
from api.application.protocols.password_hasher import PasswordHasher
|
||||
|
||||
|
||||
class Pbkdf2PasswordHasher(PasswordHasher):
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
return pbkdf2_sha256.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def verify_password(password: str, hashed_password: str) -> bool:
|
||||
return pbkdf2_sha256.verify(password, hashed_password)
|
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from api.application.contracts.user import UserCreateRequest, UserResponse
|
||||
from api.application.usecase.create_user import CreateUser
|
||||
from api.application.usecase.user.create_user import CreateUser
|
||||
from api.infrastructure.dependencies.stub import Stub
|
||||
|
||||
user_router = APIRouter(prefix="/users", tags=["Users"])
|
||||
@@ -14,7 +14,7 @@ async def get_all_users() -> list[UserResponse]:
|
||||
return []
|
||||
|
||||
|
||||
@user_router.post("/", status_code=201)
|
||||
@user_router.post("/")
|
||||
async def create_user(
|
||||
request: UserCreateRequest,
|
||||
usecase: Annotated[CreateUser, Depends(Stub(CreateUser))],
|
||||
|
Reference in New Issue
Block a user