mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
重构响应为标准响应格式
This commit is contained in:
@@ -164,12 +164,13 @@ class SubdomainViewSet(viewsets.ModelViewSet):
|
||||
|
||||
响应:
|
||||
{
|
||||
"message": "批量创建完成",
|
||||
"createdCount": 10,
|
||||
"skippedCount": 2,
|
||||
"invalidCount": 1,
|
||||
"mismatchedCount": 1,
|
||||
"totalReceived": 14
|
||||
"data": {
|
||||
"createdCount": 10,
|
||||
"skippedCount": 2,
|
||||
"invalidCount": 1,
|
||||
"mismatchedCount": 1,
|
||||
"totalReceived": 14
|
||||
}
|
||||
}
|
||||
"""
|
||||
from apps.targets.models import Target
|
||||
@@ -220,12 +221,13 @@ class SubdomainViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
return Response({
|
||||
'message': '批量创建完成',
|
||||
'createdCount': result.created_count,
|
||||
'skippedCount': result.skipped_count,
|
||||
'invalidCount': result.invalid_count,
|
||||
'mismatchedCount': result.mismatched_count,
|
||||
'totalReceived': result.total_received,
|
||||
'data': {
|
||||
'createdCount': result.created_count,
|
||||
'skippedCount': result.skipped_count,
|
||||
'invalidCount': result.invalid_count,
|
||||
'mismatchedCount': result.mismatched_count,
|
||||
'totalReceived': result.total_received,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='export')
|
||||
@@ -299,9 +301,9 @@ class WebSiteViewSet(viewsets.ModelViewSet):
|
||||
|
||||
响应:
|
||||
{
|
||||
"message": "批量创建完成",
|
||||
"createdCount": 10,
|
||||
"mismatchedCount": 2
|
||||
"data": {
|
||||
"createdCount": 10
|
||||
}
|
||||
}
|
||||
"""
|
||||
from apps.targets.models import Target
|
||||
@@ -346,8 +348,9 @@ class WebSiteViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
return Response({
|
||||
'message': '批量创建完成',
|
||||
'createdCount': created_count,
|
||||
'data': {
|
||||
'createdCount': created_count,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='export')
|
||||
@@ -426,9 +429,9 @@ class DirectoryViewSet(viewsets.ModelViewSet):
|
||||
|
||||
响应:
|
||||
{
|
||||
"message": "批量创建完成",
|
||||
"createdCount": 10,
|
||||
"mismatchedCount": 2
|
||||
"data": {
|
||||
"createdCount": 10
|
||||
}
|
||||
}
|
||||
"""
|
||||
from apps.targets.models import Target
|
||||
@@ -473,8 +476,9 @@ class DirectoryViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
return Response({
|
||||
'message': '批量创建完成',
|
||||
'createdCount': created_count,
|
||||
'data': {
|
||||
'createdCount': created_count,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='export')
|
||||
@@ -553,9 +557,9 @@ class EndpointViewSet(viewsets.ModelViewSet):
|
||||
|
||||
响应:
|
||||
{
|
||||
"message": "批量创建完成",
|
||||
"createdCount": 10,
|
||||
"mismatchedCount": 2
|
||||
"data": {
|
||||
"createdCount": 10
|
||||
}
|
||||
}
|
||||
"""
|
||||
from apps.targets.models import Target
|
||||
@@ -600,8 +604,9 @@ class EndpointViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
return Response({
|
||||
'message': '批量创建完成',
|
||||
'createdCount': created_count,
|
||||
'data': {
|
||||
'createdCount': created_count,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='export')
|
||||
|
||||
31
backend/apps/common/error_codes.py
Normal file
31
backend/apps/common/error_codes.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
标准化错误码定义
|
||||
|
||||
采用简化方案(参考 Stripe、GitHub 等大厂做法):
|
||||
- 只定义 5-10 个通用错误码
|
||||
- 未知错误使用通用错误码
|
||||
- 错误码格式:大写字母和下划线组成
|
||||
"""
|
||||
|
||||
|
||||
class ErrorCodes:
|
||||
"""标准化错误码
|
||||
|
||||
只定义通用错误码,其他错误使用通用消息。
|
||||
这是 Stripe、GitHub 等大厂的标准做法。
|
||||
|
||||
错误码格式规范:
|
||||
- 使用大写字母和下划线
|
||||
- 简洁明了,易于理解
|
||||
- 前端通过错误码映射到 i18n 键
|
||||
"""
|
||||
|
||||
# 通用错误码(8 个)
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR' # 输入验证失败
|
||||
NOT_FOUND = 'NOT_FOUND' # 资源未找到
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED' # 权限不足
|
||||
SERVER_ERROR = 'SERVER_ERROR' # 服务器内部错误
|
||||
BAD_REQUEST = 'BAD_REQUEST' # 请求格式错误
|
||||
CONFLICT = 'CONFLICT' # 资源冲突(如重复创建)
|
||||
UNAUTHORIZED = 'UNAUTHORIZED' # 未认证
|
||||
RATE_LIMITED = 'RATE_LIMITED' # 请求过于频繁
|
||||
93
backend/apps/common/response_helpers.py
Normal file
93
backend/apps/common/response_helpers.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
标准化 API 响应辅助函数
|
||||
|
||||
遵循行业标准(RFC 9457 Problem Details)和大厂实践(Google、Stripe、GitHub):
|
||||
- 成功响应只包含数据,不包含 message 字段
|
||||
- 错误响应使用机器可读的错误码,前端映射到 i18n 消息
|
||||
"""
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success_response(
|
||||
data: Optional[Union[Dict[str, Any], List[Any]]] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
status_code: int = status.HTTP_200_OK
|
||||
) -> Response:
|
||||
"""
|
||||
标准化成功响应
|
||||
|
||||
Args:
|
||||
data: 响应数据(dict 或 list)
|
||||
meta: 元数据(如 count、total、page)
|
||||
status_code: HTTP 状态码,默认 200
|
||||
|
||||
Returns:
|
||||
Response: DRF Response 对象
|
||||
|
||||
Examples:
|
||||
# 单个资源
|
||||
>>> success_response(data={'id': 1, 'name': 'Test'})
|
||||
{'data': {'id': 1, 'name': 'Test'}}
|
||||
|
||||
# 列表资源带分页
|
||||
>>> success_response(data=[...], meta={'total': 100, 'page': 1})
|
||||
{'data': [...], 'meta': {'total': 100, 'page': 1}}
|
||||
|
||||
# 创建资源
|
||||
>>> success_response(data={'id': 1}, status_code=201)
|
||||
"""
|
||||
response_body: Dict[str, Any] = {}
|
||||
|
||||
if data is not None:
|
||||
response_body['data'] = data
|
||||
|
||||
if meta is not None:
|
||||
response_body['meta'] = meta
|
||||
|
||||
return Response(response_body, status=status_code)
|
||||
|
||||
|
||||
def error_response(
|
||||
code: str,
|
||||
message: Optional[str] = None,
|
||||
details: Optional[List[Dict[str, Any]]] = None,
|
||||
status_code: int = status.HTTP_400_BAD_REQUEST
|
||||
) -> Response:
|
||||
"""
|
||||
标准化错误响应
|
||||
|
||||
Args:
|
||||
code: 错误码(如 'VALIDATION_ERROR', 'NOT_FOUND')
|
||||
格式:大写字母和下划线组成
|
||||
message: 开发者调试信息(非用户显示)
|
||||
details: 详细错误信息(如字段级验证错误)
|
||||
status_code: HTTP 状态码,默认 400
|
||||
|
||||
Returns:
|
||||
Response: DRF Response 对象
|
||||
|
||||
Examples:
|
||||
# 简单错误
|
||||
>>> error_response(code='NOT_FOUND', status_code=404)
|
||||
{'error': {'code': 'NOT_FOUND'}}
|
||||
|
||||
# 带调试信息
|
||||
>>> error_response(
|
||||
... code='VALIDATION_ERROR',
|
||||
... message='Invalid input data',
|
||||
... details=[{'field': 'name', 'message': 'Required'}]
|
||||
... )
|
||||
{'error': {'code': 'VALIDATION_ERROR', 'message': '...', 'details': [...]}}
|
||||
"""
|
||||
error_body: Dict[str, Any] = {'code': code}
|
||||
|
||||
if message:
|
||||
error_body['message'] = message
|
||||
|
||||
if details:
|
||||
error_body['details'] = details
|
||||
|
||||
return Response({'error': error_body}, status=status_code)
|
||||
@@ -11,6 +11,9 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -28,9 +31,10 @@ class LoginView(APIView):
|
||||
password = request.data.get('password')
|
||||
|
||||
if not username or not password:
|
||||
return Response(
|
||||
{'error': '请提供用户名和密码'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Username and password are required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
user = authenticate(request, username=username, password=password)
|
||||
@@ -38,20 +42,22 @@ class LoginView(APIView):
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
logger.info(f"用户 {username} 登录成功")
|
||||
return Response({
|
||||
'message': '登录成功',
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'isStaff': user.is_staff,
|
||||
'isSuperuser': user.is_superuser,
|
||||
return success_response(
|
||||
data={
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'isStaff': user.is_staff,
|
||||
'isSuperuser': user.is_superuser,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
else:
|
||||
logger.warning(f"用户 {username} 登录失败:用户名或密码错误")
|
||||
return Response(
|
||||
{'error': '用户名或密码错误'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
return error_response(
|
||||
code=ErrorCodes.UNAUTHORIZED,
|
||||
message='Invalid username or password',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
|
||||
@@ -79,7 +85,7 @@ class LogoutView(APIView):
|
||||
logout(request)
|
||||
else:
|
||||
logout(request)
|
||||
return Response({'message': '已登出'})
|
||||
return success_response()
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@@ -100,22 +106,26 @@ class MeView(APIView):
|
||||
if user_id:
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
return Response({
|
||||
'authenticated': True,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'isStaff': user.is_staff,
|
||||
'isSuperuser': user.is_superuser,
|
||||
return success_response(
|
||||
data={
|
||||
'authenticated': True,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'isStaff': user.is_staff,
|
||||
'isSuperuser': user.is_superuser,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
return Response({
|
||||
'authenticated': False,
|
||||
'user': None
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'authenticated': False,
|
||||
'user': None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@@ -134,17 +144,19 @@ class ChangePasswordView(APIView):
|
||||
|
||||
user_id = request.session.get('_auth_user_id')
|
||||
if not user_id:
|
||||
return Response(
|
||||
{'error': '请先登录'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
return error_response(
|
||||
code=ErrorCodes.UNAUTHORIZED,
|
||||
message='Please login first',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return Response(
|
||||
{'error': '用户不存在'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
return error_response(
|
||||
code=ErrorCodes.UNAUTHORIZED,
|
||||
message='User does not exist',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
# CamelCaseParser 将 oldPassword -> old_password
|
||||
@@ -152,15 +164,17 @@ class ChangePasswordView(APIView):
|
||||
new_password = request.data.get('new_password')
|
||||
|
||||
if not old_password or not new_password:
|
||||
return Response(
|
||||
{'error': '请提供旧密码和新密码'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Old password and new password are required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not user.check_password(old_password):
|
||||
return Response(
|
||||
{'error': '旧密码错误'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Old password is incorrect',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
user.set_password(new_password)
|
||||
@@ -170,4 +184,4 @@ class ChangePasswordView(APIView):
|
||||
update_session_auth_hash(request, user)
|
||||
|
||||
logger.info(f"用户 {user.username} 已修改密码")
|
||||
return Response({'message': '密码修改成功'})
|
||||
return success_response()
|
||||
|
||||
@@ -13,6 +13,8 @@ from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
from apps.common.services.system_log_service import SystemLogService
|
||||
|
||||
|
||||
@@ -61,9 +63,17 @@ class SystemLogsView(APIView):
|
||||
|
||||
# 调用服务获取日志内容
|
||||
content = self.service.get_logs_content(lines=lines)
|
||||
return Response({"content": content})
|
||||
return success_response(data={"content": content})
|
||||
except ValueError:
|
||||
return Response({"error": "lines 参数必须是整数"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='lines must be an integer',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("获取系统日志失败")
|
||||
return Response({"error": "获取系统日志失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Failed to get system logs',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@@ -31,6 +31,8 @@ from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
from apps.engine.models import NucleiTemplateRepo
|
||||
from apps.engine.serializers import NucleiTemplateRepoSerializer
|
||||
from apps.engine.services import NucleiTemplateRepoService
|
||||
@@ -107,18 +109,30 @@ class NucleiTemplateRepoViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
repo_id = int(pk) if pk is not None else None
|
||||
except (TypeError, ValueError):
|
||||
return Response({"message": "无效的仓库 ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Invalid repository ID',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 调用 Service 层
|
||||
try:
|
||||
result = self.service.refresh_repo(repo_id)
|
||||
except ValidationError as exc:
|
||||
return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(exc),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("刷新 Nuclei 模板仓库失败: %s", exc, exc_info=True)
|
||||
return Response({"message": f"刷新仓库失败: {exc}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message=f'Refresh failed: {exc}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return Response({"message": "刷新成功", "result": result}, status=status.HTTP_200_OK)
|
||||
return success_response(data={'result': result})
|
||||
|
||||
# ==================== 自定义 Action: 模板只读浏览 ====================
|
||||
|
||||
@@ -142,18 +156,30 @@ class NucleiTemplateRepoViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
repo_id = int(pk) if pk is not None else None
|
||||
except (TypeError, ValueError):
|
||||
return Response({"message": "无效的仓库 ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Invalid repository ID',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 调用 Service 层,仅从当前本地目录读取目录树
|
||||
try:
|
||||
roots = self.service.get_template_tree(repo_id)
|
||||
except ValidationError as exc:
|
||||
return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(exc),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("获取 Nuclei 模板目录树失败: %s", exc, exc_info=True)
|
||||
return Response({"message": "获取模板目录树失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Failed to get template tree',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return Response({"roots": roots})
|
||||
return success_response(data={'roots': roots})
|
||||
|
||||
@action(detail=True, methods=["get"], url_path="templates/content")
|
||||
def templates_content(self, request: Request, pk: str | None = None) -> Response:
|
||||
@@ -174,23 +200,43 @@ class NucleiTemplateRepoViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
repo_id = int(pk) if pk is not None else None
|
||||
except (TypeError, ValueError):
|
||||
return Response({"message": "无效的仓库 ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Invalid repository ID',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 解析 path 参数
|
||||
rel_path = (request.query_params.get("path", "") or "").strip()
|
||||
if not rel_path:
|
||||
return Response({"message": "缺少 path 参数"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Missing path parameter',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 调用 Service 层
|
||||
try:
|
||||
result = self.service.get_template_content(repo_id, rel_path)
|
||||
except ValidationError as exc:
|
||||
return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(exc),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("获取 Nuclei 模板内容失败: %s", exc, exc_info=True)
|
||||
return Response({"message": "获取模板内容失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Failed to get template content',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# 文件不存在
|
||||
if result is None:
|
||||
return Response({"message": "模板不存在或无法读取"}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(result)
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message='Template not found or unreadable',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return success_response(data=result)
|
||||
|
||||
@@ -9,6 +9,8 @@ from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.common.pagination import BasePagination
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
from apps.engine.serializers.wordlist_serializer import WordlistSerializer
|
||||
from apps.engine.services.wordlist_service import WordlistService
|
||||
|
||||
@@ -46,7 +48,11 @@ class WordlistViewSet(viewsets.ViewSet):
|
||||
uploaded_file = request.FILES.get("file")
|
||||
|
||||
if not uploaded_file:
|
||||
return Response({"error": "缺少字典文件"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Missing wordlist file',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
wordlist = self.service.create_wordlist(
|
||||
@@ -55,21 +61,32 @@ class WordlistViewSet(viewsets.ViewSet):
|
||||
uploaded_file=uploaded_file,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(exc),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
serializer = WordlistSerializer(wordlist)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return success_response(data=serializer.data, status_code=status.HTTP_201_CREATED)
|
||||
|
||||
def destroy(self, request, pk=None):
|
||||
"""删除字典记录"""
|
||||
try:
|
||||
wordlist_id = int(pk)
|
||||
except (TypeError, ValueError):
|
||||
return Response({"error": "无效的 ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Invalid ID',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
success = self.service.delete_wordlist(wordlist_id)
|
||||
if not success:
|
||||
return Response({"error": "字典不存在"}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -82,15 +99,27 @@ class WordlistViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
name = (request.query_params.get("wordlist", "") or "").strip()
|
||||
if not name:
|
||||
return Response({"error": "缺少参数 wordlist"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Missing parameter: wordlist',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
wordlist = self.service.get_wordlist_by_name(name)
|
||||
if not wordlist:
|
||||
return Response({"error": "字典不存在"}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message='Wordlist not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
file_path = wordlist.file_path
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return Response({"error": "字典文件不存在"}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message='Wordlist file not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
filename = os.path.basename(file_path)
|
||||
response = FileResponse(open(file_path, "rb"), as_attachment=True, filename=filename)
|
||||
@@ -106,22 +135,38 @@ class WordlistViewSet(viewsets.ViewSet):
|
||||
try:
|
||||
wordlist_id = int(pk)
|
||||
except (TypeError, ValueError):
|
||||
return Response({"error": "无效的 ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Invalid ID',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if request.method == "GET":
|
||||
content = self.service.get_wordlist_content(wordlist_id)
|
||||
if content is None:
|
||||
return Response({"error": "字典不存在或文件无法读取"}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response({"content": content})
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message='Wordlist not found or file unreadable',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return success_response(data={"content": content})
|
||||
|
||||
elif request.method == "PUT":
|
||||
content = request.data.get("content")
|
||||
if content is None:
|
||||
return Response({"error": "缺少 content 参数"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Missing content parameter',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
wordlist = self.service.update_wordlist_content(wordlist_id, content)
|
||||
if not wordlist:
|
||||
return Response({"error": "字典不存在或更新失败"}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message='Wordlist not found or update failed',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
serializer = WordlistSerializer(wordlist)
|
||||
return Response(serializer.data)
|
||||
return success_response(data=serializer.data)
|
||||
|
||||
@@ -9,6 +9,8 @@ from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
from apps.engine.serializers import WorkerNodeSerializer
|
||||
from apps.engine.services import WorkerService
|
||||
from apps.common.signals import worker_delete_failed
|
||||
@@ -111,9 +113,8 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
threading.Thread(target=_async_remote_uninstall, daemon=True).start()
|
||||
|
||||
# 3. 立即返回成功
|
||||
return Response(
|
||||
{"message": f"节点 {worker_name} 已删除"},
|
||||
status=status.HTTP_200_OK
|
||||
return success_response(
|
||||
data={'name': worker_name}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
@@ -190,11 +191,13 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
worker.status = 'online'
|
||||
worker.save(update_fields=['status'])
|
||||
|
||||
return Response({
|
||||
'status': 'ok',
|
||||
'need_update': need_update,
|
||||
'server_version': server_version
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'status': 'ok',
|
||||
'needUpdate': need_update,
|
||||
'serverVersion': server_version
|
||||
}
|
||||
)
|
||||
|
||||
def _trigger_remote_agent_update(self, worker, target_version: str):
|
||||
"""
|
||||
@@ -304,9 +307,10 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
is_local = request.data.get('is_local', True)
|
||||
|
||||
if not name:
|
||||
return Response(
|
||||
{'error': '缺少 name 参数'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Missing name parameter',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
worker, created = self.worker_service.register_worker(
|
||||
@@ -314,11 +318,13 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
is_local=is_local
|
||||
)
|
||||
|
||||
return Response({
|
||||
'worker_id': worker.id,
|
||||
'name': worker.name,
|
||||
'created': created
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'workerId': worker.id,
|
||||
'name': worker.name,
|
||||
'created': created
|
||||
}
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def config(self, request):
|
||||
@@ -380,24 +386,26 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
logger.info("返回 Worker 配置 - db_host: %s, redis_url: %s", worker_db_host, worker_redis_url)
|
||||
|
||||
return Response({
|
||||
'db': {
|
||||
'host': worker_db_host,
|
||||
'port': str(settings.DATABASES['default']['PORT']),
|
||||
'name': settings.DATABASES['default']['NAME'],
|
||||
'user': settings.DATABASES['default']['USER'],
|
||||
'password': settings.DATABASES['default']['PASSWORD'],
|
||||
},
|
||||
'redisUrl': worker_redis_url,
|
||||
'paths': {
|
||||
'results': getattr(settings, 'CONTAINER_RESULTS_MOUNT', '/opt/xingrin/results'),
|
||||
'logs': getattr(settings, 'CONTAINER_LOGS_MOUNT', '/opt/xingrin/logs'),
|
||||
},
|
||||
'logging': {
|
||||
'level': os.getenv('LOG_LEVEL', 'INFO'),
|
||||
'enableCommandLogging': os.getenv('ENABLE_COMMAND_LOGGING', 'true').lower() == 'true',
|
||||
},
|
||||
'debug': settings.DEBUG,
|
||||
# Git 加速配置(用于 Git clone 加速,如 Nuclei 模板仓库)
|
||||
'gitMirror': settings.GIT_MIRROR,
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'db': {
|
||||
'host': worker_db_host,
|
||||
'port': str(settings.DATABASES['default']['PORT']),
|
||||
'name': settings.DATABASES['default']['NAME'],
|
||||
'user': settings.DATABASES['default']['USER'],
|
||||
'password': settings.DATABASES['default']['PASSWORD'],
|
||||
},
|
||||
'redisUrl': worker_redis_url,
|
||||
'paths': {
|
||||
'results': getattr(settings, 'CONTAINER_RESULTS_MOUNT', '/opt/xingrin/results'),
|
||||
'logs': getattr(settings, 'CONTAINER_LOGS_MOUNT', '/opt/xingrin/logs'),
|
||||
},
|
||||
'logging': {
|
||||
'level': os.getenv('LOG_LEVEL', 'INFO'),
|
||||
'enableCommandLogging': os.getenv('ENABLE_COMMAND_LOGGING', 'true').lower() == 'true',
|
||||
},
|
||||
'debug': settings.DEBUG,
|
||||
# Git 加速配置(用于 Git clone 加速,如 Nuclei 模板仓库)
|
||||
'gitMirror': settings.GIT_MIRROR,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -14,6 +14,8 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.common.pagination import BasePagination
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
from .models import Notification
|
||||
from .serializers import NotificationSerializer
|
||||
from .types import NotificationLevel
|
||||
@@ -60,34 +62,7 @@ def notifications_test(request):
|
||||
}, status=500)
|
||||
|
||||
|
||||
def build_api_response(
|
||||
data: Any = None,
|
||||
*,
|
||||
message: str = '操作成功',
|
||||
code: str = '200',
|
||||
state: str = 'success',
|
||||
status_code: int = status.HTTP_200_OK
|
||||
) -> Response:
|
||||
"""构建统一的 API 响应格式
|
||||
|
||||
Args:
|
||||
data: 响应数据体(可选)
|
||||
message: 响应消息
|
||||
code: 响应代码
|
||||
state: 响应状态(success/error)
|
||||
status_code: HTTP 状态码
|
||||
|
||||
Returns:
|
||||
DRF Response 对象
|
||||
"""
|
||||
payload = {
|
||||
'code': code,
|
||||
'state': state,
|
||||
'message': message,
|
||||
}
|
||||
if data is not None:
|
||||
payload['data'] = data
|
||||
return Response(payload, status=status_code)
|
||||
# build_api_response 已废弃,请使用 success_response/error_response
|
||||
|
||||
|
||||
def _parse_bool(value: str | None) -> bool | None:
|
||||
@@ -172,7 +147,7 @@ class NotificationUnreadCountView(APIView):
|
||||
"""获取未读通知数量"""
|
||||
service = NotificationService()
|
||||
count = service.get_unread_count()
|
||||
return build_api_response({'count': count}, message='获取未读数量成功')
|
||||
return success_response(data={'count': count})
|
||||
|
||||
|
||||
class NotificationMarkAllAsReadView(APIView):
|
||||
@@ -192,7 +167,7 @@ class NotificationMarkAllAsReadView(APIView):
|
||||
"""标记全部通知为已读"""
|
||||
service = NotificationService()
|
||||
updated = service.mark_all_as_read()
|
||||
return build_api_response({'updated': updated}, message='全部标记已读成功')
|
||||
return success_response(data={'updated': updated})
|
||||
|
||||
|
||||
class NotificationSettingsView(APIView):
|
||||
@@ -209,13 +184,13 @@ class NotificationSettingsView(APIView):
|
||||
"""获取通知设置"""
|
||||
service = NotificationSettingsService()
|
||||
settings = service.get_settings()
|
||||
return Response(settings)
|
||||
return success_response(data=settings)
|
||||
|
||||
def put(self, request: Request) -> Response:
|
||||
"""更新通知设置"""
|
||||
service = NotificationSettingsService()
|
||||
settings = service.update_settings(request.data)
|
||||
return Response({'message': '已保存通知设置', **settings})
|
||||
return success_response(data=settings)
|
||||
|
||||
|
||||
# ============================================
|
||||
@@ -247,22 +222,24 @@ def notification_callback(request):
|
||||
required_fields = ['id', 'category', 'title', 'message', 'level', 'created_at']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return Response(
|
||||
{'error': f'缺少字段: {field}'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=f'Missing field: {field}',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 推送到 WebSocket
|
||||
_push_notification_to_websocket(data)
|
||||
|
||||
logger.debug(f"回调通知推送成功 - ID: {data['id']}, Title: {data['title']}")
|
||||
return Response({'status': 'ok'})
|
||||
return success_response(data={'status': 'ok'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"回调通知处理失败: {e}", exc_info=True)
|
||||
return Response(
|
||||
{'error': str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db.utils import DatabaseError, IntegrityError, OperationalError
|
||||
import logging
|
||||
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from ..models import Scan, ScheduledScan
|
||||
@@ -75,20 +78,31 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
scan_service = ScanService()
|
||||
result = scan_service.delete_scans_two_phase([scan.id])
|
||||
|
||||
return Response({
|
||||
'message': f'已删除扫描任务: Scan #{scan.id}',
|
||||
'scanId': scan.id,
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedScans': result['scan_names']
|
||||
}, status=status.HTTP_200_OK)
|
||||
return success_response(
|
||||
data={
|
||||
'scanId': scan.id,
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedScans': result['scan_names']
|
||||
}
|
||||
)
|
||||
|
||||
except Scan.DoesNotExist:
|
||||
raise NotFound('扫描任务不存在')
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except ValueError as e:
|
||||
raise NotFound(str(e))
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("删除扫描任务时发生错误")
|
||||
raise APIException('服务器错误,请稍后重试')
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def quick(self, request):
|
||||
@@ -132,10 +146,12 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
targets = result['targets']
|
||||
|
||||
if not targets:
|
||||
return Response({
|
||||
'error': '没有有效的目标可供扫描',
|
||||
'errors': result.get('errors', [])
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='No valid targets for scanning',
|
||||
details=result.get('errors', []),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 2. 获取扫描引擎
|
||||
engine_service = EngineService()
|
||||
@@ -153,21 +169,28 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
# 序列化返回结果
|
||||
scan_serializer = ScanSerializer(created_scans, many=True)
|
||||
|
||||
return Response({
|
||||
'message': f'快速扫描已启动:{len(created_scans)} 个任务',
|
||||
'target_stats': result['target_stats'],
|
||||
'asset_stats': result['asset_stats'],
|
||||
'errors': result.get('errors', []),
|
||||
'scans': scan_serializer.data
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
return success_response(
|
||||
data={
|
||||
'count': len(created_scans),
|
||||
'targetStats': result['target_stats'],
|
||||
'assetStats': result['asset_stats'],
|
||||
'errors': result.get('errors', []),
|
||||
'scans': scan_serializer.data
|
||||
},
|
||||
status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("快速扫描启动失败")
|
||||
return Response(
|
||||
{'error': '服务器内部错误,请稍后重试'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
@@ -208,35 +231,36 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
# 序列化返回结果
|
||||
scan_serializer = ScanSerializer(created_scans, many=True)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'message': f'已成功发起 {len(created_scans)} 个扫描任务',
|
||||
return success_response(
|
||||
data={
|
||||
'count': len(created_scans),
|
||||
'scans': scan_serializer.data
|
||||
},
|
||||
status=status.HTTP_201_CREATED
|
||||
status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
except ObjectDoesNotExist as e:
|
||||
# 资源不存在错误(由 service 层抛出)
|
||||
error_msg = str(e)
|
||||
return Response(
|
||||
{'error': error_msg},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
# 参数验证错误(由 service 层抛出)
|
||||
return Response(
|
||||
{'error': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
except (DatabaseError, IntegrityError, OperationalError):
|
||||
# 数据库错误
|
||||
return Response(
|
||||
{'error': '数据库错误,请稍后重试'},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Database error',
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
# 所有快照相关的 action 和 export 已迁移到 asset/views.py 中的快照 ViewSet
|
||||
@@ -278,21 +302,24 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
|
||||
# 参数验证
|
||||
if not ids:
|
||||
return Response(
|
||||
{'error': '缺少必填参数: ids'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='Missing required parameter: ids',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not isinstance(ids, list):
|
||||
return Response(
|
||||
{'error': 'ids 必须是数组'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='ids must be an array',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not all(isinstance(i, int) for i in ids):
|
||||
return Response(
|
||||
{'error': 'ids 数组中的所有元素必须是整数'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message='All elements in ids array must be integers',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -300,19 +327,27 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
scan_service = ScanService()
|
||||
result = scan_service.delete_scans_two_phase(ids)
|
||||
|
||||
return Response({
|
||||
'message': f"已删除 {result['soft_deleted_count']} 个扫描任务",
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedScans': result['scan_names']
|
||||
}, status=status.HTTP_200_OK)
|
||||
return success_response(
|
||||
data={
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedScans': result['scan_names']
|
||||
}
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# 未找到记录
|
||||
raise NotFound(str(e))
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("批量删除扫描任务时发生错误")
|
||||
raise APIException('服务器错误,请稍后重试')
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def statistics(self, request):
|
||||
@@ -337,22 +372,25 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
scan_service = ScanService()
|
||||
stats = scan_service.get_statistics()
|
||||
|
||||
return Response({
|
||||
'total': stats['total'],
|
||||
'running': stats['running'],
|
||||
'completed': stats['completed'],
|
||||
'failed': stats['failed'],
|
||||
'totalVulns': stats['total_vulns'],
|
||||
'totalSubdomains': stats['total_subdomains'],
|
||||
'totalEndpoints': stats['total_endpoints'],
|
||||
'totalWebsites': stats['total_websites'],
|
||||
'totalAssets': stats['total_assets'],
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'total': stats['total'],
|
||||
'running': stats['running'],
|
||||
'completed': stats['completed'],
|
||||
'failed': stats['failed'],
|
||||
'totalVulns': stats['total_vulns'],
|
||||
'totalSubdomains': stats['total_subdomains'],
|
||||
'totalEndpoints': stats['total_endpoints'],
|
||||
'totalWebsites': stats['total_websites'],
|
||||
'totalAssets': stats['total_assets'],
|
||||
}
|
||||
)
|
||||
|
||||
except (DatabaseError, OperationalError):
|
||||
return Response(
|
||||
{'error': '数据库错误,请稍后重试'},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Database error',
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
@@ -383,35 +421,31 @@ class ScanViewSet(viewsets.ModelViewSet):
|
||||
# 检查是否是状态不允许的问题
|
||||
scan = scan_service.get_scan(scan_id=pk, prefetch_relations=False)
|
||||
if scan and scan.status not in [ScanStatus.RUNNING, ScanStatus.INITIATED]:
|
||||
return Response(
|
||||
{
|
||||
'error': f'无法停止扫描:当前状态为 {ScanStatus(scan.status).label}',
|
||||
'detail': '只能停止运行中或初始化状态的扫描'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
return error_response(
|
||||
code=ErrorCodes.BAD_REQUEST,
|
||||
message=f'Cannot stop scan: current status is {ScanStatus(scan.status).label}',
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
# 其他失败原因
|
||||
return Response(
|
||||
{'error': '停止扫描失败'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'message': f'扫描已停止,已撤销 {revoked_count} 个任务',
|
||||
'revokedTaskCount': revoked_count
|
||||
},
|
||||
status=status.HTTP_200_OK
|
||||
return success_response(
|
||||
data={'revokedTaskCount': revoked_count}
|
||||
)
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
return Response(
|
||||
{'error': f'扫描 ID {pk} 不存在'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message=f'Scan ID {pk} not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
except (DatabaseError, IntegrityError, OperationalError):
|
||||
return Response(
|
||||
{'error': '数据库错误,请稍后重试'},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Database error',
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ from ..serializers import (
|
||||
from ..services.scheduled_scan_service import ScheduledScanService
|
||||
from ..repositories import ScheduledScanDTO
|
||||
from apps.common.pagination import BasePagination
|
||||
from apps.common.response_helpers import success_response, error_response
|
||||
from apps.common.error_codes import ErrorCodes
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -75,15 +77,16 @@ class ScheduledScanViewSet(viewsets.ModelViewSet):
|
||||
scheduled_scan = self.service.create(dto)
|
||||
response_serializer = ScheduledScanSerializer(scheduled_scan)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'message': f'创建定时扫描任务成功: {scheduled_scan.name}',
|
||||
'scheduled_scan': response_serializer.data
|
||||
},
|
||||
status=status.HTTP_201_CREATED
|
||||
return success_response(
|
||||
data=response_serializer.data,
|
||||
status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
except ValidationError as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""更新定时扫描任务"""
|
||||
@@ -105,24 +108,27 @@ class ScheduledScanViewSet(viewsets.ModelViewSet):
|
||||
scheduled_scan = self.service.update(instance.id, dto)
|
||||
response_serializer = ScheduledScanSerializer(scheduled_scan)
|
||||
|
||||
return Response({
|
||||
'message': f'更新定时扫描任务成功: {scheduled_scan.name}',
|
||||
'scheduled_scan': response_serializer.data
|
||||
})
|
||||
return success_response(data=response_serializer.data)
|
||||
except ValidationError as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
code=ErrorCodes.VALIDATION_ERROR,
|
||||
message=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""删除定时扫描任务"""
|
||||
instance = self.get_object()
|
||||
scan_id = instance.id
|
||||
name = instance.name
|
||||
|
||||
if self.service.delete(instance.id):
|
||||
return Response({
|
||||
'message': f'删除定时扫描任务成功: {name}',
|
||||
'id': instance.id
|
||||
})
|
||||
return Response({'error': '删除失败'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
if self.service.delete(scan_id):
|
||||
return success_response(data={'id': scan_id, 'name': name})
|
||||
return error_response(
|
||||
code=ErrorCodes.SERVER_ERROR,
|
||||
message='Failed to delete scheduled scan',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def toggle(self, request, pk=None):
|
||||
@@ -136,14 +142,11 @@ class ScheduledScanViewSet(viewsets.ModelViewSet):
|
||||
scheduled_scan = self.get_object()
|
||||
response_serializer = ScheduledScanSerializer(scheduled_scan)
|
||||
|
||||
status_text = '启用' if is_enabled else '禁用'
|
||||
return Response({
|
||||
'message': f'已{status_text}定时扫描任务',
|
||||
'scheduled_scan': response_serializer.data
|
||||
})
|
||||
return success_response(data=response_serializer.data)
|
||||
|
||||
return Response(
|
||||
{'error': f'定时扫描任务 ID {pk} 不存在或操作失败'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
return error_response(
|
||||
code=ErrorCodes.NOT_FOUND,
|
||||
message=f'Scheduled scan with ID {pk} not found or operation failed',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@@ -95,8 +95,9 @@ class OrganizationViewSet(viewsets.ModelViewSet):
|
||||
organization.targets.remove(*existing_target_ids)
|
||||
|
||||
return Response({
|
||||
'unlinked_count': existing_count,
|
||||
'message': f'成功解除 {existing_count} 个目标的关联'
|
||||
'data': {
|
||||
'unlinkedCount': existing_count
|
||||
}
|
||||
})
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
@@ -125,11 +126,12 @@ class OrganizationViewSet(viewsets.ModelViewSet):
|
||||
result = self.org_service.delete_organizations_two_phase([organization.id])
|
||||
|
||||
return Response({
|
||||
'message': f'已删除组织: {organization.name}',
|
||||
'organizationId': organization.id,
|
||||
'organizationName': organization.name,
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedOrganizations': result['organization_names']
|
||||
'data': {
|
||||
'organizationId': organization.id,
|
||||
'organizationName': organization.name,
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedOrganizations': result['organization_names']
|
||||
}
|
||||
}, status=200)
|
||||
|
||||
except Organization.DoesNotExist:
|
||||
@@ -182,9 +184,10 @@ class OrganizationViewSet(viewsets.ModelViewSet):
|
||||
result = self.org_service.delete_organizations_two_phase(ids)
|
||||
|
||||
return Response({
|
||||
'message': f"已删除 {result['soft_deleted_count']} 个组织",
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedOrganizations': result['organization_names']
|
||||
'data': {
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedOrganizations': result['organization_names']
|
||||
}
|
||||
}, status=200)
|
||||
|
||||
except ValueError as e:
|
||||
@@ -272,10 +275,11 @@ class TargetViewSet(viewsets.ModelViewSet):
|
||||
result = self.target_service.delete_targets_two_phase([target.id])
|
||||
|
||||
return Response({
|
||||
'message': f'已删除目标: {target.name}',
|
||||
'targetId': target.id,
|
||||
'targetName': target.name,
|
||||
'deletedCount': result['soft_deleted_count']
|
||||
'data': {
|
||||
'targetId': target.id,
|
||||
'targetName': target.name,
|
||||
'deletedCount': result['soft_deleted_count']
|
||||
}
|
||||
}, status=200)
|
||||
|
||||
except Target.DoesNotExist:
|
||||
@@ -331,9 +335,10 @@ class TargetViewSet(viewsets.ModelViewSet):
|
||||
result = self.target_service.delete_targets_two_phase(ids)
|
||||
|
||||
return Response({
|
||||
'message': f"已删除 {result['soft_deleted_count']} 个目标",
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedTargets': result['target_names']
|
||||
'data': {
|
||||
'deletedCount': result['soft_deleted_count'],
|
||||
'deletedTargets': result['target_names']
|
||||
}
|
||||
}, status=200)
|
||||
|
||||
except ValueError as e:
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { toast } from 'sonner'
|
||||
import { login, logout, getMe, changePassword } from '@/services/auth.service'
|
||||
import { getErrorMessage } from '@/lib/api-client'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import type { LoginRequest, ChangePasswordRequest } from '@/types/auth.types'
|
||||
|
||||
/**
|
||||
@@ -30,16 +30,22 @@ export function useAuth() {
|
||||
export function useLogin() {
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: LoginRequest) => login(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['auth', 'me'] })
|
||||
toast.success('Login successful')
|
||||
toastMessages.success('toast.auth.login.success')
|
||||
router.push('/dashboard/')
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('auth.loginFailed')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -50,16 +56,22 @@ export function useLogin() {
|
||||
export function useLogout() {
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: logout,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['auth', 'me'] })
|
||||
toast.success('Logged out')
|
||||
toastMessages.success('toast.auth.logout.success')
|
||||
router.push('/login/')
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('errors.unknown')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -68,13 +80,20 @@ export function useLogout() {
|
||||
* Change password
|
||||
*/
|
||||
export function useChangePassword() {
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: ChangePasswordRequest) => changePassword(data),
|
||||
onSuccess: () => {
|
||||
toast.success('Password changed successfully')
|
||||
toastMessages.success('toast.auth.changePassword.success')
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.auth.changePassword.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
GetCommandsResponse,
|
||||
Command,
|
||||
} from "@/types/command.types"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
|
||||
// Mock data
|
||||
const MOCK_COMMANDS: Command[] = [
|
||||
@@ -273,16 +274,17 @@ export function useCommand(id: number) {
|
||||
*/
|
||||
export function useCreateCommand() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateCommandRequest) => CommandService.createCommand(data),
|
||||
onSuccess: () => {
|
||||
toast.success("Command created successfully")
|
||||
toastMessages.success('toast.command.create.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["commands"] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Failed to create command:", error)
|
||||
toast.error("Failed to create command")
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.command.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -292,18 +294,19 @@ export function useCreateCommand() {
|
||||
*/
|
||||
export function useUpdateCommand() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateCommandRequest }) =>
|
||||
CommandService.updateCommand(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success("Command updated successfully")
|
||||
toastMessages.success('toast.command.update.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["commands"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["command"] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Failed to update command:", error)
|
||||
toast.error("Failed to update command")
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.command.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -313,16 +316,17 @@ export function useUpdateCommand() {
|
||||
*/
|
||||
export function useDeleteCommand() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => CommandService.deleteCommand(id),
|
||||
onSuccess: () => {
|
||||
toast.success("Command deleted successfully")
|
||||
toastMessages.success('toast.command.delete.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["commands"] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Failed to delete command:", error)
|
||||
toast.error("Failed to delete command")
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.command.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -332,6 +336,7 @@ export function useDeleteCommand() {
|
||||
*/
|
||||
export function useBatchDeleteCommands() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (ids: number[]) => {
|
||||
@@ -351,12 +356,12 @@ export function useBatchDeleteCommands() {
|
||||
}
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.success(`Successfully deleted ${response.data?.deletedCount} commands`)
|
||||
toastMessages.success('toast.command.delete.bulkSuccess', { count: response.data?.deletedCount || 0 })
|
||||
queryClient.invalidateQueries({ queryKey: ["commands"] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Failed to batch delete commands:", error)
|
||||
toast.error("Failed to batch delete commands")
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.command.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { DirectoryService } from '@/services/directory.service'
|
||||
import type { Directory, DirectoryListResponse } from '@/types/directory.types'
|
||||
|
||||
@@ -108,28 +109,25 @@ export function useScanDirectories(
|
||||
// 删除单个目录(使用单独的 DELETE API)
|
||||
export function useDeleteDirectory() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: directoryService.deleteDirectory,
|
||||
onMutate: (id) => {
|
||||
toast.loading('正在删除目录...', { id: `delete-directory-${id}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-directory-${id}`)
|
||||
},
|
||||
onSuccess: (response, id) => {
|
||||
toast.dismiss(`delete-directory-${id}`)
|
||||
toastMessages.dismiss(`delete-directory-${id}`)
|
||||
toastMessages.success('toast.asset.directory.delete.success')
|
||||
|
||||
// 显示删除成功信息
|
||||
const { directoryUrl } = response
|
||||
toast.success(`目录 "${directoryUrl}" 已成功删除`)
|
||||
|
||||
// 刷新相关查询
|
||||
queryClient.invalidateQueries({ queryKey: ['target-directories'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-directories'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
},
|
||||
onError: (error: Error, id) => {
|
||||
toast.dismiss(`delete-directory-${id}`)
|
||||
toast.error(error.message || '删除目录失败')
|
||||
onError: (error: any, id) => {
|
||||
toastMessages.dismiss(`delete-directory-${id}`)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.directory.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -137,25 +135,25 @@ export function useDeleteDirectory() {
|
||||
// 批量删除目录(使用统一的批量删除接口)
|
||||
export function useBulkDeleteDirectories() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: directoryService.bulkDeleteDirectories,
|
||||
onMutate: () => {
|
||||
toast.loading('正在批量删除目录...', { id: 'bulk-delete-directories' })
|
||||
toastMessages.loading('common.status.batchDeleting', {}, 'bulk-delete-directories')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('bulk-delete-directories')
|
||||
toast.success(`成功删除 ${response.deletedCount} 个目录`)
|
||||
toastMessages.dismiss('bulk-delete-directories')
|
||||
toastMessages.success('toast.asset.directory.delete.bulkSuccess', { count: response.deletedCount })
|
||||
|
||||
// 刷新相关查询
|
||||
queryClient.invalidateQueries({ queryKey: ['target-directories'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-directories'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.dismiss('bulk-delete-directories')
|
||||
toast.error(error.message || '批量删除目录失败')
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('bulk-delete-directories')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.directory.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -164,32 +162,31 @@ export function useBulkDeleteDirectories() {
|
||||
// 批量创建目录(绑定到目标)
|
||||
export function useBulkCreateDirectories() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { targetId: number; urls: string[] }) =>
|
||||
DirectoryService.bulkCreateDirectories(data.targetId, data.urls),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在批量创建目录...', { id: 'bulk-create-directories' })
|
||||
toastMessages.loading('common.status.batchCreating', {}, 'bulk-create-directories')
|
||||
},
|
||||
onSuccess: (response, { targetId }) => {
|
||||
toast.dismiss('bulk-create-directories')
|
||||
toastMessages.dismiss('bulk-create-directories')
|
||||
const { createdCount } = response
|
||||
|
||||
if (createdCount > 0) {
|
||||
toast.success(`成功创建 ${createdCount} 个目录`)
|
||||
toastMessages.success('toast.asset.directory.create.success', { count: createdCount })
|
||||
} else {
|
||||
toast.warning('没有新目录被创建(可能已存在)')
|
||||
toastMessages.warning('toast.asset.directory.create.partialSuccess', { success: 0, skipped: 0 })
|
||||
}
|
||||
|
||||
// 刷新目录列表
|
||||
queryClient.invalidateQueries({ queryKey: ['target-directories', targetId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-directories'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('bulk-create-directories')
|
||||
console.error('批量创建目录失败:', error)
|
||||
const errorMessage = error?.response?.data?.error || '批量创建失败,请查看控制台日志'
|
||||
toast.error(errorMessage)
|
||||
toastMessages.dismiss('bulk-create-directories')
|
||||
console.error('Failed to bulk create directories:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.directory.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { EndpointService } from "@/services/endpoint.service"
|
||||
import type {
|
||||
Endpoint,
|
||||
@@ -111,45 +112,35 @@ export function useScanEndpoints(scanId: number, params?: Omit<GetEndpointsReque
|
||||
// 创建 Endpoint(完全自动化)
|
||||
export function useCreateEndpoint() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: {
|
||||
endpoints: Array<CreateEndpointRequest>
|
||||
}) => EndpointService.createEndpoints(data),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在创建端点...', { id: 'create-endpoint' })
|
||||
toastMessages.loading('common.status.creating', {}, 'create-endpoint')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss('create-endpoint')
|
||||
toastMessages.dismiss('create-endpoint')
|
||||
|
||||
const { createdCount, existedCount } = response
|
||||
|
||||
// 打印后端响应
|
||||
console.log('创建端点成功')
|
||||
console.log('后端响应:', response)
|
||||
|
||||
// 前端自己构造成功提示消息
|
||||
if (existedCount > 0) {
|
||||
toast.warning(
|
||||
`成功创建 ${createdCount} 个端点(${existedCount} 个已存在)`
|
||||
)
|
||||
toastMessages.warning('toast.asset.endpoint.create.partialSuccess', {
|
||||
success: createdCount,
|
||||
skipped: existedCount
|
||||
})
|
||||
} else {
|
||||
toast.success(`成功创建 ${createdCount} 个端点`)
|
||||
toastMessages.success('toast.asset.endpoint.create.success', { count: createdCount })
|
||||
}
|
||||
|
||||
// 刷新所有端点相关查询(通配符匹配)
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoints'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss('create-endpoint')
|
||||
|
||||
console.error('创建端点失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('创建端点失败,请查看控制台日志')
|
||||
toastMessages.dismiss('create-endpoint')
|
||||
console.error('Failed to create endpoint:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.endpoint.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -157,31 +148,22 @@ export function useCreateEndpoint() {
|
||||
// 删除单个 Endpoint
|
||||
export function useDeleteEndpoint() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => EndpointService.deleteEndpoint(id),
|
||||
onMutate: (id) => {
|
||||
toast.loading('正在删除端点...', { id: `delete-endpoint-${id}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-endpoint-${id}`)
|
||||
},
|
||||
onSuccess: (response, id) => {
|
||||
toast.dismiss(`delete-endpoint-${id}`)
|
||||
|
||||
// 打印后端响应
|
||||
console.log('删除端点成功')
|
||||
|
||||
toast.success('删除成功')
|
||||
|
||||
// 刷新所有端点相关查询(通配符匹配)
|
||||
toastMessages.dismiss(`delete-endpoint-${id}`)
|
||||
toastMessages.success('toast.asset.endpoint.delete.success')
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoints'] })
|
||||
},
|
||||
onError: (error: any, id) => {
|
||||
toast.dismiss(`delete-endpoint-${id}`)
|
||||
|
||||
console.error('删除端点失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('删除端点失败,请查看控制台日志')
|
||||
toastMessages.dismiss(`delete-endpoint-${id}`)
|
||||
console.error('Failed to delete endpoint:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.endpoint.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -189,33 +171,23 @@ export function useDeleteEndpoint() {
|
||||
// 批量删除 Endpoint
|
||||
export function useBatchDeleteEndpoints() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: BatchDeleteEndpointsRequest) => EndpointService.batchDeleteEndpoints(data),
|
||||
onMutate: () => {
|
||||
toast.loading('正在批量删除端点...', { id: 'batch-delete-endpoints' })
|
||||
toastMessages.loading('common.status.batchDeleting', {}, 'batch-delete-endpoints')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('batch-delete-endpoints')
|
||||
|
||||
// 打印后端响应
|
||||
console.log('批量删除端点成功')
|
||||
console.log('后端响应:', response)
|
||||
|
||||
toastMessages.dismiss('batch-delete-endpoints')
|
||||
const { deletedCount } = response
|
||||
toast.success(`成功删除 ${deletedCount} 个端点`)
|
||||
|
||||
// 刷新所有端点相关查询(通配符匹配)
|
||||
toastMessages.success('toast.asset.endpoint.delete.bulkSuccess', { count: deletedCount })
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoints'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('batch-delete-endpoints')
|
||||
|
||||
console.error('批量删除端点失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('批量删除失败,请查看控制台日志')
|
||||
toastMessages.dismiss('batch-delete-endpoints')
|
||||
console.error('Failed to batch delete endpoints:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.endpoint.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -223,32 +195,31 @@ export function useBatchDeleteEndpoints() {
|
||||
// 批量创建端点(绑定到目标)
|
||||
export function useBulkCreateEndpoints() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { targetId: number; urls: string[] }) =>
|
||||
EndpointService.bulkCreateEndpoints(data.targetId, data.urls),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在批量创建端点...', { id: 'bulk-create-endpoints' })
|
||||
toastMessages.loading('common.status.batchCreating', {}, 'bulk-create-endpoints')
|
||||
},
|
||||
onSuccess: (response, { targetId }) => {
|
||||
toast.dismiss('bulk-create-endpoints')
|
||||
toastMessages.dismiss('bulk-create-endpoints')
|
||||
const { createdCount } = response
|
||||
|
||||
if (createdCount > 0) {
|
||||
toast.success(`成功创建 ${createdCount} 个端点`)
|
||||
toastMessages.success('toast.asset.endpoint.create.success', { count: createdCount })
|
||||
} else {
|
||||
toast.warning('没有新端点被创建(可能已存在)')
|
||||
toastMessages.warning('toast.asset.endpoint.create.partialSuccess', { success: 0, skipped: 0 })
|
||||
}
|
||||
|
||||
// 刷新端点列表
|
||||
queryClient.invalidateQueries({ queryKey: endpointKeys.byTarget(targetId, {}) })
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoints'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('bulk-create-endpoints')
|
||||
console.error('批量创建端点失败:', error)
|
||||
const errorMessage = error?.response?.data?.error || '批量创建失败,请查看控制台日志'
|
||||
toast.error(errorMessage)
|
||||
toastMessages.dismiss('bulk-create-endpoints')
|
||||
console.error('Failed to bulk create endpoints:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.endpoint.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import {
|
||||
getEngines,
|
||||
getEngine,
|
||||
@@ -35,17 +36,16 @@ export function useEngine(id: number) {
|
||||
*/
|
||||
export function useCreateEngine() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createEngine,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['engines'] })
|
||||
toast.success('Engine created successfully')
|
||||
toastMessages.success('toast.engine.create.success')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error('Failed to create engine', {
|
||||
description: error?.response?.data?.error || error.message,
|
||||
})
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.engine.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -55,6 +55,7 @@ export function useCreateEngine() {
|
||||
*/
|
||||
export function useUpdateEngine() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Parameters<typeof updateEngine>[1] }) =>
|
||||
@@ -62,12 +63,10 @@ export function useUpdateEngine() {
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['engines'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['engines', variables.id] })
|
||||
toast.success('Engine updated successfully')
|
||||
toastMessages.success('toast.engine.update.success')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error('Failed to update engine', {
|
||||
description: error?.response?.data?.error || error.message,
|
||||
})
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.engine.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -77,17 +76,16 @@ export function useUpdateEngine() {
|
||||
*/
|
||||
export function useDeleteEngine() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteEngine,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['engines'] })
|
||||
toast.success('Engine deleted successfully')
|
||||
toastMessages.success('toast.engine.delete.success')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error('Failed to delete engine', {
|
||||
description: error?.response?.data?.error || error.message,
|
||||
})
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.engine.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { NotificationSettingsService } from '@/services/notification-settings.service'
|
||||
import type { UpdateNotificationSettingsRequest } from '@/types/notification-settings.types'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
|
||||
export function useNotificationSettings() {
|
||||
return useQuery({
|
||||
@@ -12,15 +13,17 @@ export function useNotificationSettings() {
|
||||
|
||||
export function useUpdateNotificationSettings() {
|
||||
const qc = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateNotificationSettingsRequest) =>
|
||||
NotificationSettingsService.updateSettings(data),
|
||||
onSuccess: (res) => {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['notification-settings'] })
|
||||
toast.success(res?.message || 'Notification settings saved')
|
||||
toastMessages.success('toast.notification.settings.success')
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Save failed, please try again')
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.notification.settings.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import type { BackendNotification, Notification, BackendNotificationLevel, NotificationSeverity } from '@/types/notification.types'
|
||||
import { getBackendBaseUrl } from '@/lib/env'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
|
||||
const severityMap: Record<BackendNotificationLevel, NotificationSeverity> = {
|
||||
critical: 'critical',
|
||||
@@ -70,6 +70,7 @@ export function useNotificationSSE() {
|
||||
const reconnectAttempts = useRef(0)
|
||||
const maxReconnectAttempts = 10
|
||||
const baseReconnectDelay = 1000 // 1秒
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
const markNotificationsAsRead = useCallback((ids?: number[]) => {
|
||||
setNotifications(prev => prev.map(notification => {
|
||||
@@ -176,7 +177,7 @@ export function useNotificationSSE() {
|
||||
|
||||
if (data.type === 'error') {
|
||||
console.error('[ERROR] WebSocket 错误:', data.message)
|
||||
toast.error(`通知连接错误: ${data.message}`)
|
||||
toastMessages.error('toast.notification.connection.error', { message: data.message })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,7 +259,7 @@ export function useNotificationSSE() {
|
||||
setIsConnected(false)
|
||||
isConnectingRef.current = false
|
||||
}
|
||||
}, [queryClient, startHeartbeat, stopHeartbeat, getReconnectDelay])
|
||||
}, [queryClient, startHeartbeat, stopHeartbeat, getReconnectDelay, toastMessages])
|
||||
|
||||
// 断开连接
|
||||
const disconnect = useCallback(() => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { NucleiGitService } from "@/services/nuclei-git.service"
|
||||
import type { UpdateNucleiGitSettingsRequest } from "@/types/nuclei-git.types"
|
||||
|
||||
@@ -14,15 +15,16 @@ export function useNucleiGitSettings() {
|
||||
|
||||
export function useUpdateNucleiGitSettings() {
|
||||
const qc = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateNucleiGitSettingsRequest) => NucleiGitService.updateSettings(data),
|
||||
onSuccess: (res) => {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["nuclei", "git", "settings"] })
|
||||
toast.success(res?.message || "Git repository configuration saved")
|
||||
toastMessages.success('toast.nucleiGit.settings.success')
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to save Git repository configuration, please try again")
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiGit.settings.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { nucleiRepoApi } from "../services/nuclei-repo.api"
|
||||
import { getErrorMessage } from "@/lib/api-client"
|
||||
import type { NucleiTemplateTreeNode, NucleiTemplateContent } from "@/types/nuclei.types"
|
||||
|
||||
// ==================== 仓库 CRUD ====================
|
||||
@@ -41,15 +41,16 @@ export function useNucleiRepo(repoId: number | null) {
|
||||
/** 创建仓库 */
|
||||
export function useCreateNucleiRepo() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: nucleiRepoApi.createRepo,
|
||||
onSuccess: (data) => {
|
||||
toast.success(`仓库「${data.name}」创建成功`)
|
||||
onSuccess: () => {
|
||||
toastMessages.success('toast.nucleiRepo.create.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiRepo.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -57,6 +58,7 @@ export function useCreateNucleiRepo() {
|
||||
/** 更新仓库 */
|
||||
export function useUpdateNucleiRepo() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: {
|
||||
@@ -64,12 +66,12 @@ export function useUpdateNucleiRepo() {
|
||||
repoUrl?: string
|
||||
}) => nucleiRepoApi.updateRepo(data.id, { repoUrl: data.repoUrl }),
|
||||
onSuccess: (_data, variables) => {
|
||||
toast.success("仓库配置已更新")
|
||||
toastMessages.success('toast.nucleiRepo.update.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repos", variables.id] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiRepo.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -77,15 +79,16 @@ export function useUpdateNucleiRepo() {
|
||||
/** 删除仓库 */
|
||||
export function useDeleteNucleiRepo() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: nucleiRepoApi.deleteRepo,
|
||||
onSuccess: () => {
|
||||
toast.success("仓库已删除")
|
||||
toastMessages.success('toast.nucleiRepo.delete.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiRepo.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -95,18 +98,18 @@ export function useDeleteNucleiRepo() {
|
||||
/** 刷新仓库(Git clone/pull) */
|
||||
export function useRefreshNucleiRepo() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: nucleiRepoApi.refreshRepo,
|
||||
onSuccess: (_data, repoId) => {
|
||||
toast.success("仓库同步成功")
|
||||
// 刷新仓库列表(last_synced_at 会更新)
|
||||
toastMessages.success('toast.nucleiRepo.sync.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repos"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repos", repoId] })
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei-repo-tree", repoId] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error))
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiRepo.sync.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { getNucleiTemplateTree, getNucleiTemplateContent, refreshNucleiTemplates, saveNucleiTemplate, uploadNucleiTemplate } from "@/services/nuclei.service"
|
||||
import type { NucleiTemplateTreeNode, NucleiTemplateContent, UploadNucleiTemplatePayload, SaveNucleiTemplatePayload } from "@/types/nuclei.types"
|
||||
|
||||
@@ -22,60 +23,63 @@ export function useNucleiTemplateContent(path: string | null) {
|
||||
|
||||
export function useRefreshNucleiTemplates() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => refreshNucleiTemplates(),
|
||||
onMutate: () => {
|
||||
toast.loading("正在更新 Nuclei 官方模板...", { id: "refresh-nuclei-templates" })
|
||||
toastMessages.loading('toast.nucleiTemplate.refresh.loading', {}, 'refresh-nuclei-templates')
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.dismiss("refresh-nuclei-templates")
|
||||
toast.success("模板更新完成")
|
||||
toastMessages.dismiss('refresh-nuclei-templates')
|
||||
toastMessages.success('toast.nucleiTemplate.refresh.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei", "templates", "tree"] })
|
||||
},
|
||||
onError: () => {
|
||||
toast.dismiss("refresh-nuclei-templates")
|
||||
toast.error("模板更新失败")
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('refresh-nuclei-templates')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiTemplate.refresh.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUploadNucleiTemplate() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation<void, Error, UploadNucleiTemplatePayload>({
|
||||
mutationFn: (payload) => uploadNucleiTemplate(payload),
|
||||
onMutate: () => {
|
||||
toast.loading("正在上传模板...", { id: "upload-nuclei-template" })
|
||||
toastMessages.loading('common.status.uploading', {}, 'upload-nuclei-template')
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.dismiss("upload-nuclei-template")
|
||||
toast.success("模板上传成功")
|
||||
toastMessages.dismiss('upload-nuclei-template')
|
||||
toastMessages.success('toast.nucleiTemplate.upload.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei", "templates", "tree"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.dismiss("upload-nuclei-template")
|
||||
toast.error(error.message || "模板上传失败")
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('upload-nuclei-template')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiTemplate.upload.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSaveNucleiTemplate() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation<void, Error, SaveNucleiTemplatePayload>({
|
||||
mutationFn: (payload) => saveNucleiTemplate(payload),
|
||||
onMutate: () => {
|
||||
toast.loading("正在保存模板...", { id: "save-nuclei-template" })
|
||||
toastMessages.loading('common.actions.saving', {}, 'save-nuclei-template')
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
toast.dismiss("save-nuclei-template")
|
||||
toast.success("模板保存成功")
|
||||
toastMessages.dismiss('save-nuclei-template')
|
||||
toastMessages.success('toast.nucleiTemplate.save.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["nuclei", "templates", "content", variables.path] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.dismiss("save-nuclei-template")
|
||||
toast.error(error.message || "模板保存失败")
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('save-nuclei-template')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.nucleiTemplate.save.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { OrganizationService } from '@/services/organization.service'
|
||||
import type { Organization, CreateOrganizationRequest, UpdateOrganizationRequest } from '@/types/organization.types'
|
||||
|
||||
@@ -107,33 +108,23 @@ export function useOrganizationTargets(
|
||||
*/
|
||||
export function useCreateOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateOrganizationRequest) =>
|
||||
OrganizationService.createOrganization(data),
|
||||
onMutate: () => {
|
||||
// Show creation start notification
|
||||
toast.loading('Creating organization...', { id: 'create-organization' })
|
||||
toastMessages.loading('common.status.creating', {}, 'create-organization')
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Close loading notification
|
||||
toast.dismiss('create-organization')
|
||||
|
||||
// Refresh all organization-related queries (wildcard matching)
|
||||
toastMessages.dismiss('create-organization')
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
|
||||
// Show success notification
|
||||
toast.success('Created successfully')
|
||||
toastMessages.success('toast.organization.create.success')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
// Close loading notification
|
||||
toast.dismiss('create-organization')
|
||||
|
||||
toastMessages.dismiss('create-organization')
|
||||
console.error('Failed to create organization:', error)
|
||||
console.error('Backend response:', error?.response?.data || error)
|
||||
|
||||
// Frontend constructs error message
|
||||
toast.error('Failed to create organization, please check console logs')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.organization.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -143,33 +134,23 @@ export function useCreateOrganization() {
|
||||
*/
|
||||
export function useUpdateOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateOrganizationRequest }) =>
|
||||
OrganizationService.updateOrganization({ id, ...data }),
|
||||
onMutate: ({ id, data }) => {
|
||||
// 显示更新开始的提示
|
||||
toast.loading('正在更新组织...', { id: `update-${id}` })
|
||||
onMutate: ({ id }) => {
|
||||
toastMessages.loading('common.status.updating', {}, `update-${id}`)
|
||||
},
|
||||
onSuccess: ({ id }) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss(`update-${id}`)
|
||||
|
||||
// 刷新所有组织相关查询(通配符匹配)
|
||||
toastMessages.dismiss(`update-${id}`)
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
|
||||
// 显示成功提示
|
||||
toast.success('更新成功')
|
||||
toastMessages.success('toast.organization.update.success')
|
||||
},
|
||||
onError: (error: any, { id }) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss(`update-${id}`)
|
||||
|
||||
console.error('更新组织失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('更新组织失败,请查看控制台日志')
|
||||
toastMessages.dismiss(`update-${id}`)
|
||||
console.error('Failed to update organization:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.organization.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -179,20 +160,16 @@ export function useUpdateOrganization() {
|
||||
*/
|
||||
export function useDeleteOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => OrganizationService.deleteOrganization(id),
|
||||
onMutate: async (deletedId) => {
|
||||
// 显示删除开始的提示
|
||||
toast.loading('正在删除组织...', { id: `delete-${deletedId}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-${deletedId}`)
|
||||
|
||||
// 取消正在进行的查询
|
||||
await queryClient.cancelQueries({ queryKey: ['organizations'] })
|
||||
|
||||
// 获取当前数据作为备份
|
||||
const previousData = queryClient.getQueriesData({ queryKey: ['organizations'] })
|
||||
|
||||
// 乐观更新:从所有列表查询中移除该组织
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ['organizations'] },
|
||||
(old: any) => {
|
||||
@@ -206,38 +183,27 @@ export function useDeleteOrganization() {
|
||||
}
|
||||
)
|
||||
|
||||
// 返回备份数据用于回滚
|
||||
return { previousData, deletedId }
|
||||
},
|
||||
onSuccess: (response, deletedId, context) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss(`delete-${deletedId}`)
|
||||
|
||||
// 显示删除成功信息
|
||||
onSuccess: (response, deletedId) => {
|
||||
toastMessages.dismiss(`delete-${deletedId}`)
|
||||
const { organizationName } = response
|
||||
toast.success(`组织 "${organizationName}" 已成功删除`)
|
||||
toastMessages.success('toast.organization.delete.success', { name: organizationName })
|
||||
},
|
||||
onError: (error: any, deletedId, context) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss(`delete-${deletedId}`)
|
||||
toastMessages.dismiss(`delete-${deletedId}`)
|
||||
|
||||
// 回滚乐观更新
|
||||
if (context?.previousData) {
|
||||
context.previousData.forEach(([queryKey, data]) => {
|
||||
queryClient.setQueryData(queryKey, data)
|
||||
})
|
||||
}
|
||||
|
||||
console.error('删除组织失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('删除组织失败,请查看控制台日志')
|
||||
console.error('Failed to delete organization:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.organization.delete.error')
|
||||
},
|
||||
onSettled: () => {
|
||||
// 无论成功失败都刷新数据
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
// 刷新目标查询,因为删除组织会解除目标的关联关系,需要更新目标的 organizations 字段
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
},
|
||||
})
|
||||
@@ -248,21 +214,17 @@ export function useDeleteOrganization() {
|
||||
*/
|
||||
export function useBatchDeleteOrganizations() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (ids: number[]) =>
|
||||
OrganizationService.batchDeleteOrganizations(ids),
|
||||
onMutate: async (deletedIds) => {
|
||||
// 显示批量删除开始的提示
|
||||
toast.loading('正在批量删除组织...', { id: 'batch-delete' })
|
||||
toastMessages.loading('common.status.batchDeleting', {}, 'batch-delete')
|
||||
|
||||
// 取消正在进行的查询
|
||||
await queryClient.cancelQueries({ queryKey: ['organizations'] })
|
||||
|
||||
// 获取当前数据作为备份
|
||||
const previousData = queryClient.getQueriesData({ queryKey: ['organizations'] })
|
||||
|
||||
// 乐观更新:从所有列表查询中移除这些组织
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ['organizations'] },
|
||||
(old: any) => {
|
||||
@@ -278,42 +240,27 @@ export function useBatchDeleteOrganizations() {
|
||||
}
|
||||
)
|
||||
|
||||
// 返回备份数据用于回滚
|
||||
return { previousData, deletedIds }
|
||||
},
|
||||
onSuccess: (response, deletedIds) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss('batch-delete')
|
||||
|
||||
// 打印后端响应
|
||||
console.log('批量删除组织成功')
|
||||
console.log('后端响应:', response)
|
||||
|
||||
// 显示删除成功信息
|
||||
onSuccess: (response) => {
|
||||
toastMessages.dismiss('batch-delete')
|
||||
const { deletedOrganizationCount } = response
|
||||
toast.success(`成功删除 ${deletedOrganizationCount} 个组织`)
|
||||
toastMessages.success('toast.organization.delete.bulkSuccess', { count: deletedOrganizationCount })
|
||||
},
|
||||
onError: (error: any, deletedIds, context) => {
|
||||
// 关闭加载提示
|
||||
toast.dismiss('batch-delete')
|
||||
toastMessages.dismiss('batch-delete')
|
||||
|
||||
// 回滚乐观更新
|
||||
if (context?.previousData) {
|
||||
context.previousData.forEach(([queryKey, data]) => {
|
||||
queryClient.setQueryData(queryKey, data)
|
||||
})
|
||||
}
|
||||
|
||||
console.error('批量删除组织失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('批量删除失败,请查看控制台日志')
|
||||
console.error('Failed to batch delete organizations:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.organization.delete.error')
|
||||
},
|
||||
onSettled: () => {
|
||||
// 无论成功失败都刷新数据
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
// 刷新目标查询,因为删除组织会解除目标的关联关系,需要更新目标的 organizations 字段
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
},
|
||||
})
|
||||
@@ -326,29 +273,25 @@ export function useBatchDeleteOrganizations() {
|
||||
*/
|
||||
export function useUnlinkTargetsFromOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { organizationId: number; targetIds: number[] }) =>
|
||||
OrganizationService.unlinkTargetsFromOrganization(data),
|
||||
onMutate: ({ organizationId, targetIds }) => {
|
||||
toast.loading('正在解除关联...', { id: `unlink-${organizationId}` })
|
||||
onMutate: ({ organizationId }) => {
|
||||
toastMessages.loading('common.status.unlinking', {}, `unlink-${organizationId}`)
|
||||
},
|
||||
onSuccess: (response, { organizationId }) => {
|
||||
toast.dismiss(`unlink-${organizationId}`)
|
||||
toast.success(response.message || '已成功解除关联')
|
||||
onSuccess: (response, { organizationId, targetIds }) => {
|
||||
toastMessages.dismiss(`unlink-${organizationId}`)
|
||||
toastMessages.success('toast.target.unlink.bulkSuccess', { count: targetIds.length })
|
||||
|
||||
// 刷新所有目标和组织相关查询
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: any, { organizationId }) => {
|
||||
toast.dismiss(`unlink-${organizationId}`)
|
||||
|
||||
console.error('解除关联失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
const errorMessage = error?.response?.data?.error || '解除关联失败'
|
||||
toast.error(errorMessage)
|
||||
toastMessages.dismiss(`unlink-${organizationId}`)
|
||||
console.error('Failed to unlink targets:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.unlink.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { useQuery, keepPreviousData } from '@tanstack/react-query'
|
||||
import { getScans, getScan, getScanStatistics } from '@/services/scan.service'
|
||||
import type { GetScansParams } from '@/types/scan.types'
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
||||
import {
|
||||
getScans,
|
||||
getScan,
|
||||
getScanStatistics,
|
||||
quickScan,
|
||||
initiateScan,
|
||||
deleteScan,
|
||||
bulkDeleteScans,
|
||||
stopScan
|
||||
} from '@/services/scan.service'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { parseResponse, getErrorCode } from '@/lib/response-parser'
|
||||
import type {
|
||||
GetScansParams,
|
||||
QuickScanRequest,
|
||||
InitiateScanRequest
|
||||
} from '@/types/scan.types'
|
||||
|
||||
export function useScans(params: GetScansParams = { page: 1, pageSize: 10 }) {
|
||||
return useQuery({
|
||||
@@ -31,3 +46,156 @@ export function useScanStatistics() {
|
||||
queryFn: getScanStatistics,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速扫描 mutation hook
|
||||
*/
|
||||
export function useQuickScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: QuickScanRequest) => quickScan(data),
|
||||
onSuccess: (response) => {
|
||||
const data = parseResponse<any>(response)
|
||||
if (data) {
|
||||
// 使用 i18n 消息显示成功提示
|
||||
const count = data.scans?.length || data.targetStats?.created || data.count || 0
|
||||
toastMessages.success('toast.scan.quick.success', { count })
|
||||
// 刷新扫描列表
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-statistics'] })
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.scan.quick.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起扫描 mutation hook
|
||||
*/
|
||||
export function useInitiateScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: InitiateScanRequest) => initiateScan(data),
|
||||
onSuccess: (response) => {
|
||||
const data = parseResponse<any>(response)
|
||||
if (data) {
|
||||
// 使用 i18n 消息显示成功提示
|
||||
toastMessages.success('toast.scan.initiate.success')
|
||||
// 刷新扫描列表
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-statistics'] })
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.scan.initiate.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除扫描 mutation hook
|
||||
*/
|
||||
export function useDeleteScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteScan(id),
|
||||
onSuccess: (response, id) => {
|
||||
const data = parseResponse<any>(response)
|
||||
// 使用 i18n 消息显示成功提示
|
||||
toastMessages.success('toast.scan.delete.success', {
|
||||
name: `Scan #${id}`
|
||||
})
|
||||
// 刷新扫描列表
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-statistics'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.deleteFailed')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除扫描 mutation hook
|
||||
*/
|
||||
export function useBulkDeleteScans() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (ids: number[]) => bulkDeleteScans(ids),
|
||||
onSuccess: (response, ids) => {
|
||||
const data = parseResponse<any>(response)
|
||||
if (data) {
|
||||
// 使用 i18n 消息显示成功提示
|
||||
const count = data.deletedCount || ids.length || 0
|
||||
toastMessages.success('toast.scan.delete.bulkSuccess', { count })
|
||||
// 刷新扫描列表
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-statistics'] })
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.bulkDeleteFailed')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止扫描 mutation hook
|
||||
*/
|
||||
export function useStopScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => stopScan(id),
|
||||
onSuccess: (response) => {
|
||||
const data = parseResponse<any>(response)
|
||||
if (data) {
|
||||
// 使用 i18n 消息显示成功提示
|
||||
const count = data.revokedTaskCount || 1
|
||||
toastMessages.success('toast.scan.stop.success', { count })
|
||||
// 刷新扫描列表
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-statistics'] })
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.stopFailed')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
getScheduledScans,
|
||||
getScheduledScan,
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
deleteScheduledScan,
|
||||
toggleScheduledScan,
|
||||
} from '@/services/scheduled-scan.service'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { parseResponse, getErrorCode } from '@/lib/response-parser'
|
||||
import type { CreateScheduledScanRequest, UpdateScheduledScanRequest } from '@/types/scheduled-scan.types'
|
||||
|
||||
/**
|
||||
@@ -37,15 +38,23 @@ export function useScheduledScan(id: number) {
|
||||
*/
|
||||
export function useCreateScheduledScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateScheduledScanRequest) => createScheduledScan(data),
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message)
|
||||
onSuccess: (response) => {
|
||||
const data = parseResponse<any>(response)
|
||||
// 使用 i18n 消息显示成功提示
|
||||
toastMessages.success('toast.scheduledScan.create.success')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`创建失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.scheduledScan.create.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -55,17 +64,25 @@ export function useCreateScheduledScan() {
|
||||
*/
|
||||
export function useUpdateScheduledScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateScheduledScanRequest }) =>
|
||||
updateScheduledScan(id, data),
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message)
|
||||
onSuccess: (response) => {
|
||||
const data = parseResponse<any>(response)
|
||||
// 使用 i18n 消息显示成功提示
|
||||
toastMessages.success('toast.scheduledScan.update.success')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-scan'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`更新失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.scheduledScan.update.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -75,15 +92,23 @@ export function useUpdateScheduledScan() {
|
||||
*/
|
||||
export function useDeleteScheduledScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteScheduledScan(id),
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message)
|
||||
onSuccess: (response) => {
|
||||
const data = parseResponse<any>(response)
|
||||
// 使用 i18n 消息显示成功提示
|
||||
toastMessages.success('toast.scheduledScan.delete.success')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-scans'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`删除失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.scheduledScan.delete.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -94,6 +119,7 @@ export function useDeleteScheduledScan() {
|
||||
*/
|
||||
export function useToggleScheduledScan() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, isEnabled }: { id: number; isEnabled: boolean }) =>
|
||||
@@ -122,18 +148,29 @@ export function useToggleScheduledScan() {
|
||||
// 返回上下文用于回滚
|
||||
return { previousQueries }
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message)
|
||||
onSuccess: (response, { isEnabled }) => {
|
||||
const data = parseResponse<any>(response)
|
||||
// 使用 i18n 消息显示成功提示
|
||||
if (isEnabled) {
|
||||
toastMessages.success('toast.scheduledScan.toggle.enabled')
|
||||
} else {
|
||||
toastMessages.success('toast.scheduledScan.toggle.disabled')
|
||||
}
|
||||
// 不调用 invalidateQueries,保持当前排序
|
||||
},
|
||||
onError: (error: Error, _variables, context) => {
|
||||
onError: (error: any, _variables, context) => {
|
||||
// 回滚到之前的状态
|
||||
if (context?.previousQueries) {
|
||||
context.previousQueries.forEach(([queryKey, data]) => {
|
||||
queryClient.setQueryData(queryKey, data)
|
||||
})
|
||||
}
|
||||
toast.error(`操作失败: ${error.message}`)
|
||||
const errorCode = getErrorCode(error.response?.data)
|
||||
if (errorCode) {
|
||||
toastMessages.errorFromCode(errorCode)
|
||||
} else {
|
||||
toastMessages.error('toast.scheduledScan.toggle.error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { SubdomainService } from "@/services/subdomain.service"
|
||||
import { OrganizationService } from "@/services/organization.service"
|
||||
import type { Subdomain, GetSubdomainsResponse, GetAllSubdomainsParams } from "@/types/subdomain.types"
|
||||
@@ -57,33 +58,32 @@ export function useOrganizationSubdomains(
|
||||
// 创建子域名(绑定到资产)
|
||||
export function useCreateSubdomain() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { domains: Array<{ name: string }>; assetId: number }) =>
|
||||
SubdomainService.createSubdomains(data),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在创建子域名...', { id: 'create-subdomain' })
|
||||
toastMessages.loading('common.status.creating', {}, 'create-subdomain')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('create-subdomain')
|
||||
toastMessages.dismiss('create-subdomain')
|
||||
const { createdCount, existedCount, skippedCount = 0 } = response
|
||||
if (skippedCount > 0 && existedCount > 0) {
|
||||
toast.warning(`成功创建 ${createdCount} 个子域名(${existedCount} 个已存在,${skippedCount} 个已跳过)`)
|
||||
} else if (skippedCount > 0) {
|
||||
toast.warning(`成功创建 ${createdCount} 个子域名(${skippedCount} 个已跳过)`)
|
||||
} else if (existedCount > 0) {
|
||||
toast.warning(`成功创建 ${createdCount} 个子域名(${existedCount} 个已存在)`)
|
||||
if (skippedCount > 0 || existedCount > 0) {
|
||||
toastMessages.warning('toast.asset.subdomain.create.partialSuccess', {
|
||||
success: createdCount,
|
||||
skipped: (existedCount || 0) + (skippedCount || 0)
|
||||
})
|
||||
} else {
|
||||
toast.success(`成功创建 ${createdCount} 个子域名`)
|
||||
toastMessages.success('toast.asset.subdomain.create.success', { count: createdCount })
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['assets'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('create-subdomain')
|
||||
console.error('创建子域名失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
toast.error('创建子域名失败,请查看控制台日志')
|
||||
toastMessages.dismiss('create-subdomain')
|
||||
console.error('Failed to create subdomain:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.subdomain.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -91,6 +91,7 @@ export function useCreateSubdomain() {
|
||||
// 从组织中移除子域名
|
||||
export function useDeleteSubdomainFromOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { organizationId: number; targetId: number }) =>
|
||||
@@ -99,19 +100,18 @@ export function useDeleteSubdomainFromOrganization() {
|
||||
targetIds: [data.targetId],
|
||||
}),
|
||||
onMutate: ({ organizationId, targetId }) => {
|
||||
toast.loading('正在移除子域名...', { id: `delete-${organizationId}-${targetId}` })
|
||||
toastMessages.loading('common.status.removing', {}, `delete-${organizationId}-${targetId}`)
|
||||
},
|
||||
onSuccess: (_response, { organizationId }) => {
|
||||
toast.dismiss(`delete-${organizationId}`)
|
||||
toast.success('子域名已成功移除')
|
||||
onSuccess: (_response, { organizationId, targetId }) => {
|
||||
toastMessages.dismiss(`delete-${organizationId}-${targetId}`)
|
||||
toastMessages.success('toast.asset.subdomain.delete.success')
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: any, { organizationId, targetId }) => {
|
||||
toast.dismiss(`delete-${organizationId}-${targetId}`)
|
||||
console.error('移除子域名失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
toast.error('移除子域名失败,请查看控制台日志')
|
||||
toastMessages.dismiss(`delete-${organizationId}-${targetId}`)
|
||||
console.error('Failed to remove subdomain:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.subdomain.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -119,30 +119,25 @@ export function useDeleteSubdomainFromOrganization() {
|
||||
// 批量从组织中移除子域名
|
||||
export function useBatchDeleteSubdomainsFromOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { organizationId: number; domainIds: number[] }) =>
|
||||
SubdomainService.batchDeleteSubdomainsFromOrganization(data),
|
||||
onMutate: ({ organizationId }) => {
|
||||
toast.loading('正在批量移除子域名...', { id: `batch-delete-${organizationId}` })
|
||||
toastMessages.loading('common.status.batchRemoving', {}, `batch-delete-${organizationId}`)
|
||||
},
|
||||
onSuccess: (response, { organizationId }) => {
|
||||
toast.dismiss(`batch-delete-${organizationId}`)
|
||||
toastMessages.dismiss(`batch-delete-${organizationId}`)
|
||||
const successCount = response.successCount || 0
|
||||
const failedCount = response.failedCount || 0
|
||||
if (failedCount > 0) {
|
||||
toast.warning(`批量移除完成(成功:${successCount},失败:${failedCount})`)
|
||||
} else {
|
||||
toast.success(`成功移除 ${successCount} 个子域名`)
|
||||
}
|
||||
toastMessages.success('toast.asset.subdomain.delete.bulkSuccess', { count: successCount })
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: any, { organizationId }) => {
|
||||
toast.dismiss(`batch-delete-${organizationId}`)
|
||||
console.error('批量移除子域名失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
toast.error('批量移除失败,请查看控制台日志')
|
||||
toastMessages.dismiss(`batch-delete-${organizationId}`)
|
||||
console.error('Failed to batch remove subdomains:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.subdomain.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -150,18 +145,16 @@ export function useBatchDeleteSubdomainsFromOrganization() {
|
||||
// 删除单个子域名(使用单独的 DELETE API)
|
||||
export function useDeleteSubdomain() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => SubdomainService.deleteSubdomain(id),
|
||||
onMutate: (id) => {
|
||||
toast.loading('正在删除子域名...', { id: `delete-subdomain-${id}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-subdomain-${id}`)
|
||||
},
|
||||
onSuccess: (response, id) => {
|
||||
toast.dismiss(`delete-subdomain-${id}`)
|
||||
|
||||
// 显示删除成功信息
|
||||
const { subdomainName } = response
|
||||
toast.success(`子域名 "${subdomainName}" 已成功删除`)
|
||||
toastMessages.dismiss(`delete-subdomain-${id}`)
|
||||
toastMessages.success('toast.asset.subdomain.delete.success')
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
@@ -169,10 +162,9 @@ export function useDeleteSubdomain() {
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: any, id) => {
|
||||
toast.dismiss(`delete-subdomain-${id}`)
|
||||
console.error('删除子域名失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
toast.error('删除子域名失败,请查看控制台日志')
|
||||
toastMessages.dismiss(`delete-subdomain-${id}`)
|
||||
console.error('Failed to delete subdomain:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.subdomain.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -180,29 +172,16 @@ export function useDeleteSubdomain() {
|
||||
// 批量删除子域名(使用统一的批量删除接口)
|
||||
export function useBatchDeleteSubdomains() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (ids: number[]) => SubdomainService.batchDeleteSubdomains(ids),
|
||||
onMutate: () => {
|
||||
toast.loading('正在批量删除子域名...', { id: 'batch-delete-subdomains' })
|
||||
toastMessages.loading('common.status.batchDeleting', {}, 'batch-delete-subdomains')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('batch-delete-subdomains')
|
||||
|
||||
// 显示级联删除信息
|
||||
const cascadeInfo = Object.entries(response.cascadeDeleted || {})
|
||||
.filter(([key, count]) => key !== 'asset.Subdomain' && count > 0)
|
||||
.map(([key, count]) => {
|
||||
const modelName = key.split('.')[1]
|
||||
return `${modelName}: ${count}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
if (cascadeInfo) {
|
||||
toast.success(`成功删除 ${response.deletedCount} 个子域名(级联删除: ${cascadeInfo})`)
|
||||
} else {
|
||||
toast.success(`成功删除 ${response.deletedCount} 个子域名`)
|
||||
}
|
||||
toastMessages.dismiss('batch-delete-subdomains')
|
||||
toastMessages.success('toast.asset.subdomain.delete.bulkSuccess', { count: response.deletedCount })
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
@@ -210,10 +189,9 @@ export function useBatchDeleteSubdomains() {
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('batch-delete-subdomains')
|
||||
console.error('批量删除子域名失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
toast.error('批量删除失败,请查看控制台日志')
|
||||
toastMessages.dismiss('batch-delete-subdomains')
|
||||
console.error('Failed to batch delete subdomains:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.subdomain.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -221,24 +199,24 @@ export function useBatchDeleteSubdomains() {
|
||||
// 更新子域名
|
||||
export function useUpdateSubdomain() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: { name?: string; description?: string } }) =>
|
||||
SubdomainService.updateSubdomain({ id, ...data }),
|
||||
onMutate: ({ id }) => {
|
||||
toast.loading('正在更新子域名...', { id: `update-subdomain-${id}` })
|
||||
toastMessages.loading('common.status.updating', {}, `update-subdomain-${id}`)
|
||||
},
|
||||
onSuccess: (_response, { id }) => {
|
||||
toast.dismiss(`update-subdomain-${id}`)
|
||||
toast.success('更新成功')
|
||||
toastMessages.dismiss(`update-subdomain-${id}`)
|
||||
toastMessages.success('common.status.updateSuccess')
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: any, { id }) => {
|
||||
toast.dismiss(`update-subdomain-${id}`)
|
||||
console.error('更新子域名失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
toast.error('更新子域名失败,请查看控制台日志')
|
||||
toastMessages.dismiss(`update-subdomain-${id}`)
|
||||
console.error('Failed to update subdomain:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'common.status.updateFailed')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -295,49 +273,40 @@ export function useScanSubdomains(
|
||||
// 批量创建子域名(绑定到目标)
|
||||
export function useBulkCreateSubdomains() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { targetId: number; subdomains: string[] }) =>
|
||||
SubdomainService.bulkCreateSubdomains(data.targetId, data.subdomains),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在批量创建子域名...', { id: 'bulk-create-subdomains' })
|
||||
toastMessages.loading('common.status.batchCreating', {}, 'bulk-create-subdomains')
|
||||
},
|
||||
onSuccess: (response, { targetId }) => {
|
||||
toast.dismiss('bulk-create-subdomains')
|
||||
const { createdCount, skippedCount, invalidCount, mismatchedCount } = response
|
||||
toastMessages.dismiss('bulk-create-subdomains')
|
||||
const { createdCount, skippedCount = 0, invalidCount = 0, mismatchedCount = 0 } = response
|
||||
const totalSkipped = skippedCount + invalidCount + mismatchedCount
|
||||
|
||||
let message = `成功创建 ${createdCount} 个子域名`
|
||||
const details: string[] = []
|
||||
|
||||
if (skippedCount > 0) {
|
||||
details.push(`${skippedCount} 个重复`)
|
||||
}
|
||||
if (invalidCount > 0) {
|
||||
details.push(`${invalidCount} 个格式无效`)
|
||||
}
|
||||
if (mismatchedCount > 0) {
|
||||
details.push(`${mismatchedCount} 个不匹配`)
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
message += `(${details.join(',')})`
|
||||
}
|
||||
|
||||
if (createdCount > 0) {
|
||||
toast.success(message)
|
||||
if (totalSkipped > 0) {
|
||||
toastMessages.warning('toast.asset.subdomain.create.partialSuccess', {
|
||||
success: createdCount,
|
||||
skipped: totalSkipped
|
||||
})
|
||||
} else if (createdCount > 0) {
|
||||
toastMessages.success('toast.asset.subdomain.create.success', { count: createdCount })
|
||||
} else {
|
||||
toast.warning(message)
|
||||
toastMessages.warning('toast.asset.subdomain.create.partialSuccess', {
|
||||
success: 0,
|
||||
skipped: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新子域名列表
|
||||
queryClient.invalidateQueries({ queryKey: ['targets', targetId, 'subdomains'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['subdomains'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('bulk-create-subdomains')
|
||||
console.error('批量创建子域名失败:', error)
|
||||
const errorMessage = error?.response?.data?.error || '批量创建失败,请查看控制台日志'
|
||||
toast.error(errorMessage)
|
||||
toastMessages.dismiss('bulk-create-subdomains')
|
||||
console.error('Failed to bulk create subdomains:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.subdomain.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { systemLogService } from "@/services/system-log.service"
|
||||
import { useToastMessages } from "@/lib/toast-helpers"
|
||||
|
||||
export function useSystemLogs(options?: { lines?: number; enabled?: boolean }) {
|
||||
const hadErrorRef = useRef(false)
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["system", "logs", { lines: options?.lines ?? null }],
|
||||
@@ -19,14 +20,14 @@ export function useSystemLogs(options?: { lines?: number; enabled?: boolean }) {
|
||||
useEffect(() => {
|
||||
if (query.isError && !hadErrorRef.current) {
|
||||
hadErrorRef.current = true
|
||||
toast.error("系统日志获取失败,请检查后端接口")
|
||||
toastMessages.error('toast.systemLog.fetch.error')
|
||||
}
|
||||
|
||||
if (query.isSuccess && hadErrorRef.current) {
|
||||
hadErrorRef.current = false
|
||||
toast.success("系统日志连接已恢复")
|
||||
toastMessages.success('toast.systemLog.fetch.recovered')
|
||||
}
|
||||
}, [query.isError, query.isSuccess])
|
||||
}, [query.isError, query.isSuccess, toastMessages])
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Targets Hooks - 目标管理相关 hooks
|
||||
*/
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import {
|
||||
getTargets,
|
||||
getTargetById,
|
||||
@@ -110,15 +111,16 @@ export function useTarget(id: number, options?: { enabled?: boolean }) {
|
||||
*/
|
||||
export function useCreateTarget() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateTargetRequest) => createTarget(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
toast.success('目标创建成功')
|
||||
toastMessages.success('toast.target.create.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`创建失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -128,6 +130,7 @@ export function useCreateTarget() {
|
||||
*/
|
||||
export function useUpdateTarget() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateTargetRequest }) =>
|
||||
@@ -135,10 +138,10 @@ export function useUpdateTarget() {
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets', variables.id] })
|
||||
toast.success('目标更新成功')
|
||||
toastMessages.success('toast.target.update.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`更新失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -148,25 +151,26 @@ export function useUpdateTarget() {
|
||||
*/
|
||||
export function useDeleteTarget() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTarget(id),
|
||||
onMutate: (id) => {
|
||||
toast.loading('正在删除目标...', { id: `delete-target-${id}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-target-${id}`)
|
||||
},
|
||||
onSuccess: (response, id) => {
|
||||
toast.dismiss(`delete-target-${id}`)
|
||||
toastMessages.dismiss(`delete-target-${id}`)
|
||||
|
||||
// 显示删除成功信息
|
||||
const { targetName } = response
|
||||
toast.success(`目标 "${targetName}" 已成功删除`)
|
||||
toastMessages.success('toast.target.delete.success', { name: targetName })
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
},
|
||||
onError: (error: Error, id) => {
|
||||
toast.dismiss(`delete-target-${id}`)
|
||||
toast.error(`删除失败: ${error.message}`)
|
||||
onError: (error: any, id) => {
|
||||
toastMessages.dismiss(`delete-target-${id}`)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -176,15 +180,16 @@ export function useDeleteTarget() {
|
||||
*/
|
||||
export function useBatchDeleteTargets() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: BatchDeleteTargetsRequest) => batchDeleteTargets(data),
|
||||
onSuccess: (response) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
toast.success(`成功删除 ${response.deletedCount} 个目标`)
|
||||
toastMessages.success('toast.target.delete.bulkSuccess', { count: response.deletedCount })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`批量删除失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -194,16 +199,17 @@ export function useBatchDeleteTargets() {
|
||||
*/
|
||||
export function useBatchCreateTargets() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: BatchCreateTargetsRequest) => batchCreateTargets(data),
|
||||
onSuccess: (response) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
||||
toast.success(response.message)
|
||||
toastMessages.success('toast.target.create.bulkSuccess', { count: response.createdCount || 0 })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`批量创建失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -224,6 +230,7 @@ export function useTargetOrganizations(targetId: number, page = 1, pageSize = 10
|
||||
*/
|
||||
export function useLinkTargetOrganizations() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ targetId, organizationIds }: { targetId: number; organizationIds: number[] }) =>
|
||||
@@ -231,10 +238,10 @@ export function useLinkTargetOrganizations() {
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId, 'organizations'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId] })
|
||||
toast.success('组织关联成功')
|
||||
toastMessages.success('toast.target.link.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`关联失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.link.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -244,6 +251,7 @@ export function useLinkTargetOrganizations() {
|
||||
*/
|
||||
export function useUnlinkTargetOrganizations() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ targetId, organizationIds }: { targetId: number; organizationIds: number[] }) =>
|
||||
@@ -251,10 +259,10 @@ export function useUnlinkTargetOrganizations() {
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId, 'organizations'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets', variables.targetId] })
|
||||
toast.success('取消关联成功')
|
||||
toastMessages.success('toast.target.unlink.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`取消关联失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.target.unlink.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { ToolService } from "@/services/tool.service"
|
||||
import type { Tool, GetToolsParams, CreateToolRequest, UpdateToolRequest } from "@/types/tool.types"
|
||||
|
||||
@@ -35,35 +36,25 @@ export function useTools(params: GetToolsParams = {}) {
|
||||
// 创建工具
|
||||
export function useCreateTool() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateToolRequest) => ToolService.createTool(data),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在创建工具...', { id: 'create-tool' })
|
||||
toastMessages.loading('common.status.creating', {}, 'create-tool')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('create-tool')
|
||||
|
||||
// 打印后端响应
|
||||
console.log('创建工具成功')
|
||||
console.log('后端响应:', response)
|
||||
|
||||
toast.success('创建成功')
|
||||
|
||||
// 刷新工具列表和分类列表
|
||||
onSuccess: () => {
|
||||
toastMessages.dismiss('create-tool')
|
||||
toastMessages.success('toast.tool.create.success')
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: toolKeys.all,
|
||||
refetchType: 'active'
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('create-tool')
|
||||
|
||||
console.error('创建工具失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
// 前端自己构造错误提示
|
||||
toast.error('创建工具失败,请查看控制台日志')
|
||||
toastMessages.dismiss('create-tool')
|
||||
console.error('Failed to create tool:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.tool.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -71,34 +62,26 @@ export function useCreateTool() {
|
||||
// 更新工具
|
||||
export function useUpdateTool() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateToolRequest }) =>
|
||||
ToolService.updateTool(id, data),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在更新工具...', { id: 'update-tool' })
|
||||
toastMessages.loading('common.status.updating', {}, 'update-tool')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('update-tool')
|
||||
|
||||
console.log('更新工具成功')
|
||||
console.log('后端响应:', response)
|
||||
|
||||
toast.success('更新成功')
|
||||
|
||||
// 刷新工具列表
|
||||
onSuccess: () => {
|
||||
toastMessages.dismiss('update-tool')
|
||||
toastMessages.success('toast.tool.update.success')
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: toolKeys.all,
|
||||
refetchType: 'active'
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('update-tool')
|
||||
|
||||
console.error('更新工具失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
toast.error('更新工具失败,请查看控制台日志')
|
||||
toastMessages.dismiss('update-tool')
|
||||
console.error('Failed to update tool:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.tool.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -106,32 +89,25 @@ export function useUpdateTool() {
|
||||
// 删除工具
|
||||
export function useDeleteTool() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => ToolService.deleteTool(id),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在删除工具...', { id: 'delete-tool' })
|
||||
toastMessages.loading('common.status.deleting', {}, 'delete-tool')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('delete-tool')
|
||||
|
||||
console.log('删除工具成功')
|
||||
|
||||
toast.success('删除成功')
|
||||
|
||||
// 刷新工具列表
|
||||
onSuccess: () => {
|
||||
toastMessages.dismiss('delete-tool')
|
||||
toastMessages.success('toast.tool.delete.success')
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: toolKeys.all,
|
||||
refetchType: 'active'
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('delete-tool')
|
||||
|
||||
console.error('删除工具失败:', error)
|
||||
console.error('后端响应:', error?.response?.data || error)
|
||||
|
||||
toast.error('删除工具失败,请查看控制台日志')
|
||||
toastMessages.dismiss('delete-tool')
|
||||
console.error('Failed to delete tool:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.tool.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import { WebsiteService } from '@/services/website.service'
|
||||
import type { WebSite, WebSiteListResponse } from '@/types/website.types'
|
||||
|
||||
@@ -108,28 +109,25 @@ export function useScanWebSites(
|
||||
// 删除单个网站(使用单独的 DELETE API)
|
||||
export function useDeleteWebSite() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: websiteService.deleteWebSite,
|
||||
onMutate: (id) => {
|
||||
toast.loading('正在删除网站...', { id: `delete-website-${id}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-website-${id}`)
|
||||
},
|
||||
onSuccess: (response, id) => {
|
||||
toast.dismiss(`delete-website-${id}`)
|
||||
toastMessages.dismiss(`delete-website-${id}`)
|
||||
toastMessages.success('toast.asset.website.delete.success')
|
||||
|
||||
// 显示删除成功信息
|
||||
const { websiteUrl } = response
|
||||
toast.success(`网站 "${websiteUrl}" 已成功删除`)
|
||||
|
||||
// 刷新相关查询
|
||||
queryClient.invalidateQueries({ queryKey: ['target-websites'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-websites'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
},
|
||||
onError: (error: Error, id) => {
|
||||
toast.dismiss(`delete-website-${id}`)
|
||||
toast.error(error.message || '删除网站失败')
|
||||
onError: (error: any, id) => {
|
||||
toastMessages.dismiss(`delete-website-${id}`)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.website.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -137,39 +135,25 @@ export function useDeleteWebSite() {
|
||||
// 批量删除网站(使用统一的批量删除接口)
|
||||
export function useBulkDeleteWebSites() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: websiteService.bulkDeleteWebSites,
|
||||
onMutate: () => {
|
||||
toast.loading('正在批量删除网站...', { id: 'bulk-delete-websites' })
|
||||
toastMessages.loading('common.status.batchDeleting', {}, 'bulk-delete-websites')
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
toast.dismiss('bulk-delete-websites')
|
||||
toastMessages.dismiss('bulk-delete-websites')
|
||||
toastMessages.success('toast.asset.website.delete.bulkSuccess', { count: response.deletedCount })
|
||||
|
||||
// 显示级联删除信息
|
||||
const cascadeInfo = Object.entries(response.cascadeDeleted || {})
|
||||
.filter(([key, count]) => key !== 'asset.WebSite' && count > 0)
|
||||
.map(([key, count]) => {
|
||||
const modelName = key.split('.')[1]
|
||||
return `${modelName}: ${count}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
if (cascadeInfo) {
|
||||
toast.success(`成功删除 ${response.deletedCount} 个网站(级联删除: ${cascadeInfo})`)
|
||||
} else {
|
||||
toast.success(`成功删除 ${response.deletedCount} 个网站`)
|
||||
}
|
||||
|
||||
// 刷新相关查询
|
||||
queryClient.invalidateQueries({ queryKey: ['target-websites'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-websites'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['targets'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scans'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.dismiss('bulk-delete-websites')
|
||||
toast.error(error.message || '批量删除网站失败')
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('bulk-delete-websites')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.website.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -178,32 +162,31 @@ export function useBulkDeleteWebSites() {
|
||||
// 批量创建网站(绑定到目标)
|
||||
export function useBulkCreateWebsites() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: { targetId: number; urls: string[] }) =>
|
||||
WebsiteService.bulkCreateWebsites(data.targetId, data.urls),
|
||||
onMutate: async () => {
|
||||
toast.loading('正在批量创建网站...', { id: 'bulk-create-websites' })
|
||||
toastMessages.loading('common.status.batchCreating', {}, 'bulk-create-websites')
|
||||
},
|
||||
onSuccess: (response, { targetId }) => {
|
||||
toast.dismiss('bulk-create-websites')
|
||||
toastMessages.dismiss('bulk-create-websites')
|
||||
const { createdCount } = response
|
||||
|
||||
if (createdCount > 0) {
|
||||
toast.success(`成功创建 ${createdCount} 个网站`)
|
||||
toastMessages.success('toast.asset.website.create.success', { count: createdCount })
|
||||
} else {
|
||||
toast.warning('没有新网站被创建(可能已存在)')
|
||||
toastMessages.warning('toast.asset.website.create.partialSuccess', { success: 0, skipped: 0 })
|
||||
}
|
||||
|
||||
// 刷新网站列表
|
||||
queryClient.invalidateQueries({ queryKey: ['target-websites', targetId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['scan-websites'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.dismiss('bulk-create-websites')
|
||||
console.error('批量创建网站失败:', error)
|
||||
const errorMessage = error?.response?.data?.error || '批量创建失败,请查看控制台日志'
|
||||
toast.error(errorMessage)
|
||||
toastMessages.dismiss('bulk-create-websites')
|
||||
console.error('Failed to bulk create websites:', error)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.asset.website.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
import {
|
||||
getWordlists,
|
||||
uploadWordlist,
|
||||
@@ -25,20 +26,21 @@ export function useWordlists(params?: { page?: number; pageSize?: number }) {
|
||||
// Upload wordlist
|
||||
export function useUploadWordlist() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation<{}, Error, { name: string; description?: string; file: File }>({
|
||||
mutationFn: (payload) => uploadWordlist(payload),
|
||||
onMutate: () => {
|
||||
toast.loading("Uploading wordlist...", { id: "upload-wordlist" })
|
||||
toastMessages.loading('common.status.uploading', {}, 'upload-wordlist')
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.dismiss("upload-wordlist")
|
||||
toast.success("Wordlist uploaded successfully")
|
||||
toastMessages.dismiss('upload-wordlist')
|
||||
toastMessages.success('toast.wordlist.upload.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["wordlists"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.dismiss("upload-wordlist")
|
||||
toast.error(`Upload failed: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('upload-wordlist')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.wordlist.upload.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -46,20 +48,21 @@ export function useUploadWordlist() {
|
||||
// Delete wordlist
|
||||
export function useDeleteWordlist() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation<void, Error, number>({
|
||||
mutationFn: (id: number) => deleteWordlist(id),
|
||||
onMutate: (id) => {
|
||||
toast.loading("Deleting wordlist...", { id: `delete-wordlist-${id}` })
|
||||
toastMessages.loading('common.status.deleting', {}, `delete-wordlist-${id}`)
|
||||
},
|
||||
onSuccess: (_data, id) => {
|
||||
toast.dismiss(`delete-wordlist-${id}`)
|
||||
toast.success("Wordlist deleted successfully")
|
||||
toastMessages.dismiss(`delete-wordlist-${id}`)
|
||||
toastMessages.success('toast.wordlist.delete.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["wordlists"] })
|
||||
},
|
||||
onError: (error, id) => {
|
||||
toast.dismiss(`delete-wordlist-${id}`)
|
||||
toast.error(`Delete failed: ${error.message}`)
|
||||
onError: (error: any, id) => {
|
||||
toastMessages.dismiss(`delete-wordlist-${id}`)
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.wordlist.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -76,21 +79,22 @@ export function useWordlistContent(id: number | null) {
|
||||
// Update wordlist content
|
||||
export function useUpdateWordlistContent() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation<Wordlist, Error, { id: number; content: string }>({
|
||||
mutationFn: ({ id, content }) => updateWordlistContent(id, content),
|
||||
onMutate: () => {
|
||||
toast.loading("Saving...", { id: "update-wordlist-content" })
|
||||
toastMessages.loading('common.actions.saving', {}, 'update-wordlist-content')
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.dismiss("update-wordlist-content")
|
||||
toast.success("Wordlist saved successfully")
|
||||
toastMessages.dismiss('update-wordlist-content')
|
||||
toastMessages.success('toast.wordlist.update.success')
|
||||
queryClient.invalidateQueries({ queryKey: ["wordlists"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["wordlist-content", data.id] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.dismiss("update-wordlist-content")
|
||||
toast.error(`Save failed: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.dismiss('update-wordlist-content')
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.wordlist.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { workerService } from '@/services/worker.service'
|
||||
import type { CreateWorkerRequest, UpdateWorkerRequest } from '@/types/worker.types'
|
||||
import { toast } from 'sonner'
|
||||
import { useToastMessages } from '@/lib/toast-helpers'
|
||||
import { getErrorCode } from '@/lib/response-parser'
|
||||
|
||||
// Query Keys
|
||||
export const workerKeys = {
|
||||
@@ -42,15 +43,16 @@ export function useWorker(id: number) {
|
||||
*/
|
||||
export function useCreateWorker() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateWorkerRequest) => workerService.createWorker(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.lists() })
|
||||
toast.success('Worker 节点创建成功')
|
||||
toastMessages.success('toast.worker.create.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`创建失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.worker.create.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -60,6 +62,7 @@ export function useCreateWorker() {
|
||||
*/
|
||||
export function useUpdateWorker() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateWorkerRequest }) =>
|
||||
@@ -67,10 +70,10 @@ export function useUpdateWorker() {
|
||||
onSuccess: (_: unknown, { id }: { id: number; data: UpdateWorkerRequest }) => {
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) })
|
||||
toast.success('Worker 节点更新成功')
|
||||
toastMessages.success('toast.worker.update.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`更新失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.worker.update.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -80,19 +83,19 @@ export function useUpdateWorker() {
|
||||
*/
|
||||
export function useDeleteWorker() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => workerService.deleteWorker(id),
|
||||
onSuccess: () => {
|
||||
// 立即刷新活跃的列表查询
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workerKeys.lists(),
|
||||
refetchType: 'active',
|
||||
})
|
||||
toast.success('Worker 节点已删除')
|
||||
toastMessages.success('toast.worker.delete.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`删除失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.worker.delete.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -102,16 +105,17 @@ export function useDeleteWorker() {
|
||||
*/
|
||||
export function useDeployWorker() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => workerService.deployWorker(id),
|
||||
onSuccess: (_: unknown, id: number) => {
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) })
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.lists() })
|
||||
toast.success('部署已启动')
|
||||
toastMessages.success('toast.worker.deploy.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`部署失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.worker.deploy.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -121,16 +125,17 @@ export function useDeployWorker() {
|
||||
*/
|
||||
export function useRestartWorker() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => workerService.restartWorker(id),
|
||||
onSuccess: (_: unknown, id: number) => {
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) })
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.lists() })
|
||||
toast.success('Worker 正在重启')
|
||||
toastMessages.success('toast.worker.restart.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`重启失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.worker.restart.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -140,16 +145,17 @@ export function useRestartWorker() {
|
||||
*/
|
||||
export function useStopWorker() {
|
||||
const queryClient = useQueryClient()
|
||||
const toastMessages = useToastMessages()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => workerService.stopWorker(id),
|
||||
onSuccess: (_: unknown, id: number) => {
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.detail(id) })
|
||||
queryClient.invalidateQueries({ queryKey: workerKeys.lists() })
|
||||
toast.success('Worker 已停止')
|
||||
toastMessages.success('toast.worker.stop.success')
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`停止失败: ${error.message}`)
|
||||
onError: (error: any) => {
|
||||
toastMessages.errorFromCode(getErrorCode(error?.response?.data), 'toast.worker.stop.error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
70
frontend/lib/error-code-map.ts
Normal file
70
frontend/lib/error-code-map.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 错误码到 i18n 键的映射
|
||||
*
|
||||
* 采用简化方案(参考 Stripe、GitHub 等大厂做法):
|
||||
* - 只映射通用错误码(5-10 个)
|
||||
* - 未知错误码使用 errors.unknown
|
||||
* - 错误码与后端 ErrorCodes 类保持一致
|
||||
*
|
||||
* 后端错误码定义: backend/apps/common/error_codes.py
|
||||
*/
|
||||
|
||||
/**
|
||||
* 错误码到 i18n 键的映射表
|
||||
*
|
||||
* 键: 后端返回的错误码(大写字母和下划线)
|
||||
* 值: 前端 i18n 键(在 messages/en.json 和 messages/zh.json 中定义)
|
||||
*/
|
||||
export const ERROR_CODE_MAP: Record<string, string> = {
|
||||
// 通用错误码(8 个,与后端 ErrorCodes 类一致)
|
||||
VALIDATION_ERROR: 'errors.validation',
|
||||
NOT_FOUND: 'errors.notFound',
|
||||
PERMISSION_DENIED: 'errors.permissionDenied',
|
||||
SERVER_ERROR: 'errors.serverError',
|
||||
BAD_REQUEST: 'errors.badRequest',
|
||||
CONFLICT: 'errors.conflict',
|
||||
UNAUTHORIZED: 'errors.unauthorized',
|
||||
RATE_LIMITED: 'errors.rateLimited',
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认错误 i18n 键
|
||||
* 用于未知错误码的回退
|
||||
*/
|
||||
export const DEFAULT_ERROR_KEY = 'errors.unknown';
|
||||
|
||||
/**
|
||||
* 获取错误码对应的 i18n 键
|
||||
*
|
||||
* @param code - 后端返回的错误码
|
||||
* @returns 对应的 i18n 键,未知错误码返回 'errors.unknown'
|
||||
*
|
||||
* @example
|
||||
* const errorKey = getErrorI18nKey('NOT_FOUND');
|
||||
* // 返回: 'errors.notFound'
|
||||
*
|
||||
* const unknownKey = getErrorI18nKey('SOME_UNKNOWN_ERROR');
|
||||
* // 返回: 'errors.unknown'
|
||||
*/
|
||||
export function getErrorI18nKey(code: string): string {
|
||||
return ERROR_CODE_MAP[code] ?? DEFAULT_ERROR_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查错误码是否已知
|
||||
*
|
||||
* @param code - 后端返回的错误码
|
||||
* @returns 如果错误码在映射表中返回 true
|
||||
*/
|
||||
export function isKnownErrorCode(code: string): boolean {
|
||||
return code in ERROR_CODE_MAP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已知的错误码列表
|
||||
*
|
||||
* @returns 错误码数组
|
||||
*/
|
||||
export function getAllErrorCodes(): string[] {
|
||||
return Object.keys(ERROR_CODE_MAP);
|
||||
}
|
||||
232
frontend/lib/response-parser.ts
Normal file
232
frontend/lib/response-parser.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* API 响应解析器
|
||||
*
|
||||
* 统一处理后端 API 响应,支持新的标准化响应格式:
|
||||
* - 成功响应: { data?: T, meta?: {...} }
|
||||
* - 错误响应: { error: { code: string, message?: string, details?: unknown[] } }
|
||||
*
|
||||
* 同时保持对旧格式的向后兼容性:
|
||||
* - 旧格式: { code: string, state: string, message: string, data?: T }
|
||||
*/
|
||||
|
||||
/**
|
||||
* 标准化成功响应类型
|
||||
*/
|
||||
export interface ApiSuccessResponse<T = unknown> {
|
||||
data?: T;
|
||||
meta?: {
|
||||
count?: number;
|
||||
total?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化错误响应类型
|
||||
*/
|
||||
export interface ApiErrorResponse {
|
||||
error: {
|
||||
code: string;
|
||||
message?: string;
|
||||
details?: unknown[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一 API 响应类型(新格式)
|
||||
*/
|
||||
export type ApiResponse<T = unknown> = ApiSuccessResponse<T> | ApiErrorResponse;
|
||||
|
||||
/**
|
||||
* 旧版 API 响应类型(向后兼容)
|
||||
*/
|
||||
export interface LegacyApiResponse<T = unknown> {
|
||||
code: string;
|
||||
state: string;
|
||||
message: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断响应是否为错误响应
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 如果是错误响应返回 true
|
||||
*
|
||||
* @example
|
||||
* const response = await api.get('/scans');
|
||||
* if (isErrorResponse(response)) {
|
||||
* console.error('Error:', response.error.code);
|
||||
* }
|
||||
*/
|
||||
export function isErrorResponse(response: unknown): response is ApiErrorResponse {
|
||||
return (
|
||||
typeof response === 'object' &&
|
||||
response !== null &&
|
||||
'error' in response &&
|
||||
typeof (response as ApiErrorResponse).error === 'object' &&
|
||||
(response as ApiErrorResponse).error !== null &&
|
||||
typeof (response as ApiErrorResponse).error.code === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断响应是否为成功响应
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 如果是成功响应返回 true
|
||||
*/
|
||||
export function isSuccessResponse<T = unknown>(
|
||||
response: unknown
|
||||
): response is ApiSuccessResponse<T> {
|
||||
// 新格式:没有 error 字段
|
||||
if (typeof response !== 'object' || response === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有 error 字段,则不是成功响应
|
||||
if ('error' in response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断响应是否为旧版格式
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 如果是旧版格式返回 true
|
||||
*/
|
||||
export function isLegacyResponse<T = unknown>(
|
||||
response: unknown
|
||||
): response is LegacyApiResponse<T> {
|
||||
return (
|
||||
typeof response === 'object' &&
|
||||
response !== null &&
|
||||
'state' in response &&
|
||||
'code' in response &&
|
||||
typeof (response as LegacyApiResponse).state === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断旧版响应是否为错误
|
||||
*
|
||||
* @param response - 旧版 API 响应对象
|
||||
* @returns 如果是错误响应返回 true
|
||||
*/
|
||||
export function isLegacyErrorResponse<T = unknown>(
|
||||
response: LegacyApiResponse<T>
|
||||
): boolean {
|
||||
return response.state !== 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中解析数据
|
||||
*
|
||||
* 支持新旧两种响应格式:
|
||||
* - 新格式: { data: T }
|
||||
* - 旧格式: { state: 'success', data: T }
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 解析出的数据,如果是错误响应则返回 null
|
||||
*
|
||||
* @example
|
||||
* const response = await api.get('/scans');
|
||||
* const data = parseResponse<Scan[]>(response);
|
||||
* if (data) {
|
||||
* console.log('Scans:', data);
|
||||
* }
|
||||
*/
|
||||
export function parseResponse<T>(response: unknown): T | null {
|
||||
// 处理新格式错误响应
|
||||
if (isErrorResponse(response)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理旧格式响应
|
||||
if (isLegacyResponse<T>(response)) {
|
||||
if (isLegacyErrorResponse(response)) {
|
||||
return null;
|
||||
}
|
||||
return response.data ?? null;
|
||||
}
|
||||
|
||||
// 处理新格式成功响应
|
||||
if (isSuccessResponse<T>(response)) {
|
||||
return response.data ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中获取错误码
|
||||
*
|
||||
* 支持新旧两种响应格式:
|
||||
* - 新格式: { error: { code: 'ERROR_CODE' } }
|
||||
* - 旧格式: { state: 'error', code: '400' }
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 错误码字符串,如果不是错误响应则返回 null
|
||||
*
|
||||
* @example
|
||||
* const response = await api.delete('/scans/123');
|
||||
* const errorCode = getErrorCode(response);
|
||||
* if (errorCode) {
|
||||
* toast.error(t(`errors.${errorCode}`));
|
||||
* }
|
||||
*/
|
||||
export function getErrorCode(response: unknown): string | null {
|
||||
// 处理新格式错误响应
|
||||
if (isErrorResponse(response)) {
|
||||
return response.error.code;
|
||||
}
|
||||
|
||||
// 处理旧格式错误响应
|
||||
if (isLegacyResponse(response) && isLegacyErrorResponse(response)) {
|
||||
// 旧格式的 code 是 HTTP 状态码,不是错误码
|
||||
// 返回通用错误码
|
||||
return 'SERVER_ERROR';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中获取错误消息(用于调试)
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 错误消息字符串,如果不是错误响应则返回 null
|
||||
*/
|
||||
export function getErrorMessage(response: unknown): string | null {
|
||||
// 处理新格式错误响应
|
||||
if (isErrorResponse(response)) {
|
||||
return response.error.message ?? null;
|
||||
}
|
||||
|
||||
// 处理旧格式错误响应
|
||||
if (isLegacyResponse(response) && isLegacyErrorResponse(response)) {
|
||||
return response.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中获取元数据
|
||||
*
|
||||
* @param response - API 响应对象
|
||||
* @returns 元数据对象,如果没有则返回 null
|
||||
*/
|
||||
export function getResponseMeta(
|
||||
response: unknown
|
||||
): ApiSuccessResponse['meta'] | null {
|
||||
if (isSuccessResponse(response)) {
|
||||
return response.meta ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
234
frontend/lib/toast-helpers.ts
Normal file
234
frontend/lib/toast-helpers.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Toast 消息辅助函数
|
||||
*
|
||||
* 提供 i18n 感知的 toast 消息显示功能:
|
||||
* - success(): 显示成功消息
|
||||
* - error(): 显示错误消息
|
||||
* - errorFromCode(): 根据错误码显示错误消息
|
||||
* - loading(): 显示加载消息
|
||||
* - dismiss(): 关闭指定 toast
|
||||
*
|
||||
* 使用方式:
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const toastMessages = useToastMessages();
|
||||
*
|
||||
* const handleDelete = async () => {
|
||||
* try {
|
||||
* await deleteItem(id);
|
||||
* toastMessages.success('toast.item.delete.success', { name: item.name });
|
||||
* } catch (error) {
|
||||
* toastMessages.errorFromCode(getErrorCode(error));
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { getErrorI18nKey, DEFAULT_ERROR_KEY } from './error-code-map';
|
||||
|
||||
/**
|
||||
* Toast 消息参数类型
|
||||
* 支持字符串和数字类型的插值变量
|
||||
*/
|
||||
export type ToastParams = Record<string, string | number>;
|
||||
|
||||
/**
|
||||
* Toast 消息 Hook 返回类型
|
||||
*/
|
||||
export interface ToastMessages {
|
||||
/**
|
||||
* 显示成功消息
|
||||
* @param key - i18n 消息键
|
||||
* @param params - 插值参数
|
||||
* @param toastId - 可选的 toast ID(用于替换或关闭)
|
||||
*/
|
||||
success: (key: string, params?: ToastParams, toastId?: string) => void;
|
||||
|
||||
/**
|
||||
* 显示错误消息
|
||||
* @param key - i18n 消息键
|
||||
* @param params - 插值参数
|
||||
* @param toastId - 可选的 toast ID(用于替换或关闭)
|
||||
*/
|
||||
error: (key: string, params?: ToastParams, toastId?: string) => void;
|
||||
|
||||
/**
|
||||
* 根据错误码显示错误消息
|
||||
* @param code - 后端返回的错误码
|
||||
* @param fallbackKey - 未知错误码的回退键(默认 'errors.unknown')
|
||||
* @param toastId - 可选的 toast ID(用于替换或关闭)
|
||||
*/
|
||||
errorFromCode: (code: string | null, fallbackKey?: string, toastId?: string) => void;
|
||||
|
||||
/**
|
||||
* 显示加载消息
|
||||
* @param key - i18n 消息键
|
||||
* @param params - 插值参数
|
||||
* @param toastId - toast ID(用于后续关闭)
|
||||
*/
|
||||
loading: (key: string, params?: ToastParams, toastId?: string) => void;
|
||||
|
||||
/**
|
||||
* 显示警告消息
|
||||
* @param key - i18n 消息键
|
||||
* @param params - 插值参数
|
||||
* @param toastId - 可选的 toast ID(用于替换或关闭)
|
||||
*/
|
||||
warning: (key: string, params?: ToastParams, toastId?: string) => void;
|
||||
|
||||
/**
|
||||
* 关闭指定 toast
|
||||
* @param toastId - toast ID
|
||||
*/
|
||||
dismiss: (toastId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n 感知的 Toast 消息 Hook
|
||||
*
|
||||
* 提供统一的 toast 消息显示接口,自动处理 i18n 翻译和参数插值。
|
||||
*
|
||||
* @returns ToastMessages 对象,包含 success、error、errorFromCode、loading、warning、dismiss 方法
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function DeleteButton({ item }) {
|
||||
* const toastMessages = useToastMessages();
|
||||
* const { mutate: deleteItem } = useDeleteItem();
|
||||
*
|
||||
* const handleDelete = () => {
|
||||
* toastMessages.loading('toast.item.delete.loading', {}, 'delete-item');
|
||||
*
|
||||
* deleteItem(item.id, {
|
||||
* onSuccess: () => {
|
||||
* toastMessages.dismiss('delete-item');
|
||||
* toastMessages.success('toast.item.delete.success', { name: item.name });
|
||||
* },
|
||||
* onError: (error) => {
|
||||
* toastMessages.dismiss('delete-item');
|
||||
* toastMessages.errorFromCode(getErrorCode(error.response?.data));
|
||||
* }
|
||||
* });
|
||||
* };
|
||||
*
|
||||
* return <button onClick={handleDelete}>Delete</button>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useToastMessages(): ToastMessages {
|
||||
// 使用根命名空间,允许访问所有翻译键
|
||||
const t = useTranslations();
|
||||
|
||||
return {
|
||||
success: (key: string, params?: ToastParams, toastId?: string) => {
|
||||
const message = t(key, params);
|
||||
if (toastId) {
|
||||
toast.success(message, { id: toastId });
|
||||
} else {
|
||||
toast.success(message);
|
||||
}
|
||||
},
|
||||
|
||||
error: (key: string, params?: ToastParams, toastId?: string) => {
|
||||
const message = t(key, params);
|
||||
if (toastId) {
|
||||
toast.error(message, { id: toastId });
|
||||
} else {
|
||||
toast.error(message);
|
||||
}
|
||||
},
|
||||
|
||||
errorFromCode: (code: string | null, fallbackKey = DEFAULT_ERROR_KEY, toastId?: string) => {
|
||||
const errorKey = code ? getErrorI18nKey(code) : fallbackKey;
|
||||
const message = t(errorKey);
|
||||
if (toastId) {
|
||||
toast.error(message, { id: toastId });
|
||||
} else {
|
||||
toast.error(message);
|
||||
}
|
||||
},
|
||||
|
||||
loading: (key: string, params?: ToastParams, toastId?: string) => {
|
||||
const message = t(key, params);
|
||||
if (toastId) {
|
||||
toast.loading(message, { id: toastId });
|
||||
} else {
|
||||
toast.loading(message);
|
||||
}
|
||||
},
|
||||
|
||||
warning: (key: string, params?: ToastParams, toastId?: string) => {
|
||||
const message = t(key, params);
|
||||
if (toastId) {
|
||||
toast.warning(message, { id: toastId });
|
||||
} else {
|
||||
toast.warning(message);
|
||||
}
|
||||
},
|
||||
|
||||
dismiss: (toastId: string) => {
|
||||
toast.dismiss(toastId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 非 Hook 版本的 toast 辅助函数
|
||||
*
|
||||
* 用于不在 React 组件中的场景(如 API 拦截器)。
|
||||
* 注意:这些函数不支持 i18n,只能显示原始字符串。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 在 API 拦截器中使用
|
||||
* apiClient.interceptors.response.use(
|
||||
* (response) => response,
|
||||
* (error) => {
|
||||
* if (error.response?.status === 401) {
|
||||
* showToast.error('Session expired, please login again');
|
||||
* }
|
||||
* return Promise.reject(error);
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const showToast = {
|
||||
success: (message: string, toastId?: string) => {
|
||||
if (toastId) {
|
||||
toast.success(message, { id: toastId });
|
||||
} else {
|
||||
toast.success(message);
|
||||
}
|
||||
},
|
||||
|
||||
error: (message: string, toastId?: string) => {
|
||||
if (toastId) {
|
||||
toast.error(message, { id: toastId });
|
||||
} else {
|
||||
toast.error(message);
|
||||
}
|
||||
},
|
||||
|
||||
loading: (message: string, toastId?: string) => {
|
||||
if (toastId) {
|
||||
toast.loading(message, { id: toastId });
|
||||
} else {
|
||||
toast.loading(message);
|
||||
}
|
||||
},
|
||||
|
||||
warning: (message: string, toastId?: string) => {
|
||||
if (toastId) {
|
||||
toast.warning(message, { id: toastId });
|
||||
} else {
|
||||
toast.warning(message);
|
||||
}
|
||||
},
|
||||
|
||||
dismiss: (toastId: string) => {
|
||||
toast.dismiss(toastId);
|
||||
},
|
||||
};
|
||||
@@ -209,7 +209,18 @@
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
"disabled": "Disabled",
|
||||
"creating": "Creating...",
|
||||
"updating": "Updating...",
|
||||
"deleting": "Deleting...",
|
||||
"removing": "Removing...",
|
||||
"unlinking": "Unlinking...",
|
||||
"batchCreating": "Batch creating...",
|
||||
"batchDeleting": "Batch deleting...",
|
||||
"batchRemoving": "Batch removing...",
|
||||
"updateSuccess": "Update successful",
|
||||
"updateFailed": "Update failed",
|
||||
"uploading": "Uploading..."
|
||||
},
|
||||
"pagination": {
|
||||
"page": "Page {current} of {total}",
|
||||
@@ -1225,7 +1236,286 @@
|
||||
"invalidJsonFile": "Invalid JSON file",
|
||||
"importSuccess": "Import successful",
|
||||
"importFailed": "Import failed",
|
||||
"loadScanHistoryFailed": "Failed to load scan history"
|
||||
"loadScanHistoryFailed": "Failed to load scan history",
|
||||
"scan": {
|
||||
"quick": {
|
||||
"success": "Started {count} scan tasks",
|
||||
"error": "Failed to start scan"
|
||||
},
|
||||
"stop": {
|
||||
"success": "Scan stopped, {count} tasks cancelled"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Deleted scan task: {name}",
|
||||
"bulkSuccess": "Deleted {count} scan tasks"
|
||||
},
|
||||
"initiate": {
|
||||
"success": "Scan initiated successfully",
|
||||
"error": "Failed to initiate scan"
|
||||
}
|
||||
},
|
||||
"scheduledScan": {
|
||||
"create": {
|
||||
"success": "Scheduled scan created successfully",
|
||||
"error": "Failed to create scheduled scan"
|
||||
},
|
||||
"update": {
|
||||
"success": "Scheduled scan updated successfully",
|
||||
"error": "Failed to update scheduled scan"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Scheduled scan deleted successfully",
|
||||
"error": "Failed to delete scheduled scan"
|
||||
},
|
||||
"toggle": {
|
||||
"enabled": "Scheduled scan enabled",
|
||||
"disabled": "Scheduled scan disabled",
|
||||
"error": "Failed to toggle scheduled scan"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"create": {
|
||||
"success": "Target created successfully",
|
||||
"bulkSuccess": "Created {count} targets",
|
||||
"error": "Failed to create target"
|
||||
},
|
||||
"update": {
|
||||
"success": "Target updated successfully",
|
||||
"error": "Failed to update target"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Target \"{name}\" deleted",
|
||||
"bulkSuccess": "Deleted {count} targets",
|
||||
"error": "Failed to delete target"
|
||||
},
|
||||
"link": {
|
||||
"success": "Target linked to organization",
|
||||
"error": "Failed to link target"
|
||||
},
|
||||
"unlink": {
|
||||
"success": "Target unlinked from organization",
|
||||
"bulkSuccess": "Unlinked {count} targets",
|
||||
"error": "Failed to unlink target"
|
||||
}
|
||||
},
|
||||
"organization": {
|
||||
"create": {
|
||||
"success": "Organization created successfully",
|
||||
"error": "Failed to create organization"
|
||||
},
|
||||
"update": {
|
||||
"success": "Organization updated successfully",
|
||||
"error": "Failed to update organization"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Organization \"{name}\" deleted",
|
||||
"bulkSuccess": "Deleted {count} organizations",
|
||||
"error": "Failed to delete organization"
|
||||
}
|
||||
},
|
||||
"asset": {
|
||||
"subdomain": {
|
||||
"create": {
|
||||
"success": "Created {count} subdomains",
|
||||
"partialSuccess": "Created {success} subdomains, skipped {skipped}",
|
||||
"error": "Failed to create subdomains"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Subdomain deleted",
|
||||
"bulkSuccess": "Deleted {count} subdomains",
|
||||
"error": "Failed to delete subdomain"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"create": {
|
||||
"success": "Created {count} websites",
|
||||
"partialSuccess": "Created {success} websites, skipped {skipped}",
|
||||
"error": "Failed to create websites"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Website deleted",
|
||||
"bulkSuccess": "Deleted {count} websites",
|
||||
"error": "Failed to delete website"
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"create": {
|
||||
"success": "Created {count} endpoints",
|
||||
"partialSuccess": "Created {success} endpoints, skipped {skipped}",
|
||||
"error": "Failed to create endpoints"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Endpoint deleted",
|
||||
"bulkSuccess": "Deleted {count} endpoints",
|
||||
"error": "Failed to delete endpoint"
|
||||
}
|
||||
},
|
||||
"directory": {
|
||||
"create": {
|
||||
"success": "Created {count} directories",
|
||||
"partialSuccess": "Created {success} directories, skipped {skipped}",
|
||||
"error": "Failed to create directories"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Directory deleted",
|
||||
"bulkSuccess": "Deleted {count} directories",
|
||||
"error": "Failed to delete directory"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"success": "Login successful"
|
||||
},
|
||||
"logout": {
|
||||
"success": "Logged out"
|
||||
},
|
||||
"changePassword": {
|
||||
"success": "Password changed successfully",
|
||||
"error": "Failed to change password"
|
||||
}
|
||||
},
|
||||
"worker": {
|
||||
"create": {
|
||||
"success": "Worker created successfully",
|
||||
"error": "Failed to create worker"
|
||||
},
|
||||
"update": {
|
||||
"success": "Worker updated successfully",
|
||||
"error": "Failed to update worker"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Worker deleted successfully",
|
||||
"error": "Failed to delete worker"
|
||||
},
|
||||
"deploy": {
|
||||
"success": "Worker deployment started",
|
||||
"error": "Failed to deploy worker"
|
||||
},
|
||||
"restart": {
|
||||
"success": "Worker is restarting",
|
||||
"error": "Failed to restart worker"
|
||||
},
|
||||
"stop": {
|
||||
"success": "Worker stopped",
|
||||
"error": "Failed to stop worker"
|
||||
}
|
||||
},
|
||||
"nucleiRepo": {
|
||||
"create": {
|
||||
"success": "Repository added successfully",
|
||||
"error": "Failed to add repository"
|
||||
},
|
||||
"update": {
|
||||
"success": "Repository updated successfully",
|
||||
"error": "Failed to update repository"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Repository deleted successfully",
|
||||
"error": "Failed to delete repository"
|
||||
},
|
||||
"sync": {
|
||||
"success": "Repository synced successfully",
|
||||
"error": "Failed to sync repository"
|
||||
}
|
||||
},
|
||||
"wordlist": {
|
||||
"upload": {
|
||||
"success": "Wordlist uploaded successfully",
|
||||
"error": "Failed to upload wordlist"
|
||||
},
|
||||
"update": {
|
||||
"success": "Wordlist updated successfully",
|
||||
"error": "Failed to update wordlist"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Wordlist deleted successfully",
|
||||
"error": "Failed to delete wordlist"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"settings": {
|
||||
"success": "Notification settings saved",
|
||||
"error": "Failed to save notification settings"
|
||||
},
|
||||
"markRead": {
|
||||
"success": "Marked as read",
|
||||
"error": "Failed to mark as read"
|
||||
},
|
||||
"connection": {
|
||||
"error": "Notification connection error: {message}"
|
||||
}
|
||||
},
|
||||
"tool": {
|
||||
"create": {
|
||||
"success": "Tool created successfully",
|
||||
"error": "Failed to create tool"
|
||||
},
|
||||
"update": {
|
||||
"success": "Tool updated successfully",
|
||||
"error": "Failed to update tool"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Tool deleted successfully",
|
||||
"error": "Failed to delete tool"
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"create": {
|
||||
"success": "Engine created successfully",
|
||||
"error": "Failed to create engine"
|
||||
},
|
||||
"update": {
|
||||
"success": "Engine updated successfully",
|
||||
"error": "Failed to update engine"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Engine deleted successfully",
|
||||
"error": "Failed to delete engine"
|
||||
}
|
||||
},
|
||||
"command": {
|
||||
"create": {
|
||||
"success": "Command created successfully",
|
||||
"error": "Failed to create command"
|
||||
},
|
||||
"update": {
|
||||
"success": "Command updated successfully",
|
||||
"error": "Failed to update command"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Command deleted successfully",
|
||||
"bulkSuccess": "Deleted {count} commands",
|
||||
"error": "Failed to delete command"
|
||||
}
|
||||
},
|
||||
"nucleiTemplate": {
|
||||
"refresh": {
|
||||
"loading": "Syncing templates...",
|
||||
"success": "Templates synced successfully",
|
||||
"error": "Failed to sync templates"
|
||||
},
|
||||
"upload": {
|
||||
"success": "Template uploaded successfully",
|
||||
"error": "Failed to upload template"
|
||||
},
|
||||
"save": {
|
||||
"success": "Template saved successfully",
|
||||
"error": "Failed to save template"
|
||||
}
|
||||
},
|
||||
"nucleiGit": {
|
||||
"settings": {
|
||||
"success": "Git settings saved successfully",
|
||||
"error": "Failed to save Git settings"
|
||||
}
|
||||
},
|
||||
"systemLog": {
|
||||
"fetch": {
|
||||
"error": "Failed to fetch system logs, please check backend",
|
||||
"recovered": "System log connection recovered"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quickScan": {
|
||||
"title": "Quick Scan",
|
||||
@@ -1488,5 +1778,16 @@
|
||||
"bulkAdd": "Bulk Add",
|
||||
"formatInvalid": "Invalid format"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"unknown": "Operation failed, please try again later",
|
||||
"validation": "Invalid input data",
|
||||
"notFound": "Resource not found",
|
||||
"permissionDenied": "Permission denied",
|
||||
"serverError": "Server error, please try again later",
|
||||
"badRequest": "Invalid request format",
|
||||
"conflict": "Resource conflict, please check and try again",
|
||||
"unauthorized": "Please login first",
|
||||
"rateLimited": "Too many requests, please try again later"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,18 @@
|
||||
"active": "活跃",
|
||||
"inactive": "未激活",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用"
|
||||
"disabled": "已禁用",
|
||||
"creating": "正在创建...",
|
||||
"updating": "正在更新...",
|
||||
"deleting": "正在删除...",
|
||||
"removing": "正在移除...",
|
||||
"unlinking": "正在解除关联...",
|
||||
"batchCreating": "正在批量创建...",
|
||||
"batchDeleting": "正在批量删除...",
|
||||
"batchRemoving": "正在批量移除...",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateFailed": "更新失败",
|
||||
"uploading": "正在上传..."
|
||||
},
|
||||
"pagination": {
|
||||
"page": "第 {current} 页,共 {total} 页",
|
||||
@@ -1225,7 +1236,286 @@
|
||||
"invalidJsonFile": "无效的 JSON 文件",
|
||||
"importSuccess": "导入成功",
|
||||
"importFailed": "导入失败",
|
||||
"loadScanHistoryFailed": "加载扫描历史失败"
|
||||
"loadScanHistoryFailed": "加载扫描历史失败",
|
||||
"scan": {
|
||||
"quick": {
|
||||
"success": "已启动 {count} 个扫描任务",
|
||||
"error": "启动扫描失败"
|
||||
},
|
||||
"stop": {
|
||||
"success": "扫描已停止,已撤销 {count} 个任务"
|
||||
},
|
||||
"delete": {
|
||||
"success": "已删除扫描任务: {name}",
|
||||
"bulkSuccess": "已删除 {count} 个扫描任务"
|
||||
},
|
||||
"initiate": {
|
||||
"success": "扫描发起成功",
|
||||
"error": "发起扫描失败"
|
||||
}
|
||||
},
|
||||
"scheduledScan": {
|
||||
"create": {
|
||||
"success": "定时扫描创建成功",
|
||||
"error": "创建定时扫描失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "定时扫描更新成功",
|
||||
"error": "更新定时扫描失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "定时扫描删除成功",
|
||||
"error": "删除定时扫描失败"
|
||||
},
|
||||
"toggle": {
|
||||
"enabled": "定时扫描已启用",
|
||||
"disabled": "定时扫描已禁用",
|
||||
"error": "切换定时扫描状态失败"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"create": {
|
||||
"success": "目标创建成功",
|
||||
"bulkSuccess": "已创建 {count} 个目标",
|
||||
"error": "创建目标失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "目标更新成功",
|
||||
"error": "更新目标失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "目标 \"{name}\" 已删除",
|
||||
"bulkSuccess": "已删除 {count} 个目标",
|
||||
"error": "删除目标失败"
|
||||
},
|
||||
"link": {
|
||||
"success": "目标已关联到组织",
|
||||
"error": "关联目标失败"
|
||||
},
|
||||
"unlink": {
|
||||
"success": "目标已从组织解除关联",
|
||||
"bulkSuccess": "已解除 {count} 个目标的关联",
|
||||
"error": "解除关联失败"
|
||||
}
|
||||
},
|
||||
"organization": {
|
||||
"create": {
|
||||
"success": "组织创建成功",
|
||||
"error": "创建组织失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "组织更新成功",
|
||||
"error": "更新组织失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "组织 \"{name}\" 已删除",
|
||||
"bulkSuccess": "已删除 {count} 个组织",
|
||||
"error": "删除组织失败"
|
||||
}
|
||||
},
|
||||
"asset": {
|
||||
"subdomain": {
|
||||
"create": {
|
||||
"success": "已创建 {count} 个子域名",
|
||||
"partialSuccess": "已创建 {success} 个子域名,跳过 {skipped} 个",
|
||||
"error": "创建子域名失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "子域名已删除",
|
||||
"bulkSuccess": "已删除 {count} 个子域名",
|
||||
"error": "删除子域名失败"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"create": {
|
||||
"success": "已创建 {count} 个网站",
|
||||
"partialSuccess": "已创建 {success} 个网站,跳过 {skipped} 个",
|
||||
"error": "创建网站失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "网站已删除",
|
||||
"bulkSuccess": "已删除 {count} 个网站",
|
||||
"error": "删除网站失败"
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"create": {
|
||||
"success": "已创建 {count} 个端点",
|
||||
"partialSuccess": "已创建 {success} 个端点,跳过 {skipped} 个",
|
||||
"error": "创建端点失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "端点已删除",
|
||||
"bulkSuccess": "已删除 {count} 个端点",
|
||||
"error": "删除端点失败"
|
||||
}
|
||||
},
|
||||
"directory": {
|
||||
"create": {
|
||||
"success": "已创建 {count} 个目录",
|
||||
"partialSuccess": "已创建 {success} 个目录,跳过 {skipped} 个",
|
||||
"error": "创建目录失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "目录已删除",
|
||||
"bulkSuccess": "已删除 {count} 个目录",
|
||||
"error": "删除目录失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"success": "登录成功"
|
||||
},
|
||||
"logout": {
|
||||
"success": "已登出"
|
||||
},
|
||||
"changePassword": {
|
||||
"success": "密码修改成功",
|
||||
"error": "修改密码失败"
|
||||
}
|
||||
},
|
||||
"worker": {
|
||||
"create": {
|
||||
"success": "节点创建成功",
|
||||
"error": "创建节点失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "节点更新成功",
|
||||
"error": "更新节点失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "节点删除成功",
|
||||
"error": "删除节点失败"
|
||||
},
|
||||
"deploy": {
|
||||
"success": "节点部署已启动",
|
||||
"error": "部署节点失败"
|
||||
},
|
||||
"restart": {
|
||||
"success": "节点正在重启",
|
||||
"error": "重启节点失败"
|
||||
},
|
||||
"stop": {
|
||||
"success": "节点已停止",
|
||||
"error": "停止节点失败"
|
||||
}
|
||||
},
|
||||
"nucleiRepo": {
|
||||
"create": {
|
||||
"success": "仓库添加成功",
|
||||
"error": "添加仓库失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "仓库更新成功",
|
||||
"error": "更新仓库失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "仓库删除成功",
|
||||
"error": "删除仓库失败"
|
||||
},
|
||||
"sync": {
|
||||
"success": "仓库同步成功",
|
||||
"error": "同步仓库失败"
|
||||
}
|
||||
},
|
||||
"wordlist": {
|
||||
"upload": {
|
||||
"success": "字典上传成功",
|
||||
"error": "上传字典失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "字典更新成功",
|
||||
"error": "更新字典失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "字典删除成功",
|
||||
"error": "删除字典失败"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"settings": {
|
||||
"success": "通知设置已保存",
|
||||
"error": "保存通知设置失败"
|
||||
},
|
||||
"markRead": {
|
||||
"success": "已标记为已读",
|
||||
"error": "标记已读失败"
|
||||
},
|
||||
"connection": {
|
||||
"error": "通知连接错误: {message}"
|
||||
}
|
||||
},
|
||||
"tool": {
|
||||
"create": {
|
||||
"success": "工具创建成功",
|
||||
"error": "创建工具失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "工具更新成功",
|
||||
"error": "更新工具失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "工具删除成功",
|
||||
"error": "删除工具失败"
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"create": {
|
||||
"success": "引擎创建成功",
|
||||
"error": "创建引擎失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "引擎更新成功",
|
||||
"error": "更新引擎失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "引擎删除成功",
|
||||
"error": "删除引擎失败"
|
||||
}
|
||||
},
|
||||
"command": {
|
||||
"create": {
|
||||
"success": "命令创建成功",
|
||||
"error": "创建命令失败"
|
||||
},
|
||||
"update": {
|
||||
"success": "命令更新成功",
|
||||
"error": "更新命令失败"
|
||||
},
|
||||
"delete": {
|
||||
"success": "命令删除成功",
|
||||
"bulkSuccess": "已删除 {count} 个命令",
|
||||
"error": "删除命令失败"
|
||||
}
|
||||
},
|
||||
"nucleiTemplate": {
|
||||
"refresh": {
|
||||
"loading": "正在同步模板...",
|
||||
"success": "模板同步成功",
|
||||
"error": "同步模板失败"
|
||||
},
|
||||
"upload": {
|
||||
"success": "模板上传成功",
|
||||
"error": "上传模板失败"
|
||||
},
|
||||
"save": {
|
||||
"success": "模板保存成功",
|
||||
"error": "保存模板失败"
|
||||
}
|
||||
},
|
||||
"nucleiGit": {
|
||||
"settings": {
|
||||
"success": "Git 设置保存成功",
|
||||
"error": "保存 Git 设置失败"
|
||||
}
|
||||
},
|
||||
"systemLog": {
|
||||
"fetch": {
|
||||
"error": "系统日志获取失败,请检查后端接口",
|
||||
"recovered": "系统日志连接已恢复"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quickScan": {
|
||||
"title": "快速扫描",
|
||||
@@ -1488,5 +1778,16 @@
|
||||
"bulkAdd": "批量添加",
|
||||
"formatInvalid": "格式无效"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"unknown": "操作失败,请稍后重试",
|
||||
"validation": "输入数据无效",
|
||||
"notFound": "资源未找到",
|
||||
"permissionDenied": "权限不足",
|
||||
"serverError": "服务器错误,请稍后重试",
|
||||
"badRequest": "请求格式错误",
|
||||
"conflict": "资源冲突,请检查后重试",
|
||||
"unauthorized": "请先登录",
|
||||
"rateLimited": "请求过于频繁,请稍后重试"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user