From 0992e6c03817930b9a6c056e1e07864a0bdb91b2 Mon Sep 17 00:00:00 2001 From: doas root Date: Tue, 18 Nov 2025 23:53:58 +0300 Subject: [PATCH] feat(): yandex sign up interactor --- src/template_project/adapters/oauth/yandex.py | 4 +- .../application/auth_identity/entity.py | 1 + .../application/auth_identity/errors.py | 10 ++++ .../auth_identity/interactors/sign_up.py | 56 ++++++++++++++++++- .../application/common/oauth/__init__.py | 0 .../{oauth_client.py => oauth/yandex.py} | 0 src/template_project/web_api/configuration.py | 7 +++ src/template_project/web_api/ioc/make.py | 4 ++ src/template_project/web_api/ioc/oauth.py | 16 ++++++ 9 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 src/template_project/application/common/oauth/__init__.py rename src/template_project/application/common/{oauth_client.py => oauth/yandex.py} (100%) create mode 100644 src/template_project/web_api/ioc/oauth.py diff --git a/src/template_project/adapters/oauth/yandex.py b/src/template_project/adapters/oauth/yandex.py index 6891709..9988794 100644 --- a/src/template_project/adapters/oauth/yandex.py +++ b/src/template_project/adapters/oauth/yandex.py @@ -1,10 +1,10 @@ -from typing import override, Any from json import JSONDecodeError +from typing import Any, override import httpx from template_project.application.common.containers import SecretString -from template_project.application.common.oauth_client import ( +from template_project.application.common.oauth.yandex import ( OAuthClient, OAuthExchangeCodeError, OAuthLoadUserInfoError, diff --git a/src/template_project/application/auth_identity/entity.py b/src/template_project/application/auth_identity/entity.py index 90b8919..b6d92ae 100644 --- a/src/template_project/application/auth_identity/entity.py +++ b/src/template_project/application/auth_identity/entity.py @@ -13,6 +13,7 @@ AuthIdentityId = NewType("AuthIdentityId", UUID) class AuthMethod(StrEnum): EMAIL = "email" + YANDEX = "yandex" @to_entity diff --git a/src/template_project/application/auth_identity/errors.py b/src/template_project/application/auth_identity/errors.py index 6cac465..32da91b 100644 --- a/src/template_project/application/auth_identity/errors.py +++ b/src/template_project/application/auth_identity/errors.py @@ -12,3 +12,13 @@ class UserAlreadyExistsError(ApplicationError): @override def __str__(self) -> str: return f"User with identifier={self.identifier!r} and auth method={self.auth_method.value} already exists" + + +@to_error +class AuthError(ApplicationError): + pass + + +@to_error +class InvalidCodeError(ApplicationError): + pass diff --git a/src/template_project/application/auth_identity/interactors/sign_up.py b/src/template_project/application/auth_identity/interactors/sign_up.py index ead8492..1e313a5 100644 --- a/src/template_project/application/auth_identity/interactors/sign_up.py +++ b/src/template_project/application/auth_identity/interactors/sign_up.py @@ -2,10 +2,15 @@ from template_project.application.access_token.cryptographer import AccessTokenC 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.auth_identity.errors import AuthError, InvalidCodeError, 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.oauth.yandex import ( + OAuthClient as YandexOAuthClient, + OAuthExchangeCodeError, + OAuthLoadUserInfoError, +) from template_project.application.common.unit_of_work import UnitOfWork from template_project.application.user.entity import User, UserId from template_project.application.user.password_utils import PasswordHasher @@ -24,18 +29,19 @@ class AuthIdentityInteractor: auth_identity_data_gateway: AuthIdentityDataGateway access_token_factory: AccessTokenFactory access_token_cryptographer: AccessTokenCryptographer + yandex_oauth_client: YandexOAuthClient async def sign_up_email( self, email: str, password: SecretString, ) -> UserSignUpResponse: - existing_user = await self.auth_identity_data_gateway.load_by_method_and_identifier( + existing_auth_identity = await self.auth_identity_data_gateway.load_by_method_and_identifier( method=AuthMethod.EMAIL, identifier=email, ) - if existing_user: + if existing_auth_identity: raise UserAlreadyExistsError(identifier=email, auth_method=AuthMethod.EMAIL) hashed_password = self.password_hasher.hash(password) @@ -61,3 +67,47 @@ class AuthIdentityInteractor: await self.unit_of_work.commit() return UserSignUpResponse(user_id=user.id, access_token=crypted_access_token) + + async def sign_up_yandex(self, code: str) -> UserSignUpResponse: + try: + token_response = await self.yandex_oauth_client.exchange_code(code) + access_token_yandex = token_response.access_token + + except OAuthExchangeCodeError as error: + raise InvalidCodeError from error + + try: + user_info = await self.yandex_oauth_client.load_user_info(access_token_yandex) + + except OAuthLoadUserInfoError as error: + raise AuthError from error + + existing_auth_identity = await self.auth_identity_data_gateway.load_by_method_and_identifier( + method=AuthMethod.YANDEX, + identifier=user_info.user_id, + ) + + if existing_auth_identity: + user_id = existing_auth_identity.user_id + + else: + user = User.factory() + user_id = user.id + + auth_identity = AuthIdentity.factory( + user_id=user_id, + method=AuthMethod.YANDEX, + identifier=user_info.user_id, + secret_key=None, + ) + + await self.unit_of_work.add(user) + await self.unit_of_work.add(auth_identity) + + access_token = self.access_token_factory.execute(user_id) + crypted_access_token = self.access_token_cryptographer.crypto(access_token.id) + + await self.unit_of_work.add(access_token) + await self.unit_of_work.commit() + + return UserSignUpResponse(user_id=user_id, access_token=crypted_access_token) diff --git a/src/template_project/application/common/oauth/__init__.py b/src/template_project/application/common/oauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/template_project/application/common/oauth_client.py b/src/template_project/application/common/oauth/yandex.py similarity index 100% rename from src/template_project/application/common/oauth_client.py rename to src/template_project/application/common/oauth/yandex.py diff --git a/src/template_project/web_api/configuration.py b/src/template_project/web_api/configuration.py index fb8d959..17f57b4 100644 --- a/src/template_project/web_api/configuration.py +++ b/src/template_project/web_api/configuration.py @@ -36,11 +36,18 @@ class ServerConfiguration: return f"http://{self.host}:{self.port}" +@to_configuration +class YandexOAuthConfiguration: + client_id: str + client_secret: SecretString + + @to_configuration class Configuration: server: ServerConfiguration database: DatabaseConfiguration access_token: AccessTokenConfiguration + yandex_oauth: YandexOAuthConfiguration retort = Retort( diff --git a/src/template_project/web_api/ioc/make.py b/src/template_project/web_api/ioc/make.py index 8f74dd9..44114bc 100644 --- a/src/template_project/web_api/ioc/make.py +++ b/src/template_project/web_api/ioc/make.py @@ -6,6 +6,7 @@ from template_project.web_api.configuration import ( Configuration, DatabaseConfiguration, ServerConfiguration, + YandexOAuthConfiguration, ) from template_project.web_api.ioc.connection import ConnectionProvider from template_project.web_api.ioc.cryptographer import CryptographerProvider @@ -13,6 +14,7 @@ from template_project.web_api.ioc.data_gateway import DataGatewayProvider from template_project.web_api.ioc.factory import FactoryProvider from template_project.web_api.ioc.idp import IdPProvider from template_project.web_api.ioc.interactor import InteractorProvider +from template_project.web_api.ioc.oauth import OAuthClientProvider def make_ioc(configuration: Configuration) -> AsyncContainer: @@ -24,10 +26,12 @@ def make_ioc(configuration: Configuration) -> AsyncContainer: InteractorProvider(), DataGatewayProvider(), CryptographerProvider(), + OAuthClientProvider(), validation_settings=STRICT_VALIDATION, context={ ServerConfiguration: configuration.server, DatabaseConfiguration: configuration.database, AccessTokenConfiguration: configuration.access_token, + YandexOAuthConfiguration: configuration.yandex_oauth, }, ) diff --git a/src/template_project/web_api/ioc/oauth.py b/src/template_project/web_api/ioc/oauth.py new file mode 100644 index 0000000..e87cdd1 --- /dev/null +++ b/src/template_project/web_api/ioc/oauth.py @@ -0,0 +1,16 @@ +from dishka import BaseScope, Provider, Scope, provide + +from template_project.adapters.oauth.yandex import YandexOAuthClient as YandexOAuthClientImpl +from template_project.application.common.oauth.yandex import OAuthClient as YandexOAuthClient +from template_project.web_api.configuration import YandexOAuthConfiguration + + +class OAuthClientProvider(Provider): + scope: BaseScope | None = Scope.REQUEST + + @provide(scope=Scope.REQUEST) + async def yandex_oauth_client(self, configuration: YandexOAuthConfiguration) -> YandexOAuthClient: + return YandexOAuthClientImpl( + client_id=configuration.client_id, + client_secret=configuration.client_secret, + )