You've already forked RekomenciBackend
feat(): yandex oauth client
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
from typing import override, Any
|
||||||
|
from json import JSONDecodeError
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from template_project.application.common.containers import SecretString
|
||||||
|
from template_project.application.common.oauth_client import (
|
||||||
|
OAuthClient,
|
||||||
|
OAuthExchangeCodeError,
|
||||||
|
OAuthLoadUserInfoError,
|
||||||
|
OAuthTokenResponse,
|
||||||
|
OAuthUserInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class YandexOAuthClient(OAuthClient):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client_id: str,
|
||||||
|
client_secret: SecretString,
|
||||||
|
*,
|
||||||
|
timeout: float = 10.0,
|
||||||
|
) -> None:
|
||||||
|
self._client_id = client_id
|
||||||
|
self._client_secret = client_secret
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
self._token_url = "https://oauth.yandex.ru/token" # noqa: S105
|
||||||
|
self._userinfo_url = "https://login.yandex.ru/info"
|
||||||
|
self._avatar_url_interpolate = "https://avatars.yandex.net/get-yapic/{avatar_id}/islands-200"
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def exchange_code(self, code: str) -> OAuthTokenResponse:
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||||
|
response = await client.post(
|
||||||
|
self._token_url,
|
||||||
|
data={
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": code,
|
||||||
|
"client_id": self._client_id,
|
||||||
|
"client_secret": self._client_secret.get_value(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
except (httpx.HTTPError, JSONDecodeError) as e:
|
||||||
|
raise OAuthExchangeCodeError from e
|
||||||
|
|
||||||
|
return OAuthTokenResponse(
|
||||||
|
access_token=data["access_token"],
|
||||||
|
expires_in=data.get("expires_in"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def load_user_info(self, access_token: str) -> OAuthUserInfo:
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||||
|
response = await client.get(
|
||||||
|
self._userinfo_url,
|
||||||
|
params={"format": "json"},
|
||||||
|
headers={"Authorization": f"OAuth {access_token}"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
except (httpx.HTTPError, JSONDecodeError) as e:
|
||||||
|
raise OAuthLoadUserInfoError from e
|
||||||
|
|
||||||
|
return OAuthUserInfo(
|
||||||
|
user_id=str(data["id"]),
|
||||||
|
email=data.get("default_email"),
|
||||||
|
display_name=data.get("display_name"),
|
||||||
|
first_name=data.get("first_name"),
|
||||||
|
last_name=data.get("last_name"),
|
||||||
|
avatar_url=self._extract_avatar(data),
|
||||||
|
phone=data.get("default_phone", {}).get("number"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_avatar(self, data: dict[str, Any]) -> str | None:
|
||||||
|
avatar_id = data.get("default_avatar_id")
|
||||||
|
|
||||||
|
if not avatar_id or not isinstance(avatar_id, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._avatar_url_interpolate.format(avatar_id=avatar_id)
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
from typing import TYPE_CHECKING, override
|
from typing import override
|
||||||
|
|
||||||
from template_project.application.auth_identity.entity import AuthMethod
|
from template_project.application.auth_identity.entity import AuthMethod
|
||||||
from template_project.application.common.errors import ApplicationError, to_error
|
from template_project.application.common.errors import ApplicationError, to_error
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@to_error
|
@to_error
|
||||||
class UserAlreadyExistsError(ApplicationError):
|
class UserAlreadyExistsError(ApplicationError):
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from template_project.application.common.data_structure import to_data_structure
|
||||||
|
from template_project.application.common.errors import ApplicationError, to_error
|
||||||
|
|
||||||
|
|
||||||
|
@to_data_structure
|
||||||
|
class OAuthTokenResponse:
|
||||||
|
access_token: str
|
||||||
|
expires_in: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@to_data_structure
|
||||||
|
class OAuthUserInfo:
|
||||||
|
user_id: str
|
||||||
|
email: str | None = None
|
||||||
|
display_name: str | None = None
|
||||||
|
first_name: str | None = None
|
||||||
|
last_name: str | None = None
|
||||||
|
avatar_url: str | None = None
|
||||||
|
phone: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@to_error
|
||||||
|
class OAuthExchangeCodeError(ApplicationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@to_error
|
||||||
|
class OAuthLoadUserInfoError(ApplicationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthClient(Protocol):
|
||||||
|
@abstractmethod
|
||||||
|
async def exchange_code(self, code: str) -> OAuthTokenResponse:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def load_user_info(self, access_token: str) -> OAuthUserInfo:
|
||||||
|
raise NotImplementedError
|
||||||
Reference in New Issue
Block a user