commit
fd72748258
@ -2,8 +2,7 @@ FROM node:18.12.0-alpine3.16 as web
|
||||
|
||||
WORKDIR /opt/vue-fastapi-admin
|
||||
COPY /web ./web
|
||||
RUN cd /opt/vue-fastapi-admin/web && npm i -g pnpm --registry=https://registry.npmmirror.com \
|
||||
&& pnpm i --registry=https://registry.npmmirror.com && pnpm run build
|
||||
RUN cd /opt/vue-fastapi-admin/web && npm i --registry=https://registry.npmmirror.com && npm run build
|
||||
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
|
||||
15
README.md
15
README.md
@ -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
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.routing import APIRoute
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.api import api_controller
|
||||
from app.log import logger
|
||||
from app.models.admin import Api
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.apis import *
|
||||
|
||||
@ -66,33 +63,5 @@ async def delete_api(
|
||||
|
||||
@router.post("/refresh", summary="刷新API列表")
|
||||
async def refresh_api():
|
||||
from app import app
|
||||
|
||||
# 删除废弃API数据
|
||||
all_api_list = []
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
all_api_list.append((list(route.methods)[0], route.path_format))
|
||||
delete_api = []
|
||||
for api in await Api.all():
|
||||
if (api.method, api.path) not in all_api_list:
|
||||
delete_api.append((api.method, api.path))
|
||||
for item in delete_api:
|
||||
method, path = item
|
||||
logger.debug(f"API Deleted {method} {path}")
|
||||
await Api.filter(method=method, path=path).delete()
|
||||
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
method = list(route.methods)[0]
|
||||
path = route.path_format
|
||||
summary = route.summary
|
||||
tags = list(route.tags)[0]
|
||||
api_obj = await Api.filter(method=method, path=path).first()
|
||||
if api_obj:
|
||||
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
|
||||
else:
|
||||
logger.debug(f"API Created {method} {path}")
|
||||
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
|
||||
|
||||
await api_controller.refresh_api()
|
||||
return Success(msg="OK")
|
||||
|
||||
8
app/api/v1/auditlog/__init__.py
Normal file
8
app/api/v1/auditlog/__init__.py
Normal 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"]
|
||||
39
app/api/v1/auditlog/auditlog.py
Normal file
39
app/api/v1/auditlog/auditlog.py
Normal 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)
|
||||
@ -1,8 +1,8 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.controllers.user import UserController, user_controller
|
||||
from app.controllers.user import user_controller
|
||||
from app.core.ctx import CTX_USER_ID
|
||||
from app.core.dependency import DependAuth
|
||||
from app.models.admin import Api, Menu, Role, User
|
||||
@ -21,7 +21,7 @@ async def login_access_token(credentials: CredentialsSchema):
|
||||
user: User = await user_controller.authenticate(credentials)
|
||||
await user_controller.update_last_login(user.id)
|
||||
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
expire = datetime.utcnow() + access_token_expires
|
||||
expire = datetime.now(timezone.utc) + access_token_expires
|
||||
|
||||
data = JWTOut(
|
||||
access_token=create_access_token(
|
||||
@ -91,10 +91,10 @@ async def get_user_api():
|
||||
return Success(data=apis)
|
||||
|
||||
|
||||
@router.post("/update_password", summary="更新用户密码", dependencies=[DependAuth])
|
||||
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth])
|
||||
async def update_user_password(req_in: UpdatePassword):
|
||||
user_controller = UserController()
|
||||
user = await user_controller.get(req_in.id)
|
||||
user_id = CTX_USER_ID.get()
|
||||
user = await user_controller.get(user_id)
|
||||
verified = verify_password(req_in.old_password, user.password)
|
||||
if not verified:
|
||||
return Fail(msg="旧密码验证错误!")
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi import APIRouter, Body, Query
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.dept import dept_controller
|
||||
from app.controllers.user import UserController
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.controllers.user import user_controller
|
||||
from app.schemas.base import Fail, Success, SuccessExtra
|
||||
from app.schemas.users import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -22,7 +21,6 @@ async def list_user(
|
||||
email: str = Query("", description="邮箱地址"),
|
||||
dept_id: int = Query(None, description="部门ID"),
|
||||
):
|
||||
user_controller = UserController()
|
||||
q = Q()
|
||||
if username:
|
||||
q &= Q(username__contains=username)
|
||||
@ -43,7 +41,6 @@ async def list_user(
|
||||
async def get_user(
|
||||
user_id: int = Query(..., description="用户ID"),
|
||||
):
|
||||
user_controller = UserController()
|
||||
user_obj = await user_controller.get(id=user_id)
|
||||
user_dict = await user_obj.to_dict(exclude_fields=["password"])
|
||||
return Success(data=user_dict)
|
||||
@ -53,14 +50,10 @@ async def get_user(
|
||||
async def create_user(
|
||||
user_in: UserCreate,
|
||||
):
|
||||
user_controller = UserController()
|
||||
user = await user_controller.get_by_email(user_in.email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this email already exists in the system.",
|
||||
)
|
||||
new_user = await user_controller.create(obj_in=user_in)
|
||||
return Fail(code=400, msg="The user with this email already exists in the system.")
|
||||
new_user = await user_controller.create_user(obj_in=user_in)
|
||||
await user_controller.update_roles(new_user, user_in.role_ids)
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
@ -69,8 +62,7 @@ async def create_user(
|
||||
async def update_user(
|
||||
user_in: UserUpdate,
|
||||
):
|
||||
user_controller = UserController()
|
||||
user = await user_controller.update(obj_in=user_in)
|
||||
user = await user_controller.update(id=user_in.id, obj_in=user_in)
|
||||
await user_controller.update_roles(user, user_in.role_ids)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
@ -79,6 +71,11 @@ async def update_user(
|
||||
async def delete_user(
|
||||
user_id: int = Query(..., description="用户ID"),
|
||||
):
|
||||
user_controller = UserController()
|
||||
await user_controller.remove(id=user_id)
|
||||
return Success(msg="Deleted Successfully")
|
||||
|
||||
|
||||
@router.post("/reset_password", summary="重置密码")
|
||||
async def reset_password(user_id: int = Body(..., description="用户ID")):
|
||||
await user_controller.reset_password(user_id)
|
||||
return Success(msg="密码已重置为123456")
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
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
|
||||
|
||||
@ -7,5 +10,35 @@ class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
|
||||
def __init__(self):
|
||||
super().__init__(model=Api)
|
||||
|
||||
async def refresh_api(self):
|
||||
from app import app
|
||||
|
||||
# 删除废弃API数据
|
||||
all_api_list = []
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
all_api_list.append((list(route.methods)[0], route.path_format))
|
||||
delete_api = []
|
||||
for api in await Api.all():
|
||||
if (api.method, api.path) not in all_api_list:
|
||||
delete_api.append((api.method, api.path))
|
||||
for item in delete_api:
|
||||
method, path = item
|
||||
logger.debug(f"API Deleted {method} {path}")
|
||||
await Api.filter(method=method, path=path).delete()
|
||||
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
method = list(route.methods)[0]
|
||||
path = route.path_format
|
||||
summary = route.summary
|
||||
tags = list(route.tags)[0]
|
||||
api_obj = await Api.filter(method=method, path=path).first()
|
||||
if api_obj:
|
||||
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
|
||||
else:
|
||||
logger.debug(f"API Created {method} {path}")
|
||||
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
|
||||
|
||||
|
||||
api_controller = ApiController()
|
||||
|
||||
@ -22,14 +22,11 @@ class UserController(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
async def get_by_username(self, username: str) -> Optional[User]:
|
||||
return await self.model.filter(username=username).first()
|
||||
|
||||
async def create(self, obj_in: UserCreate) -> User:
|
||||
async def create_user(self, obj_in: UserCreate) -> User:
|
||||
obj_in.password = get_password_hash(password=obj_in.password)
|
||||
obj = await super().create(obj_in.create_dict())
|
||||
obj = await self.create(obj_in)
|
||||
return obj
|
||||
|
||||
async def update(self, obj_in: UserUpdate) -> User:
|
||||
return await super().update(id=obj_in.id, obj_in=obj_in)
|
||||
|
||||
async def update_last_login(self, id: int) -> None:
|
||||
user = await self.model.get(id=id)
|
||||
user.last_login = datetime.now()
|
||||
@ -52,5 +49,12 @@ class UserController(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
role_obj = await role_controller.get(id=role_id)
|
||||
await user.roles.add(role_obj)
|
||||
|
||||
async def reset_password(self, user_id: int):
|
||||
user_obj = await self.get(id=user_id)
|
||||
if user_obj.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="不允许重置超级管理员密码")
|
||||
user_obj.password = get_password_hash(password="123456")
|
||||
await user_obj.save()
|
||||
|
||||
|
||||
user_controller = UserController()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -52,7 +60,7 @@ def register_routers(app: FastAPI, prefix: str = "/api"):
|
||||
async def init_superuser():
|
||||
user = await user_controller.model.exists()
|
||||
if not user:
|
||||
await user_controller.create(
|
||||
await user_controller.create_user(
|
||||
UserCreate(
|
||||
username="admin",
|
||||
email="admin@admin.com",
|
||||
@ -75,7 +83,7 @@ async def init_menus():
|
||||
icon="carbon:gui-management",
|
||||
is_hidden=False,
|
||||
component="Layout",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
redirect="/system/user",
|
||||
)
|
||||
children_menu = [
|
||||
@ -88,7 +96,7 @@ async def init_menus():
|
||||
icon="material-symbols:person-outline-rounded",
|
||||
is_hidden=False,
|
||||
component="/system/user",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
@ -99,7 +107,7 @@ async def init_menus():
|
||||
icon="carbon:user-role",
|
||||
is_hidden=False,
|
||||
component="/system/role",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
@ -110,7 +118,7 @@ async def init_menus():
|
||||
icon="material-symbols:list-alt-outline",
|
||||
is_hidden=False,
|
||||
component="/system/menu",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
@ -121,7 +129,7 @@ async def init_menus():
|
||||
icon="ant-design:api-outlined",
|
||||
is_hidden=False,
|
||||
component="/system/api",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
@ -132,8 +140,19 @@ async def init_menus():
|
||||
icon="mingcute:department-line",
|
||||
is_hidden=False,
|
||||
component="/system/dept",
|
||||
keepalive=True,
|
||||
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(
|
||||
@ -145,7 +164,7 @@ async def init_menus():
|
||||
icon="mdi-fan-speed-1",
|
||||
is_hidden=False,
|
||||
component="Layout",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
redirect="",
|
||||
)
|
||||
await Menu.create(
|
||||
@ -157,5 +176,5 @@ async def init_menus():
|
||||
icon="mdi-fan-speed-1",
|
||||
is_hidden=False,
|
||||
component="/top-menu",
|
||||
keepalive=True,
|
||||
keepalive=False,
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -40,6 +40,5 @@ class UserUpdate(BaseModel):
|
||||
|
||||
|
||||
class UpdatePassword(BaseModel):
|
||||
id: int = Field(description="用户ID")
|
||||
old_password: str = Field(description="旧密码")
|
||||
new_password: str = Field(description="新密码")
|
||||
|
||||
@ -36,7 +36,7 @@ class Settings(BaseSettings):
|
||||
LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs")
|
||||
SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 day
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 24 * 7 # 7 day
|
||||
TORTOISE_ORM: dict = {
|
||||
"connections": {
|
||||
"sqlite": {
|
||||
|
||||
BIN
deploy/sample-picture/1.jpg
Normal file
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
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
BIN
deploy/sample-picture/3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
@ -12,7 +12,7 @@
|
||||
"prettier": "npx prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/json": "^2.2.101",
|
||||
"@iconify/json": "^2.2.228",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@unocss/eslint-config": "^0.55.0",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
|
||||
6144
web/pnpm-lock.yaml
generated
6144
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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 }),
|
||||
}
|
||||
|
||||
@ -74,7 +74,23 @@ const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||
const loading = ref(false)
|
||||
const initQuery = { ...props.queryItems }
|
||||
const tableData = ref([])
|
||||
const pagination = reactive({ page: 1, page_size: 10 })
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
showSizePicker: true,
|
||||
prefix({ itemCount }) {
|
||||
return `共 ${itemCount} 条`
|
||||
},
|
||||
onChange: (page) => {
|
||||
pagination.page = page
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pagination.page_size = pageSize
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
},
|
||||
})
|
||||
|
||||
async function handleQuery() {
|
||||
try {
|
||||
@ -90,7 +106,7 @@ async function handleQuery() {
|
||||
...paginationParams,
|
||||
})
|
||||
tableData.value = data
|
||||
pagination.itemCount = total
|
||||
pagination.itemCount = total || 0
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
|
||||
231
web/src/views/system/auditlog/index.vue
Normal file
231
web/src/views/system/auditlog/index.vue
Normal 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) // 将小时设置为23,分钟设置为59,秒设置为59,毫秒设置为999
|
||||
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>
|
||||
Loading…
x
Reference in New Issue
Block a user