ваше сообщение коммита
This commit is contained in:
@@ -70,7 +70,8 @@ let ws = null;
|
||||
|
||||
function connectWebSocket() {
|
||||
if (ws) ws.close();
|
||||
ws = new WebSocket('ws://localhost:8000');
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
||||
ws.onopen = () => {
|
||||
console.log('[CRM] WebSocket соединение установлено');
|
||||
};
|
||||
|
||||
@@ -40,8 +40,7 @@ async function reloadDleList() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
dleList.value = await dleService.getAllDLEs() || [];
|
||||
onMounted(reloadDleList);
|
||||
await reloadDleList();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
@send-message="handleSendMessage"
|
||||
@load-more="loadMessages"
|
||||
/>
|
||||
<!-- Можно добавить заглушку или пояснение -->
|
||||
<div class="empty-table-placeholder">Вы видите только свои сообщения. Данные других пользователей недоступны.</div>
|
||||
</template>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
@@ -87,33 +87,33 @@ const filteredEmbeddingModels = computed(() => {
|
||||
return embeddingModels.value.filter(m => m.provider === selectedLLM.value.provider);
|
||||
});
|
||||
async function loadUserTables() {
|
||||
const { data } = await axios.get('/api/tables');
|
||||
const { data } = await axios.get('/tables');
|
||||
userTables.value = Array.isArray(data) ? data : [];
|
||||
}
|
||||
async function loadRules() {
|
||||
const { data } = await axios.get('/api/settings/ai-assistant-rules');
|
||||
const { data } = await axios.get('/settings/ai-assistant-rules');
|
||||
rulesList.value = data.rules || [];
|
||||
}
|
||||
async function loadSettings() {
|
||||
const { data } = await axios.get('/api/settings/ai-assistant');
|
||||
const { data } = await axios.get('/settings/ai-assistant');
|
||||
if (data.success && data.settings) {
|
||||
settings.value = data.settings;
|
||||
}
|
||||
}
|
||||
async function loadTelegramBots() {
|
||||
const { data } = await axios.get('/api/settings/telegram-settings/list');
|
||||
const { data } = await axios.get('/settings/telegram-settings/list');
|
||||
telegramBots.value = data.items || [];
|
||||
}
|
||||
async function loadEmailList() {
|
||||
const { data } = await axios.get('/api/settings/email-settings/list');
|
||||
const { data } = await axios.get('/settings/email-settings/list');
|
||||
emailList.value = data.items || [];
|
||||
}
|
||||
async function loadLLMModels() {
|
||||
const { data } = await axios.get('/api/settings/llm-models');
|
||||
const { data } = await axios.get('/settings/llm-models');
|
||||
llmModels.value = data.models || [];
|
||||
}
|
||||
async function loadEmbeddingModels() {
|
||||
const { data } = await axios.get('/api/settings/embedding-models');
|
||||
const { data } = await axios.get('/settings/embedding-models');
|
||||
embeddingModels.value = data.models || [];
|
||||
}
|
||||
onMounted(() => {
|
||||
@@ -126,7 +126,7 @@ onMounted(() => {
|
||||
loadEmbeddingModels();
|
||||
});
|
||||
async function saveSettings() {
|
||||
await axios.put('/api/settings/ai-assistant', settings.value);
|
||||
await axios.put('/settings/ai-assistant', settings.value);
|
||||
goBack();
|
||||
}
|
||||
function openRuleEditor(ruleId = null) {
|
||||
@@ -139,7 +139,7 @@ function openRuleEditor(ruleId = null) {
|
||||
}
|
||||
async function deleteRule(ruleId) {
|
||||
if (!confirm('Удалить этот набор правил?')) return;
|
||||
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
|
||||
await axios.delete(`/settings/ai-assistant-rules/${ruleId}`);
|
||||
await loadRules();
|
||||
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const editMode = ref(false);
|
||||
|
||||
const loadDbSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/settings/db-settings');
|
||||
const res = await api.get('/settings/db-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.dbHost = s.db_host;
|
||||
@@ -85,7 +85,7 @@ onMounted(async () => {
|
||||
|
||||
const saveDbSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/settings/db-settings', {
|
||||
await api.put('/settings/db-settings', {
|
||||
db_host: form.dbHost,
|
||||
db_port: form.dbPort,
|
||||
db_name: form.dbName,
|
||||
|
||||
@@ -74,7 +74,7 @@ const editMode = ref(false);
|
||||
|
||||
const loadEmailSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/settings/email-settings');
|
||||
const res = await api.get('/settings/email-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.smtpHost = s.smtp_host;
|
||||
@@ -98,7 +98,7 @@ onMounted(async () => {
|
||||
|
||||
const saveEmailSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/settings/email-settings', {
|
||||
await api.put('/settings/email-settings', {
|
||||
smtp_host: form.smtpHost,
|
||||
smtp_port: form.smtpPort,
|
||||
smtp_user: form.smtpUser,
|
||||
|
||||
@@ -45,7 +45,7 @@ const editMode = ref(false);
|
||||
|
||||
const loadTelegramSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/settings/telegram-settings');
|
||||
const res = await api.get('/settings/telegram-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.botToken = s.bot_token || '';
|
||||
@@ -64,7 +64,7 @@ onMounted(async () => {
|
||||
|
||||
const saveTelegramSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/telegram-settings', {
|
||||
await api.put('/telegram-settings', {
|
||||
bot_token: form.botToken,
|
||||
bot_username: form.botUsername
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ const saveError = ref('');
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}`);
|
||||
const { data } = await axios.get(`/settings/ai-settings/${props.provider}`);
|
||||
if (data.settings) {
|
||||
apiKey.value = data.settings.api_key || '';
|
||||
baseUrl.value = data.settings.base_url || '';
|
||||
@@ -91,7 +91,7 @@ async function loadSettings() {
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
||||
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
|
||||
models.value = data.models || [];
|
||||
if (!selectedModel.value && models.value.length) {
|
||||
const first = models.value[0];
|
||||
@@ -104,7 +104,7 @@ async function loadModels() {
|
||||
|
||||
async function loadEmbeddingModels() {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
||||
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
|
||||
embeddingModels.value = (data.models || []).filter(m => {
|
||||
const name = m.id || m.name || m;
|
||||
return name && name.toLowerCase().includes('embed');
|
||||
@@ -123,7 +123,7 @@ async function onVerify() {
|
||||
verifyStatus.value = null;
|
||||
verifyError.value = '';
|
||||
try {
|
||||
const { data } = await axios.post(`/api/settings/ai-settings/${props.provider}/verify`, {
|
||||
const { data } = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
});
|
||||
@@ -144,7 +144,7 @@ async function onSave() {
|
||||
saveStatus.value = null;
|
||||
saveError.value = '';
|
||||
try {
|
||||
await axios.put(`/api/settings/ai-settings/${props.provider}`, {
|
||||
await axios.put(`/settings/ai-settings/${props.provider}`, {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
selected_model: selectedModel.value,
|
||||
@@ -161,7 +161,7 @@ async function onSave() {
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
await axios.delete(`/api/settings/ai-settings/${props.provider}`);
|
||||
await axios.delete(`/settings/ai-settings/${props.provider}`);
|
||||
apiKey.value = '';
|
||||
baseUrl.value = '';
|
||||
selectedModel.value = '';
|
||||
|
||||
@@ -93,7 +93,7 @@ async function addToken() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.post('/api/settings/auth-token', {
|
||||
await api.post('/settings/auth-token', {
|
||||
...newToken,
|
||||
minBalance: Number(newToken.minBalance) || 0
|
||||
});
|
||||
|
||||
@@ -446,7 +446,7 @@ const fetchIsicCodes = async (params = {}, optionsRef, loadingRef) => {
|
||||
console.debug(`[BlockchainSettingsView] Fetching ISIC codes with params: ${queryParams}`);
|
||||
|
||||
// Убедитесь, что базовый URL настроен правильно (например, через axios interceptors или .env)
|
||||
const response = await axios.get(`/api/isic/codes?${queryParams}`);
|
||||
const response = await axios.get(`/isic/codes?${queryParams}`);
|
||||
|
||||
if (response.data && Array.isArray(response.data.codes)) {
|
||||
optionsRef.value = response.data.codes.map(code => ({
|
||||
@@ -716,7 +716,7 @@ const fetchAddressByZipcode = async () => {
|
||||
}
|
||||
|
||||
console.log(`[FetchByZipcode] Querying backend proxy for Nominatim with: ${params.toString()}`);
|
||||
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
|
||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
|
||||
if (response.data && response.data.length > 0) {
|
||||
const bestMatch = response.data[0];
|
||||
@@ -818,7 +818,7 @@ const verifyAddress = async () => {
|
||||
|
||||
console.log(`[VerifyAddress] Querying backend proxy for Nominatim with: ${params.toString()}`);
|
||||
// Запрос теперь идет на ваш бэкенд-прокси
|
||||
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
|
||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
|
||||
// Ответ от бэкенд-прокси должен иметь ту же структуру, что и прямой ответ от Nominatim
|
||||
if (response.data && Array.isArray(response.data)) { // Проверяем, что это массив (как отвечает Nominatim)
|
||||
@@ -901,7 +901,7 @@ const toggleShowDeployerKey = () => {
|
||||
// Функция загрузки настроек RPC с сервера
|
||||
const loadRpcSettings = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/settings/rpc');
|
||||
const response = await axios.get('/settings/rpc');
|
||||
console.log('Ответ сервера на /api/settings/rpc:', response.data);
|
||||
if (response.data && response.data.success) {
|
||||
securitySettings.rpcConfigs = (response.data.data || []).map(rpc => ({
|
||||
@@ -921,7 +921,7 @@ const loadRpcSettings = async () => {
|
||||
const saveRpcSettings = async () => {
|
||||
try {
|
||||
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
|
||||
const response = await axios.post('/api/settings/rpc', {
|
||||
const response = await axios.post('/settings/rpc', {
|
||||
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ async function addRpc() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.post('/api/settings/rpc', { networkId, rpcUrl, chainId });
|
||||
await api.post('/settings/rpc', { networkId, rpcUrl, chainId });
|
||||
emit('update'); // сигнал родителю перезагрузить список
|
||||
resetNetworkEntry();
|
||||
} catch (e) {
|
||||
|
||||
@@ -138,7 +138,7 @@ const loadSettings = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// Загрузка RPC конфигураций
|
||||
const rpcResponse = await api.get('/api/settings/rpc');
|
||||
const rpcResponse = await api.get('/settings/rpc');
|
||||
if (rpcResponse.data && rpcResponse.data.success) {
|
||||
securitySettings.rpcConfigs = (rpcResponse.data.data || []).map(rpc => ({
|
||||
networkId: rpc.network_id,
|
||||
@@ -149,7 +149,7 @@ const loadSettings = async () => {
|
||||
}
|
||||
|
||||
// Загрузка токенов для аутентификации
|
||||
const authResponse = await api.get('/api/settings/auth-tokens');
|
||||
const authResponse = await api.get('/settings/auth-tokens');
|
||||
if (authResponse.data && authResponse.data.success) {
|
||||
securitySettings.authTokens = (authResponse.data.data || []).map(token => ({
|
||||
name: token.name,
|
||||
@@ -195,12 +195,12 @@ const saveSecuritySettings = async () => {
|
||||
console.log('[SecuritySettingsView] Отправка настроек на сервер:', settingsData);
|
||||
|
||||
// Отправка RPC конфигураций
|
||||
const rpcResponse = await api.post('/api/settings/rpc', {
|
||||
const rpcResponse = await api.post('/settings/rpc', {
|
||||
rpcConfigs: settingsData.rpcConfigs
|
||||
});
|
||||
|
||||
// Отправка токенов для аутентификации
|
||||
const authResponse = await api.post('/api/settings/auth-tokens', {
|
||||
const authResponse = await api.post('/settings/auth-tokens', {
|
||||
authTokens: settingsData.authTokens
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
<p>Настройки внешнего вида, локализации и пользовательского опыта.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block">
|
||||
<h3>WEB SSH</h3>
|
||||
<p>Автоматическая публикация приложения в интернете через SSH-туннель.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/webssh')">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
703
frontend/src/views/settings/WebSshSettingsView.vue
Normal file
703
frontend/src/views/settings/WebSshSettingsView.vue
Normal file
@@ -0,0 +1,703 @@
|
||||
<template>
|
||||
<div class="web-ssh-settings">
|
||||
<div class="settings-header">
|
||||
<h2>WEB SSH Туннель</h2>
|
||||
<p>Автоматическая публикация локального приложения в интернете через SSH-туннель и NGINX</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!agentAvailable" class="agent-instruction-block">
|
||||
<h3>Установка локального агента</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<b>Windows:</b><br>
|
||||
Откройте <b>PowerShell</b> или <b>Командную строку</b> и выполните:
|
||||
<div class="copy-block" @click="copyToClipboard('wsl')">
|
||||
<pre><code>wsl</code></pre>
|
||||
<span class="copy-icon">
|
||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
Затем в открывшемся терминале WSL выполните:
|
||||
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
|
||||
<pre><code>cd ~/DApp-for-Business
|
||||
sudo bash webssh-agent/install.sh</code></pre>
|
||||
<span class="copy-icon">
|
||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<b>Linux:</b><br>
|
||||
Откройте терминал и выполните:
|
||||
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
|
||||
<pre><code>cd ~/DApp-for-Business
|
||||
sudo bash webssh-agent/install.sh</code></pre>
|
||||
<span class="copy-icon">
|
||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<button @click="checkAgent" class="check-btn">Проверить</button>
|
||||
<div v-if="copied" class="copied-indicator">Скопировано!</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма публикации всегда доступна -->
|
||||
<div>
|
||||
<!-- Статус подключения -->
|
||||
<div class="connection-status">
|
||||
<div class="status-indicator" :class="{ 'active': isConnected, 'inactive': !isConnected }"></div>
|
||||
<span class="status-text">{{ connectionStatus }}</span>
|
||||
<button v-if="isConnected" @click="disconnectTunnel" class="disconnect-btn">Отключить</button>
|
||||
</div>
|
||||
|
||||
<!-- Форма настроек -->
|
||||
<form @submit.prevent="handleSubmit" class="tunnel-form">
|
||||
<div class="form-section">
|
||||
<h3>Настройки домена</h3>
|
||||
<div class="form-group">
|
||||
<label for="domain">Домен *</label>
|
||||
<input
|
||||
id="domain"
|
||||
v-model="form.domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
required
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email для SSL *</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder="admin@example.com"
|
||||
required
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Настройки SSH сервера</h3>
|
||||
<div class="form-group">
|
||||
<label for="sshHost">SSH Host/IP *</label>
|
||||
<input
|
||||
id="sshHost"
|
||||
v-model="form.sshHost"
|
||||
type="text"
|
||||
placeholder="192.168.1.100 или server.example.com"
|
||||
required
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sshUser">SSH Пользователь *</label>
|
||||
<input
|
||||
id="sshUser"
|
||||
v-model="form.sshUser"
|
||||
type="text"
|
||||
placeholder="root"
|
||||
required
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sshKey">SSH Приватный ключ *</label>
|
||||
<textarea
|
||||
id="sshKey"
|
||||
v-model="form.sshKey"
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----"
|
||||
rows="6"
|
||||
required
|
||||
:disabled="isConnected"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section advanced-section">
|
||||
<h3>Дополнительные настройки</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="localPort">Локальный порт</label>
|
||||
<input
|
||||
id="localPort"
|
||||
v-model="form.localPort"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="serverPort">Порт сервера</label>
|
||||
<input
|
||||
id="serverPort"
|
||||
v-model="form.serverPort"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sshPort">SSH порт</label>
|
||||
<input
|
||||
id="sshPort"
|
||||
v-model="form.sshPort"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading || isConnected"
|
||||
class="publish-btn"
|
||||
>
|
||||
{{ isLoading ? 'Настройка...' : 'Опубликовать' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="resetForm"
|
||||
:disabled="isLoading || isConnected"
|
||||
class="reset-btn"
|
||||
>
|
||||
Сбросить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Лог операций -->
|
||||
<div class="operation-log" v-if="logs.length > 0">
|
||||
<h3>Лог операций</h3>
|
||||
<div class="log-container">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
class="log-entry"
|
||||
:class="log.type"
|
||||
>
|
||||
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||
import { useWebSshService } from '../../services/webSshService';
|
||||
|
||||
const webSshService = useWebSshService();
|
||||
const agentAvailable = ref(false);
|
||||
|
||||
// Реактивные данные
|
||||
const isLoading = ref(false);
|
||||
const isConnected = ref(false);
|
||||
const connectionStatus = ref('Не подключено');
|
||||
const logs = ref([]);
|
||||
|
||||
// Форма
|
||||
const form = reactive({
|
||||
domain: '',
|
||||
email: '',
|
||||
sshHost: '',
|
||||
sshUser: '',
|
||||
sshKey: '',
|
||||
localPort: 5173,
|
||||
serverPort: 9000,
|
||||
sshPort: 22
|
||||
});
|
||||
|
||||
const copied = ref(false);
|
||||
let copyTimeout = null;
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text.replace(/\\n/g, '\n')).then(() => {
|
||||
copied.value = true;
|
||||
clearTimeout(copyTimeout);
|
||||
copyTimeout = setTimeout(() => copied.value = false, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function validatePrivateKey(key) {
|
||||
if (!key) return false;
|
||||
const trimmed = key.trim();
|
||||
if (!trimmed.startsWith('-----BEGIN OPENSSH PRIVATE KEY-----')) {
|
||||
console.error('Ключ не начинается с -----BEGIN OPENSSH PRIVATE KEY-----');
|
||||
return false;
|
||||
}
|
||||
if (!trimmed.endsWith('-----END OPENSSH PRIVATE KEY-----')) {
|
||||
console.error('Ключ не заканчивается на -----END OPENSSH PRIVATE KEY-----');
|
||||
return false;
|
||||
}
|
||||
if (trimmed.split('\n').length < 3) {
|
||||
console.error('Ключ слишком короткий или не содержит переносов строк');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Методы
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
// Дополнительная валидация приватного ключа
|
||||
if (!validatePrivateKey(form.sshKey)) {
|
||||
addLog('error', 'Проверьте формат приватного ключа!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Логирование ключа (только для отладки!)
|
||||
console.log('SSH ключ (начало):', form.sshKey.slice(0, 40));
|
||||
console.log('SSH ключ (конец):', form.sshKey.slice(-40));
|
||||
console.log('Длина ключа:', form.sshKey.length);
|
||||
|
||||
// Логирование отправляемых данных (без самого ключа)
|
||||
console.log('Данные для агента:', {
|
||||
...form,
|
||||
sshKey: form.sshKey ? `[скрыто, длина: ${form.sshKey.length}]` : 'нет ключа'
|
||||
});
|
||||
|
||||
isLoading.value = true;
|
||||
addLog('info', 'Запуск публикации...');
|
||||
try {
|
||||
// Публикация через агента
|
||||
const result = await webSshService.createTunnel(form);
|
||||
if (result.success) {
|
||||
isConnected.value = true;
|
||||
connectionStatus.value = `Подключено к ${form.domain}`;
|
||||
addLog('success', 'SSH туннель успешно создан и настроен');
|
||||
addLog('info', `Ваше приложение доступно по адресу: https://${form.domain}`);
|
||||
} else {
|
||||
addLog('error', result.message || 'Ошибка при создании туннеля');
|
||||
}
|
||||
} catch (error) {
|
||||
addLog('error', `Ошибка: ${error.message}`);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectTunnel = async () => {
|
||||
isLoading.value = true;
|
||||
addLog('info', 'Отключаю SSH туннель...');
|
||||
|
||||
try {
|
||||
const result = await webSshService.disconnectTunnel();
|
||||
|
||||
if (result.success) {
|
||||
isConnected.value = false;
|
||||
connectionStatus.value = 'Не подключено';
|
||||
addLog('success', 'SSH туннель отключен');
|
||||
} else {
|
||||
addLog('error', result.message || 'Ошибка при отключении туннеля');
|
||||
}
|
||||
} catch (error) {
|
||||
addLog('error', `Ошибка: ${error.message}`);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
if (!form.domain || !form.email || !form.sshHost || !form.sshUser || !form.sshKey) {
|
||||
addLog('error', 'Заполните все обязательные поля');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!form.email.includes('@')) {
|
||||
addLog('error', 'Введите корректный email');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!form.sshKey.includes('-----BEGIN') || !form.sshKey.includes('-----END')) {
|
||||
addLog('error', 'SSH ключ должен быть в формате OpenSSH');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
domain: '',
|
||||
email: '',
|
||||
sshHost: '',
|
||||
sshUser: '',
|
||||
sshKey: '',
|
||||
localPort: 5173,
|
||||
serverPort: 9000,
|
||||
sshPort: 22
|
||||
});
|
||||
logs.value = [];
|
||||
};
|
||||
|
||||
const addLog = (type, message) => {
|
||||
logs.value.push({
|
||||
type,
|
||||
message,
|
||||
timestamp: new Date()
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return timestamp.toLocaleTimeString();
|
||||
};
|
||||
|
||||
const checkConnectionStatus = async () => {
|
||||
try {
|
||||
const status = await webSshService.getStatus();
|
||||
isConnected.value = status.connected;
|
||||
connectionStatus.value = status.connected
|
||||
? `Подключено к ${status.domain}`
|
||||
: 'Не подключено';
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки статуса:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkAgent = async () => {
|
||||
const status = await webSshService.checkAgentStatus();
|
||||
agentAvailable.value = status.running;
|
||||
};
|
||||
|
||||
// Жизненный цикл
|
||||
onMounted(() => {
|
||||
checkAgent();
|
||||
checkConnectionStatus();
|
||||
|
||||
// Проверяем статус каждые 30 секунд
|
||||
const interval = setInterval(checkConnectionStatus, 30000);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.web-ssh-settings {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-header p {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-indicator.inactive {
|
||||
background-color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.disconnect-btn:hover {
|
||||
background: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
.tunnel-form {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group textarea:disabled {
|
||||
background-color: var(--color-background);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.advanced-section {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.publish-btn {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.publish-btn:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.publish-btn:disabled {
|
||||
background: var(--color-text-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.reset-btn:hover:not(:disabled) {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.reset-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.operation-log {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.operation-log h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--color-text-secondary);
|
||||
font-family: 'Courier New', monospace;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-entry.info .log-message {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.log-entry.success .log-message {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.log-entry.error .log-message {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* Адаптивный дизайн */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-instruction-block {
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
margin-top: 1rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 0.5em 1em;
|
||||
margin: 0.5em 0;
|
||||
font-size: 1em;
|
||||
user-select: all;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.copy-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.copy-icon {
|
||||
margin-left: 0.5em;
|
||||
font-size: 1.2em;
|
||||
color: #888;
|
||||
transition: color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.copy-block:hover .copy-icon svg {
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
.copied-indicator {
|
||||
color: var(--color-success, #27ae60);
|
||||
font-weight: 500;
|
||||
margin-top: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@ const router = useRouter();
|
||||
const { isAdmin } = useAuthContext();
|
||||
|
||||
async function remove() {
|
||||
await axios.delete(`/api/tables/${$route.params.id}`);
|
||||
await axios.delete(`/tables/${$route.params.id}`);
|
||||
router.push({ name: 'tables-list' });
|
||||
}
|
||||
function cancel() {
|
||||
|
||||
@@ -32,7 +32,7 @@ const description = ref('');
|
||||
const isRagSourceId = ref(2);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await axios.get(`/api/tables/${$route.params.id}`);
|
||||
const { data } = await axios.get(`/tables/${$route.params.id}`);
|
||||
name.value = data.name;
|
||||
description.value = data.description;
|
||||
isRagSourceId.value = data.is_rag_source_id || 2;
|
||||
|
||||
Reference in New Issue
Block a user