From e301a35b427030b41c9b38986bd0849b6c3698a1 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Sep 2024 13:34:14 -0300 Subject: [PATCH 1/5] New LoginForm for 2FA use --- app/routers/auth.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routers/auth.py b/app/routers/auth.py index e85d4cb..11218a4 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -7,6 +7,7 @@ from fastapi.responses import StreamingResponse from app.models import User +from app.types.frontend import LoginFormWith2FA from app.types.pydantic_models import Token, Enable2FA from app.utils import authenticate_user, generate_user_token from app.security import TwoFactorAuth @@ -46,7 +47,7 @@ async def login_without_2fa( @router.post("/2fa/is-2fa-active/") async def is_2fa_active( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + form_data: Annotated[LoginFormWith2FA, Depends()], ) -> bool: user = await authenticate_user(form_data.username, form_data.password) if not user: @@ -61,8 +62,7 @@ async def is_2fa_active( @router.post("/2fa/login/") async def login_with_2fa( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - totp_code: str, + form_data: Annotated[LoginFormWith2FA, Depends()], ) -> Token: user = await authenticate_user(form_data.username, form_data.password) @@ -76,7 +76,7 @@ async def login_with_2fa( secret_key = await TwoFactorAuth.get_or_create_secret_key(user.id) two_factor_auth = TwoFactorAuth(user.id, secret_key) - is_valid = two_factor_auth.verify_totp_code(totp_code) + is_valid = two_factor_auth.verify_totp_code(form_data.totp_code) if not is_valid: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -107,7 +107,7 @@ async def enable_2fa( @router.post("/2fa/generate-qrcode/") async def generate_qrcode( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + form_data: Annotated[LoginFormWith2FA, Depends()], ) -> bytes: current_user = await authenticate_user(form_data.username, form_data.password) if not current_user: From a3f9111ffe42f63c82b460e2e5f40b128cb4699e Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Sep 2024 13:34:29 -0300 Subject: [PATCH 2/5] using cpf_particao to query --- app/routers/frontend.py | 6 +++--- app/types/frontend.py | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/routers/frontend.py b/app/routers/frontend.py index c780cc8..562dff1 100644 --- a/app/routers/frontend.py +++ b/app/routers/frontend.py @@ -59,7 +59,7 @@ async def get_patient_header( f""" SELECT * FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_HEADER_TABLE_ID} - WHERE cpf = '{cpf}' + WHERE cpf_particao = {cpf} """, from_file="/tmp/credentials.json", ) @@ -88,7 +88,7 @@ async def get_patient_summary( f""" SELECT * FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_SUMMARY_TABLE_ID} - WHERE cpf = '{cpf}' + WHERE cpf_particao = {cpf} """, from_file="/tmp/credentials.json", ) @@ -123,7 +123,7 @@ async def get_patient_encounters( f""" SELECT * FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID} - WHERE cpf = '{cpf}' and exibicao.indicador = true + WHERE cpf_particao = {cpf} and exibicao.indicador = true """, from_file="/tmp/credentials.json", ) diff --git a/app/types/frontend.py b/app/types/frontend.py index 60743cc..044ab9a 100644 --- a/app/types/frontend.py +++ b/app/types/frontend.py @@ -2,6 +2,19 @@ from typing import Optional, List from pydantic import BaseModel +from fastapi.security import OAuth2PasswordRequestForm + + +class LoginFormWith2FA(OAuth2PasswordRequestForm): + def __init__( + self, + username: str, + password: str, + totp: str, + ): + super().__init__(username=username, password=password) + self.totp = totp + # Clinic Family model class FamilyClinic(BaseModel): @@ -16,11 +29,13 @@ class FamilyHealthTeam(BaseModel): name: Optional[str] phone: Optional[str] + # Clinical Exam Model class ClinicalExam(BaseModel): type: str description: Optional[str] + # Medical Conditions model class PatientSummary(BaseModel): allergies: List[str] @@ -29,7 +44,7 @@ class PatientSummary(BaseModel): # Responsible model class Responsible(BaseModel): - name: str + name: Optional[str] # Temporary role: str @@ -40,7 +55,7 @@ class Encounter(BaseModel): location: str type: str subtype: Optional[str] - exhibition_type: str = 'default' + exhibition_type: str = "default" active_cids: List[str] responsible: Optional[Responsible] clinical_motivation: Optional[str] @@ -56,8 +71,9 @@ class UserInfo(BaseModel): email: Optional[str] role: Optional[str] + class Professional(BaseModel): - name: str + name: Optional[str] registry: Optional[str] From f8aba0484cd5ea277d7dff08a9fdd6104b63d176 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Sep 2024 13:53:40 -0300 Subject: [PATCH 3/5] refactor: Update login endpoint to use OAuth2PasswordRequestForm for 2FA activation --- app/routers/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/auth.py b/app/routers/auth.py index 11218a4..1640ff5 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -47,7 +47,7 @@ async def login_without_2fa( @router.post("/2fa/is-2fa-active/") async def is_2fa_active( - form_data: Annotated[LoginFormWith2FA, Depends()], + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], ) -> bool: user = await authenticate_user(form_data.username, form_data.password) if not user: From d3d0740ba16c14e9d2ee8a6d2fa44066674d86e4 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Sep 2024 14:19:21 -0300 Subject: [PATCH 4/5] refactor: Update login endpoint to use OAuth2PasswordRequestForm for 2FA activation --- app/routers/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/auth.py b/app/routers/auth.py index 1640ff5..9c8113f 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -107,7 +107,7 @@ async def enable_2fa( @router.post("/2fa/generate-qrcode/") async def generate_qrcode( - form_data: Annotated[LoginFormWith2FA, Depends()], + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], ) -> bytes: current_user = await authenticate_user(form_data.username, form_data.password) if not current_user: From b0f89002fc731c2bdd4297bfef9f4d126e175311 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Sep 2024 16:56:58 -0300 Subject: [PATCH 5/5] Implement Rate Limiter --- app/routers/frontend.py | 10 ++++++++-- app/types/frontend.py | 4 ++-- poetry.lock | 34 +++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/app/routers/frontend.py b/app/routers/frontend.py index 562dff1..dd5aeea 100644 --- a/app/routers/frontend.py +++ b/app/routers/frontend.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from typing import Annotated, List -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from tortoise.exceptions import ValidationError - +from fastapi_simple_rate_limiter import rate_limiter from app.dependencies import ( get_current_frontend_user ) @@ -45,9 +45,11 @@ async def get_user_info( @router.get("/patient/header/{cpf}") +@rate_limiter(limit=5, seconds=60) async def get_patient_header( _: Annotated[User, Depends(get_current_frontend_user)], cpf: str, + request: Request, ) -> PatientHeader: validator = CPFValidator() try: @@ -79,9 +81,11 @@ async def get_patient_header( @router.get("/patient/summary/{cpf}") +@rate_limiter(limit=5, seconds=60) async def get_patient_summary( _: Annotated[User, Depends(get_current_frontend_user)], cpf: str, + request: Request, ) -> PatientSummary: results = await read_bq( @@ -114,9 +118,11 @@ async def get_filter_tags( @router.get("/patient/encounters/{cpf}") +@rate_limiter(limit=5, seconds=60) async def get_patient_encounters( _: Annotated[User, Depends(get_current_frontend_user)], cpf: str, + request: Request, ) -> List[Encounter]: results = await read_bq( diff --git a/app/types/frontend.py b/app/types/frontend.py index 044ab9a..5f48c89 100644 --- a/app/types/frontend.py +++ b/app/types/frontend.py @@ -10,10 +10,10 @@ def __init__( self, username: str, password: str, - totp: str, + totp_code: str, ): super().__init__(username=username, password=password) - self.totp = totp + self.totp_code = totp_code # Clinic Family model diff --git a/poetry.lock b/poetry.lock index 7dad60e..6e99979 100644 --- a/poetry.lock +++ b/poetry.lock @@ -613,6 +613,20 @@ typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "fastapi-simple-rate-limiter" +version = "0.0.4" +description = "Rate limiter to limit the number of API requests in FastAPI" +optional = false +python-versions = "<4.0,>=3.10" +files = [ + {file = "fastapi_simple_rate_limiter-0.0.4-py3-none-any.whl", hash = "sha256:2e7e23897793a1e22ad31c4c4674c2f2ace464624c16f998265d9fe36e0f0faa"}, + {file = "fastapi_simple_rate_limiter-0.0.4.tar.gz", hash = "sha256:fa4e473728ecded6f433240697f7422d84ceeaa92521d0c72b00989c0a573cc8"}, +] + +[package.dependencies] +redis = ">=5.0.1,<6.0.0" + [[package]] name = "filelock" version = "3.13.1" @@ -2194,6 +2208,24 @@ maintainer = ["zest.releaser[recommended]"] pil = ["pillow (>=9.1.0)"] test = ["coverage", "pytest"] +[[package]] +name = "redis" +version = "5.0.8" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, + {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "regex" version = "2024.7.24" @@ -2855,4 +2887,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "5715de4721d690e2ecff456af1ed2d255f810d0c660d5cb7f26bd307d92e4199" +content-hash = "5fc1242cb6d7fb62f1eb325524eab82ec4506e439623f47e82bf1dbcf4ba0a8e" diff --git a/pyproject.toml b/pyproject.toml index f9022a8..dc6887e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ nltk = "^3.9.1" asyncer = "^0.0.8" qrcode = "^7.4.2" pyotp = "^2.9.0" +fastapi-simple-rate-limiter = "^0.0.4" [tool.poetry.group.dev.dependencies]