auth next part

main
Сергей Ванюшкин 2024-04-02 22:33:15 +03:00
parent 949ea9fdcf
commit b04eba9bc4
22 changed files with 298 additions and 29 deletions

View File

@ -1,17 +1,22 @@
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession, from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
async_sessionmaker)
from api.application.abstractions.uow import UnitOfWork from api.application.abstractions.uow import UnitOfWork
from api.application.protocols.date_time import DateTimeProvider
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.application.usecase.auth.create_user import CreateUser from api.application.usecase.auth.create_user import CreateUser
from api.domain.user.repository import UserRepository from api.domain.user.repository import UserRepository
from api.infrastructure.dependencies.adapters import (create_engine, from api.infrastructure.dependencies.adapters import (
create_engine,
create_session_maker, create_session_maker,
new_session, new_session,
new_unit_of_work) new_unit_of_work,
)
from api.infrastructure.dependencies.configs import app_settings from api.infrastructure.dependencies.configs import app_settings
from api.infrastructure.dependencies.protocols import get_password_hasher from api.infrastructure.dependencies.protocols import (
get_date_time_provider,
get_password_hasher,
)
from api.infrastructure.dependencies.repositories import get_user_repository from api.infrastructure.dependencies.repositories import get_user_repository
from api.infrastructure.dependencies.usecases import provide_create_user from api.infrastructure.dependencies.usecases import provide_create_user
from api.infrastructure.settings import Settings from api.infrastructure.settings import Settings
@ -26,6 +31,7 @@ def init_dependencies(app: FastAPI) -> None:
app.dependency_overrides[UnitOfWork] = new_unit_of_work app.dependency_overrides[UnitOfWork] = new_unit_of_work
app.dependency_overrides[DateTimeProvider] = get_date_time_provider
app.dependency_overrides[PasswordHasher] = get_password_hasher app.dependency_overrides[PasswordHasher] = get_password_hasher
app.dependency_overrides[UserRepository] = get_user_repository app.dependency_overrides[UserRepository] = get_user_repository

View File

@ -1,7 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from api.presentation.routers import (auth_router, healthcheck_router, from api.presentation.routers import auth_router, healthcheck_router, user_router
user_router)
def init_routers(app: FastAPI) -> None: def init_routers(app: FastAPI) -> None:

View File

@ -1,3 +1,3 @@
from .auth_request import UserCreateRequest from .auth_request import LoginRequest, UserCreateRequest
__all__ = ("UserCreateRequest",) __all__ = ("UserCreateRequest", "LoginRequest")

View File

@ -6,3 +6,9 @@ class UserCreateRequest:
name: str name: str
email: str email: str
password: str password: str
@dataclass(frozen=True)
class LoginRequest:
email: str
password: str

View File

@ -0,0 +1,9 @@
from dataclasses import dataclass
from uuid import UUID
@dataclass(frozen=True)
class AuthenticationResponse:
id: UUID
name: str
email: str

View File

@ -0,0 +1,7 @@
from datetime import datetime
from typing import Protocol
class DateTimeProvider(Protocol):
def get_current_time(self) -> datetime:
raise NotImplementedError

View File

@ -0,0 +1,11 @@
from typing import Protocol
from api.domain.user.model import UserId
class JwtTokenProcessor(Protocol):
def generate_token(self, user_id: UserId) -> str:
raise NotImplementedError
def validate_token(self, token: str) -> UserId | None:
raise NotImplementedError

View File

@ -0,0 +1,30 @@
from api.application.contracts.auth.auth_request import LoginRequest
from api.application.contracts.auth.auth_response import AuthenticationResponse
from api.application.protocols.password_hasher import PasswordHasher
from api.domain.user.error import UserInvalidCredentialsError
from api.domain.user.repository import UserRepository
class LoginUser:
def __init__(
self,
user_repository: UserRepository,
password_hasher: PasswordHasher,
) -> None:
self.user_repository = user_repository
self.hasher = password_hasher
async def __call__(self, request: LoginRequest) -> AuthenticationResponse:
user = await self.user_repository.get_user(filter={"email": request.email})
error = UserInvalidCredentialsError("Email or password is incorrect")
if user is None:
raise error
if not self.hasher.verify_password(request.password, user.hashed_password):
raise error
return AuthenticationResponse(
id=user.id.value,
name=user.name.value,
email=user.email.value,
)

View File

View File

@ -0,0 +1,37 @@
from datetime import timedelta
from uuid import UUID
from jose import JWTError
from jose.jwt import decode, encode
from api.application.protocols.date_time import DateTimeProvider
from api.application.protocols.jwt import JwtTokenProcessor
from api.domain.user.model import UserId
from api.infrastructure.auth.jwt_settings import JwtSettings
class JoseJwtTokenProcessor(JwtTokenProcessor):
def __init__(self, jwt_options: JwtSettings, date_time_provider: DateTimeProvider) -> None:
self.jwt_options = jwt_options
self.date_time_provider = date_time_provider
def generate_token(self, user_id: UserId) -> str:
issued_at = self.date_time_provider.get_current_time()
expiration_time = issued_at + timedelta(hours=self.jwt_options.expires_in)
claims = {
"iat": issued_at,
"exp": expiration_time,
"sub": str(user_id.value),
}
return encode(claims, self.jwt_options.secret, self.jwt_options.algorithm)
def validate_token(self, token: str) -> UserId | None:
try:
payload = decode(token, self.jwt_options.secret, [self.jwt_options.algorithm])
return UserId(UUID(payload["sub"]))
except (JWTError, ValueError, KeyError):
return None

