You've already forked RekomenciBackend
feat(): migrate to auth identity
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from template_project.application.auth_identity.entity import AuthIdentity, AuthMethod
|
||||
from template_project.application.user.entity import UserId
|
||||
|
||||
|
||||
class AuthIdentityDataGateway(Protocol):
|
||||
@abstractmethod
|
||||
async def load_by_method_and_identifier(self, method: AuthMethod, identifier: str) -> AuthIdentity | None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def load_all_for_user(self, user_id: UserId) -> list[AuthIdentity]:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,40 @@
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
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
|
||||
from template_project.application.user.entity import UserId
|
||||
|
||||
AuthIdentityId = NewType("AuthIdentityId", UUID)
|
||||
|
||||
|
||||
class AuthMethod(StrEnum):
|
||||
EMAIL = "email"
|
||||
|
||||
|
||||
@to_entity
|
||||
class AuthIdentity(Entity[AuthIdentityId]):
|
||||
user_id: UserId
|
||||
method: AuthMethod
|
||||
identifier: str
|
||||
secret_key: str | None
|
||||
|
||||
@classmethod
|
||||
def factory(
|
||||
cls,
|
||||
user_id: UserId,
|
||||
method: AuthMethod,
|
||||
identifier: str,
|
||||
secret_key: str | None = None,
|
||||
) -> Self:
|
||||
return cls(
|
||||
id=AuthIdentityId(uuid7()),
|
||||
user_id=user_id,
|
||||
method=method,
|
||||
identifier=identifier,
|
||||
secret_key=secret_key,
|
||||
created_at=datetime.now(tz=UTC),
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from template_project.application.auth_identity.entity import AuthMethod
|
||||
from template_project.application.common.errors import ApplicationError, to_error
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@to_error
|
||||
class UserAlreadyExistsError(ApplicationError):
|
||||
identifier: str
|
||||
auth_method: AuthMethod
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"User with identifier={self.identifier!r} and auth method={self.auth_method.value} already exists"
|
||||
+25
-13
@@ -1,36 +1,42 @@
|
||||
from template_project.application.access_token.cryptographer import AccessTokenCryptographer
|
||||
from template_project.application.access_token.entity_factory import AccessTokenFactory
|
||||
from template_project.application.auth_identity.data_gateway import AuthIdentityDataGateway
|
||||
from template_project.application.auth_identity.entity import AuthIdentity, AuthMethod
|
||||
from template_project.application.auth_identity.errors import UserAlreadyExistsError
|
||||
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.entity import User, UserId
|
||||
from template_project.application.user.password_utils import PasswordHasher
|
||||
|
||||
|
||||
@to_data_structure
|
||||
class UserSignUpResponse:
|
||||
user_id: UserId
|
||||
access_token: str
|
||||
|
||||
|
||||
@to_interactor
|
||||
class UserSignUpInteractor:
|
||||
class AuthIdentityInteractor:
|
||||
unit_of_work: UnitOfWork
|
||||
password_hasher: PasswordHasher
|
||||
user_data_gateway: UserDataGateway
|
||||
auth_identity_data_gateway: AuthIdentityDataGateway
|
||||
access_token_factory: AccessTokenFactory
|
||||
access_token_cryptographer: AccessTokenCryptographer
|
||||
|
||||
async def execute(
|
||||
async def sign_up_email(
|
||||
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)
|
||||
existing_user = await self.auth_identity_data_gateway.load_by_method_and_identifier(
|
||||
method=AuthMethod.EMAIL,
|
||||
identifier=email,
|
||||
)
|
||||
|
||||
if existing_user:
|
||||
raise UserAlreadyExistsError(identifier=email, auth_method=AuthMethod.EMAIL)
|
||||
|
||||
hashed_password = self.password_hasher.hash(password)
|
||||
|
||||
@@ -38,14 +44,20 @@ class UserSignUpInteractor:
|
||||
email=email,
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
|
||||
auth_identity = AuthIdentity.factory(
|
||||
user_id=user.id,
|
||||
method=AuthMethod.EMAIL,
|
||||
identifier=email,
|
||||
secret_key=hashed_password,
|
||||
)
|
||||
|
||||
access_token = self.access_token_factory.execute(user.id)
|
||||
crypted_access_token = self.access_token_cryptographer.crypto(access_token.id)
|
||||
|
||||
response = UserSignUpResponse(access_token=crypted_access_token)
|
||||
|
||||
for entity in (user, access_token): # preserve creation order
|
||||
for entity in (user, auth_identity, access_token):
|
||||
await self.unit_of_work.add(entity)
|
||||
|
||||
await self.unit_of_work.commit()
|
||||
|
||||
return response
|
||||
return UserSignUpResponse(user_id=user.id, access_token=crypted_access_token)
|
||||
@@ -8,7 +8,3 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user