Skip to content

Commit

Permalink
Merge pull request #41 from flavien-hugs/develop
Browse files Browse the repository at this point in the history
feat: add internal api
  • Loading branch information
flavien-hugs authored Oct 5, 2024
2 parents f52fb93 + 2b8c72c commit d19c734
Show file tree
Hide file tree
Showing 24 changed files with 318 additions and 209 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,3 @@ notebook.ipynb
dotenv/
.ruff_cache
.tokens.txt
fixtures
data
2 changes: 1 addition & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[settings]
known_third_party = beanie,email_validator,fastapi,fastapi_cache,fastapi_jwt,fastapi_pagination,httpx,jinja2,jose,mongomock_motor,pwdlib,pydantic,pydantic_settings,pymongo,pyotp,pytest,pytest_asyncio,redis,slugify,starlette,typer,uvicorn,yaml
known_third_party = beanie,email_validator,fastapi,fastapi_cache,fastapi_jwt,fastapi_pagination,httpx,jinja2,jose,mongomock_motor,pwdlib,pydantic,pydantic_settings,pymongo,pyotp,pytest,pytest_asyncio,slugify,starlette,typer,uvicorn,yaml
35 changes: 35 additions & 0 deletions data/params.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
params:
- name: "Spécialités"
data:
- "Allergologie ou Immunologie"
- "Anesthésiologie"
- "Cardiologie"
- "Chirurgie"
- "Dermatologie"
- "Endocrinologie"
- "Gastro-entérologie"
- "Gériatrie"
- "Gynécologie"
- "Hématologie"
- "Hépatologie"
- "Infectiologie"
- "Néonatologie"
- "Odontologie"
- "Oncologie"
- "Obstétrique"
- "Ophtalmologie"
- "Orthopédie"
- "Oto-rhino-laryngologie"
- "Pédiatrie"
- "Pneumologie"
- "Psychiatrie"
- "Radiologie"
- "Radiothérapie"
- "Rhumatologie"
- "Urologie"
- "Andrologie"

- name: "Sexe"
data:
- "Masculin"
- "Féminin"
11 changes: 4 additions & 7 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,20 @@
from fastapi import FastAPI, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import RedirectResponse
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_pagination import add_pagination
from httpx import AsyncClient
from redis import asyncio as aioredis
from slugify import slugify
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse

from src.common.helpers.appdesc import load_app_description, load_permissions
from src.common.helpers.caching import init_redis_cache
from src.common.helpers.error_codes import AppErrorCode
from src.common.helpers.exceptions import setup_exception_handlers
from src.config import settings, shutdown_db, startup_db
from src.models import Role, User, Params
from src.routers import auth_router, perm_router, role_router, user_router, param_router
from src.models import Params, Role, User
from src.routers import auth_router, param_router, perm_router, role_router, user_router
from src.services import roles, users
from src.shared import blacklist_token

Expand All @@ -44,8 +42,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[State]:

blacklist_token.init_blacklist_token_file()

redis = aioredis.from_url(settings.CACHE_DB_URL)
FastAPICache.init(RedisBackend(redis), prefix=f"__{settings.APP_NAME.lower()}")
await init_redis_cache(app_name=BASE_URL, cache_db_url=settings.CACHE_DB_URL)

