new file: .claude/settings.local.json

new file:   .gitignore
	new file:   backend/.env.example
	new file:   backend/.gitignore
	new file:   backend/ecosystem.config.js
	new file:   backend/nodemon.json
	new file:   backend/package-lock.json
	new file:   backend/package.json
	new file:   backend/src/config/database.ts
	new file:   backend/src/config/dynamicSwagger.ts
	new file:   backend/src/config/environment.ts
	new file:   backend/src/config/swagger.ts
	new file:   backend/src/controllers/apiKeyController.ts
	new file:   backend/src/controllers/authController.ts
	new file:   backend/src/controllers/databaseController.ts
	new file:   backend/src/controllers/databaseManagementController.ts
	new file:   backend/src/controllers/dynamicApiController.ts
	new file:   backend/src/controllers/endpointController.ts
	new file:   backend/src/controllers/folderController.ts
	new file:   backend/src/controllers/logsController.ts
	new file:   backend/src/controllers/userController.ts
	new file:   backend/src/middleware/apiKey.ts
	new file:   backend/src/middleware/auth.ts
	new file:   backend/src/middleware/logging.ts
	new file:   backend/src/migrations/001_initial_schema.sql
	new file:   backend/src/migrations/002_add_logging.sql
	new file:   backend/src/migrations/003_add_scripting.sql
	new file:   backend/src/migrations/004_add_superadmin.sql
	new file:   backend/src/migrations/run.ts
	new file:   backend/src/migrations/seed.ts
	new file:   backend/src/routes/apiKeys.ts
	new file:   backend/src/routes/auth.ts
	new file:   backend/src/routes/databaseManagement.ts
	new file:   backend/src/routes/databases.ts
	new file:   backend/src/routes/dynamic.ts
	new file:   backend/src/routes/endpoints.ts
	new file:   backend/src/routes/folders.ts
	new file:   backend/src/routes/logs.ts
	new file:   backend/src/routes/users.ts
	new file:   backend/src/server.ts
	new file:   backend/src/services/DatabasePoolManager.ts
	new file:   backend/src/services/ScriptExecutor.ts
	new file:   backend/src/services/SqlExecutor.ts
	new file:   backend/src/types/index.ts
	new file:   backend/tsconfig.json
	new file:   frontend/.gitignore
	new file:   frontend/index.html
	new file:   frontend/nginx.conf
	new file:   frontend/package-lock.json
	new file:   frontend/package.json
	new file:   frontend/postcss.config.js
	new file:   frontend/src/App.tsx
	new file:   frontend/src/components/CodeEditor.tsx
This commit is contained in:
GEgorov
2025-10-07 00:04:04 +03:00
commit 8943f5a070
79 changed files with 17032 additions and 0 deletions

342
frontend/src/pages/Logs.tsx Normal file
View File

