重构响应为标准响应格式

This commit is contained in:
yyhuni
2025-12-30 10:56:26 +08:00
parent 38ed8bc642
commit 0628eef025
37 changed files with 2366 additions and 859 deletions

View File

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

View 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' # 请求过于频繁

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}

View 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;
}

View 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);
},
};

View File

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

View File

@@ -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": "请求过于频繁,请稍后重试"
}
}