From 2382f0becef4ecdea0d4ad89f4b127ed60030777 Mon Sep 17 00:00:00 2001 From: icecheng Date: Fri, 18 Jul 2025 18:05:57 +0800 Subject: [PATCH] feat(role_management): add crud for role and permission --- apps/authentication/__init__.py | 0 apps/authentication/backend/__init__.py | 0 .../backend/annotation/__init__.py | 0 .../infra/permission/permission_handler.py | 76 +++++++++++++++++++ .../backend/infra/permission/role_handler.py | 73 ++++++++++++++++++ .../authentication/backend/models/__init__.py | 2 + .../backend/models/permission/__init__.py | 3 + .../backend/models/permission/constants.py | 29 ++++++- .../backend/models/permission/models.py | 37 +++++++++ .../backend/models/user/models.py | 3 +- .../services/permission/permission_service.py | 32 ++++++++ .../services/permission/role_service.py | 36 +++++++++ apps/authentication/common/__init__.py | 0 apps/authentication/local.env | 24 ++++++ .../webapi/bootstrap/application.py | 2 + .../webapi/providers/exception_handler.py | 8 ++ .../webapi/providers/permission_initialize.py | 37 +++++++++ apps/authentication/webapi/routes/__init__.py | 4 + .../webapi/routes/permission/__init__.py | 11 +++ .../routes/permission/create_permission.py | 36 +++++++++ .../routes/permission/query_permission.py | 50 ++++++++++++ .../routes/permission/update_permission.py | 38 ++++++++++ .../webapi/routes/role/__init__.py | 10 +++ .../webapi/routes/role/create_role.py | 40 ++++++++++ .../webapi/routes/role/query_role.py | 52 +++++++++++++ .../webapi/routes/role/update_role.py | 41 ++++++++++ .../webapi/routes/user/__init__.py | 0 .../webapi/routes/user/assign_role.py | 0 .../webapi/routes/user/remove_role.py | 0 apps/notification/.env.local | 1 + 30 files changed, 643 insertions(+), 2 deletions(-) create mode 100644 apps/authentication/__init__.py create mode 100644 apps/authentication/backend/__init__.py create mode 100644 apps/authentication/backend/annotation/__init__.py create mode 100644 apps/authentication/backend/infra/permission/permission_handler.py create mode 100644 apps/authentication/backend/infra/permission/role_handler.py create mode 100644 apps/authentication/backend/models/permission/__init__.py create mode 100644 apps/authentication/backend/models/permission/models.py create mode 100644 apps/authentication/backend/services/permission/permission_service.py create mode 100644 apps/authentication/backend/services/permission/role_service.py create mode 100644 apps/authentication/common/__init__.py create mode 100644 apps/authentication/local.env create mode 100644 apps/authentication/webapi/providers/permission_initialize.py create mode 100644 apps/authentication/webapi/routes/permission/__init__.py create mode 100644 apps/authentication/webapi/routes/permission/create_permission.py create mode 100644 apps/authentication/webapi/routes/permission/query_permission.py create mode 100644 apps/authentication/webapi/routes/permission/update_permission.py create mode 100644 apps/authentication/webapi/routes/role/__init__.py create mode 100644 apps/authentication/webapi/routes/role/create_role.py create mode 100644 apps/authentication/webapi/routes/role/query_role.py create mode 100644 apps/authentication/webapi/routes/role/update_role.py create mode 100644 apps/authentication/webapi/routes/user/__init__.py create mode 100644 apps/authentication/webapi/routes/user/assign_role.py create mode 100644 apps/authentication/webapi/routes/user/remove_role.py create mode 100644 apps/notification/.env.local diff --git a/apps/authentication/__init__.py b/apps/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/backend/__init__.py b/apps/authentication/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/backend/annotation/__init__.py b/apps/authentication/backend/annotation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/backend/infra/permission/permission_handler.py b/apps/authentication/backend/infra/permission/permission_handler.py new file mode 100644 index 0000000..ea05694 --- /dev/null +++ b/apps/authentication/backend/infra/permission/permission_handler.py @@ -0,0 +1,76 @@ +from typing import Optional, List, Tuple + +from fastapi.exceptions import RequestValidationError + +from backend.models.permission.models import PermissionDoc +from beanie import PydanticObjectId +from datetime import datetime + +class PermissionHandler: + def __init__(self): + pass + + async def create_permission(self, permission_key: str, permission_name: str, + description: Optional[str] = None) -> Optional[PermissionDoc]: + """Create a new permission document""" + if not permission_key or not permission_name: + raise RequestValidationError("permission_key and permission_name are required.") + # if exists. + if await PermissionDoc.find_one( + {str(PermissionDoc.permission_key): permission_key}) or await PermissionDoc.find_one( + {str(PermissionDoc.permission_name): permission_name}): + raise RequestValidationError("permission has already been created.") + doc = PermissionDoc( + permission_key=permission_key, + permission_name=permission_name, + description=description, + created_at=datetime.now(), + updated_at=datetime.now() + ) + await doc.insert() + return doc + + async def update_permission(self, permission_id: PydanticObjectId, permission_key: Optional[str] = None, + permission_name: Optional[str] = None, description: Optional[str] = None) -> Optional[ + PermissionDoc]: + """Update an existing permission document by id, ensuring permission_key is unique""" + if not permission_id or not permission_key or not permission_name: + raise RequestValidationError("permission_id, permission_key and permission_name is required.") + doc = await PermissionDoc.get(permission_id) + if not doc: + raise RequestValidationError("Permission not found.") + # Check for uniqueness (exclude self) + if permission_key and permission_name: + conflict = await PermissionDoc.find_one({ + "$and": [ + {"_id": {"$ne": permission_id}}, + {"$or": [ + {str(PermissionDoc.permission_key): permission_key}, + {str(PermissionDoc.permission_name): permission_name} + ]} + ] + }) + if conflict: + raise RequestValidationError("Permission name or permission key already exists.") + doc.permission_key = permission_key + doc.updated_at = datetime.now() + await doc.save() + return doc + + async def query_permissions( + self, + permission_key: Optional[str] = None, + permission_name: Optional[str] = None, + skip: int = 0, + limit: int = 10 + ) -> Tuple[List[PermissionDoc], int]: + """Query permissions with pagination and fuzzy search""" + query = {} + if permission_key: + query[str(PermissionDoc.permission_key)] = {"$regex": permission_key, "$options": "i"} + if permission_name: + query[str(PermissionDoc.permission_name)] = {"$regex": permission_name, "$options": "i"} + cursor = PermissionDoc.find(query) + total = await cursor.count() + docs = await cursor.skip(skip).limit(limit).to_list() + return docs, total diff --git a/apps/authentication/backend/infra/permission/role_handler.py b/apps/authentication/backend/infra/permission/role_handler.py new file mode 100644 index 0000000..b18df20 --- /dev/null +++ b/apps/authentication/backend/infra/permission/role_handler.py @@ -0,0 +1,73 @@ +from typing import Optional, List, Tuple + +from fastapi.exceptions import RequestValidationError + +from backend.models.permission.models import RoleDoc +from beanie import PydanticObjectId +from datetime import datetime + + +class RoleHandler: + def __init__(self): + pass + + async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> Optional[RoleDoc]: + """Create a new role, ensuring role_key and role_name are unique and not empty""" + if not role_key or not role_name: + raise RequestValidationError("role_key and role_name are required.") + if await RoleDoc.find_one({str(RoleDoc.role_key): role_key}) or await RoleDoc.find_one( + {str(RoleDoc.role_name): role_name}): + raise RequestValidationError("role_key or role_name has already been created.") + doc = RoleDoc( + role_key=role_key, + role_name=role_name, + role_description=role_description, + permission_ids=[], + role_level=role_level, + created_at=datetime.now(), + updated_at=datetime.now() + ) + await doc.insert() + return doc + + async def update_role(self, role_id: PydanticObjectId, role_key: str, role_name: str, + role_description: Optional[str], role_level: int) -> Optional[ + RoleDoc]: + """Update an existing role, ensuring role_key and role_name are unique and not empty""" + if not role_id or not role_key or not role_name: + raise RequestValidationError("role_id, role_key and role_name are required.") + doc = await RoleDoc.get(role_id) + if not doc: + raise RequestValidationError("role not found.") + # Check for uniqueness (exclude self) + conflict = await RoleDoc.find_one({ + "$and": [ + {"_id": {"$ne": role_id}}, + {"$or": [ + {str(RoleDoc.role_key): role_key}, + {str(RoleDoc.role_name): role_name} + ]} + ] + }) + if conflict: + raise RequestValidationError("role_key or role_name already exists.") + doc.role_key = role_key + doc.role_name = role_name + doc.role_description = role_description + doc.role_level = role_level + doc.updated_at = datetime.now() + await doc.save() + return doc + + async def query_roles(self, role_key: Optional[str], role_name: Optional[str], skip: int = 0, limit: int = 10) -> \ + Tuple[List[RoleDoc], int]: + """Query roles with pagination and fuzzy search by role_key and role_name""" + query = {} + if role_key: + query[str(RoleDoc.role_key)] = {"$regex": role_key, "$options": "i"} + if role_name: + query[str(RoleDoc.role_name)] = {"$regex": role_name, "$options": "i"} + cursor = RoleDoc.find(query) + total = await cursor.count() + docs = await cursor.skip(skip).limit(limit).to_list() + return docs, total diff --git a/apps/authentication/backend/models/__init__.py b/apps/authentication/backend/models/__init__.py index b8bad29..4cf8898 100644 --- a/apps/authentication/backend/models/__init__.py +++ b/apps/authentication/backend/models/__init__.py @@ -1,6 +1,8 @@ from .user import user_models from .user_profile import profile_models +from .permission import permission_models backend_models = [] backend_models.extend(user_models) backend_models.extend(profile_models) +backend_models.extend(permission_models) diff --git a/apps/authentication/backend/models/permission/__init__.py b/apps/authentication/backend/models/permission/__init__.py new file mode 100644 index 0000000..4a14d68 --- /dev/null +++ b/apps/authentication/backend/models/permission/__init__.py @@ -0,0 +1,3 @@ +from .models import PermissionDoc, RoleDoc + +permission_models = [PermissionDoc, RoleDoc] diff --git a/apps/authentication/backend/models/permission/constants.py b/apps/authentication/backend/models/permission/constants.py index 89c6104..d957893 100644 --- a/apps/authentication/backend/models/permission/constants.py +++ b/apps/authentication/backend/models/permission/constants.py @@ -1,4 +1,31 @@ -from enum import IntEnum +from dataclasses import dataclass +from enum import IntEnum, Enum + + +@dataclass(frozen=True) # frozen=True +class DefaultRole: + role_name: str + role_key: str + role_description: str + role_level: int + + +# Default roles, which all tenants will have, cannot be modified. +class DefaultRoleEnum(Enum): + ADMIN = DefaultRole("Administrator", "admin", "Have all permissions", 0) + + +@dataclass(frozen=True) # frozen=True +class DefaultPermission: + permission_key: str + permission_name: str + permission_description: str + + +# Default permissions, which all tenants will have, cannot be modified. +class DefaultPermissionEnum(Enum): + CHANGE_ROLES = DefaultPermission("change:roles", "Change roles", "Add/Update/Delete roles") + CHANGE_PERMISSIONS = DefaultPermission("change:permissions", "Change permissions", "Add/Update/Remove permissions") class AdministrativeRole(IntEnum): diff --git a/apps/authentication/backend/models/permission/models.py b/apps/authentication/backend/models/permission/models.py new file mode 100644 index 0000000..423495d --- /dev/null +++ b/apps/authentication/backend/models/permission/models.py @@ -0,0 +1,37 @@ +from beanie import Document +from datetime import datetime +from typing import Optional + + +class PermissionDoc(Document): + permission_name: str + permission_key: str + description: Optional[str] = None # Description of the permission, optional + created_at: datetime = datetime.now() # Creation timestamp, auto-generated + updated_at: datetime = datetime.now() # Last update timestamp, auto-updated + + class Settings: + # Default collections created by Freeleaps for tenant databases use '_' prefix + # to prevent naming conflicts with tenant-created collections + name = "_permission" + indexes = [ + "permission_key" + ] + + +class RoleDoc(Document): + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: list[str] + role_level: int + created_at: datetime = datetime.now() # Creation timestamp, auto-generated + updated_at: datetime = datetime.now() # Last update timestamp, auto-updated + + class Settings: + # Default collections created by Freeleaps for tenant databases use '_' prefix + # to prevent naming conflicts with tenant-created collections + name = "_role" + indexes = [ + "role_level" + ] \ No newline at end of file diff --git a/apps/authentication/backend/models/user/models.py b/apps/authentication/backend/models/user/models.py index 826c2be..97ad23a 100644 --- a/apps/authentication/backend/models/user/models.py +++ b/apps/authentication/backend/models/user/models.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List from beanie import Document @@ -18,6 +18,7 @@ class UserAccountDoc(Document): service_plan_id: Optional[str] properties: UserAccountProperty capabilities: Capability + user_role_ids: List[str] user_role: int = AdministrativeRole.NONE preferred_region: UserRegion = UserRegion.ZH_CN diff --git a/apps/authentication/backend/services/permission/permission_service.py b/apps/authentication/backend/services/permission/permission_service.py new file mode 100644 index 0000000..7de6838 --- /dev/null +++ b/apps/authentication/backend/services/permission/permission_service.py @@ -0,0 +1,32 @@ +from typing import Optional, Dict, Any + +from fastapi.exceptions import RequestValidationError + +from backend.infra.permission.permission_handler import PermissionHandler +from backend.models.permission.models import PermissionDoc +from beanie import PydanticObjectId + +class PermissionService: + def __init__(self): + self.permission_handler = PermissionHandler() + + async def create_permission(self, permission_key: str, permission_name: str, description: Optional[str] = None) -> PermissionDoc: + """Create a new permission document""" + return await self.permission_handler.create_permission(permission_key, permission_name, description) + + async def update_permission(self, permission_id: str, permission_key: Optional[str] = None, permission_name: Optional[str] = None, description: Optional[str] = None) -> PermissionDoc: + """Update an existing permission document by id""" + return await self.permission_handler.update_permission(PydanticObjectId(permission_id), permission_key, permission_name, description) + + async def query_permissions(self, permission_key: Optional[str] = None, permission_name: Optional[str] = None, page: int = 1, page_size: int = 10) -> Dict[str, Any]: + """Query permissions with pagination and fuzzy search""" + if page < 1 or page_size < 1: + raise RequestValidationError("page and page_size must be positive integers.") + skip = (page - 1) * page_size + docs, total = await self.permission_handler.query_permissions(permission_key, permission_name, skip, page_size) + return { + "items": [doc.dict() for doc in docs], + "total": total, + "page": page, + "page_size": page_size + } \ No newline at end of file diff --git a/apps/authentication/backend/services/permission/role_service.py b/apps/authentication/backend/services/permission/role_service.py new file mode 100644 index 0000000..8aba63e --- /dev/null +++ b/apps/authentication/backend/services/permission/role_service.py @@ -0,0 +1,36 @@ +from typing import Optional, Dict, Any + +from fastapi.exceptions import RequestValidationError + +from backend.infra.permission.role_handler import RoleHandler +from backend.models.permission.models import RoleDoc +from beanie import PydanticObjectId + +class RoleService: + def __init__(self): + self.role_handler = RoleHandler() + + async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc: + """Create a new role, ensuring role_key and role_name are unique and not empty""" + + doc = await self.role_handler.create_role(role_key, role_name, role_description, role_level) + return doc + + async def update_role(self, role_id: str, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc: + """Update an existing role, ensuring role_key and role_name are unique and not empty""" + + doc = await self.role_handler.update_role(PydanticObjectId(role_id), role_key, role_name, role_description, role_level) + return doc + + async def query_roles(self, role_key: Optional[str], role_name: Optional[str], page: int = 1, page_size: int = 10) -> Dict[str, Any]: + """Query roles with pagination and fuzzy search by role_key and role_name""" + if page < 1 or page_size < 1: + raise RequestValidationError("page and page_size must be positive integers.") + skip = (page - 1) * page_size + docs, total = await self.role_handler.query_roles(role_key, role_name, skip, page_size) + return { + "items": [doc.dict() for doc in docs], + "total": total, + "page": page, + "page_size": page_size + } \ No newline at end of file diff --git a/apps/authentication/common/__init__.py b/apps/authentication/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/local.env b/apps/authentication/local.env new file mode 100644 index 0000000..23ac8e5 --- /dev/null +++ b/apps/authentication/local.env @@ -0,0 +1,24 @@ +APP_NAME=authentication +export SERVICE_API_ACCESS_HOST=0.0.0.0 +export SERVICE_API_ACCESS_PORT=8004 +export CONTAINER_APP_ROOT=/app +export LOG_BASE_PATH=$CONTAINER_APP_ROOT/log/$APP_NAME +export BACKEND_LOG_FILE_NAME=$APP_NAME +export APPLICATION_ACTIVITY_LOG=$APP_NAME-activity +export MONGODB_NAME=freeleaps2 +export MONGODB_PORT=27017 +export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0 +GIT_REPO_ROOT=/mnt/freeleaps/freeleaps-service-hub +CODEBASE_ROOT=/mnt/freeleaps/freeleaps-service-hub/apps/authentication +SITE_DEPLOY_FOLDER=/mnt/freeleaps/freeleaps-service-hub/sites/authentication/deploy +#!/bin/bash +export VENV_DIR=venv_t +export VENV_ACTIVATE=venv_t/bin/activate +export DOCKER_HOME=/var/lib/docker +export DOCKER_APP_HOME=$DOCKER_HOME/app +export DOCKER_BACKEND_HOME=$DOCKER_APP_HOME/$APP_NAME +export DOCKER_BACKEND_LOG_HOME=$DOCKER_BACKEND_HOME/log +export MONGODB_URI=mongodb://localhost:27017/ +export FREELEAPS_ENV=local +export LOG_BASE_PATH=${CODEBASE_ROOT}/log + diff --git a/apps/authentication/webapi/bootstrap/application.py b/apps/authentication/webapi/bootstrap/application.py index 3f6dfb3..d857e7f 100644 --- a/apps/authentication/webapi/bootstrap/application.py +++ b/apps/authentication/webapi/bootstrap/application.py @@ -11,6 +11,7 @@ from webapi.providers import metrics # from webapi.providers import scheduler from webapi.providers import exception_handler +from webapi.providers import permission_initialize from .freeleaps_app import FreeleapsApp from common.config.app_settings import app_settings @@ -23,6 +24,7 @@ def create_app() -> FastAPI: register(app, exception_handler) register(app, database) register(app, router) + register(app, permission_initialize) # register(app, scheduler) register(app, common) diff --git a/apps/authentication/webapi/providers/exception_handler.py b/apps/authentication/webapi/providers/exception_handler.py index 21117a5..fd8cf43 100644 --- a/apps/authentication/webapi/providers/exception_handler.py +++ b/apps/authentication/webapi/providers/exception_handler.py @@ -1,3 +1,4 @@ +from bson.errors import InvalidId from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from starlette.requests import Request @@ -26,6 +27,12 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE content={"error": str(exc)}, ) +async def validation_error_exception_handler(request: Request, exc: InvalidId): + return JSONResponse( + status_code=HTTP_400_BAD_REQUEST, + content={"error": str(exc)}, + ) + async def exception_handler(request: Request, exc: Exception): return JSONResponse( status_code=HTTP_500_INTERNAL_SERVER_ERROR, @@ -36,4 +43,5 @@ async def exception_handler(request: Request, exc: Exception): def register(app: FastAPI): app.add_exception_handler(HTTPException, custom_http_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(InvalidId, exception_handler) app.add_exception_handler(Exception, exception_handler) diff --git a/apps/authentication/webapi/providers/permission_initialize.py b/apps/authentication/webapi/providers/permission_initialize.py new file mode 100644 index 0000000..3107e12 --- /dev/null +++ b/apps/authentication/webapi/providers/permission_initialize.py @@ -0,0 +1,37 @@ +import logging + +from backend.models.permission import PermissionDoc, RoleDoc +from backend.models.permission.constants import DefaultPermissionEnum, DefaultRoleEnum + + +def register(app): + # Configure logging for pymongo + logging.getLogger("init_admin_permission").setLevel(logging.INFO) # Suppress DEBUG logs + + @app.on_event("startup") + async def init_admin_permission(): + # Initialize permissions if not exist + default_permission_ids = [] + for default_permission in [DefaultPermissionEnum.CHANGE_PERMISSIONS, DefaultPermissionEnum.CHANGE_ROLES]: + if not await PermissionDoc.find_one( + {str(PermissionDoc.permission_key): default_permission.value.permission_key}): + doc = await PermissionDoc( + permission_key=default_permission.value.permission_key, + permission_name=default_permission.value.permission_name, + description=default_permission.value.permission_description, + ).insert() + default_permission_ids.append(str(doc.id)) + logging.info(f"default permissions initialized {default_permission_ids}") + # Initialize roles if not exist + default_role_ids = [] + for default_role in [DefaultRoleEnum.ADMIN]: + if not await RoleDoc.find_one({str(RoleDoc.role_key): default_role.value.role_key}): + doc = await RoleDoc( + role_key=default_role.value.role_key, + role_name=default_role.value.role_name, + role_description=default_role.value.role_description, + permission_ids=default_permission_ids, + role_level=default_role.value.role_level, + ).insert() + default_role_ids.append(str(doc.id)) + logging.info(f"default roles initialized {default_role_ids}") diff --git a/apps/authentication/webapi/routes/__init__.py b/apps/authentication/webapi/routes/__init__.py index fce1be0..19a5ec5 100644 --- a/apps/authentication/webapi/routes/__init__.py +++ b/apps/authentication/webapi/routes/__init__.py @@ -2,9 +2,13 @@ from fastapi import APIRouter from .signin import router as signin_router from .tokens import router as token_router from .auth import router as auth_router +from .permission import router as permission_router +from .role import router as role_router api_router = APIRouter(prefix="/auth") api_router.include_router(signin_router, tags=["user"]) api_router.include_router(token_router, tags=["token"]) api_router.include_router(auth_router, tags=["auth"]) +api_router.include_router(permission_router, tags=["permission"]) +api_router.include_router(role_router, tags=["role"]) websocket_router = APIRouter() diff --git a/apps/authentication/webapi/routes/permission/__init__.py b/apps/authentication/webapi/routes/permission/__init__.py new file mode 100644 index 0000000..d79f009 --- /dev/null +++ b/apps/authentication/webapi/routes/permission/__init__.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter +from .create_permission import router as cp_router +from .query_permission import router as qp_router +from .update_permission import router as up_router + + +router = APIRouter() + +router.include_router(cp_router, prefix="/permission", tags=["permission"]) +router.include_router(qp_router, prefix="/permission", tags=["permission"]) +router.include_router(up_router, prefix="/permission", tags=["permission"]) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/permission/create_permission.py b/apps/authentication/webapi/routes/permission/create_permission.py new file mode 100644 index 0000000..19ee383 --- /dev/null +++ b/apps/authentication/webapi/routes/permission/create_permission.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional +from backend.services.permission.permission_service import PermissionService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +permission_service = PermissionService() + +class CreatePermissionRequest(BaseModel): + permission_key: str + permission_name: str + description: Optional[str] = None + +class PermissionResponse(BaseModel): + id: str + permission_key: str + permission_name: str + description: Optional[str] = None + created_at: str + updated_at: str + +@router.post( + "/create", + response_model=PermissionResponse, + operation_id="create-permission", + summary="Create Permission", + description="Create a new permission." +) +async def create_permission( + req: CreatePermissionRequest, +) -> PermissionResponse: + doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description) + + return PermissionResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/permission/query_permission.py b/apps/authentication/webapi/routes/permission/query_permission.py new file mode 100644 index 0000000..50769dd --- /dev/null +++ b/apps/authentication/webapi/routes/permission/query_permission.py @@ -0,0 +1,50 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional, List +from backend.services.permission.permission_service import PermissionService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +permission_service = PermissionService() + +class QueryPermissionRequest(BaseModel): + permission_key: Optional[str] = None + permission_name: Optional[str] = None + page: int = 1 + page_size: int = 10 + +class PermissionResponse(BaseModel): + id: str + permission_key: str + permission_name: str + description: Optional[str] = None + created_at: datetime + updated_at: datetime + +class QueryPermissionResponse(BaseModel): + items: List[PermissionResponse] + total: int + page: int + page_size: int + +@router.post( + "/query", + response_model=QueryPermissionResponse, + operation_id="query-permission", + summary="Query Permissions (paginated)", + description="Query permissions with pagination and fuzzy search. Only Admin role allowed." +) +async def query_permissions( + req: QueryPermissionRequest, +) -> QueryPermissionResponse: + result = await permission_service.query_permissions(req.permission_key, req.permission_name, req.page, req.page_size) + items = [PermissionResponse(**item) for item in result["items"]] + return QueryPermissionResponse( + items=items, + total=result["total"], + page=result["page"], + page_size=result["page_size"] + ) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/permission/update_permission.py b/apps/authentication/webapi/routes/permission/update_permission.py new file mode 100644 index 0000000..5498bd6 --- /dev/null +++ b/apps/authentication/webapi/routes/permission/update_permission.py @@ -0,0 +1,38 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional +from backend.services.permission.permission_service import PermissionService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +permission_service = PermissionService() + +class UpdatePermissionRequest(BaseModel): + permission_id: str + permission_key: str + permission_name: str + description: Optional[str] = None + +class PermissionResponse(BaseModel): + id: str + permission_key: str + permission_name: str + description: Optional[str] = None + created_at: datetime + updated_at: datetime + +@router.post( + "/update", + response_model=PermissionResponse, + operation_id="update-permission", + summary="Update Permission", + description="Update an existing permission by id. Only Admin role allowed." +) +async def update_permission( + req: UpdatePermissionRequest, +) -> PermissionResponse: + doc = await permission_service.update_permission(req.permission_id, req.permission_key, req.permission_name, req.description) + return PermissionResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/__init__.py b/apps/authentication/webapi/routes/role/__init__.py new file mode 100644 index 0000000..eaad8a3 --- /dev/null +++ b/apps/authentication/webapi/routes/role/__init__.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter +from .create_role import router as create_role_router +from .update_role import router as update_role_router +from .query_role import router as query_role_router + +router = APIRouter() + +router.include_router(create_role_router, prefix="/role", tags=["role"]) +router.include_router(update_role_router, prefix="/role", tags=["role"]) +router.include_router(query_role_router, prefix="/role", tags=["role"]) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/create_role.py b/apps/authentication/webapi/routes/role/create_role.py new file mode 100644 index 0000000..2a21fae --- /dev/null +++ b/apps/authentication/webapi/routes/role/create_role.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional, List +from backend.services.permission.role_service import RoleService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +role_service = RoleService() + +class CreateRoleRequest(BaseModel): + role_key: str + role_name: str + role_description: Optional[str] = None + role_level: int + +class RoleResponse(BaseModel): + id: str + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: List[str] + role_level: int + created_at: datetime + updated_at: datetime + +@router.post( + "/create", + response_model=RoleResponse, + operation_id="create-role", + summary="Create Role", + description="Create a new role." +) +async def create_role( + req: CreateRoleRequest, +) -> RoleResponse: + doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level) + return RoleResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/query_role.py b/apps/authentication/webapi/routes/role/query_role.py new file mode 100644 index 0000000..8abc853 --- /dev/null +++ b/apps/authentication/webapi/routes/role/query_role.py @@ -0,0 +1,52 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional, List +from backend.services.permission.role_service import RoleService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +role_service = RoleService() + +class QueryRoleRequest(BaseModel): + role_key: Optional[str] = None + role_name: Optional[str] = None + page: int = 1 + page_size: int = 10 + +class RoleResponse(BaseModel): + id: str + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: List[str] + role_level: int + created_at: datetime + updated_at: datetime + +class QueryRoleResponse(BaseModel): + items: List[RoleResponse] + total: int + page: int + page_size: int + +@router.post( + "/query", + response_model=QueryRoleResponse, + operation_id="query-role", + summary="Query Roles (paginated)", + description="Query roles with pagination and fuzzy search. Only Admin role allowed." +) +async def query_roles( + req: QueryRoleRequest, +) -> QueryRoleResponse: + result = await role_service.query_roles(req.role_key, req.role_name, req.page, req.page_size) + items = [RoleResponse(**item) for item in result["items"]] + return QueryRoleResponse( + items=items, + total=result["total"], + page=result["page"], + page_size=result["page_size"] + ) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/update_role.py b/apps/authentication/webapi/routes/role/update_role.py new file mode 100644 index 0000000..862f79d --- /dev/null +++ b/apps/authentication/webapi/routes/role/update_role.py @@ -0,0 +1,41 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional, List +from backend.services.permission.role_service import RoleService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +role_service = RoleService() + +class UpdateRoleRequest(BaseModel): + role_id: str + role_key: str + role_name: str + role_description: Optional[str] = None + role_level: int + +class RoleResponse(BaseModel): + id: str + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: List[str] + role_level: int + created_at: datetime + updated_at: datetime + +@router.post( + "/update", + response_model=RoleResponse, + operation_id="update-role", + summary="Update Role", + description="Update an existing role by id. Only Admin role allowed." +) +async def update_role( + req: UpdateRoleRequest, +) -> RoleResponse: + doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level) + return RoleResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/user/__init__.py b/apps/authentication/webapi/routes/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/webapi/routes/user/assign_role.py b/apps/authentication/webapi/routes/user/assign_role.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/webapi/routes/user/remove_role.py b/apps/authentication/webapi/routes/user/remove_role.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/notification/.env.local b/apps/notification/.env.local new file mode 100644 index 0000000..fc8415f --- /dev/null +++ b/apps/notification/.env.local @@ -0,0 +1 @@ +export RABBITMQ_HOST=localhost \ No newline at end of file