yield
await shutdown_db(app=app)
Expand Down
2 changes: 1 addition & 1 deletion src/common
13 changes: 7 additions & 6 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ class AuthBaseConfig(BaseSettings):
APP_ACCESS_LOG: Optional[bool] = Field(default=True, alias="APP_ACCESS_LOG")
APP_DEFAULT_PORT: Optional[PositiveInt] = Field(default=9077, alias="APP_DEFAULT_PORT")
PASSWORD_MIN_LENGTH: Optional[PositiveInt] = Field(default=3, alias="PASSWORD_MIN_LENGTH")
ENABLE_OTP_CODE: Optional[bool] = Field(default=True, alias="ENABLE_OTP_CODE")
ENABLE_OTP_CODE: Optional[bool] = Field(..., alias="ENABLE_OTP_CODE")
OTP_CODE_DIGIT_LENGTH: Optional[PositiveInt] = Field(default=4, alias="OTP_CODE_DIGIT_LENGTH")
REGISTER_WITH_EMAIL: Optional[bool] = Field(default=False, alias="REGISTER_WITH_EMAIL")
LIST_ROLES_ENDPOINT_SECURITY_ENABLED: Optional[bool] = Field(
default=False, alias="LIST_ROLES_ENDPOINT_SECURITY_ENABLED"
)
REGISTER_WITH_EMAIL: Optional[bool] = Field(..., alias="REGISTER_WITH_EMAIL")
LIST_ROLES_ENDPOINT_SECURITY_ENABLED: Optional[bool] = Field(..., alias="LIST_ROLES_ENDPOINT_SECURITY_ENABLED")
REGISTER_USER_ENDPOINT_SECURITY_ENABLED: Optional[bool] = Field(
default=False, alias="REGISTER_USER_ENDPOINT_SECURITY_ENABLED"
..., alias="REGISTER_USER_ENDPOINT_SECURITY_ENABLED"
)
LIST_PARAMETERS_ENDPOINT_SECURITY_ENABLED: Optional[bool] = Field(
..., alias="LIST_PARAMETERS_ENDPOINT_SECURITY_ENABLED"
)
LIST_PARAMETERS_ENDPOINT_SECURITY_ENABLED: Optional[bool] = Field(
default=False, alias="LIST_PARAMETERS_ENDPOINT_SECURITY_ENABLED"
Expand Down
4 changes: 2 additions & 2 deletions src/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
from pwdlib.hashers.bcrypt import BcryptHasher
from slugify import slugify

from src.common.helpers.caching import custom_key_builder as cache_key_builder
from src.common.helpers.exceptions import CustomHTTException
from src.config import jwt_settings, settings
from src.services.roles import get_one_role
from src.shared import blacklist_token
from src.shared.error_codes import AuthErrorCode
from src.shared.utils import custom_key_builder

logging.basicConfig(format="%(message)s", level=logging.INFO)

Expand Down Expand Up @@ -82,7 +82,6 @@ def decode_access_token(cls, token: str) -> dict:
return result

@classmethod
@cache(expire=settings.EXPIRE_CACHE, key_builder=custom_key_builder) # noqa
async def verify_access_token(cls, token: str) -> bool:
"""
Verifies the validity of an access token by checking the cache and token properties.
Expand Down Expand Up @@ -119,6 +118,7 @@ async def verify_access_token(cls, token: str) -> bool:
) from err

@classmethod
@cache(expire=settings.EXPIRE_CACHE, key_builder=cache_key_builder(settings.APP_NAME + "check-permissions")) # noqa
async def check_permissions(cls, token: str, required_permissions: Set[str] = ()) -> bool:
"""
Checks if the token has the required permissions.
Expand Down
22 changes: 22 additions & 0 deletions src/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
LoginUser,
PhonenumberModel,
RequestChangePassword,
SendEmailMessage,
SendSmsMessage,
UserBaseSchema,
VerifyOTP,
)
from src.services import auth
from src.shared import mail_service, sms_service
from src.shared.utils import custom_key_builder

auth_router = APIRouter(prefix="", tags=["AUTH"], redirect_slashes=False)
Expand Down Expand Up @@ -121,3 +124,22 @@ async def check_user_attributes(
key: str = Query(...), value: str = Query(...), in_attributes: Optional[bool] = Query(default=False)
):
return await auth.check_user_attribute(key=key, value=value, in_attributes=in_attributes)


auth_router.tags = ["SEND MESSAGE"]
auth_router.prefix = "/send"


@auth_router.post("-sms", summary="Send a message", status_code=status.HTTP_200_OK, include_in_schema=False)
async def send_sms(background: BackgroundTasks, payload: SendSmsMessage = Body(...)):
phone = payload.phone_number.replace("+", "")
await sms_service.send_sms(background, recipient=phone, message=payload.message)
return {"message": "SMS sent successfully."}


@auth_router.post("-email", summary="Send a e-mail", status_code=status.HTTP_200_OK, include_in_schema=False)
async def send_email(background: BackgroundTasks, payload: SendEmailMessage = Body(...)):
mail_service.send_email_background(
background, receiver_email=payload.recipients, subject=payload.subject, body=payload.message
)
return {"message": "E-mail sent successfully."}
25 changes: 21 additions & 4 deletions src/routers/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from src.config import enable_endpoint, settings
from src.middleware import AuthorizedHTTPBearer, CheckPermissionsHandler
from src.common.helpers.caching import delete_custom_key
from src.models import Role
from src.schemas import RoleModel
from src.services import roles
Expand All @@ -15,6 +16,13 @@
role_router = APIRouter(prefix="/roles", tags=["ROLES"], redirect_slashes=False)


