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:
minhtrannhat 2024-03-23 14:53:33 -04:00
parent 0bd4508d11
commit 4a72d88ba2
Signed by: minhtrannhat
GPG Key ID: E13CFA85C53F8062
12 changed files with 366 additions and 115 deletions

View File

@ -14,13 +14,14 @@
## Structure ## Structure
- Use FastAPI's `router` to organize different API routes - Use FastAPI's `router` to organize different API routes
- `member` route for getting the current member/user - `member` route for getting the current member/user
- `todo` route to create/read/update/delete todos - `todo` route to create/read/update/delete todos
- `token` route for authentication (login/logout) - `token` route for authentication (login/logout)
- Separate folder for PostgreSQL migrations: Might need a better migration tool. Right now, `alembic` only works with SQLalchemy. - Separate folder for PostgreSQL migrations: Might need a better migration tool. Right now, `alembic` only works with SQLalchemy.
- 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

View File

@ -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
View File

@ -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]]

View File

@ -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"

View File

@ -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)

View File

@ -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')
""" """
) )
) )

View 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

View File

@ -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
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
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
if username is None:
raise credentials_exception
async def get_current_member(token: Annotated[str, Depends(oauth2_scheme)]): token_data = TokenData(username=username)
member = fake_decode_token(token)
except JWTError:
raise credentials_exception
member = await select_member_by_email(db_pool, token_data.username) # type: ignore
if not member: if not member:
raise HTTPException( raise credentials_exception
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return member 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

View File

@ -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")

View File

@ -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

View 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"}