View File

@ -0,0 +1,8 @@
from dataclasses import dataclass, field
@dataclass(frozen=True)
class JwtSettings:
secret: str
expires_in: int = field(default=2)
algorithm: str = field(default="HS256")

View File

@ -0,0 +1,26 @@
from datetime import datetime, timedelta, timezone
from enum import Enum
from api.application.protocols.date_time import DateTimeProvider
class Timezone(Enum):
UTC = timezone.utc
GMT = timezone(timedelta(hours=0))
CET = timezone(timedelta(hours=1))
EET = timezone(timedelta(hours=2))
MSK = timezone(timedelta(hours=3))
IST = timezone(timedelta(hours=5, minutes=30))
WIB = timezone(timedelta(hours=7))
CST = timezone(timedelta(hours=8))
JST = timezone(timedelta(hours=9))
AEST = timezone(timedelta(hours=10))
NZST = timezone(timedelta(hours=12))
class SystemDateTimeProvider(DateTimeProvider):
def __init__(self, tz: Timezone) -> None:
self.tz = tz
def get_current_time(self) -> datetime:
return datetime.now(tz=self.tz.value)

View File

@ -3,6 +3,7 @@ from functools import lru_cache
import yaml # type: ignore import yaml # type: ignore
from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.persistence.db_setings import DBSettings from api.infrastructure.persistence.db_setings import DBSettings
from api.infrastructure.settings import Settings from api.infrastructure.settings import Settings
@ -27,4 +28,9 @@ def app_settings() -> Settings:
pg_port=int(config_data["db"]["port"]), pg_port=int(config_data["db"]["port"]),
pg_db=config_data["db"]["database"], pg_db=config_data["db"]["database"],
), ),
jwt=JwtSettings(
secret=config_data["jwt"]["secret_key"],
expires_in=int(config_data["jwt"]["expires_in"]),
algorithm=config_data["jwt"]["algorithm"],
),
) )

View File

@ -1,6 +1,12 @@
from api.application.protocols.date_time import DateTimeProvider
from api.application.protocols.password_hasher import PasswordHasher from api.application.protocols.password_hasher import PasswordHasher
from api.infrastructure.date_time import SystemDateTimeProvider, Timezone
from api.infrastructure.security.password_hasher import Pbkdf2PasswordHasher from api.infrastructure.security.password_hasher import Pbkdf2PasswordHasher
def get_password_hasher() -> PasswordHasher: def get_password_hasher() -> PasswordHasher:
return Pbkdf2PasswordHasher() return Pbkdf2PasswordHasher()
def get_date_time_provider() -> DateTimeProvider:
return SystemDateTimeProvider(Timezone.UTC)

View File

@ -14,6 +14,4 @@ def provide_create_user(
uow: Annotated[UnitOfWork, Depends(Stub(UnitOfWork))], uow: Annotated[UnitOfWork, Depends(Stub(UnitOfWork))],
password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))], password_hasher: Annotated[PasswordHasher, Depends(Stub(PasswordHasher))],
) -> CreateUser: ) -> CreateUser:
return CreateUser( return CreateUser(uow=uow, user_repository=user_repository, password_hasher=password_hasher)
uow=uow, user_repository=user_repository, password_hasher=password_hasher
)

View File