@@ -0,0 +1,342 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { logsApi, endpointsApi, apiKeysApi } from '@/services/api';
import { Trash2, Eye, Filter, X } from 'lucide-react';
import toast from 'react-hot-toast';
import { format } from 'date-fns';
import Dialog from '@/components/Dialog';
export default function Logs() {
const queryClient = useQueryClient();
const [selectedLog, setSelectedLog] = useState<any | null>(null);
const [filters, setFilters] = useState({
endpoint_id: '',
api_key_id: '',
});
const [dialog, setDialog] = useState<{
isOpen: boolean;
title: string;
message: string;
type: 'alert' | 'confirm';
onConfirm?: () => void;
}>({
isOpen: false,
title: '',
message: '',
type: 'alert',
});
const { data: logs, isLoading } = useQuery({
queryKey: ['logs', filters],
queryFn: () => logsApi.getAll(filters).then(res => res.data),
});
const { data: endpoints } = useQuery({
queryKey: ['endpoints'],
queryFn: () => endpointsApi.getAll().then(res => res.data),
});
const { data: apiKeys } = useQuery({
queryKey: ['apiKeys'],
queryFn: () => apiKeysApi.getAll().then(res => res.data),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => logsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['logs'] });
toast.success('Лог удален');
},
onError: () => toast.error('Ошибка удаления лога'),
});
const clearMutation = useMutation({
mutationFn: (data: any) => logsApi.clear(data),
onSuccess: (response) => {
queryClient.invalidateQueries({ queryKey: ['logs'] });
toast.success(`Удалено ${response.data.deleted} лог(ов)`);
},
onError: () => toast.error('Ошибка очистки логов'),
});
const handleClearAll = () => {
setDialog({
isOpen: true,
title: 'Подтверждение',
message: 'Вы уверены, что хотите очистить все логи?',
type: 'confirm',
onConfirm: () => {
clearMutation.mutate({});
},
});
};
const handleClearFiltered = () => {
setDialog({
isOpen: true,
title: 'Подтверждение',
message: 'Вы уверены, что хотите очистить логи с текущими фильтрами?',
type: 'confirm',
onConfirm: () => {
clearMutation.mutate(filters);
},
});
};
return (
<div>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Логи запросов</h1>
<p className="text-gray-600">История запросов к API эндпоинтам</p>
</div>
<div className="flex gap-2">
{Object.values(filters).some(v => v) && (
<button onClick={handleClearFiltered} className="btn btn-secondary">
Очистить отфильтрованные
</button>
)}
<button onClick={handleClearAll} className="btn btn-danger">
Очистить все логи
</button>
</div>
</div>
<div className="card p-4 mb-6">
<div className="flex items-center gap-3 mb-4">
<Filter size={20} className="text-gray-400" />
<h3 className="font-semibold">Фильтры</h3>
{Object.values(filters).some(v => v) && (
<button
onClick={() => setFilters({ endpoint_id: '', api_key_id: '' })}
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
>
<X size={16} />
Сбросить
</button>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Эндпоинт</label>
<select
value={filters.endpoint_id}
onChange={(e) => setFilters({ ...filters, endpoint_id: e.target.value })}
className="input w-full"
>
<option value="">Все эндпоинты</option>
{endpoints?.map((ep) => (
<option key={ep.id} value={ep.id}>{ep.name} ({ep.method} {ep.path})</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">API Ключ</label>
<select
value={filters.api_key_id}
onChange={(e) => setFilters({ ...filters, api_key_id: e.target.value })}
className="input w-full"
>
<option value="">Все ключи</option>
{apiKeys?.map((key) => (
<option key={key.id} value={key.id}>{key.name}</option>
))}
</select>
</div>
</div>
</div>
{isLoading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
</div>
) : (
<div className="space-y-2">
{logs?.map((log: any) => (
<div key={log.id} className="card p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className={`px-2 py-1 text-xs font-semibold rounded ${
log.response_status >= 200 && log.response_status < 300
? 'bg-green-100 text-green-700'
: log.response_status >= 400
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
}`}>
{log.response_status}
</span>
<span className="font-semibold text-gray-900">{log.method}</span>
<span className="text-gray-600">{log.path}</span>
{log.endpoint_name && (
<span className="text-sm text-gray-500"> {log.endpoint_name}</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span> {log.execution_time}мс</span>
<span>📅 {format(new Date(log.created_at), 'dd.MM.yyyy HH:mm:ss')}</span>
{log.api_key_name && (
<span>🔑 {log.api_key_name}</span>
)}
{log.ip_address && log.ip_address !== 'unknown' && (
<span>📍 {log.ip_address}</span>
)}
</div>
{log.error_message && (
<div className="mt-2 p-2 bg-red-50 rounded text-sm text-red-700">
{log.error_message}
</div>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => setSelectedLog(log)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
title="Детали"
>
<Eye size={18} className="text-gray-600" />
</button>
<button
onClick={() => {
setDialog({
isOpen: true,
title: 'Подтверждение',
message: 'Удалить этот лог?',
type: 'confirm',
onConfirm: () => {
deleteMutation.mutate(log.id);
},
});
}}
className="p-2 hover:bg-red-50 rounded-lg transition-colors"
title="Удалить"
>
<Trash2 size={18} className="text-red-600" />
</button>
</div>
</div>
</div>
))}
{logs?.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500">Логи не найдены</p>
</div>
)}
</div>
)}
{selectedLog && (
<LogDetailModal log={selectedLog} onClose={() => setSelectedLog(null)} />
)}
<Dialog
isOpen={dialog.isOpen}
onClose={() => setDialog({ ...dialog, isOpen: false })}
title={dialog.title}
message={dialog.message}
type={dialog.type}
onConfirm={dialog.onConfirm}
/>
</div>
);
}
function LogDetailModal({ log, onClose }: { log: any; onClose: () => void }) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">Детали лога</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded">
<X size={24} />
</button>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-gray-700">Метод</label>
<p className="text-gray-900">{log.method}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Путь</label>
<p className="text-gray-900">{log.path}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Статус</label>
<p className="text-gray-900">{log.response_status}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Время выполнения</label>
<p className="text-gray-900">{log.execution_time}мс</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Эндпоинт</label>
<p className="text-gray-900">{log.endpoint_name || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">API Ключ</label>
<p className="text-gray-900">{log.api_key_name || 'Без ключа'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">IP адрес</label>
<p className="text-gray-900">{log.ip_address}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Дата/время</label>
<p className="text-gray-900">{format(new Date(log.created_at), 'dd.MM.yyyy HH:mm:ss')}</p>
</div>
</div>
{log.request_params && Object.keys(log.request_params).length > 0 && (
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">Параметры запроса</label>
<pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
{JSON.stringify(log.request_params, null, 2)}
</pre>
</div>
)}
{log.request_body && Object.keys(log.request_body).length > 0 && (
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">Тело запроса</label>
<pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
{JSON.stringify(log.request_body, null, 2)}
</pre>
</div>
)}
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">Ответ</label>
<pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm">
{JSON.stringify(log.response_data, null, 2)}
</pre>
</div>
{log.error_message && (
<div>
<label className="text-sm font-medium text-red-700 mb-2 block">Ошибка</label>
<div className="bg-red-50 border border-red-200 rounded p-4 text-red-700">
{log.error_message}
</div>
</div>
)}
{log.user_agent && log.user_agent !== 'unknown' && (
<div>
<label className="text-sm font-medium text-gray-700">User Agent</label>
<p className="text-sm text-gray-600">{log.user_agent}</p>
</div>
)}
</div>
<div className="p-6 border-t border-gray-200">
<button onClick={onClose} className="btn btn-secondary">
Закрыть
</button>
</div>
</div>
</div>
);
}