diff --git a/backend/pdm.lock b/backend/pdm.lock index 7c5e8d4..a515609 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:c967cac24f6f70dbbca2f704a73e986d1e00e940145b4dc6471c8bfd85440e79" +content_hash = "sha256:a15732e99160a9c0463d7809d12e0405520e386b75c8cabcf9ce911abd4c1001" [[package]] name = "annotated-types" @@ -134,6 +134,19 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "deprecated" +version = "1.2.14" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Python @deprecated decorator to deprecate old python classes, functions or methods." +dependencies = [ + "wrapt<2,>=1.10", +] +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + [[package]] name = "dnspython" version = "2.5.0" @@ -173,20 +186,6 @@ files = [ {file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"}, ] -[[package]] -name = "fastapi-limiter" -version = "0.1.6" -requires_python = ">=3.9,<4.0" -summary = "A request rate limiter for fastapi" -dependencies = [ - "fastapi", - "redis>=4.2.0rc1", -] -files = [ - {file = "fastapi_limiter-0.1.6-py3-none-any.whl", hash = "sha256:2e53179a4208b8f2c8795e38bb001324d3dc37d2800ff49fd28ec5caabf7a240"}, - {file = "fastapi_limiter-0.1.6.tar.gz", hash = "sha256:6f5fde8efebe12eb33861bdffb91009f699369a3c2862cdc7c1d9acf912ff443"}, -] - [[package]] name = "freezegun" version = "1.4.0" @@ -267,6 +266,16 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "importlib-resources" +version = "6.1.1" +requires_python = ">=3.8" +summary = "Read resources from Python packages" +files = [ + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -287,6 +296,22 @@ files = [ {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, ] +[[package]] +name = "limits" +version = "3.7.0" +requires_python = ">=3.7" +summary = "Rate limiting utilities" +dependencies = [ + "deprecated>=1.2", + "importlib-resources>=1.3", + "packaging<24,>=21", + "typing-extensions", +] +files = [ + {file = "limits-3.7.0-py3-none-any.whl", hash = "sha256:c528817b7fc15f3e86ad091ba3e40231f6430a91b753db864767684cda8a7f2e"}, + {file = "limits-3.7.0.tar.gz", hash = "sha256:124c6a04d2f4b20990fb1de019eec9474d6c1346c70d8fd0561609b86998b64a"}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -512,15 +537,15 @@ files = [ [[package]] name = "redis" -version = "5.1.0b3" -requires_python = ">=3.8" +version = "5.0.1" +requires_python = ">=3.7" summary = "Python client for Redis database and key-value store" dependencies = [ - "async-timeout>=4.0.3", + "async-timeout>=4.0.2; python_full_version <= \"3.11.2\"", ] files = [ - {file = "redis-5.1.0b3-py3-none-any.whl", hash = "sha256:8e83465ce69eb7b86b96d4e793ad1a8888d368815e47f1f6081d2d65d655a89c"}, - {file = "redis-5.1.0b3.tar.gz", hash = "sha256:e5386f40168f16e4b136aa03a74cb13bccfb042280fd443f81482fc10548aae6"}, + {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, + {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, ] [[package]] @@ -558,6 +583,19 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "slowapi" +version = "0.1.8" +requires_python = ">=3.7,<4.0" +summary = "A rate limiting extension for Starlette and Fastapi" +dependencies = [ + "limits>=2.3", +] +files = [ + {file = "slowapi-0.1.8-py3-none-any.whl", hash = "sha256:629fc415575bbffcd9d8621cc3ce326a78402c5f9b7b50b127979118d485c72e"}, + {file = "slowapi-0.1.8.tar.gz", hash = "sha256:8cc268f5a7e3624efa3f7bd2859b895f9f2376c4ed4e0378dd2f7f3343ca608e"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -710,6 +748,36 @@ files = [ {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, ] +[[package]] +name = "wrapt" +version = "1.16.0" +requires_python = ">=3.6" +summary = "Module for decorators, wrappers and monkey patching." +files = [ + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "zxcvbn" version = "4.4.28" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index a629bdd..8868334 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -13,7 +13,8 @@ dependencies = [ "bcrypt>=4.1.2", # hashing passwords "zxcvbn>=4.4.28", # rate password strength "itsdangerous>=2.1.2", # signing user tokens - "fastapi-limiter>=0.1.6", + "slowapi>=0.1.8", + "redis>=5.0.1", ] requires-python = ">=3.11" readme = "README.md" diff --git a/backend/src/neo_neo_todo/control/control.py b/backend/src/neo_neo_todo/control/control.py index 5b87e0c..8f7f590 100644 --- a/backend/src/neo_neo_todo/control/control.py +++ b/backend/src/neo_neo_todo/control/control.py @@ -1,4 +1,7 @@ from fastapi import APIRouter +from starlette.requests import Request + +from src.neo_neo_todo.utils.rate_limit import limiter router = APIRouter( prefix="/control", @@ -7,5 +10,6 @@ router = APIRouter( @router.get("/ping") -async def ping(): +@limiter.exempt +async def ping(request: Request): return {"ping": "pong"} diff --git a/backend/src/neo_neo_todo/main.py b/backend/src/neo_neo_todo/main.py index eb3a167..a6de28c 100644 --- a/backend/src/neo_neo_todo/main.py +++ b/backend/src/neo_neo_todo/main.py @@ -1,20 +1,14 @@ -from contextlib import asynccontextmanager - -import redis.asyncio as redis from fastapi import FastAPI -from fastapi_limiter import FastAPILimiter +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware from src.neo_neo_todo.control import control +from src.neo_neo_todo.utils.rate_limit import limiter - -@asynccontextmanager -async def lifespan(_: FastAPI): - redis_connection = redis.from_url("redis://localhost:6379", encoding="utf8") - await FastAPILimiter.init(redis_connection) - yield - await FastAPILimiter.close() - - -app = FastAPI(lifespan=lifespan) - +app = FastAPI() app.include_router(control.router) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore +app.add_middleware(SlowAPIMiddleware) diff --git a/backend/src/neo_neo_todo/utils/rate_limit.py b/backend/src/neo_neo_todo/utils/rate_limit.py new file mode 100644 index 0000000..c67536c --- /dev/null +++ b/backend/src/neo_neo_todo/utils/rate_limit.py @@ -0,0 +1,8 @@ +from slowapi import Limiter +from slowapi.util import get_remote_address + +limiter = Limiter( + key_func=get_remote_address, + default_limits=["70/minute"], + storage_uri="redis://localhost:6379/1", +) diff --git a/backend/tests/test_control.py b/backend/tests/test_control.py index 01b3b0a..5b13f80 100644 --- a/backend/tests/test_control.py +++ b/backend/tests/test_control.py @@ -1,11 +1,8 @@ import pytest from httpx import AsyncClient -from starlette.testclient import TestClient from src.neo_neo_todo.main import app -client = TestClient(app) - @pytest.mark.anyio async def test_ping():