-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2b098c8
commit cc09194
Showing
16 changed files
with
272 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,58 @@ | ||
from fastapi import APIRouter | ||
from fastapi import APIRouter, Depends, status | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
from sqlalchemy.orm import selectinload | ||
|
||
from src import errors | ||
from src._types import BaseListResponse, BaseResponse | ||
from src.auth import schemas | ||
from src.auth.models import Group, Role, User | ||
from src.auth.services import user_dto | ||
from src.cbv import cbv | ||
from src.deps import auth, get_session | ||
from src.exceptions import GenerError | ||
|
||
router = APIRouter() | ||
|
||
|
||
@cbv(router) | ||
class UserCBV: | ||
user: User = Depends(auth) | ||
session: AsyncSession = Depends(get_session) | ||
|
||
@router.post("/users", operation_id="e0fe80d5-cbe0-4c2c-9eff-57e80ecba522") | ||
async def create_user(self, user: schemas.UserCreate) -> BaseResponse[int]: | ||
new_user = await user_dto.create(self.session, user) | ||
return BaseResponse(data=new_user.id) | ||
|
||
@router.get("/users/{id}", operation_id="8057d614-150f-42ee-984c-d0af35796da3") | ||
async def get_user(self, id: int) -> BaseResponse[schemas.UserDetail]: | ||
db_user = await user_dto.get_one_or_404( | ||
self.session, | ||
id, | ||
options=( | ||
selectinload(User.role).load_only(Role.id, Role.name), | ||
selectinload(User.group).load_only(Group.id, Group.name), | ||
), | ||
) | ||
return BaseResponse(data=db_user) | ||
|
||
@router.get("/users", operation_id="c5f793b1-7adf-4b4e-a498-732b0fa7d758") | ||
async def get_users(self, query: schemas.UserQuery) -> BaseListResponse[list[schemas.UserDetail]]: | ||
count, results = await user_dto.list_and_count( | ||
self.session, | ||
query, | ||
options=( | ||
selectinload(User.role).load_only(Role.id, Role.name), | ||
selectinload(User.group).load_only(Group.id, Group.name), | ||
), | ||
) | ||
return BaseListResponse(count=count, results=results) | ||
|
||
@router.put("/users/{id}", operation_id="2fda2e00-ad86-4296-a1d4-c7f02366b52e") | ||
async def update_user(self, id: int, user: schemas.UserUpdate) -> BaseResponse[int]: | ||
update_user = user.model_dump(exclude_unset=True) | ||
if "password" in update_user and update_user["password"] is None: | ||
raise GenerError(errors.ERR_10006, status_code=status.HTTP_406_NOT_ACCEPTABLE) | ||
db_user = await user_dto.get_one_or_404(self.session, id) | ||
await user_dto.update(self.session, db_user, user) | ||
return BaseResponse(data=id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,7 @@ class GroupBrief(BaseModel): | |
|
||
class RoleBase(BaseModel): | ||
name: str | ||
slug: str | ||
description: str | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
from collections.abc import AsyncGenerator | ||
from datetime import UTC, datetime | ||
|
||
import jwt | ||
from fastapi import Request | ||
from fastapi.security import Depends, HTTPBearer | ||
from sqlalchemy import select | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
from sqlalchemy.orm import selectinload | ||
|
||
from src import exceptions | ||
from src.auth.models import RolePermission, User | ||
from src.config import settings | ||
from src.context import locale_ctx | ||
from src.db.session import async_session | ||
from src.enums import ReservedRoleSlug | ||
from src.security import API_WHITE_LISTS, JWT_ALGORITHM, JwtTokenPayload | ||
from src.utils.cache import CacheNamespace, redis_client | ||
|
||
token = HTTPBearer() | ||
|
||
|
||
async def get_session() -> AsyncGenerator[AsyncSession, None]: | ||
async with async_session() as session: | ||
yield session | ||
|
||
|
||
async def auth(request: Request, session: AsyncSession = Depends(get_session), token: str = Depends(token)) -> User: # noqa: B008 | ||
try: | ||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[JWT_ALGORITHM]) | ||
except jwt.DecodeError as e: | ||
raise exceptions.TokenInvalidError from e | ||
token_data = JwtTokenPayload(**payload) | ||
if token_data.refresh: | ||
raise exceptions.TokenInvalidError | ||
now = datetime.now(tz=UTC) | ||
if now < token_data.issued_at or now > token_data.expires_at: | ||
raise exceptions.TokenExpireError | ||
user = await session.get(User, token_data.sub, options=[selectinload(User.role)]) | ||
if not user: | ||
raise exceptions.NotFoundError(user.__visible_name__[locale_ctx.get()], "id", id) | ||
operation_id = request.scope.get("operation_id") | ||
privileged = check_privileged_role(user.role.slug, operation_id) | ||
if privileged: | ||
return User | ||
check_privileged_role(user.role_id, session, operation_id) | ||
return User | ||
|
||
|
||
def check_privileged_role(slug: str, operation_id: str) -> bool: | ||
if slug == ReservedRoleSlug.ADMIN: | ||
return True | ||
if operation_id in API_WHITE_LISTS: | ||
return True | ||
return False | ||
|
||
|
||
async def check_role_permissions(role_id: int, session: AsyncSession, operation_id: str) -> None: | ||
permissions: list[str] | None = await redis_client.get_cache(name=str(role_id), namespace=CacheNamespace.ROLE_CACHE) | ||
if not permissions: | ||
permissions = ( | ||
await session.scalars(select(RolePermission.permission_id).where(RolePermission.role_id == role_id)) | ||
).all() | ||
if not permissions: | ||
raise exceptions.PermissionDenyError | ||
permissions = [str(p) for p in permissions] | ||
redis_client.set_nx(name=str(role_id), value=permissions, namespace=CacheNamespace.ROLE_CACHE) | ||
if operation_id not in permissions: | ||
raise exceptions.PermissionDenyError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,12 @@ | ||
from enum import IntEnum | ||
|
||
from src._types import AppStrEnum | ||
|
||
|
||
class Env(IntEnum): | ||
PRD = 0 | ||
DEV = 1 | ||
|
||
|
||
class ReservedRoleSlug(AppStrEnum): | ||
ADMIN = "admin" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,22 @@ | ||
from typing import NamedTuple | ||
from typing import Any, NamedTuple | ||
|
||
|
||
class ErrorCode(NamedTuple): | ||
code: int | ||
error: int | ||
message: str | ||
details: list[Any] | None = None | ||
|
||
def dict(self): # noqa: ANN201 | ||
return self._asdict() | ||
|
||
|
||
ERR_404 = ErrorCode(404, "app.not_found") | ||
ERR_409 = ErrorCode(409, "app.already_exist") | ||
ERR_500 = ErrorCode(500, "app.internal_server_error") | ||
ERR_10001 = ErrorCode(10001, "User's password can not set as null.") | ||
ERR_10002 = ErrorCode(10002, "Invalid bearer token.") | ||
ERR_10003 = ErrorCode(10003, "Bearer token was expired.") | ||
ERR_10004 = ErrorCode(10004, "Bearer token is invalid for refresh token was provided.") | ||
ERR_10005 = ErrorCode(10005, "Permission deny, user with limited access for current API.") | ||
ERR_10005 = ErrorCode(10005, "Permission deny, user with limited access for current API.") | ||
ERR_10006 = ErrorCode(10006, "Update user failed, password can not be null.") |
Oops, something went wrong.