diff --git a/README.md b/README.md index 0ec6924..0e2c186 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,20 @@ pnpm dev 你可以在群里提出任何疑问,我会尽快回复答疑。 -Vue FastAPI Admin Logo + + +## 打赏 +如果项目有帮助到你,可以请作者喝杯咖啡~ + +
+ + +
+ +## 定制开发 +如果有基于该项目的定制需求或其他合作,请添加下方微信,备注来意 + + ### Visitors Count diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 76a14be..1813810 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -8,6 +8,7 @@ from .depts import depts_router from .menus import menus_router from .roles import roles_router from .users import users_router +from .auditlog import auditlog_router v1_router = APIRouter() @@ -17,3 +18,4 @@ v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermi v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermisson]) v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermisson]) v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermisson]) +v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermisson]) diff --git a/app/api/v1/apis/apis.py b/app/api/v1/apis/apis.py index 67f9c88..9cdca28 100644 --- a/app/api/v1/apis/apis.py +++ b/app/api/v1/apis/apis.py @@ -1,9 +1,7 @@ from fastapi import APIRouter, Query - from tortoise.expressions import Q from app.controllers.api import api_controller - from app.schemas import Success, SuccessExtra from app.schemas.apis import * diff --git a/app/api/v1/auditlog/__init__.py b/app/api/v1/auditlog/__init__.py new file mode 100644 index 0000000..d90f07c --- /dev/null +++ b/app/api/v1/auditlog/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .auditlog import router + +auditlog_router = APIRouter() +auditlog_router.include_router(router, tags=["审计日志模块"]) + +__all__ = ["auditlog_router"] diff --git a/app/api/v1/auditlog/auditlog.py b/app/api/v1/auditlog/auditlog.py new file mode 100644 index 0000000..7a43b6f --- /dev/null +++ b/app/api/v1/auditlog/auditlog.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Query +from tortoise.expressions import Q +from app.models.admin import AuditLog + +from app.schemas import SuccessExtra +from app.schemas.apis import * +from app.core.dependency import DependPermisson + +router = APIRouter() + +@router.get('/list', summary="查看操作日志", dependencies=[DependPermisson]) +async def get_audit_log_list( + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + username: str = Query("", description="操作人名称"), + module: str = Query("", description="功能模块"), + summary: str = Query("", description="接口描述"), + start_time: str = Query("", description="开始时间"), + end_time: str = Query("", description="结束时间"), +): + + q = Q() + if username: + q &= Q(username__icontains=username) + if module: + q &= Q(module__icontains=module) + if summary: + q &= Q(summary__icontains=summary) + if start_time and end_time: + q &= Q(created_at__range=[start_time, end_time]) + elif start_time: + q &= Q(created_at__gte=start_time) + elif end_time: + q &= Q(created_at__lte=end_time) + + audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at") + total = await AuditLog.filter(q).count() + data = [await audit_log.to_dict() for audit_log in audit_log_objs] + return SuccessExtra(data=data, total=total, page=page, page_size=page_size) diff --git a/app/controllers/api.py b/app/controllers/api.py index 86cce96..41469e9 100644 --- a/app/controllers/api.py +++ b/app/controllers/api.py @@ -1,15 +1,15 @@ +from fastapi.routing import APIRoute + from app.core.crud import CRUDBase +from app.log import logger from app.models.admin import Api from app.schemas.apis import ApiCreate, ApiUpdate -from fastapi.routing import APIRoute -from app.log import logger class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]): def __init__(self): super().__init__(model=Api) - async def refresh_api(self): from app import app @@ -40,4 +40,5 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]): logger.debug(f"API Created {method} {path}") await Api.create(**dict(method=method, path=path, summary=summary, tags=tags)) + api_controller = ApiController() diff --git a/app/core/init_app.py b/app/core/init_app.py index 1c1e5f8..ef6342c 100644 --- a/app/core/init_app.py +++ b/app/core/init_app.py @@ -20,7 +20,7 @@ from app.models.admin import Menu from app.schemas.menus import MenuType from app.settings.config import settings -from .middlewares import BackGroundTaskMiddleware +from .middlewares import BackGroundTaskMiddleware, HttpAuditLogMiddleware def make_middlewares(): @@ -33,6 +33,14 @@ def make_middlewares(): allow_headers=settings.CORS_ALLOW_HEADERS, ), Middleware(BackGroundTaskMiddleware), + Middleware( + HttpAuditLogMiddleware, + methods=["GET", "POST", "PUT", "DELETE"], + exclude_paths=[ + "/docs", + "/openapi.json", + ], + ), ] return middleware @@ -134,6 +142,17 @@ async def init_menus(): component="/system/dept", keepalive=False, ), + Menu( + menu_type=MenuType.MENU, + name="审计日志", + path="auditlog", + order=6, + parent_id=parent_menu.id, + icon="ph:clipboard-text-bold", + is_hidden=False, + component="/system/auditlog", + keepalive=False, + ) ] await Menu.bulk_create(children_menu) parent_menu = await Menu.create( diff --git a/app/core/middlewares.py b/app/core/middlewares.py index a6456b5..db7396e 100644 --- a/app/core/middlewares.py +++ b/app/core/middlewares.py @@ -1,6 +1,16 @@ +import re +from datetime import datetime + +from fastapi import FastAPI +from fastapi.responses import Response +from fastapi.routing import APIRoute +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.types import ASGIApp, Receive, Scope, Send +from app.core.dependency import AuthControl +from app.models.admin import AuditLog, User + from .bgtask import BgTasks @@ -32,3 +42,55 @@ class BackGroundTaskMiddleware(SimpleBaseMiddleware): async def after_request(self, request): await BgTasks.execute_tasks() + + +class HttpAuditLogMiddleware(BaseHTTPMiddleware): + def __init__(self, app, methods: list, exclude_paths: list): + super().__init__(app) + self.methods = methods + self.exclude_paths = exclude_paths + + async def get_request_log(self, request: Request, response: Response) -> dict: + """ + 根据request和response对象获取对应的日志记录数据 + """ + data: dict = {"path": request.url.path, "status": response.status_code, "method": request.method} + # 路由信息 + app: FastAPI = request.app + for route in app.routes: + if ( + isinstance(route, APIRoute) + and route.path_regex.match(request.url.path) + and request.method in route.methods + ): + data["module"] = ",".join(route.tags) + data["summary"] = route.summary + # 获取用户信息 + token = request.headers.get("token") + user_obj = None + if token: + user_obj: User = await AuthControl.is_authed(token) + data["user_id"] = user_obj.id if user_obj else 0 + data["username"] = user_obj.username if user_obj else "" + return data + + async def before_request(self, request: Request): + pass + + async def after_request(self, request: Request, response: Response, process_time: int): + if request.method in self.methods: # 请求方法为配置的记录方法 + for path in self.exclude_paths: + if re.search(path, request.url.path, re.I) is not None: + return + data: dict = await self.get_request_log(request=request, response=response) + data["response_time"] = process_time # 响应时间 + await AuditLog.create(**data) + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + start_time: datetime = datetime.now() + await self.before_request(request) + response = await call_next(request) + end_time: datetime = datetime.now() + process_time = int((end_time.timestamp() - start_time.timestamp()) * 1000) + await self.after_request(request, response, process_time) + return response diff --git a/app/models/admin.py b/app/models/admin.py index 81b5843..9ba602f 100644 --- a/app/models/admin.py +++ b/app/models/admin.py @@ -79,3 +79,14 @@ class DeptClosure(BaseModel, TimestampMixin): ancestor = fields.IntField(description="父代") descendant = fields.IntField(description="子代") level = fields.IntField(default=0, description="深度") + + +class AuditLog(BaseModel, TimestampMixin): + user_id = fields.IntField(description="用户ID", index=True) + username = fields.CharField(max_length=64, default="", description="用户名称", index=True) + module = fields.CharField(max_length=64, default="", description="功能模块", index=True) + summary = fields.CharField(max_length=128, default="", description="请求描述", index=True) + method = fields.CharField(max_length=10, default="", description="请求方法", index=True) + path = fields.CharField(max_length=255, default="", description="请求路径", index=True) + status = fields.IntField(default=-1, description="状态码", index=True) + response_time = fields.IntField(default=0, description="响应时间(单位ms)") diff --git a/deploy/sample-picture/1.jpg b/deploy/sample-picture/1.jpg new file mode 100644 index 0000000..d8e4f08 Binary files /dev/null and b/deploy/sample-picture/1.jpg differ diff --git a/deploy/sample-picture/2.jpg b/deploy/sample-picture/2.jpg new file mode 100644 index 0000000..e9961f3 Binary files /dev/null and b/deploy/sample-picture/2.jpg differ diff --git a/deploy/sample-picture/3.jpg b/deploy/sample-picture/3.jpg new file mode 100644 index 0000000..6a6bf83 Binary files /dev/null and b/deploy/sample-picture/3.jpg differ diff --git a/web/src/api/index.js b/web/src/api/index.js index 6337e6f..54f05f8 100644 --- a/web/src/api/index.js +++ b/web/src/api/index.js @@ -36,4 +36,6 @@ export default { createDept: (data = {}) => request.post('/dept/create', data), updateDept: (data = {}) => request.post('/dept/update', data), deleteDept: (params = {}) => request.delete('/dept/delete', { params }), + // auditlog + getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }), } diff --git a/web/src/views/system/auditlog/index.vue b/web/src/views/system/auditlog/index.vue new file mode 100644 index 0000000..5792b58 --- /dev/null +++ b/web/src/views/system/auditlog/index.vue @@ -0,0 +1,231 @@ + + +