start oauth
This commit is contained in:
parent
aeb044d1a0
commit
e4cd62d741
14 changed files with 283 additions and 46 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -1,9 +1,9 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
package
|
package
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from quart import g
|
|
||||||
|
|
||||||
from foxnouns.exceptions import ErrorCode, ForbiddenError
|
|
||||||
|
|
||||||
|
|
||||||
def require_auth(*, scope: str | None = None):
|
|
||||||
def decorator(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def wrapper(*args, **kwargs):
|
|
||||||
if "user" not in g or "token" not in g:
|
|
||||||
raise ForbiddenError("Not authenticated", type=ErrorCode.Forbidden)
|
|
||||||
|
|
||||||
if scope and not g.token.has_scope(scope):
|
|
||||||
raise ForbiddenError(
|
|
||||||
f"Missing scope '{scope}'", type=ErrorCode.MissingScope
|
|
||||||
)
|
|
||||||
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
|
@ -1,6 +1,13 @@
|
||||||
|
from .v2.auth import bp as auth_blueprint
|
||||||
|
from .v2.auth.discord import bp as discord_auth_blueprint
|
||||||
from .v2.members import bp as members_blueprint
|
from .v2.members import bp as members_blueprint
|
||||||
from .v2.meta import bp as meta_blueprint
|
from .v2.meta import bp as meta_blueprint
|
||||||
from .v2.users import bp as users_blueprint
|
from .v2.users import bp as users_blueprint
|
||||||
from .v2.auth.discord import bp as discord_auth_blueprint
|
|
||||||
|
|
||||||
__all__ = [users_blueprint, members_blueprint, meta_blueprint, discord_auth_blueprint]
|
__all__ = [
|
||||||
|
users_blueprint,
|
||||||
|
members_blueprint,
|
||||||
|
meta_blueprint,
|
||||||
|
discord_auth_blueprint,
|
||||||
|
auth_blueprint,
|
||||||
|
]
|
||||||
|
|
|
@ -1,27 +1,50 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from quart import Blueprint
|
from quart import Blueprint
|
||||||
from quart_schema import validate_response
|
from quart_schema import validate_response
|
||||||
|
|
||||||
from foxnouns.settings import BASE_DOMAIN
|
from foxnouns import settings
|
||||||
|
from foxnouns.db import redis
|
||||||
|
from foxnouns.db.redis import discord_state_key
|
||||||
from foxnouns.models.user import SelfUserModel
|
from foxnouns.models.user import SelfUserModel
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("auth_v2", __name__)
|
bp = Blueprint("auth_v2", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def discord_oauth_client(*, state: str | None = None):
|
||||||
|
"""Returns a new Discord OAuth client with the given state.
|
||||||
|
It is the caller's responsibility to check whether the client ID and client secret are set."""
|
||||||
|
|
||||||
|
return AsyncOAuth2Client(
|
||||||
|
settings.DISCORD_CLIENT_ID,
|
||||||
|
settings.DISCORD_CLIENT_SECRET,
|
||||||
|
state=state,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class URLsResponse(BaseModel):
|
class URLsResponse(BaseModel):
|
||||||
discord: str | None = Field(default=None)
|
discord: str | None = Field(default=None)
|
||||||
google: str | None = Field(default=None)
|
google: str | None = Field(default=None)
|
||||||
tumblr: str | None = Field(default=None)
|
tumblr: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/api/v2/auth/urls", host=BASE_DOMAIN)
|
@bp.post("/api/v2/auth/urls", host=settings.BASE_DOMAIN)
|
||||||
@validate_response(URLsResponse, 200)
|
@validate_response(URLsResponse, 200)
|
||||||
async def urls():
|
async def urls():
|
||||||
# TODO: build authorization URLs + callback URLs, store state in Redis
|
if settings.DISCORD_CLIENT_ID and settings.DISCORD_CLIENT_SECRET:
|
||||||
raise NotImplementedError()
|
client = discord_oauth_client()
|
||||||
|
discord_url, state = client.create_authorization_url(
|
||||||
|
"https://discord.com/api/oauth2/authorize"
|
||||||
|
)
|
||||||
|
|
||||||
|
await redis.set(
|
||||||
|
discord_state_key(state), state, ex=1800
|
||||||
|
) # Expire after 30 minutes
|
||||||
|
|
||||||
|
# TODO: return the other OAuth URLs
|
||||||
|
return URLsResponse(discord=discord_url)
|
||||||
|
|
||||||
|
|
||||||
class OAuthCallbackRequest(BaseModel):
|
class OAuthCallbackRequest(BaseModel):
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from quart import Blueprint
|
from quart import Blueprint
|
||||||
from quart_schema import validate_request, validate_response
|
from quart_schema import validate_request, validate_response
|
||||||
|
|
||||||
from foxnouns.settings import BASE_DOMAIN
|
from foxnouns import settings
|
||||||
|
from foxnouns.decorators import require_config_key
|
||||||
|
|
||||||
from . import BaseCallbackResponse, OAuthCallbackRequest
|
from . import BaseCallbackResponse, OAuthCallbackRequest
|
||||||
|
|
||||||
bp = Blueprint("discord_v2", __name__)
|
bp = Blueprint("discord_v2", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/api/v2/auth/discord/callback", host=BASE_DOMAIN)
|
@bp.post("/api/v2/auth/discord/callback", host=settings.BASE_DOMAIN)
|
||||||
|
@require_config_key(keys=[settings.DISCORD_CLIENT_ID, settings.DISCORD_CLIENT_SECRET])
|
||||||
@validate_request(OAuthCallbackRequest)
|
@validate_request(OAuthCallbackRequest)
|
||||||
@validate_response(BaseCallbackResponse)
|
@validate_response(BaseCallbackResponse)
|
||||||
async def discord_callback(data: OAuthCallbackRequest):
|
async def discord_callback(data: OAuthCallbackRequest):
|
||||||
|
|
|
@ -3,10 +3,10 @@ from quart import Blueprint, g
|
||||||
from quart_schema import validate_request, validate_response
|
from quart_schema import validate_request, validate_response
|
||||||
|
|
||||||
from foxnouns import tasks
|
from foxnouns import tasks
|
||||||
from foxnouns.auth import require_auth
|
|
||||||
from foxnouns.db import Member
|
from foxnouns.db import Member
|
||||||
from foxnouns.db.aio import async_session
|
from foxnouns.db.aio import async_session
|
||||||
from foxnouns.db.util import user_from_ref
|
from foxnouns.db.util import user_from_ref
|
||||||
|
from foxnouns.decorators import require_auth
|
||||||
from foxnouns.exceptions import ErrorCode, NotFoundError
|
from foxnouns.exceptions import ErrorCode, NotFoundError
|
||||||
from foxnouns.models.member import FullMemberModel, MemberPatchModel
|
from foxnouns.models.member import FullMemberModel, MemberPatchModel
|
||||||
from foxnouns.settings import BASE_DOMAIN
|
from foxnouns.settings import BASE_DOMAIN
|
||||||
|
|
|
@ -4,11 +4,11 @@ from quart_schema import validate_request, validate_response
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from foxnouns import tasks
|
from foxnouns import tasks
|
||||||
from foxnouns.auth import require_auth
|
|
||||||
from foxnouns.db import User
|
from foxnouns.db import User
|
||||||
from foxnouns.db.aio import async_session
|
from foxnouns.db.aio import async_session
|
||||||
from foxnouns.db.snowflake import Snowflake
|
from foxnouns.db.snowflake import Snowflake
|
||||||
from foxnouns.db.util import create_token, generate_token, is_self, user_from_ref
|
from foxnouns.db.util import create_token, generate_token, is_self, user_from_ref
|
||||||
|
from foxnouns.decorators import require_auth
|
||||||
from foxnouns.exceptions import ErrorCode, NotFoundError
|
from foxnouns.exceptions import ErrorCode, NotFoundError
|
||||||
from foxnouns.models import BasePatchModel
|
from foxnouns.models import BasePatchModel
|
||||||
from foxnouns.models.user import SelfUserModel, UserModel, check_username
|
from foxnouns.models.user import SelfUserModel, UserModel, check_username
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from .base import Base
|
from .base import Base
|
||||||
from .member import Member
|
from .member import Member
|
||||||
|
from .redis import redis
|
||||||
from .user import AuthMethod, FediverseApp, Token, User
|
from .user import AuthMethod, FediverseApp, Token, User
|
||||||
|
|
||||||
__all__ = [Base, User, Token, AuthMethod, FediverseApp, Member]
|
__all__ = [Base, User, Token, AuthMethod, FediverseApp, Member, redis]
|
||||||
|
|
|
@ -3,3 +3,7 @@ from redis import asyncio as aioredis
|
||||||
from foxnouns.settings import REDIS_URL
|
from foxnouns.settings import REDIS_URL
|
||||||
|
|
||||||
redis = aioredis.from_url(REDIS_URL)
|
redis = aioredis.from_url(REDIS_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def discord_state_key(state: str) -> str:
|
||||||
|
return f"discord-state:{state}"
|
||||||
|
|
45
foxnouns/decorators.py
Normal file
45
foxnouns/decorators.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from quart import g
|
||||||
|
|
||||||
|
from foxnouns.exceptions import ErrorCode, ForbiddenError, UnsupportedEndpointError
|
||||||
|
|
||||||
|
|
||||||
|
def require_auth(*, scope: str | None = None):
|
||||||
|
"""Decorator that requires a token with the given scopes.
|
||||||
|
If no token is given or the required scopes aren't set on it, execution is aborted."""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
if "user" not in g or "token" not in g:
|
||||||
|
raise ForbiddenError("Not authenticated", type=ErrorCode.Forbidden)
|
||||||
|
|
||||||
|
if scope and not g.token.has_scope(scope):
|
||||||
|
raise ForbiddenError(
|
||||||
|
f"Missing scope '{scope}'", type=ErrorCode.MissingScope
|
||||||
|
)
|
||||||
|
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def require_config_key(*, keys: list[Any]):
|
||||||
|
"""Decorator that requires one or more config keys to be set.
|
||||||
|
If any of them are None, execution is aborted."""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
for key in keys:
|
||||||
|
if not key:
|
||||||
|
raise UnsupportedEndpointError()
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
|
@ -80,3 +80,14 @@ class ForbiddenError(ExpectedError):
|
||||||
def __init__(self, msg: str, type=ErrorCode.Forbidden):
|
def __init__(self, msg: str, type=ErrorCode.Forbidden):
|
||||||
self.type = type
|
self.type = type
|
||||||
super().__init__(msg, type)
|
super().__init__(msg, type)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedEndpointError(ExpectedError):
|
||||||
|
status_code = 404
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.type = ErrorCode.NotFound
|
||||||
|
super().__init__(
|
||||||
|
"Endpoint is not supported on this instance",
|
||||||
|
type=ErrorCode.NotFound,
|
||||||
|
)
|
||||||
|
|
|
@ -27,6 +27,10 @@ with env.prefixed("MINIO_"):
|
||||||
"REGION": env("REGION", "auto"),
|
"REGION": env("REGION", "auto"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Discord OAuth credentials. If these are not set the Discord OAuth endpoints will not work.
|
||||||
|
DISCORD_CLIENT_ID = env("DISCORD_CLIENT_ID", None)
|
||||||
|
DISCORD_CLIENT_SECRET = env("DISCORD_CLIENT_SECRET", None)
|
||||||
|
|
||||||
# The base domain the API is served on. This must be set.
|
# The base domain the API is served on. This must be set.
|
||||||
BASE_DOMAIN = env("BASE_DOMAIN")
|
BASE_DOMAIN = env("BASE_DOMAIN")
|
||||||
# The base domain for short URLs.
|
# The base domain for short URLs.
|
||||||
|
|
164
poetry.lock
generated
164
poetry.lock
generated
|
@ -59,6 +59,27 @@ files = [
|
||||||
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
|
||||||
|
{file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = ">=2.8"
|
||||||
|
sniffio = ">=1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||||
|
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||||
|
trio = ["trio (>=0.23)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2-cffi"
|
name = "argon2-cffi"
|
||||||
version = "23.1.0"
|
version = "23.1.0"
|
||||||
|
@ -188,6 +209,21 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
|
||||||
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||||
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
|
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "authlib"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "Authlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:9637e4de1fb498310a56900b3e2043a206b03cb11c05422014b0302cbc814be3"},
|
||||||
|
{file = "Authlib-1.3.0.tar.gz", hash = "sha256:959ea62a5b7b5123c5059758296122b57cd2585ae2ed1c0622c21b371ffdae06"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cryptography = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "billiard"
|
name = "billiard"
|
||||||
version = "4.2.0"
|
version = "4.2.0"
|
||||||
|
@ -425,6 +461,61 @@ files = [
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "42.0.5"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"},
|
||||||
|
{file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"},
|
||||||
|
{file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"},
|
||||||
|
{file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"},
|
||||||
|
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"},
|
||||||
|
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"},
|
||||||
|
{file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"},
|
||||||
|
{file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"},
|
||||||
|
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"},
|
||||||
|
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"},
|
||||||
|
{file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"},
|
||||||
|
{file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
||||||
|
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||||
|
nox = ["nox"]
|
||||||
|
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||||
|
sdist = ["build"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "environs"
|
name = "environs"
|
||||||
version = "11.0.0"
|
version = "11.0.0"
|
||||||
|
@ -581,6 +672,53 @@ files = [
|
||||||
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
|
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.5"
|
||||||
|
description = "A minimal low-level HTTP client."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||||
|
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = "*"
|
||||||
|
h11 = ">=0.13,<0.15"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||||
|
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.27.0"
|
||||||
|
description = "The next generation HTTP client."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||||
|
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = "*"
|
||||||
|
certifi = "*"
|
||||||
|
httpcore = ">=1.0.0,<2.0.0"
|
||||||
|
idna = "*"
|
||||||
|
sniffio = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli", "brotlicffi"]
|
||||||
|
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hypercorn"
|
name = "hypercorn"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
@ -617,6 +755,18 @@ files = [
|
||||||
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
|
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.6"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||||
|
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -1304,6 +1454,18 @@ files = [
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
description = "Sniff out which async library your code is running under"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||||
|
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.28"
|
version = "2.0.28"
|
||||||
|
@ -1513,4 +1675,4 @@ h11 = ">=0.9.0,<1"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "3013b1d9175f34895133e708e35dea7d3401a07b5869c857e30d0aac41ce7add"
|
content-hash = "b3f536cfeeaa31d6df0ebf4debcffaf80492aca3d4f97a44ef1aa4a62e42b3eb"
|
||||||
|
|
|
@ -26,6 +26,8 @@ quart-cors = "^0.7.0"
|
||||||
minio = "^7.2.5"
|
minio = "^7.2.5"
|
||||||
pyvips = "^2.2.2"
|
pyvips = "^2.2.2"
|
||||||
redis = "^5.0.3"
|
redis = "^5.0.3"
|
||||||
|
authlib = "^1.3.0"
|
||||||
|
httpx = "^0.27.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev]
|
[tool.poetry.group.dev]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
Loading…
Reference in a new issue