@ -2,7 +2,6 @@ from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from api.domain.user import User, UserRepository from api.domain.user import User, UserRepository
from api.infrastructure.persistence.models.user import UserModel
class SqlAlchemyUserRepository(UserRepository): class SqlAlchemyUserRepository(UserRepository):
@ -10,12 +9,6 @@ class SqlAlchemyUserRepository(UserRepository):
self.session = session self.session = session
async def create_user(self, user: User) -> None: async def create_user(self, user: User) -> None:
# stmt = insert(UserModel).values(
# id=user.id.value,
# name=user.name.value,
# email=user.email.value,
# hashed_password=user.hashed_password,
# )
stmt = text( stmt = text(
"""INSERT INTO users (id, name, email, hashed_password) """INSERT INTO users (id, name, email, hashed_password)
VALUES(:id, :name, :email, :hashed_password) VALUES(:id, :name, :email, :hashed_password)

View File

@ -1,8 +1,10 @@
from dataclasses import dataclass from dataclasses import dataclass
from api.infrastructure.auth.jwt_settings import JwtSettings
from api.infrastructure.persistence.db_setings import DBSettings from api.infrastructure.persistence.db_setings import DBSettings
@dataclass() @dataclass()
class Settings: class Settings:
db: DBSettings db: DBSettings
jwt: JwtSettings

View File

@ -1,9 +1,14 @@
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Response
from fastapi.security import OAuth2PasswordRequestForm
from api.application.contracts.auth import UserCreateRequest from api.application.contracts.auth import LoginRequest, UserCreateRequest
from api.application.contracts.auth.auth_response import AuthenticationResponse
from api.application.protocols.jwt import JwtTokenProcessor
from api.application.usecase.auth.auth_user import LoginUser
from api.application.usecase.auth.create_user import CreateUser from api.application.usecase.auth.create_user import CreateUser
from api.domain.user.model import UserId
from api.infrastructure.dependencies.stub import Stub from api.infrastructure.dependencies.stub import Stub
auth_router = APIRouter(prefix="/auth", tags=["Auth"]) auth_router = APIRouter(prefix="/auth", tags=["Auth"])
@ -15,3 +20,22 @@ async def create_user(
usecase: Annotated[CreateUser, Depends(Stub(CreateUser))], usecase: Annotated[CreateUser, Depends(Stub(CreateUser))],
) -> None: ) -> None:
return await usecase.execute(request) return await usecase.execute(request)
@auth_router.post("/login", response_model=AuthenticationResponse)
async def login(
response: Response,
login_request: Annotated[OAuth2PasswordRequestForm, Depends()],
login_interactor: Annotated[LoginUser, Depends(Stub(LoginUser))],
token_processor: Annotated[JwtTokenProcessor, Depends(Stub(JwtTokenProcessor))],
) -> AuthenticationResponse:
user = await login_interactor(
LoginRequest(
email=login_request.username,
password=login_request.password,
)
)
token = token_processor.generate_token(UserId(user.id))
response.set_cookie(key="access_token", value=f"Bearer {token}", httponly=True)
return user

View File

@ -1,9 +1,6 @@
from typing import Annotated from fastapi import APIRouter
from fastapi import APIRouter, Depends
from api.application.contracts.user import UserResponse from api.application.contracts.user import UserResponse
from api.infrastructure.dependencies.stub import Stub
user_router = APIRouter(prefix="/users", tags=["Users"]) user_router = APIRouter(prefix="/users", tags=["Users"])

View File

@ -4,3 +4,8 @@ db:
database: "serviceman_db" database: "serviceman_db"
user: "demo_user" user: "demo_user"
password: "user_pass" password: "user_pass"
jwt:
secret_key: "abra-cadabra"
algorithm: "HS256"
expires_in: 2

101
poetry.lock generated
View File

@ -347,6 +347,24 @@ files = [
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
] ]
[[package]]
name = "ecdsa"
version = "0.18.0"
description = "ECDSA cryptographic signature library (pure python)"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
{file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"},
]
[package.dependencies]
six = ">=1.9.0"
[package.extras]
gmpy = ["gmpy"]
gmpy2 = ["gmpy2"]
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.0" version = "1.2.0"
@ -600,6 +618,19 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
] ]
[[package]]
name = "jose"
version = "1.0.0"
description = "An implementation of the JOSE draft"
optional = false
python-versions = "*"
files = [
{file = "jose-1.0.0.tar.gz", hash = "sha256:8436c3617cd94e1ba97828fbb1ce27c129f66c78fb855b4bb47e122b5f345fba"},
]
[package.dependencies]
pycrypto = ">=2.6"
[[package]] [[package]]
name = "magic-filter" name = "magic-filter"
version = "1.0.12" version = "1.0.12"
@ -949,6 +980,27 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1" pyyaml = ">=5.1"
virtualenv = ">=20.10.0" virtualenv = ">=20.10.0"
[[package]]
name = "pyasn1"
version = "0.6.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
{file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"},
]
[[package]]
name = "pycrypto"
version = "2.6.1"
description = "Cryptographic modules for Python."
optional = false
python-versions = "*"
files = [
{file = "pycrypto-2.6.1.tar.gz", hash = "sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c"},
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.5.3" version = "2.5.3"
@ -1107,6 +1159,27 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "python-jose"
version = "3.3.0"
description = "JOSE implementation in Python"
optional = false
python-versions = "*"
files = [
{file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"},
{file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"},
]
[package.dependencies]
ecdsa = "!=0.15"
pyasn1 = "*"
rsa = "*"
[package.extras]
cryptography = ["cryptography (>=3.4.0)"]
pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"]
pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.1" version = "6.0.1"
@ -1132,7 +1205,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -1167,6 +1239,20 @@ files = [
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
] ]
[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.2.0" version = "69.2.0"
@ -1183,6 +1269,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
@ -1476,4 +1573,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "15cfd4e717fc2a2ca5dc844e5174d992f7fd8739bc2a056f2bbbc352e91018d4" content-hash = "cb90b25778130dc1445290f65ee84f37029b7db4838a16b510a9f2efaf5ae841"

View File

@ -7,6 +7,8 @@ readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
jose = "^1.0.0"
python-jose = "^3.3.0"