feat(): migrate to auth identity

This commit is contained in:
doas root
2025-11-18 00:25:24 +03:00
parent 63a057b020
commit f44e688662
18 changed files with 219 additions and 48 deletions
@@ -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"
@@ -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