diff --git a/app/auth/routers/__init__.py b/app/auth/routers/__init__.py index 75f5996..bc949b7 100644 --- a/app/auth/routers/__init__.py +++ b/app/auth/routers/__init__.py @@ -1,6 +1,13 @@ # -*- coding: utf-8 -*- from fastapi import APIRouter +from app.auth.routers.basic import router as auth_basic +from app.auth.routers.email import router as auth_email +from app.auth.routers.totp import router as auth_totp router = APIRouter(prefix="/auth", tags=["AutenticaĆ§Ć£o"]) + +router.include_router(auth_basic) +router.include_router(auth_email) +router.include_router(auth_totp) diff --git a/app/auth/routers/basic.py b/app/auth/routers/basic.py index ef4a52a..f60ec0d 100644 --- a/app/auth/routers/basic.py +++ b/app/auth/routers/basic.py @@ -3,19 +3,19 @@ from typing import Annotated from fastapi import Depends +from fastapi import APIRouter from fastapi.security import OAuth2PasswordRequestForm from fastapi.responses import JSONResponse from loguru import logger from app import config -from app.auth.routers import router -from app.types.pydantic_models import Token -from app.utils import authenticate_user, generate_user_token -from app.enums import LoginStatusEnum -from app.types.errors import ( - AuthenticationErrorModel -) +from app.types import Token +from app.auth.utils import authenticate_user, generate_user_token +from app.auth.enums import LoginStatusEnum +from app.auth.types import AuthenticationErrorModel + +router = APIRouter(prefix="/basic") @router.post( "/token", diff --git a/app/auth/routers/email.py b/app/auth/routers/email.py index 9ae9d26..b4cd56c 100644 --- a/app/auth/routers/email.py +++ b/app/auth/routers/email.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- -from fastapi import HTTPException +from fastapi import APIRouter from fastapi.responses import JSONResponse from loguru import logger from app import config +from app.types import Token from app.auth.enums import LoginStatusEnum -from app.types.pydantic_models import Token from app.auth.types import AuthenticationErrorModel -from app.auth.routers import router from app.auth.types import LoginForm, LoginFormWith2FA from app.auth.utils import ( authenticate_user, @@ -21,25 +20,31 @@ ) +router = APIRouter(prefix="/email") + + @router.post( - "/2fa/email/generate-code/", + "/generate-code/", response_model=dict, responses={ 401: {"model": AuthenticationErrorModel} } ) -async def generate_2fa_code( +async def gen_2fa_code( login: LoginForm ): result = await authenticate_user(login.username, login.password) - if result['status'] != LoginStatusEnum.SUCCESS: + if result['status'] not in [ + LoginStatusEnum.SUCCESS, + LoginStatusEnum.REQUIRE_2FA + ]: raise JSONResponse( status_code=401, - content=AuthenticationErrorModel( - message="Something went wrong", - type=result['status'] - ), + content={ + "message": "Something went wrong", + "type": result['status'] + }, ) code = generate_2fa_code() @@ -52,12 +57,12 @@ async def generate_2fa_code( success = False if not success: - raise JSONResponse( + return JSONResponse( status_code=401, - content=AuthenticationErrorModel( - message="Error during the email queueing process. Try Again.", - type=LoginStatusEnum.EMAIL_QUEUE_ERROR - ), + content={ + "message": "Error during the email queueing process. Try Again.", + "type": LoginStatusEnum.EMAIL_QUEUE_ERROR + }, ) return { @@ -66,7 +71,7 @@ async def generate_2fa_code( @router.post( - "/2fa/email/login/", + "/login/", response_model=Token, responses={ 401: {"model": AuthenticationErrorModel} diff --git a/app/auth/routers/totp.py b/app/auth/routers/totp.py index e32a52e..a1accc8 100644 --- a/app/auth/routers/totp.py +++ b/app/auth/routers/totp.py @@ -2,24 +2,23 @@ import io from fastapi import HTTPException, status +from fastapi import APIRouter from fastapi.responses import StreamingResponse,JSONResponse from loguru import logger from app import config -from app.types.frontend import LoginFormWith2FA, LoginForm -from app.types.pydantic_models import Token -from app.security import TwoFactorAuth -from app.enums import LoginStatusEnum -from app.types.errors import ( - AuthenticationErrorModel -) -from app.auth.routers import router +from app.types import Token +from app.auth.types import LoginForm, LoginFormWith2FA +from app.auth.enums import LoginStatusEnum +from app.auth.types import AuthenticationErrorModel from app.auth.utils import authenticate_user, generate_user_token from app.auth.utils.totp import validate_code +router = APIRouter(prefix="/totp") + @router.post( - "/2fa/totp/is-2fa-active/", + "/is-2fa-active/", response_model=bool, responses={ 401: {"model": AuthenticationErrorModel} @@ -48,7 +47,7 @@ async def is_2fa_active( @router.post( - "/2fa/totp/login/", + "/login/", response_model=Token, responses={ 401: {"model": AuthenticationErrorModel} @@ -86,7 +85,7 @@ async def login_with_2fa( @router.post( - "/2fa/totp/generate-qrcode/", + "/generate-qrcode/", response_model=bytes, responses={ 400: {"model": str}, diff --git a/app/auth/utils/__init__.py b/app/auth/utils/__init__.py index 7039b2f..1936143 100644 --- a/app/auth/utils/__init__.py +++ b/app/auth/utils/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from passlib.context import CryptContext from datetime import datetime, timedelta -from typing import Optional, Tuple +from typing import Optional, Tuple, Callable import jwt from app import config @@ -33,7 +33,7 @@ def generate_user_token(user: User) -> str: async def authenticate_user( username: str, password: str, - verificator, # Async Callable[User, str] + verificator: Callable[[User, str], bool] = None, code: Optional[str] = None, ) -> Tuple[User, bool, str]: user = await User.get_or_none(username=username).prefetch_related("role") @@ -62,25 +62,23 @@ async def authenticate_user( } # 2FA TOTP SENT - if user.is_2fa_required and not code: + if (not code) or (not verificator): return { "user": user, "status": LoginStatusEnum.REQUIRE_2FA, } - # 2FA TOTP VALIDATION - if user.is_2fa_required and code: - is_2fa_valid = await verificator(user, code) - if not is_2fa_valid: - return { - "user": user, - "status": LoginStatusEnum.BAD_OTP, - } - - return { - "user": user, - "status": LoginStatusEnum.SUCCESS, - } + is_2fa_valid = await verificator(user, code) + if not is_2fa_valid: + return { + "user": user, + "status": LoginStatusEnum.BAD_OTP, + } + else: + return { + "user": user, + "status": LoginStatusEnum.SUCCESS, + } def create_access_token(data: dict, expires_delta: timedelta | None = None): diff --git a/app/auth/utils/email.py b/app/auth/utils/email.py index 718020c..5a1449b 100644 --- a/app/auth/utils/email.py +++ b/app/auth/utils/email.py @@ -31,7 +31,7 @@ def store_code(user: User, code: str, ttl: int = 300): async def validate_code(user: User, code: str): stored_code = redis_client.get(f"2fa:{user.id}") - if store_code is None: + if stored_code is None: return False if stored_code.decode() == code: @@ -48,11 +48,12 @@ def generate_2fa_code(): async def send_2fa_email_to_user(user: User, code: str): logger.info(f"Sending 2FA code {code} to {user.email}") response = requests.post( - url=DATARELAY_URL, + url=f"{DATARELAY_URL}data/mailman", headers={ - 'x-api-key': DATARELAY_MAILMAN_TOKEN + 'x-api-key': DATARELAY_MAILMAN_TOKEN, + 'Content-Type': 'application/json' }, - body={ + json={ "to_addresses": [user.email], "cc_addresses": [], "bcc_addresses": [], diff --git a/app/config/base.py b/app/config/base.py index 6cd88af..9696a25 100644 --- a/app/config/base.py +++ b/app/config/base.py @@ -18,6 +18,7 @@ "BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID", action="raise" ) BIGQUERY_ERGON_TABLE_ID = getenv_or_action("BIGQUERY_ERGON_TABLE_ID", action="raise") +BIGQUERY_PATIENT_SEARCH_TABLE_ID = getenv_or_action("BIGQUERY_PATIENT_SEARCH_TABLE_ID", action="raise") # JWT configuration JWT_SECRET_KEY = getenv_or_action("JWT_SECRET_KEY", default=token_bytes(32).hex()) diff --git a/app/main.py b/app/main.py index 1937590..0bb4018 100644 --- a/app/main.py +++ b/app/main.py @@ -7,10 +7,10 @@ from loguru import logger from app import config +from app.utils import prepare_gcp_credential from app.lifespan import api_lifespan from app.routers import entities_raw, frontend, misc -from app.auth import routers as auth -from app.utils import prepare_gcp_credential +from app.auth.routers import router as auth_routers logger.remove() logger.add(sys.stdout, level=config.LOG_LEVEL) @@ -47,7 +47,7 @@ allow_credentials=config.ALLOW_CREDENTIALS, ) +app.include_router(auth_routers) app.include_router(entities_raw.router) -app.include_router(auth.router) app.include_router(frontend.router) app.include_router(misc.router) diff --git a/app/routers/frontend.py b/app/routers/frontend.py index bc2ae07..498ae5e 100644 --- a/app/routers/frontend.py +++ b/app/routers/frontend.py @@ -20,6 +20,7 @@ from app.config import ( BIGQUERY_PROJECT, BIGQUERY_PATIENT_HEADER_TABLE_ID, + BIGQUERY_PATIENT_SEARCH_TABLE_ID, BIGQUERY_PATIENT_SUMMARY_TABLE_ID, BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID, REQUEST_LIMIT_MAX, @@ -88,6 +89,56 @@ async def accept_use_terms( ) +@router_request( + method="GET", + router=router, + path="/patient/search", + response_model=List[dict], + responses={ + 404: {"model": AccessErrorModel}, + 403: {"model": AccessErrorModel} + }, + dependencies=[Depends(RateLimiter(times=REQUEST_LIMIT_MAX, seconds=REQUEST_LIMIT_WINDOW_SIZE))] +) +async def search_patient( + request: Request, + user: Annotated[User, Depends(assert_user_is_active)], + cpf: str = None, + cns: str = None, + name: str = None, +) -> List[dict]: + if sum([bool(cpf), bool(cns), bool(name)]) != 1: + return JSONResponse( + status_code=400, + content={"message": "Only one of the parameters is allowed"}, + ) + + validation_job = validate_user_access_to_patient_data(user, cpf) + + clause = "" + if cns: + clause = f"cns = {cns}" + elif cpf: + clause = f"cpf_particao = {cpf}" + elif name: + clause = f"nome_completo = '{name}'" + + results_job = read_bq( + f""" + SELECT nome_completo, cns, cpf + FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_SEARCH_TABLE_ID} + WHERE {clause} + """, + from_file="/tmp/credentials.json", + ) + validation, results = await asyncio.gather(validation_job, results_job) + + has_access, response = validation + if not has_access: + return response + + return results.to_dict(orient="records") + @router_request( method="GET", router=router, diff --git a/app/security.py b/app/security.py index 905eea3..cae9722 100644 --- a/app/security.py +++ b/app/security.py @@ -8,7 +8,7 @@ from pyotp import TOTP from app.models import User -from app.types.pydantic_models import User2FA as UserPydantic +from app.auth.types.totp import User2FA as UserPydantic class TwoFactorAuth: