feat: 审计日志

This commit is contained in:
mizhexiaoxiao 2024-07-31 14:58:26 +08:00
parent 01ca146205
commit 155818dd1f
14 changed files with 393 additions and 7 deletions

View File

@ -190,7 +190,20 @@ pnpm dev
你可以在群里提出任何疑问,我会尽快回复答疑。
<img alt="Vue FastAPI Admin Logo" width="400" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/group.jpg">
<img width="300" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/group.jpg">
## 打赏
如果项目有帮助到你,可以请作者喝杯咖啡~
<div style="display: flex">
<img src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/1.jpg" width="300">
<img src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/2.jpg" width="300">
</div>
## 定制开发
如果有基于该项目的定制需求或其他合作,请添加下方微信,备注来意
<img width="300" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/3.jpg">
### Visitors Count

View File

@ -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])

View File

@ -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 *

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .auditlog import router
auditlog_router = APIRouter()
auditlog_router.include_router(router, tags=["审计日志模块"])
__all__ = ["auditlog_router"]

View File

@ -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)

View File

@ -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()

View File

@ -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(

View File

@ -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

View File

@ -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)")

BIN
deploy/sample-picture/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
deploy/sample-picture/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
deploy/sample-picture/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -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 }),
}

View File

@ -0,0 +1,231 @@
<script setup>
import { onMounted, ref, resolveDirective } from 'vue'
import { NInput, NSelect } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue'
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
import CrudTable from '@/components/table/CrudTable.vue'
import { useCRUD } from '@/composables'
import api from '@/api'
defineOptions({ name: '操作日志' })
const $table = ref(null)
const queryItems = ref({})
const {
modalVisible,
modalTitle,
modalLoading,
handleSave,
modalForm,
modalFormRef,
handleEdit,
handleDelete,
handleAdd,
} = useCRUD({
name: '操作日志',
initForm: {
args: {
ping_sleep: 1,
ping_threshold: 500,
agent_data_check_threshold: 2,
agent_incoming_traffic_threshold: 30,
before_start_clean_sleep: 2,
before_stop_disposal_sleep: 20,
},
},
doCreate: api.createHostMonitor,
doUpdate: api.updateHostMonitor,
doDelete: api.deleteHostMonitor,
refresh: () => $table.value?.handleSearch(),
})
onMounted(() => {
$table.value?.handleSearch()
})
function formatTimestamp(timestamp) {
const date = new Date(timestamp)
const pad = (num) => num.toString().padStart(2, '0')
const year = date.getFullYear()
const month = pad(date.getMonth() + 1) // 0+1
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
//
function getStartOfDayTimestamp() {
const now = new Date()
now.setHours(0, 0, 0, 0) // 0
return now.getTime()
}
//
function getEndOfDayTimestamp() {
const now = new Date()
now.setHours(23, 59, 59, 999) // 235959999
return now.getTime()
}
// 使
const startOfDayTimestamp = getStartOfDayTimestamp()
const endOfDayTimestamp = getEndOfDayTimestamp()
const datetimeRange = ref([startOfDayTimestamp, endOfDayTimestamp])
const handleDateRangeChange = (value) => {
queryItems.value.start_time = formatTimestamp(value[0])
queryItems.value.end_time = formatTimestamp(value[1])
}
const methodOptions = [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
{
label: 'DELETE',
value: 'DELETE',
},
]
const columns = [
{
title: '用户名称',
key: 'username',
width: 'auto',
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '接口概要',
key: 'summary',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '功能模块',
key: 'module',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '请求方法',
key: 'method',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '请求路径',
key: 'path',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '状态码',
key: 'status',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '响应时间(s)',
key: 'response_time',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '操作时间',
key: 'created_at',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
]
</script>
<template>
<!-- 业务页面 -->
<CommonPage>
<!-- 表格 -->
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:columns="columns"
:get-data="api.getAuditLogList"
>
<template #queryBar>
<QueryBarItem label="用户名称" :label-width="70">
<NInput
v-model:value="queryItems.username"
clearable
type="text"
placeholder="请输入用户名称"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="功能模块" :label-width="70">
<NInput
v-model:value="queryItems.module"
clearable
type="text"
placeholder="请输入功能模块"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="接口概要" :label-width="70">
<NInput
v-model:value="queryItems.summary"
clearable
type="text"
placeholder="请输入接口概要"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="请求方法" :label-width="70">
<NSelect
v-model:value="modalForm.method"
style="width: 150px"
:options="methodOptions"
clearable
placeholder="请选择请求方法"
/>
</QueryBarItem>
<QueryBarItem label="API路径" :label-width="70">
<NInput
v-model:value="queryItems.path"
clearable
type="text"
placeholder="请输入API路径"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="创建时间" :label-width="70">
<NDatePicker
v-model:value="datetimeRange"
type="datetimerange"
clearable
placeholder="请选择时间范围"
@update:value="handleDateRangeChange"
/>
</QueryBarItem>
</template>
</CrudTable>
</CommonPage>
</template>