You've already forked RekomenciBackend
fast init
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.access_token.entity import AccessTokenId
|
||||
|
||||
|
||||
type RawAccessToken = str
|
||||
|
||||
|
||||
class AccessTokenCryptographer(Protocol):
|
||||
@abstractmethod
|
||||
def crypto(self, access_token_id: AccessTokenId) -> RawAccessToken:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def decrypto(self, raw_access_token: RawAccessToken) -> AccessTokenId:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,10 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.access_token.entity import AccessToken, AccessTokenId
|
||||
|
||||
|
||||
class AccessTokenDataGateway(Protocol):
|
||||
@abstractmethod
|
||||
async def load_with_id(self, access_token_id: AccessTokenId) -> AccessToken | None:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,49 @@
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import NewType, Self
|
||||
from uuid import UUID
|
||||
|
||||
from uuid_utils.compat import uuid7
|
||||
|
||||
from template_project.application.access_token.errors import AccessTokenExpiredError
|
||||
from template_project.application.common.entity import Entity, to_entity
|
||||
from template_project.application.user.entity import UserId
|
||||
|
||||
AccessTokenId = NewType("AccessTokenId", UUID)
|
||||
|
||||
@to_entity
|
||||
class AccessToken(Entity[AccessTokenId]):
|
||||
user_id: UserId
|
||||
revoked: bool
|
||||
expires_in: datetime
|
||||
|
||||
@classmethod
|
||||
def factory(
|
||||
cls,
|
||||
user_id: UserId,
|
||||
expires_in: timedelta,
|
||||
) -> Self:
|
||||
current_date_time = datetime.now(tz=UTC)
|
||||
|
||||
return cls(
|
||||
id=AccessTokenId(uuid7()),
|
||||
created_at=current_date_time,
|
||||
user_id=user_id,
|
||||
expires_in=current_date_time + expires_in,
|
||||
revoked=False,
|
||||
)
|
||||
|
||||
|
||||
def ensure_expired(self) -> None:
|
||||
if self.expired_predicate():
|
||||
raise AccessTokenExpiredError(id_=self.id)
|
||||
|
||||
def expired_predicate(self) -> bool:
|
||||
return (
|
||||
(self.expires_in < datetime.now(tz=UTC))
|
||||
or self.revoked
|
||||
or self.deleted_at is not None
|
||||
)
|
||||
|
||||
def revoke(self) -> None:
|
||||
self.revoked = True
|
||||
self.deleted_at = datetime.now(tz=UTC)
|
||||
@@ -0,0 +1,11 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.access_token.entity import AccessToken
|
||||
from template_project.application.user.entity import UserId
|
||||
|
||||
|
||||
class AccessTokenFactory(Protocol):
|
||||
@abstractmethod
|
||||
def execute(self, user_id: UserId) -> AccessToken:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,13 @@
|
||||
from typing import override
|
||||
|
||||
from template_project.application.access_token.entity import AccessTokenId
|
||||
from template_project.application.common.errors import ApplicationError, to_error
|
||||
|
||||
|
||||
@to_error
|
||||
class AccessTokenExpiredError(ApplicationError):
|
||||
id_: AccessTokenId
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"Access token id={self.id_!r} expried"
|
||||
@@ -0,0 +1,42 @@
|
||||
from collections.abc import Container, Hashable, Sized
|
||||
from typing import Any, Final, override
|
||||
|
||||
_SECRET_VALUE: Final = "********" # noqa: S105
|
||||
|
||||
|
||||
class SecretString(Container[bool], Hashable, Sized):
|
||||
__slots__ = ("_value",)
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
self._value = value
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._value)
|
||||
|
||||
@override
|
||||
def __len__(self) -> int:
|
||||
return len(self._value)
|
||||
|
||||
@override
|
||||
def __eq__(self, value: object) -> bool:
|
||||
if isinstance(value, str):
|
||||
return self._value == value
|
||||
return NotImplemented
|
||||
|
||||
@override
|
||||
def __contains__(self, value: object) -> Any:
|
||||
if isinstance(value, str):
|
||||
return value in self._value
|
||||
return NotImplemented
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return _SECRET_VALUE
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}(value={_SECRET_VALUE!r})>"
|
||||
|
||||
def get_value(self) -> str:
|
||||
return self._value
|
||||
@@ -0,0 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import dataclass_transform
|
||||
|
||||
|
||||
@dataclass_transform(
|
||||
frozen_default=True,
|
||||
eq_default=False,
|
||||
kw_only_default=True,
|
||||
)
|
||||
def to_data_structure[_InteractorClsT](interactor_cls: type[_InteractorClsT]) -> type[_InteractorClsT]:
|
||||
return dataclass(
|
||||
kw_only=True,
|
||||
eq=False,
|
||||
match_args=False,
|
||||
frozen=True,
|
||||
slots=True,
|
||||
)(interactor_cls)
|
||||
@@ -0,0 +1,33 @@
|
||||
from collections.abc import Hashable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import dataclass_transform, override
|
||||
from uuid import UUID
|
||||
|
||||
from template_project.application.common.errors import EntityAlreadyDeletedError
|
||||
|
||||
|
||||
@dataclass_transform(kw_only_default=True)
|
||||
def to_entity[_EntityCLsT](entity_cls: type[_EntityCLsT]) -> type[_EntityCLsT]:
|
||||
return dataclass(kw_only=True)(entity_cls)
|
||||
|
||||
|
||||
@to_entity
|
||||
class Entity[_EntityId: UUID](Hashable):
|
||||
id: _EntityId
|
||||
created_at: datetime
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
def ensure_not_deleted(self) -> None:
|
||||
if self.deleted_at is not None:
|
||||
raise EntityAlreadyDeletedError(entity_name=self.__class__.__name__)
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Entity):
|
||||
return self.id == other.id
|
||||
return NotImplemented
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
@@ -0,0 +1,29 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import dataclass_transform, override
|
||||
|
||||
|
||||
@dataclass_transform(
|
||||
kw_only_default=True,
|
||||
eq_default=False,
|
||||
)
|
||||
def to_error[T](cls: type[T]) -> type[T]:
|
||||
return dataclass(
|
||||
kw_only=True,
|
||||
eq=False,
|
||||
repr=False,
|
||||
match_args=False,
|
||||
)(cls)
|
||||
|
||||
|
||||
@to_error
|
||||
class ApplicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@to_error
|
||||
class EntityAlreadyDeletedError(ApplicationError):
|
||||
entity_name: str
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"Entity {self.entity_name!r} already deleted"
|
||||
@@ -0,0 +1,10 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.user.entity import User
|
||||
|
||||
|
||||
class IdentityProvider(Protocol):
|
||||
@abstractmethod
|
||||
async def get_current_user(self) -> User:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,18 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import dataclass_transform
|
||||
|
||||
|
||||
@dataclass_transform(
|
||||
frozen_default=True,
|
||||
eq_default=False,
|
||||
kw_only_default=True,
|
||||
)
|
||||
def to_interactor[_InteractorClsT](interactor_cls: type[_InteractorClsT]) -> type[_InteractorClsT]:
|
||||
return dataclass(
|
||||
kw_only=True,
|
||||
eq=False,
|
||||
repr=False,
|
||||
frozen=True,
|
||||
match_args=False,
|
||||
slots=True,
|
||||
)(interactor_cls)
|
||||
@@ -0,0 +1,12 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class UnitOfWork(Protocol):
|
||||
@abstractmethod
|
||||
def add(self, *entities: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def commit(self) -> None:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,17 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.user.entity import User, UserId
|
||||
|
||||
|
||||
class UserDataGateway(Protocol):
|
||||
@abstractmethod
|
||||
async def load_with_id(self, id_: UserId) -> User | None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def exists_by_email(
|
||||
self,
|
||||
email: str
|
||||
) -> bool:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,27 @@
|
||||
from datetime import UTC, datetime
|
||||
from typing import NewType, Self
|
||||
from uuid import UUID
|
||||
|
||||
from uuid_utils.compat import uuid7
|
||||
|
||||
from template_project.application.common.entity import Entity, to_entity
|
||||
|
||||
UserId = NewType("UserId", UUID)
|
||||
|
||||
@to_entity
|
||||
class User(Entity[UserId]):
|
||||
email: str
|
||||
hashed_password: str
|
||||
|
||||
@classmethod
|
||||
def factory(
|
||||
cls,
|
||||
email: str,
|
||||
hashed_password: str,
|
||||
) -> Self:
|
||||
return cls(
|
||||
id=UserId(uuid7()),
|
||||
email=email,
|
||||
hashed_password=hashed_password,
|
||||
created_at=datetime.now(tz=UTC),
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
from typing import override
|
||||
from template_project.application.common.errors import ApplicationError, to_error
|
||||
|
||||
|
||||
@to_error
|
||||
class UserWithEmailAlreadyExistsError(ApplicationError):
|
||||
email: str
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"User with the email={self.email!r} already exists"
|
||||
|
||||
@to_error
|
||||
class UserUnauthorizedError(ApplicationError):
|
||||
pass
|
||||
@@ -0,0 +1,23 @@
|
||||
from adaptix.conversion import get_converter
|
||||
|
||||
from template_project.application.common.data_structure import to_data_structure
|
||||
from template_project.application.common.identity_provider import IdentityProvider
|
||||
from template_project.application.common.interactor import to_interactor
|
||||
from template_project.application.user.entity import User, UserId
|
||||
|
||||
|
||||
@to_data_structure
|
||||
class GetMeResponse:
|
||||
id: UserId
|
||||
email: str
|
||||
|
||||
response_converter = get_converter(User, GetMeResponse)
|
||||
|
||||
|
||||
@to_interactor
|
||||
class GetMeInteractor:
|
||||
identity_provider: IdentityProvider
|
||||
|
||||
async def execute(self) -> GetMeResponse:
|
||||
current_user = await self.identity_provider.get_current_user()
|
||||
return response_converter(current_user)
|
||||
@@ -0,0 +1,49 @@
|
||||
from template_project.application.access_token.cryptographer import AccessTokenCryptographer
|
||||
from template_project.application.access_token.entity_factory import AccessTokenFactory
|
||||
from template_project.application.common.containers import SecretString
|
||||
from template_project.application.common.data_structure import to_data_structure
|
||||
from template_project.application.common.interactor import to_interactor
|
||||
from template_project.application.common.unit_of_work import UnitOfWork
|
||||
from template_project.application.user.data_gateway import UserDataGateway
|
||||
from template_project.application.user.entity import User
|
||||
from template_project.application.user.errors import UserWithEmailAlreadyExistsError
|
||||
from template_project.application.user.password_utils import PasswordHasher
|
||||
|
||||
|
||||
@to_data_structure
|
||||
class UserSignUpResponse:
|
||||
access_token: str
|
||||
|
||||
|
||||
@to_interactor
|
||||
class UserSignUpInteractor:
|
||||
unit_of_work: UnitOfWork
|
||||
password_hasher: PasswordHasher
|
||||
user_data_gateway: UserDataGateway
|
||||
access_token_factory: AccessTokenFactory
|
||||
access_token_cryptographer: AccessTokenCryptographer
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
email: str,
|
||||
password: SecretString,
|
||||
) -> UserSignUpResponse:
|
||||
exists_by_email = await self.user_data_gateway.exists_by_email(email)
|
||||
if exists_by_email:
|
||||
raise UserWithEmailAlreadyExistsError(email=email)
|
||||
|
||||
hashed_password = self.password_hasher.hash(password)
|
||||
|
||||
user = User.factory(
|
||||
email=email,
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
access_token = self.access_token_factory.execute(user.id)
|
||||
crypted_access_token = self.access_token_cryptographer.crypto(access_token)
|
||||
|
||||
response = UserSignUpResponse(access_token=crypted_access_token)
|
||||
|
||||
self.unit_of_work.add(user, access_token)
|
||||
await self.unit_of_work.commit()
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,20 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.common.containers import SecretString
|
||||
|
||||
|
||||
class PasswordHasher(Protocol):
|
||||
@abstractmethod
|
||||
def hash(self, password: SecretString) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PasswordVerifying(Protocol):
|
||||
@abstractmethod
|
||||
def verify(
|
||||
self,
|
||||
verifiable_password: SecretString,
|
||||
hashed_password: str,
|
||||
) -> bool:
|
||||
raise NotImplementedError
|
||||
Reference in New Issue
Block a user