modified: backend/src/services/DatabasePoolManager.ts

modified:   backend/src/services/ScriptExecutor.ts
	modified:   backend/src/types/index.ts
	modified:   frontend/src/components/CodeEditor.tsx
	modified:   frontend/src/components/EndpointModal.tsx
	deleted:    frontend/src/pages/Databases.tsx
	modified:   frontend/src/pages/Settings.tsx
	modified:   frontend/src/types/index.ts
This commit is contained in:
GEgorov
2025-10-07 20:16:53 +03:00
parent 65e4f5b423
commit c85fa20634
8 changed files with 311 additions and 562 deletions

View File

@@ -1,473 +0,0 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { dbManagementApi } from '@/services/api';
import { useAuthStore } from '@/stores/authStore';
import { Database, Plus, Edit2, Trash2 } from 'lucide-react';
import toast from 'react-hot-toast';
import Dialog from '@/components/Dialog';
export default function Databases() {
const { user } = useAuthStore();
const queryClient = useQueryClient();
const [showModal, setShowModal] = useState(false);
const [editingDatabase, setEditingDatabase] = useState<any | null>(null);
const isAdmin = user?.role === 'admin';
const [dialog, setDialog] = useState<{
isOpen: boolean;
title: string;
message: string;
type: 'alert' | 'confirm';
onConfirm?: () => void;
}>({
isOpen: false,
title: '',
message: '',
type: 'alert',
});
const { data: databases, isLoading } = useQuery({
queryKey: ['databases'],
queryFn: () => dbManagementApi.getAll().then(res => res.data),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => dbManagementApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['databases'] });
toast.success('База данных удалена');
},
onError: () => toast.error('Не удалось удалить базу данных'),
});
const testConnection = async (databaseId: string) => {
try {
const { data } = await dbManagementApi.test(databaseId);
if (data.success) {
toast.success('Подключение успешно!');
} else {
toast.error('Ошибка подключения');
}
} catch (error) {
toast.error('Ошибка тестирования подключения');
}
};
const handleEdit = (database: any) => {
setEditingDatabase(database);
setShowModal(true);
};
const handleCreate = () => {
setEditingDatabase(null);
setShowModal(true);
};
const handleDelete = (id: string) => {
setDialog({
isOpen: true,
title: 'Подтверждение',
message: 'Вы уверены, что хотите удалить эту базу данных?',
type: 'confirm',
onConfirm: () => {
deleteMutation.mutate(id);
},
});
};
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">Управление подключениями к базам данных</p>
</div>
{isAdmin && (
<button onClick={handleCreate} className="btn btn-primary flex items-center gap-2">
<Plus size={20} />
Добавить базу данных
</button>
)}
</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="grid grid-cols-1 md:grid-cols-2 gap-6">
{databases?.map((db) => (
<div key={db.id} className="card p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="bg-primary-100 p-3 rounded-lg">
<Database size={24} className="text-primary-600" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900">{db.name}</h3>
<p className="text-sm text-gray-500">{db.type}</p>
</div>
</div>
{isAdmin && (
<div className="flex gap-2">
<button
onClick={() => handleEdit(db)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
title="Редактировать"
>
<Edit2 size={18} className="text-gray-600" />
</button>
<button
onClick={() => handleDelete(db.id)}
className="p-2 hover:bg-red-50 rounded-lg transition-colors"
title="Удалить"
>
<Trash2 size={18} className="text-red-600" />
</button>
</div>
)}
</div>
<div className="space-y-2 text-sm mb-4">
<div className="flex justify-between">
<span className="text-gray-600">Хост:</span>
<span className="font-medium">{db.host}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Порт:</span>
<span className="font-medium">{db.port}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">База данных:</span>
<span className="font-medium">{db.database_name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Пользователь:</span>
<span className="font-medium">{db.username}</span>
</div>
{db.is_active !== undefined && (
<div className="flex justify-between">
<span className="text-gray-600">Статус:</span>
<span className={`font-medium ${db.is_active ? 'text-green-600' : 'text-red-600'}`}>
{db.is_active ? 'Активна' : 'Неактивна'}
</span>
</div>
)}
</div>
<button
onClick={() => testConnection(db.id)}
className="btn btn-primary w-full"
>
Тест подключения
</button>
</div>
))}
{databases?.length === 0 && (
<div className="col-span-2 text-center py-12">
<p className="text-gray-500">
{isAdmin
? 'Базы данных не настроены. Добавьте первую базу данных.'
: 'Базы данных не настроены.'}
</p>
</div>
)}
</div>
)}
{isAdmin && !isLoading && databases && databases.length > 0 && (
<div className="card p-6 mt-6">
<h2 className="text-xl font-semibold mb-4">Информация</h2>
<p className="text-gray-600">
Базы данных управляются через интерфейс администратора.
Вы можете добавлять, редактировать и удалять подключения к базам данных.
Каждый эндпоинт использует одно из этих подключений для выполнения SQL запросов.
</p>
</div>
)}
{showModal && (
<DatabaseModal
database={editingDatabase}
onClose={() => setShowModal(false)}
/>
)}
<Dialog
isOpen={dialog.isOpen}
onClose={() => setDialog({ ...dialog, isOpen: false })}
title={dialog.title}
message={dialog.message}
type={dialog.type}
onConfirm={dialog.onConfirm}
/>
</div>
);
}
function DatabaseModal({
database,
onClose,
}: {
database: any | null;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const [formData, setFormData] = useState({
name: database?.name || '',
type: database?.type || 'postgresql',
host: database?.host || 'localhost',
port: database?.port || 5432,
database_name: database?.database_name || '',
username: database?.username || '',
password: database?.password || '',
ssl: database?.ssl || false,
is_active: database?.is_active !== undefined ? database.is_active : true,
// AQL-specific fields
aql_base_url: database?.aql_base_url || '',
aql_auth_type: database?.aql_auth_type || 'basic',
aql_auth_value: database?.aql_auth_value || '',
aql_headers: database?.aql_headers || {},
});
const saveMutation = useMutation({
mutationFn: (data: any) =>
database ? dbManagementApi.update(database.id, data) : dbManagementApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['databases'] });
toast.success(database ? 'База данных обновлена' : 'База данных создана');
onClose();
},
onError: (error: any) => {
toast.error(error.response?.data?.error || 'Не удалось сохранить базу данных');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
saveMutation.mutate(formData);
};
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-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900">
{database ? 'Редактировать базу данных' : 'Добавить базу данных'}
</h2>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Название</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="input w-full"
placeholder="Основная база PostgreSQL"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Тип</label>
<select
value={formData.type}
onChange={(e) => {
console.log('🔄 Database type changed to:', e.target.value);
setFormData({ ...formData, type: e.target.value });
}}
className="input w-full"
>
<option value="postgresql">PostgreSQL</option>
<option value="aql">AQL (HTTP API)</option>
</select>
{/* DEBUG: AQL option should be visible above */}
<div className="text-xs text-gray-500 mt-1">Available types: PostgreSQL, AQL (HTTP API)</div>
</div>
{formData.type === 'aql' ? (
<>
{/* AQL Fields */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Base URL *</label>
<input
type="text"
required
value={formData.aql_base_url}
onChange={(e) => setFormData({ ...formData, aql_base_url: e.target.value })}
className="input w-full"
placeholder="http://api.ehrdb.ncms-i.ru/api/rest/v1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Тип аутентификации</label>
<select
value={formData.aql_auth_type}
onChange={(e) => setFormData({ ...formData, aql_auth_type: e.target.value })}
className="input w-full"
>
<option value="basic">Basic Auth</option>
<option value="bearer">Bearer Token</option>
<option value="custom">Custom Header</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{formData.aql_auth_type === 'basic' && 'Basic Auth Value (Base64)'}
{formData.aql_auth_type === 'bearer' && 'Bearer Token'}
{formData.aql_auth_type === 'custom' && 'Custom Authorization Value'}
</label>
<input
type="password"
required={!database}
value={formData.aql_auth_value}
onChange={(e) => setFormData({ ...formData, aql_auth_value: e.target.value })}
className="input w-full"
placeholder={database ? '••••••••' : 'Введите значение'}
/>
{database && (
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Дополнительные заголовки (JSON)
<span className="text-xs text-gray-500 ml-2">необязательно</span>
</label>
<textarea
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
setFormData({ ...formData, aql_headers: parsed });
} catch {
setFormData({ ...formData, aql_headers: e.target.value });
}
}}
className="input w-full font-mono text-sm"
rows={4}
placeholder='{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}'
/>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-gray-700">Активна</span>
</label>
</div>
</>
) : (
<>
{/* PostgreSQL Fields */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Хост</label>
<input
type="text"
required
value={formData.host}
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
className="input w-full"
placeholder="localhost"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Порт</label>
<input
type="number"
required
value={formData.port}
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
className="input w-full"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Имя базы данных</label>
<input
type="text"
required
value={formData.database_name}
onChange={(e) => setFormData({ ...formData, database_name: e.target.value })}
className="input w-full"
placeholder="my_database"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Пользователь</label>
<input
type="text"
required
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
className="input w-full"
placeholder="postgres"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Пароль</label>
<input
type="password"
required={!database}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="input w-full"
placeholder={database ? '••••••••' : 'Введите пароль'}
/>
{database && (
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять пароль</p>
)}
</div>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.ssl}
onChange={(e) => setFormData({ ...formData, ssl: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-gray-700">Использовать SSL</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-gray-700">Активна</span>
</label>
</div>
</>
)}
<div className="flex gap-3 pt-4 border-t border-gray-200">
<button type="button" onClick={onClose} className="btn btn-secondary">
Отмена
</button>
<button type="submit" disabled={saveMutation.isPending} className="btn btn-primary">
{saveMutation.isPending ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/authStore';
import toast from 'react-hot-toast';
import { User, Lock, UserCircle, Database, Plus, Edit2, Trash2, Eye, EyeOff, Users } from 'lucide-react';
import Dialog from '@/components/Dialog';
import CodeEditor from '@/components/CodeEditor';
export default function Settings() {
const { user } = useAuthStore();
@@ -557,24 +558,27 @@ function DatabaseModal({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 mb-2">
Дополнительные заголовки (JSON)
<span className="text-xs text-gray-500 ml-2">необязательно</span>
</label>
<textarea
<CodeEditor
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
onChange={(e) => {
onChange={(value) => {
try {
const parsed = JSON.parse(e.target.value);
const parsed = JSON.parse(value);
setFormData({ ...formData, aql_headers: parsed });
} catch {
setFormData({ ...formData, aql_headers: e.target.value });
// Сохраняем невалидный JSON как строку для последующего редактирования
setFormData({ ...formData, aql_headers: value as any });
}
}}
className="input w-full font-mono text-sm"
rows={4}
placeholder='{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}'
language="json"
height="150px"
/>
<p className="text-xs text-gray-500 mt-1">
Пример: {`{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}`}
</p>
</div>
<div className="flex items-center gap-4">