diff --git a/src/template_project/application/auth_identity/errors.py b/src/template_project/application/auth_identity/errors.py index 32da91b..399988b 100644 --- a/src/template_project/application/auth_identity/errors.py +++ b/src/template_project/application/auth_identity/errors.py @@ -22,3 +22,13 @@ class AuthError(ApplicationError): @to_error class InvalidCodeError(ApplicationError): pass + + +@to_error +class InvalidCredentialsError(ApplicationError): + pass + + +@to_error +class UserNotFoundError(ApplicationError): + pass diff --git a/src/template_project/application/auth_identity/interactors/sign_in.py b/src/template_project/application/auth_identity/interactors/sign_in.py new file mode 100644 index 0000000..968e538 --- /dev/null +++ b/src/template_project/application/auth_identity/interactors/sign_in.py @@ -0,0 +1,99 @@ +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 AuthMethod +from template_project.application.auth_identity.errors import ( + AuthError, + InvalidCodeError, + InvalidCredentialsError, + UserNotFoundError, +) +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 UserId +from template_project.application.user.password_utils import PasswordVerifying + + +@to_data_structure +class UserSignInResponse: + user_id: UserId + access_token: str + + +@to_interactor +class SignInInteractor: + unit_of_work: UnitOfWork + password_verifying: PasswordVerifying + auth_identity_data_gateway: AuthIdentityDataGateway + access_token_factory: AccessTokenFactory + access_token_cryptographer: AccessTokenCryptographer + yandex_oauth_client: YandexOAuthClient + + async def sign_in_email( + self, + email: str, + password: SecretString, + ) -> UserSignInResponse: + auth_identity = await self.auth_identity_data_gateway.load_by_method_and_identifier( + method=AuthMethod.EMAIL, + identifier=email, + ) + + if not auth_identity: + raise UserNotFoundError + + if not auth_identity.secret_key: + raise InvalidCredentialsError + + is_password_valid = self.password_verifying.verify( + verifiable_password=password, + hashed_password=auth_identity.secret_key, + ) + + if not is_password_valid: + raise InvalidCredentialsError + + access_token = self.access_token_factory.execute(auth_identity.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 UserSignInResponse(user_id=auth_identity.user_id, access_token=crypted_access_token) + + async def sign_in_yandex(self, code: str) -> UserSignInResponse: + 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 + + auth_identity = await self.auth_identity_data_gateway.load_by_method_and_identifier( + method=AuthMethod.YANDEX, + identifier=user_info.user_id, + ) + + if not auth_identity: + raise UserNotFoundError + + access_token = self.access_token_factory.execute(auth_identity.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 UserSignInResponse(user_id=auth_identity.user_id, access_token=crypted_access_token) 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 1e313a5..17267f5 100644 --- a/src/template_project/application/auth_identity/interactors/sign_up.py +++ b/src/template_project/application/auth_identity/interactors/sign_up.py @@ -23,7 +23,7 @@ class UserSignUpResponse: @to_interactor -class AuthIdentityInteractor: +class SignUpInteractor: unit_of_work: UnitOfWork password_hasher: PasswordHasher auth_identity_data_gateway: AuthIdentityDataGateway diff --git a/src/template_project/web_api/ioc/interactor.py b/src/template_project/web_api/ioc/interactor.py index 24c51b6..81a92fe 100644 --- a/src/template_project/web_api/ioc/interactor.py +++ b/src/template_project/web_api/ioc/interactor.py @@ -1,11 +1,13 @@ from dishka import BaseScope, Provider, Scope, provide_all -from template_project.application.auth_identity.interactors.sign_up import AuthIdentityInteractor +from template_project.application.auth_identity.interactors.sign_in import SignInInteractor +from template_project.application.auth_identity.interactors.sign_up import SignUpInteractor class InteractorProvider(Provider): scope: BaseScope | None = Scope.REQUEST interactors = provide_all( - AuthIdentityInteractor, + SignInInteractor, + SignUpInteractor, ) diff --git a/src/template_project/web_api/routes/auth.py b/src/template_project/web_api/routes/auth.py index 6f570e7..6df9cbd 100644 --- a/src/template_project/web_api/routes/auth.py +++ b/src/template_project/web_api/routes/auth.py @@ -3,8 +3,15 @@ from dishka.integrations.fastapi import DishkaRoute from fastapi import APIRouter, HTTPException, status from pydantic import BaseModel, SecretStr -from template_project.application.auth_identity.errors import AuthError, InvalidCodeError -from template_project.application.auth_identity.interactors.sign_up import AuthIdentityInteractor +from template_project.application.auth_identity.errors import ( + AuthError, + InvalidCodeError, + InvalidCredentialsError, + UserAlreadyExistsError, + UserNotFoundError, +) +from template_project.application.auth_identity.interactors.sign_in import SignInInteractor +from template_project.application.auth_identity.interactors.sign_up import SignUpInteractor from template_project.application.common.containers import SecretString router = APIRouter(route_class=DishkaRoute) @@ -22,14 +29,18 @@ class EmailSignUpResponse(BaseModel): @router.post("/auth/sign_up/email") async def sign_up_email( request: EmailSignUpRequest, - interactor: FromDishka[AuthIdentityInteractor], + interactor: FromDishka[SignUpInteractor], ) -> EmailSignUpResponse: - response_interactor = await interactor.sign_up_email( - email=request.email, password=SecretString(request.password.get_secret_value()) - ) - return EmailSignUpResponse( - access_token=response_interactor.access_token, - ) + try: + response_interactor = await interactor.sign_up_email( + email=request.email, password=SecretString(request.password.get_secret_value()) + ) + except UserAlreadyExistsError as error: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User with this email already exists" + ) from error + + return EmailSignUpResponse(access_token=response_interactor.access_token) class YandexSignUpRequest(BaseModel): @@ -43,7 +54,7 @@ class YandexSignUpResponse(BaseModel): @router.post("/auth/sign_up/yandex") async def sign_up_yandex( request: YandexSignUpRequest, - interactor: FromDishka[AuthIdentityInteractor], + interactor: FromDishka[SignUpInteractor], ) -> YandexSignUpResponse: try: response_interactor = await interactor.sign_up_yandex(code=request.code) @@ -52,3 +63,59 @@ async def sign_up_yandex( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed") from error return YandexSignUpResponse(access_token=response_interactor.access_token) + + +class EmailSignInRequest(BaseModel): + email: str + password: SecretStr + + +class EmailSignInResponse(BaseModel): + access_token: str + + +@router.post("/auth/sign_in/email") +async def sign_in_email( + request: EmailSignInRequest, + interactor: FromDishka[SignInInteractor], +) -> EmailSignInResponse: + try: + response_interactor = await interactor.sign_in_email( + email=request.email, password=SecretString(request.password.get_secret_value()) + ) + + except UserNotFoundError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") from error + + except InvalidCredentialsError as error: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") from error + + return EmailSignInResponse(access_token=response_interactor.access_token) + + +class YandexSignInRequest(BaseModel): + code: str + + +class YandexSignInResponse(BaseModel): + access_token: str + + +@router.post("/auth/sign_in/yandex") +async def sign_in_yandex( + request: YandexSignInRequest, + interactor: FromDishka[SignInInteractor], +) -> YandexSignInResponse: + try: + response_interactor = await interactor.sign_in_yandex(code=request.code) + + except InvalidCodeError as error: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authorization code") from error + + except AuthError as error: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed") from error + + except UserNotFoundError as error: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") from error + + return YandexSignInResponse(access_token=response_interactor.access_token)