feat(api): login and authentication routes
- Remove fastapi-limiter, we will rate limit at load balancer level as it is too hard to get fastapi-limiter to play nice with pytest. - Wrote technical writeups on how the login flow and check for user authentication status work
This commit is contained in:
parent
0bd4508d11
commit
4a72d88ba2
@ -21,6 +21,7 @@
|
|||||||
- Use Pydantic data validation always
|
- Use Pydantic data validation always
|
||||||
|
|
||||||
## TODO list
|
## TODO list
|
||||||
|
|
||||||
- [ ] Setup Docker image and k8s for the API: 3 containers: API, Redis and PostgreSQL.
|
- [ ] Setup Docker image and k8s for the API: 3 containers: API, Redis and PostgreSQL.
|
||||||
|
|
||||||
## Authentication notes
|
## Authentication notes
|
||||||
@ -31,3 +32,17 @@ When a user logs in and is granted an access token by an OAuth 2.0 server, the t
|
|||||||
|
|
||||||
- The flow used was: Password flow but instead of username, we use the user's email instead
|
- The flow used was: Password flow but instead of username, we use the user's email instead
|
||||||
- In the Oauth2 spec, the `scope` part is a string of permission(s)
|
- In the Oauth2 spec, the `scope` part is a string of permission(s)
|
||||||
|
|
||||||
|
### Sign up flow TODO
|
||||||
|
|
||||||
|
### Login flow
|
||||||
|
|
||||||
|
- Send a POST request to API route /token
|
||||||
|
- If username and password are correct, create JWT for user
|
||||||
|
- User logged in
|
||||||
|
|
||||||
|
### Check authentication status
|
||||||
|
|
||||||
|
- Send a GET request to API route /members/meo
|
||||||
|
- If status code is 401: they are not authenticated
|
||||||
|
- If status code is 200: they are authenticated and we can get the email and email_verified status
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
UVICORN_PORT="5050"
|
UVICORN_PORT="5050"
|
||||||
TODO_DEBUG=true
|
TODO_DEBUG=true
|
||||||
TODO_SECRET_KEY="secret key"
|
TODO_SECRET_KEY="e9e53155ef8eda837f3047c949ea39cdb591fa0dc3a27c3a5858481af828d812"
|
||||||
|
TODO_SALT="o12iu3h1po2j3hklajshdfasdli2u808hhhh889009"
|
||||||
TODO_DB_DATABASE_URL="postgresql://todo_dev:todo@0.0.0.0:5432/todo_dev"
|
TODO_DB_DATABASE_URL="postgresql://todo_dev:todo@0.0.0.0:5432/todo_dev"
|
||||||
|
266
backend/pdm.lock
generated
266
backend/pdm.lock
generated
@ -5,7 +5,7 @@
|
|||||||
groups = ["default", "dev"]
|
groups = ["default", "dev"]
|
||||||
strategy = ["cross_platform"]
|
strategy = ["cross_platform"]
|
||||||
lock_version = "4.4.1"
|
lock_version = "4.4.1"
|
||||||
content_hash = "sha256:a69b9b70f0bb65c59605a80b08a73a6464df2a6407212c0094d6adef0e858584"
|
content_hash = "sha256:b88e3658add95aa61a36f3c9eee7e54a979fc4bc8598a9a6c04962c57ac3aa0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
@ -78,7 +78,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "24.2.0"
|
version = "24.3.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "The uncompromising code formatter."
|
summary = "The uncompromising code formatter."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -89,16 +89,16 @@ dependencies = [
|
|||||||
"platformdirs>=2",
|
"platformdirs>=2",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
|
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||||
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
|
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||||
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
|
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||||
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
|
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||||
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
|
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||||
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
|
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||||
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
|
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||||
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
|
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||||
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
|
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||||
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
|
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -111,6 +111,39 @@ files = [
|
|||||||
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
|
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "1.16.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Foreign Function Interface for Python calling C code."
|
||||||
|
dependencies = [
|
||||||
|
"pycparser",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
|
||||||
|
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
|
||||||
|
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
|
||||||
|
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
@ -198,6 +231,49 @@ files = [
|
|||||||
{file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
|
{file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "42.0.5"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
dependencies = [
|
||||||
|
"cffi>=1.12; platform_python_implementation != \"PyPy\"",
|
||||||
|
]
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "dnspython"
|
name = "dnspython"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -208,6 +284,19 @@ files = [
|
|||||||
{file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"},
|
{file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.18.0"
|
||||||
|
requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
summary = "ECDSA cryptographic signature library (pure python)"
|
||||||
|
dependencies = [
|
||||||
|
"six>=1.9.0",
|
||||||
|
]
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "email-validator"
|
name = "email-validator"
|
||||||
version = "2.1.0.post1"
|
version = "2.1.0.post1"
|
||||||
@ -237,20 +326,6 @@ files = [
|
|||||||
{file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"},
|
{file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "freezegun"
|
name = "freezegun"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -443,9 +518,29 @@ files = [
|
|||||||
{file = "psycopg-3.1.18.tar.gz", hash = "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b"},
|
{file = "psycopg-3.1.18.tar.gz", hash = "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1"
|
||||||
|
version = "0.5.1"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||||
|
summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
|
||||||
|
files = [
|
||||||
|
{file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"},
|
||||||
|
{file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.21"
|
||||||
|
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
summary = "C parser in Python"
|
||||||
|
files = [
|
||||||
|
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||||
|
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.6.3"
|
version = "2.6.4"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Data validation using Python type hints"
|
summary = "Data validation using Python type hints"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -454,8 +549,8 @@ dependencies = [
|
|||||||
"typing-extensions>=4.6.1",
|
"typing-extensions>=4.6.1",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"},
|
{file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"},
|
||||||
{file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"},
|
{file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -514,17 +609,17 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.6.3"
|
version = "2.6.4"
|
||||||
extras = ["email"]
|
extras = ["email"]
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Data validation using Python type hints"
|
summary = "Data validation using Python type hints"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"email-validator>=2.0.0",
|
"email-validator>=2.0.0",
|
||||||
"pydantic==2.6.3",
|
"pydantic==2.6.4",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"},
|
{file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"},
|
||||||
{file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"},
|
{file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -545,15 +640,15 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-asyncio"
|
name = "pytest-asyncio"
|
||||||
version = "0.23.5.post1"
|
version = "0.23.6"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Pytest support for asyncio"
|
summary = "Pytest support for asyncio"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pytest<9,>=7.0.0",
|
"pytest<9,>=7.0.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"},
|
{file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"},
|
||||||
{file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"},
|
{file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -593,6 +688,34 @@ files = [
|
|||||||
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
|
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-jose"
|
||||||
|
version = "3.3.0"
|
||||||
|
summary = "JOSE implementation in Python"
|
||||||
|
dependencies = [
|
||||||
|
"ecdsa!=0.15",
|
||||||
|
"pyasn1",
|
||||||
|
"rsa",
|
||||||
|
]
|
||||||
|
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]]
|
||||||
|
name = "python-jose"
|
||||||
|
version = "3.3.0"
|
||||||
|
extras = ["cryptography"]
|
||||||
|
summary = "JOSE implementation in Python"
|
||||||
|
dependencies = [
|
||||||
|
"cryptography>=3.4.0",
|
||||||
|
"python-jose==3.3.0",
|
||||||
|
]
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.9"
|
version = "0.0.9"
|
||||||
@ -628,40 +751,53 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "5.0.2"
|
version = "5.0.3"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Python client for Redis database and key-value store"
|
summary = "Python client for Redis database and key-value store"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-timeout>=4.0.3",
|
"async-timeout>=4.0.3; python_full_version < \"3.11.3\"",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "redis-5.0.2-py3-none-any.whl", hash = "sha256:4caa8e1fcb6f3c0ef28dba99535101d80934b7d4cd541bbb47f4a3826ee472d1"},
|
{file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"},
|
||||||
{file = "redis-5.0.2.tar.gz", hash = "sha256:3f82cc80d350e93042c8e6e7a5d0596e4dd68715babffba79492733e1f367037"},
|
{file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "4.9"
|
||||||
|
requires_python = ">=3.6,<4"
|
||||||
|
summary = "Pure-Python RSA implementation"
|
||||||
|
dependencies = [
|
||||||
|
"pyasn1>=0.1.3",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
|
||||||
|
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.3.2"
|
version = "0.3.4"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"},
|
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
|
||||||
{file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"},
|
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"},
|
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"},
|
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"},
|
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"},
|
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"},
|
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
|
||||||
{file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"},
|
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
|
||||||
{file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"},
|
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
|
||||||
{file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"},
|
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
|
||||||
{file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"},
|
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -719,7 +855,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.28.0"
|
version = "0.29.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "The lightning-fast ASGI server."
|
summary = "The lightning-fast ASGI server."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -727,13 +863,13 @@ dependencies = [
|
|||||||
"h11>=0.8",
|
"h11>=0.8",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"},
|
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
|
||||||
{file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"},
|
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.28.0"
|
version = "0.29.0"
|
||||||
extras = ["standard"]
|
extras = ["standard"]
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "The lightning-fast ASGI server."
|
summary = "The lightning-fast ASGI server."
|
||||||
@ -742,14 +878,14 @@ dependencies = [
|
|||||||
"httptools>=0.5.0",
|
"httptools>=0.5.0",
|
||||||
"python-dotenv>=0.13",
|
"python-dotenv>=0.13",
|
||||||
"pyyaml>=5.1",
|
"pyyaml>=5.1",
|
||||||
"uvicorn==0.28.0",
|
"uvicorn==0.29.0",
|
||||||
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
|
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
|
||||||
"watchfiles>=0.13",
|
"watchfiles>=0.13",
|
||||||
"websockets>=10.4",
|
"websockets>=10.4",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"},
|
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
|
||||||
{file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"},
|
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -10,14 +10,14 @@ dependencies = [
|
|||||||
"uvicorn[standard]>=0.23.2",
|
"uvicorn[standard]>=0.23.2",
|
||||||
"httpx>=0.26.0",
|
"httpx>=0.26.0",
|
||||||
"pydantic[email]>=2.5.3",
|
"pydantic[email]>=2.5.3",
|
||||||
"bcrypt>=4.1.2", # hashing passwords
|
|
||||||
"zxcvbn>=4.4.28", # rate password strength
|
"zxcvbn>=4.4.28", # rate password strength
|
||||||
"itsdangerous>=2.1.2", # signing user tokens
|
"itsdangerous>=2.1.2", # signing user tokens
|
||||||
"redis>=5.0.1",
|
"redis>=5.0.1",
|
||||||
"psycopg[pool]>=3.1.18",
|
"psycopg[pool]>=3.1.18",
|
||||||
"fastapi-limiter>=0.1.6",
|
|
||||||
"pytest-cov>=4.1.0",
|
"pytest-cov>=4.1.0",
|
||||||
"python-multipart>=0.0.9",
|
"python-multipart>=0.0.9",
|
||||||
|
"python-jose[cryptography]>=3.3.0",
|
||||||
|
"bcrypt>=4.1.2",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
import redis.asyncio as redis
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi_limiter import FastAPILimiter
|
|
||||||
|
|
||||||
from src.neo_neo_todo.routes import control, members, token
|
from src.neo_neo_todo.routes import control, members, token
|
||||||
from src.neo_neo_todo.utils.database import pool
|
from src.neo_neo_todo.utils.database import pool
|
||||||
@ -11,16 +9,13 @@ from src.neo_neo_todo.utils.database import pool
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(_: FastAPI):
|
async def lifespan(_: FastAPI):
|
||||||
# Set up pool
|
# Set up pool
|
||||||
redis_connection = redis.from_url("redis://localhost:6379", encoding="utf8")
|
print("Starting PostgreSQL DB connection pool")
|
||||||
await FastAPILimiter.init(redis_connection)
|
|
||||||
print("Starting PostgreSQL DB connection pool and Redis from FastAPI")
|
|
||||||
await pool.open()
|
await pool.open()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
# Clean up the DB pool
|
# Clean up the DB pool
|
||||||
await pool.close()
|
await pool.close()
|
||||||
await FastAPILimiter.close()
|
print("Closing PostgreSQL DB connection pool")
|
||||||
print("Closing PostgreSQL DB connection pool and Redis from FastAPI")
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
@ -34,7 +34,7 @@ def generate_test_data() -> None:
|
|||||||
"""
|
"""
|
||||||
INSERT INTO members (email, password_hash)
|
INSERT INTO members (email, password_hash)
|
||||||
VALUES ('member@todo.test',
|
VALUES ('member@todo.test',
|
||||||
'$2a$12$EDo2Sr6B1sptfbKK7DqMnO3VNZuNfVuDbpjJa3uUO9S9/lYpu2wzK')
|
'$2b$12$4mapMj8lcaTEIkyfp5V.3OhAd0xW5s1qYPniNTWNlx9E3unCfM..O')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
36
backend/src/neo_neo_todo/models/token.py
Normal file
36
backend/src/neo_neo_todo/models/token.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from jose import jwt
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
try:
|
||||||
|
SECRETKEY = os.environ["TODO_SECRET_KEY"]
|
||||||
|
ALGORITHM = os.environ["ALGORITHM"]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError("Can't find secret key or algorithm")
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
username: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||||
|
to_encode = data.copy()
|
||||||
|
|
||||||
|
expire = (
|
||||||
|
datetime.now(timezone.utc) + expires_delta
|
||||||
|
if expires_delta
|
||||||
|
else datetime.now(timezone.utc) + timedelta(minutes=1200)
|
||||||
|
)
|
||||||
|
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRETKEY, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
return encoded_jwt
|
@ -1,42 +1,70 @@
|
|||||||
from datetime import datetime
|
import os
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
from src.neo_neo_todo.models.member import Member
|
from neo_neo_todo.models.token import TokenData
|
||||||
|
from src.neo_neo_todo.models.member import select_member_by_email
|
||||||
|
from src.neo_neo_todo.utils.database import get_pool
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/members",
|
prefix="/members",
|
||||||
tags=["members"],
|
tags=["members"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
SECRETKEY = os.environ["TODO_SECRET_KEY"]
|
||||||
|
ALGORITHM = os.environ["ALGORITHM"]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError("Can't find secret key or algorithm")
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
|
|
||||||
|
|
||||||
def fake_decode_token(_):
|
async def get_current_member(
|
||||||
return Member(
|
token: Annotated[str, Depends(oauth2_scheme)],
|
||||||
id=1,
|
db_pool: AsyncConnectionPool = Depends(get_pool),
|
||||||
email="fake@token.com",
|
):
|
||||||
password_hash="asdfasdffqwerqwe!@#09098@%)(*)",
|
"""
|
||||||
created=datetime.now(),
|
Helper function for the /me API route below
|
||||||
email_verified=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
Mainly used for check authentication status
|
||||||
async def get_current_member(token: Annotated[str, Depends(oauth2_scheme)]):
|
"""
|
||||||
member = fake_decode_token(token)
|
credentials_exception = HTTPException(
|
||||||
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Could not validate credentials",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
payload: dict = jwt.decode(token, SECRETKEY, algorithms=[ALGORITHM])
|
||||||
|
username = payload.get(
|
||||||
|
"sub"
|
||||||
|
) # JWT subject is where we put the user's identification
|
||||||
|
|
||||||
return member
|
if username is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
token_data = TokenData(username=username)
|
||||||
|
|
||||||
|
except JWTError:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
member = await select_member_by_email(db_pool, token_data.username) # type: ignore
|
||||||
|
|
||||||
|
if not member:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
return {"email": member.email, "email_verified": member.email_verified}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
async def read_users_me(current_user: Annotated[Member, Depends(get_current_member)]):
|
async def read_users_me(current_user: Annotated[dict, Depends(get_current_member)]):
|
||||||
|
"""
|
||||||
|
Dependency inject the get_current_member function
|
||||||
|
|
||||||
|
Use this API route to check for authentication
|
||||||
|
"""
|
||||||
return current_user
|
return current_user
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from fastapi_limiter.depends import RateLimiter
|
|
||||||
from psycopg_pool import AsyncConnectionPool
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
from src.neo_neo_todo.models.member import select_member_by_email
|
from src.neo_neo_todo.models.member import select_member_by_email
|
||||||
|
from src.neo_neo_todo.models.token import Token, create_access_token
|
||||||
from src.neo_neo_todo.utils.database import get_pool
|
from src.neo_neo_todo.utils.database import get_pool
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
@ -14,12 +16,34 @@ router = APIRouter(
|
|||||||
tags=["token"],
|
tags=["token"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = os.environ["ACCESS_TOKEN_EXPIRE_MINUTES"]
|
||||||
|
TODO_SALT = os.environ["TODO_SALT"]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError("Can't find access token expire in minutes")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: bytes, hashed_password: bytes) -> bool:
|
||||||
|
"""
|
||||||
|
Verify user supplied password with the password hash in the database
|
||||||
|
|
||||||
|
Return True if matches or False if not
|
||||||
|
"""
|
||||||
|
return bcrypt.checkpw(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password):
|
||||||
|
return bcrypt.hashpw(password, TODO_SALT.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
INVALID_USERNAME_OR_PASSWORD_EXCEPTION: HTTPException = HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Incorrect email or password",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"",
|
"",
|
||||||
dependencies=[
|
|
||||||
Depends(RateLimiter(times=100, seconds=600))
|
|
||||||
], # rate limit login route
|
|
||||||
status_code=status.HTTP_200_OK, # default status code when login successful
|
status_code=status.HTTP_200_OK, # default status code when login successful
|
||||||
)
|
)
|
||||||
async def login(
|
async def login(
|
||||||
@ -29,28 +53,29 @@ async def login(
|
|||||||
"""
|
"""
|
||||||
Login to the todo app
|
Login to the todo app
|
||||||
|
|
||||||
If successful, save the returned cookie
|
If successful, return the JWT token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# username is actually email in this case
|
||||||
member = await select_member_by_email(db_pool, form_data.username)
|
member = await select_member_by_email(db_pool, form_data.username)
|
||||||
|
|
||||||
if member is None:
|
if member is None:
|
||||||
raise HTTPException(
|
raise INVALID_USERNAME_OR_PASSWORD_EXCEPTION
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Incorrect email or password",
|
|
||||||
)
|
|
||||||
|
|
||||||
passwords_match = bcrypt.checkpw(
|
passwords_match = verify_password(
|
||||||
form_data.password.encode("utf-8"),
|
form_data.password.encode("utf-8"),
|
||||||
member.password_hash.encode("utf-8"),
|
member.password_hash.encode("utf-8"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if passwords_match:
|
if passwords_match:
|
||||||
# to handle login logic here
|
# to handle login logic here
|
||||||
pass
|
access_token_expires = timedelta(minutes=int(ACCESS_TOKEN_EXPIRE_MINUTES))
|
||||||
else:
|
|
||||||
raise HTTPException(
|
access_token = create_access_token(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
data={"sub": member.email}, expires_delta=access_token_expires
|
||||||
detail="Incorrect email or password",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"access_token": member.email, "token_type": "bearer"}
|
else:
|
||||||
|
raise INVALID_USERNAME_OR_PASSWORD_EXCEPTION
|
||||||
|
|
||||||
|
return Token(access_token=access_token, token_type="bearer")
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
UVICORN_PORT="5050"
|
UVICORN_PORT="5050"
|
||||||
TODO_DEBUG=true
|
TODO_DEBUG=true
|
||||||
TODO_SECRET_KEY="secret key"
|
TODO_SECRET_KEY="e9e53155ef8eda837f3047c949ea39cdb591fa0dc3a27c3a5858481af828d812"
|
||||||
TODO_TESTING=true
|
TODO_TESTING=true
|
||||||
TODO_DB_DATABASE_URL="postgresql://todo_test:todo@0.0.0.0:5432/todo_test"
|
TODO_DB_DATABASE_URL="postgresql://todo_test:todo@0.0.0.0:5432/todo_test"
|
||||||
|
TODO_SALT="o12iu3h1po2j3hklajshdfasdli2u808hhhh889009"
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
|
10
backend/tests/test_route_member.py
Normal file
10
backend/tests/test_route_member.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from src.neo_neo_todo.main import app
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_current_member():
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.get("/members/me")
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {"detail": "Not authenticated"}
|
Loading…
x
Reference in New Issue
Block a user