@role_router.post(
"/_create",
response_model=Role,
summary="Create role (internal)",
status_code=status.HTTP_201_CREATED,
include_in_schema=False,
)
@role_router.post(
"",
dependencies=(
Expand Down Expand Up @@ -100,7 +108,7 @@ async def delete_role(id: PydanticObjectId):
if bool(enable_endpoint.SHOW_MEMBERS_IN_ROLE_ENDPOINT):

@role_router.get(
"/{id}/members",
"/{name}/members",
dependencies=[
Depends(AuthorizedHTTPBearer),
Depends(CheckPermissionsHandler(required_permissions={"auth:can-display-role"})),
Expand All @@ -110,12 +118,19 @@ async def delete_role(id: PydanticObjectId):
status_code=status.HTTP_200_OK,
)
async def get_role_members(
id: PydanticObjectId,
name: str,
sorting: Optional[SortEnum] = Query(SortEnum.DESC, description="Order by creation date: 'asc' or 'desc"),
):
return await roles.get_roles_members(role_id=PydanticObjectId(id), sorting=sorting)
return await roles.get_users_for_role(name=name, sorting=sorting)


@role_router.patch(
"/{id}/_assign-permissions",
response_model=Role,
summary="Assign permissions to role (internal)",
status_code=status.HTTP_202_ACCEPTED,
include_in_schema=False,
)
@role_router.patch(
"/{id}/assign-permissions",
dependencies=[
Expand All @@ -127,4 +142,6 @@ async def get_role_members(
status_code=status.HTTP_200_OK,
)
async def manage_permission_to_role(id: PydanticObjectId, payload: Set[str] = Body(...)):
return await roles.assign_permissions_to_role(role_id=PydanticObjectId(id), permission_codes=payload)
result = await roles.assign_permissions_to_role(role_id=PydanticObjectId(id), permission_codes=payload)
await delete_custom_key(settings.APP_NAME + "check-permissions")
return result
8 changes: 8 additions & 0 deletions src/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ async def listing_users(
summary="Get single user",
status_code=status.HTTP_200_OK,
)
@user_router.get(
"/_i_{id}",
response_model=UserOut,
response_model_exclude={"password", "is_primary", "attributes.otp_secret", "attributes.otp_created_at"},
summary="Get single user (internal)",
status_code=status.HTTP_200_OK,
include_in_schema=False,
)
async def get_user(id: PydanticObjectId):
return await users.get_one_user(user_id=PydanticObjectId(id))

Expand Down
4 changes: 3 additions & 1 deletion src/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .response import ResponseModelData
from .roles import RoleModel
from .params import ParamsModel
from .mixins import FilterParams
from .mixins import FilterParams, SendEmailMessage, SendSmsMessage
from .users import CreateUser, PhonenumberModel, UpdateUser, UserBaseSchema

__all__ = [
Expand All @@ -20,4 +20,6 @@
"RoleModel",
"ParamsModel",
"FilterParams",
"SendEmailMessage",
"SendSmsMessage",
]
13 changes: 12 additions & 1 deletion src/schemas/mixins.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional

from fastapi import Query
from pydantic import BaseModel
from pydantic import BaseModel, EmailStr


class FilterParams(BaseModel):
Expand All @@ -11,3 +11,14 @@ class FilterParams(BaseModel):

def get_filter_params(type: Optional[str] = Query(None), name: Optional[str] = Query(None)) -> FilterParams:
return FilterParams(type=type, name=name)


class SendSmsMessage(BaseModel):
message: str
phone_number: str


class SendEmailMessage(BaseModel):
subject: Optional[str] = None
message: str
recipients: EmailStr
24 changes: 4 additions & 20 deletions src/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,10 @@ async def login(payload: LoginUser) -> JSONResponse:


async def logout(request: Request) -> JSONResponse:

authorization = request.headers.get("Authorization").split()[1]
await blacklist_token.add_blacklist_token(authorization)

decode_token = CustomAccessBearer.decode_access_token(authorization)

data = decode_token["subject"]
user_data = {
"id": data["id"],
"email": data["email"],
"fullname": data["fullname"],
"role": data["role"],
"is_active": data["is_active"],
}
response_data = {
"access_token": CustomAccessBearer.access_token(data=user_data, user_id=decode_token["jti"]),
"referesh_token": CustomAccessBearer.refresh_token(data=user_data, user_id=decode_token["jti"]),
}

return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(response_data))
authorization = request.headers.get("Authorization")
token = authorization.split()[1]
await blacklist_token.add_blacklist_token(token)
return JSONResponse(content={"message": "Logout successfully !"}, status_code=status.HTTP_200_OK)


async def change_password(user_id: PydanticObjectId, change_password: ChangePassword):
Expand Down
11 changes: 8 additions & 3 deletions src/services/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,17 @@ async def update_role(role_id: PydanticObjectId, update_role: RoleModel) -> Role
return result


async def get_roles_members(role_id: PydanticObjectId, sorting: Optional[SortEnum] = SortEnum.DESC):
role = await get_one_role(role_id=role_id)
async def get_users_for_role(name: str, sorting: Optional[SortEnum] = SortEnum.DESC):
if (role := await Role.find_one({"slug": slugify(name)})) is None:
raise CustomHTTException(
code_error=RoleErrorCode.ROLE_NOT_FOUND,
message_error=f"Role with '{name}' not found.",
status_code=status.HTTP_400_BAD_REQUEST,
)
sorted = DESCENDING if sorting == SortEnum.DESC else ASCENDING
users = await User.find({"role": PydanticObjectId(role.id)}, sort=[("created_at", sorted)]).to_list()
users_list = [{**user.model_dump(by_alias=True, exclude={"password", "is_primary"})} for user in users]
result = paginate(users_list, additional_data={"role_info": role})
result = paginate(users_list)
return result


Expand Down
Loading

0 comments on commit d19c734

Please sign in to comment.