ваше сообщение коммита

This commit is contained in:
2025-12-06 12:34:14 +03:00
parent 90da3a0d12
commit e9610881c8
20 changed files with 820 additions and 421 deletions

View File

@@ -253,54 +253,52 @@ const handleSubmit = async () => {
if (!validateForm()) return;
isLoading.value = true;
addLog('info', 'Запуск настройки VDS...');
try {
// 1. Сначала всегда сохраняем настройки в БД
addLog('info', 'Сохранение настроек VDS на сервере...');
try {
// axios.defaults.baseURL = '/api', поэтому используем относительный путь
// чтобы итоговый URL был /api/vds/settings, а не /api/api/vds/settings
const response = await axios.post('/vds/settings', {
domain: form.domain,
email: form.email,
ubuntuUser: form.ubuntuUser,
dockerUser: form.dockerUser,
sshHost: form.sshHost,
sshPort: parseInt(form.sshPort, 10) || 22, // Преобразуем в число
sshUser: form.sshUser,
sshPassword: form.sshPassword
});
if (response.data && response.data.success) {
addLog('success', ' Настройки VDS сохранены в базе данных');
} else {
addLog('error', `❌ Ошибка сохранения настроек: ${response.data?.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('[WebSSH] Ошибка сохранения настроек:', error);
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
addLog('error', `❌ Ошибка сохранения настроек на сервере: ${errorMessage}`);
// Даже если сохранение настроек упало, продолжаем попытку настройки VDS через агента
}
// 2. Затем запускаем настройку VDS через агента
addLog('info', 'Запуск настройки VDS через WebSSH Agent...');
const result = await webSshService.setupVDS(form);
if (result.success) {
isConnected.value = true;
connectionStatus.value = `VDS настроен: ${form.domain}`;
addLog('success', 'VDS успешно настроена');
addLog('info', `Ваше приложение будет доступно по адресу: https://${form.domain}`);
// Сохраняем статус VDS как настроенного
// Сохраняем статус VDS как настроенного локально
localStorage.setItem('vds-config', JSON.stringify({
isConfigured: true,
domain: form.domain
}));
// Сохраняем ВСЕ настройки на сервере
try {
addLog('info', 'Сохранение настроек VDS на сервере...');
const response = await axios.post('/api/vds/settings', {
domain: form.domain,
email: form.email,
ubuntuUser: form.ubuntuUser,
dockerUser: form.dockerUser,
sshHost: form.sshHost,
sshPort: parseInt(form.sshPort, 10) || 22, // Преобразуем в число
sshUser: form.sshUser,
sshPassword: form.sshPassword
});
if (response.data && response.data.success) {
addLog('success', ' Настройки VDS успешно сохранены на сервере');
} else {
addLog('error', `❌ Ошибка сохранения настроек: ${response.data?.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('[WebSSH] Ошибка сохранения настроек:', error);
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
addLog('error', `❌ Ошибка сохранения настроек на сервере: ${errorMessage}`);
// Показываем детали ошибки в консоли для отладки
if (error.response) {
console.error('[WebSSH] Детали ошибки:', {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data
});
}
}
// Отправляем событие об изменении статуса VDS
window.dispatchEvent(new CustomEvent('vds-status-changed', {
detail: { isConfigured: true }

View File

@@ -43,6 +43,14 @@
<i class="fas fa-edit"></i>
<span>Редактировать</span>
</button>
<button
class="page-action-btn page-index-btn"
@click="reindexPage"
title="Отправить документ в поиск"
>
<i class="fas fa-search"></i>
<span>Индексировать</span>
</button>
<button
class="page-action-btn page-delete-btn"
@click="confirmDeletePage"
@@ -154,6 +162,7 @@ import { useRouter, useRoute } from 'vue-router';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import pagesService from '../../services/pagesService';
import api from '../../api/axios';
import { usePermissions } from '../../composables/usePermissions';
import { PERMISSIONS } from '../../composables/permissions';
@@ -403,6 +412,18 @@ async function confirmDeletePage() {
}
}
// Ручная переиндексация документа в векторный поиск
async function reindexPage() {
if (!page.value || !page.value.id) return;
try {
await api.post(`/pages/${page.value.id}/reindex`);
alert('Индексация выполнена');
} catch (error) {
console.error('[DocsContent] Ошибка индексации документа:', error);
alert('Ошибка индексации: ' + (error.response?.data?.error || error.message || 'Неизвестная ошибка'));
}
}
// Отслеживаем изменения pageId
watch(() => props.pageId, (newId, oldId) => {
console.log('[DocsContent] pageId изменился:', { oldId, newId });

View File

@@ -74,12 +74,10 @@ export function useWebSshLogs() {
console.log('[WebSSH Logs] Получен прогресс:', data);
if (data.type === 'webssh_progress') {
const progressMessage = `[${data.stage}] ${data.message}`;
const hasPercentage = data.percentage !== undefined && data.percentage !== null;
const progressSuffix = hasPercentage ? `${data.percentage}%` : '';
const progressMessage = `[${data.stage}] ${data.message}${progressSuffix}`;
addLog('info', progressMessage);
if (data.percentage) {
addLog('debug', `Прогресс: ${data.percentage}%`);
}
}
};

View File

@@ -172,112 +172,19 @@ class WebSshService {
/**
* Автоматическая установка и запуск агента
* В новой архитектуре агент всегда запускается в Docker (dapp-webssh-agent),
* поэтому здесь просто проверяем его доступность.
*/
async installAndStartAgent() {
try {
// Сначала проверяем, может агент уже запущен
const status = await this.checkAgentStatus();
if (status.running) {
return { success: true, message: 'Агент уже запущен' };
}
// Пытаемся запустить агент через системный вызов
const response = await fetch(`${LOCAL_AGENT_URL}/install`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'install_and_start'
})
});
if (response.ok) {
const result = await response.json();
this.isAgentRunning = true;
return { success: true, message: 'Агент успешно установлен и запущен' };
} else {
// Если агент не отвечает, пытаемся скачать и установить его
return await this.downloadAndInstallAgent();
}
} catch (error) {
// console.error('Ошибка при установке агента:', error);
return await this.downloadAndInstallAgent();
}
}
/**
* Скачивание и установка агента
*/
async downloadAndInstallAgent() {
try {
// Создаем скрипт для скачивания и установки агента
const installScript = `
#!/bin/bash
# Создаем директорию для агента
mkdir -p ~/.webssh-agent
cd ~/.webssh-agent
# Скачиваем агент (пока создаем локально)
cat > agent.js << 'EOF'
${this.getAgentCode()}
EOF
# Скачиваем package.json
cat > package.json << 'EOF'
{
"name": "webssh-agent",
"version": "1.0.0",
"description": "Local SSH tunnel agent",
"main": "agent.js",
"scripts": {
"start": "node agent.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"ssh2": "^1.14.0",
"node-ssh": "^13.1.0"
}
}
EOF
# Устанавливаем зависимости
npm install
# Запускаем агент в фоне
nohup node agent.js > agent.log 2>&1 &
echo "Агент установлен и запущен"
`;
// Создаем Blob со скриптом
const blob = new Blob([installScript], { type: 'application/x-sh' });
const url = URL.createObjectURL(blob);
// Создаем ссылку для скачивания
const a = document.createElement('a');
a.href = url;
a.download = 'install-webssh-agent.sh';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
return {
success: false,
message: 'Скачайте и запустите скрипт install-webssh-agent.sh для установки агента',
requiresManualInstall: true
};
} catch (error) {
// console.error('Ошибка при создании установочного скрипта:', error);
return {
success: false,
message: 'Ошибка при подготовке установки агента',
error: error.message
};
}
message: 'WebSSH Agent не запущен. Убедитесь, что контейнер dapp-webssh-agent работает (docker compose up -d webssh-agent).'
};
}
/**
@@ -327,10 +234,9 @@ EOF
config.vdsIp = dnsResult.ip;
}
// Проверяем, что агент запущен
// Проверяем, что агент запущен (в Docker)
const agentStatus = await this.checkAgentStatus();
if (!agentStatus.running) {
// Пытаемся установить и запустить агент
const installResult = await this.installAndStartAgent();
if (!installResult.success) {
return installResult;

View File

@@ -75,6 +75,10 @@
<label>Docker Пользователь:</label>
<div class="setting-value">{{ settings.dockerUser || 'Не задан' }}</div>
</div>
<div class="setting-item">
<label>Путь к docker-compose:</label>
<div class="setting-value">{{ settings.dappPath || '/root/dapp' }}</div>
</div>
</div>
</div>
@@ -106,7 +110,7 @@
placeholder="admin@example.com"
required
/>
<small class="form-help">Email для получения SSL сертификата от Let's Encrypt</small>
<small class="form-help">Email для получения SSL сертификата</small>
</div>
<div class="form-group">
<label for="ubuntuUser">Логин Ubuntu *</label>
@@ -130,6 +134,17 @@
/>
<small class="form-help">Пользователь для Docker (будет создан автоматически)</small>
</div>
<div class="form-group">
<label for="dappPath">Путь к docker-compose *</label>
<input
id="dappPath"
v-model="formSettings.dappPath"
type="text"
placeholder="/home/docker/dapp"
required
/>
<small class="form-help">Путь к директории с docker-compose.prod.yml на VDS сервере (обычно /home/docker/dapp или /home/ubuntu/dapp)</small>
</div>
</div>
<div class="form-section">
@@ -377,6 +392,62 @@
</div>
</div>
<!-- SSL сертификаты -->
<div class="ssl-section">
<div class="section-header">
<h2>SSL сертификат</h2>
</div>
<div v-if="!isEditor" class="access-denied-message">
<p> Управление SSL доступно только пользователям с ролью "Редактор"</p>
</div>
<div v-else>
<div class="ssl-status">
<div v-if="isLoadingSsl">
Загрузка статуса SSL...
</div>
<div v-else>
<div v-if="sslStatus && sslStatus.success && sslStatus.allCertificates && sslStatus.allCertificates.length">
<div class="ssl-info">
<div
v-for="cert in sslStatus.allCertificates"
:key="cert.name"
class="ssl-info-item"
>
<label>{{ cert.name }}</label>
<span :class="{ 'expiring-soon': isCertExpiringSoon(cert.expiryDate) }">
{{ cert.expiryDate || 'Без данных' }}
</span>
</div>
</div>
</div>
<div v-else class="ssl-no-cert">
SSL сертификат не найден для текущего домена.
</div>
</div>
</div>
<div class="ssl-actions-grid">
<button
class="action-btn ssl-btn status"
:disabled="isLoadingSsl || isLoading"
@click="checkSslStatus"
>
🔍 Проверить статус SSL
</button>
<button
v-if="isEditor"
class="action-btn ssl-btn renew"
:disabled="isLoading"
@click="renewSslCertificate"
>
🔐 Получить / обновить SSL
</button>
</div>
</div>
</div>
<!-- Модальные окна -->
<!-- Модальное окно создания пользователя -->
<div v-if="showCreateUserModal && isEditor" class="modal-overlay" @click="showCreateUserModal = false">
@@ -450,6 +521,7 @@
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
@@ -493,6 +565,8 @@ const showSendBackupModal = ref(false);
const showLogsModal = ref(false);
const logsTitle = ref('');
const logsContent = ref('');
const sslStatus = ref(null);
const isLoadingSsl = ref(false);
const newUser = reactive({
username: '',
@@ -514,6 +588,7 @@ const formSettings = reactive({
email: '',
ubuntuUser: 'ubuntu',
dockerUser: 'docker',
dappPath: '/home/docker/dapp',
sshHost: '',
sshPort: 22,
sshUser: 'root',
@@ -540,6 +615,7 @@ let statsInterval = null;
// Загрузка настроек
const loadSettings = async () => {
try {
// axios.defaults.baseURL = '/api', поэтому используем относительный путь
const response = await axios.get('/vds/settings');
if (response.data.success) {
if (response.data.settings) {
@@ -553,6 +629,7 @@ const loadSettings = async () => {
email: response.data.settings.email || '',
ubuntuUser: response.data.settings.ubuntuUser || 'ubuntu',
dockerUser: response.data.settings.dockerUser || 'docker',
dappPath: response.data.settings.dappPath || `/home/${response.data.settings.dockerUser || 'docker'}/dapp`,
sshHost: response.data.settings.sshHost || '',
sshPort: response.data.settings.sshPort || 22,
sshUser: response.data.settings.sshUser || 'root',
@@ -621,11 +698,13 @@ const saveSettings = async () => {
isSaving.value = true;
try {
// axios.defaults.baseURL = '/api', поэтому используем относительный путь
const response = await axios.post('/vds/settings', {
domain: formSettings.domain,
email: formSettings.email,
ubuntuUser: formSettings.ubuntuUser,
dockerUser: formSettings.dockerUser,
dappPath: formSettings.dappPath || '/root/dapp',
sshHost: formSettings.sshHost,
sshPort: formSettings.sshPort,
sshUser: formSettings.sshUser,
@@ -667,13 +746,22 @@ const loadStats = async () => {
const loadContainers = async () => {
isLoading.value = true;
try {
// axios.defaults.baseURL = '/api', поэтому используем относительный путь
const response = await axios.get('/vds/containers');
if (response.data.success) {
containers.value = response.data.containers;
containers.value = response.data.containers || [];
} else {
console.warn('[VDS] Загрузка контейнеров не успешна:', response.data);
containers.value = [];
if (response.data.message) {
console.info('[VDS]', response.data.message);
}
}
} catch (error) {
console.error('Ошибка загрузки контейнеров:', error);
alert('Ошибка загрузки контейнеров');
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
alert(`Ошибка загрузки контейнеров: ${errorMessage}`);
containers.value = [];
} finally {
isLoading.value = false;
}
@@ -976,13 +1064,19 @@ const viewProcesses = async () => {
const loadUsers = async () => {
isLoading.value = true;
try {
// axios.defaults.baseURL = '/api', поэтому используем относительный путь
const response = await axios.get('/vds/users');
if (response.data.success) {
users.value = response.data.users;
} else {
console.warn('[VDS] Загрузка пользователей не успешна:', response.data);
users.value = [];
}
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
alert('Ошибка загрузки пользователей');
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
alert(`Ошибка загрузки пользователей: ${errorMessage}`);
users.value = [];
} finally {
isLoading.value = false;
}
@@ -1128,7 +1222,7 @@ const sendBackup = async () => {
// SSL Сертификаты
const loadSslStatus = async () => {
if (!isEditor.value) {
alert('Только пользователи с ролью "Редактор" могут проверять SSL сертификаты');
// Не показываем ошибку, если пользователь не редактор - просто не загружаем статус
return;
}
isLoadingSsl.value = true;
@@ -1137,11 +1231,62 @@ const loadSslStatus = async () => {
if (response.data.success) {
sslStatus.value = response.data;
} else {
alert('Ошибка получения статуса SSL сертификата');
console.warn('[VDS] Получение статуса SSL не успешно:', response.data);
sslStatus.value = null;
// Не показываем alert для автоматической загрузки при монтировании компонента
// Alert показываем только при ручной проверке (через кнопку)
}
} catch (error) {
console.error('Ошибка получения статуса SSL:', error);
alert(error.response?.data?.error || 'Ошибка получения статуса SSL сертификата');
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
// Если VDS не настроена, это нормальная ситуация - не показываем ошибку
if (errorMessage.includes('VDS не настроена') || error.response?.status === 400) {
sslStatus.value = null;
return;
}
// Если ошибка аутентификации (401), это нормальная ситуация - пользователь не авторизован
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
sslStatus.value = null;
return;
}
// Для других ошибок логируем, но не показываем alert при автоматической загрузке
sslStatus.value = null;
} finally {
isLoadingSsl.value = false;
}
};
// Ручная проверка статуса (с показом ошибок пользователю)
const checkSslStatus = async () => {
if (!isEditor.value) {
alert('Только пользователи с ролью "Редактор" могут проверять SSL сертификаты');
return;
}
isLoadingSsl.value = true;
try {
const response = await axios.get('/vds/ssl/status');
if (response.data.success) {
sslStatus.value = response.data;
if (!response.data.allCertificates || response.data.allCertificates.length === 0) {
alert('SSL сертификат не найден для текущего домена');
}
} else {
alert('Ошибка получения статуса SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка получения статуса SSL:', error);
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
// Если ошибка аутентификации, показываем понятное сообщение
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
alert('Требуется аутентификация. Пожалуйста, войдите в систему.');
return;
}
alert(`Ошибка получения статуса SSL сертификата: ${errorMessage}`);
} finally {
isLoadingSsl.value = false;
}
@@ -1152,10 +1297,14 @@ const renewSslCertificate = async () => {
alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты');
return;
}
if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) return;
if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) {
return;
}
isLoading.value = true;
try {
const response = await axios.post('/vds/ssl/renew');
const response = await axios.post('/vds/ssl/renew', {
sslProvider: 'letsencrypt'
});
if (response.data.success) {
alert('SSL сертификат успешно получен/обновлен');
await loadSslStatus();
@@ -1164,7 +1313,24 @@ const renewSslCertificate = async () => {
}
} catch (error) {
console.error('Ошибка получения SSL сертификата:', error);
alert(error.response?.data?.error || 'Ошибка получения SSL сертификата');
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
const errorDetails = error.response?.data?.details || '';
// Если ошибка аутентификации, показываем понятное сообщение
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
alert('Требуется аутентификация. Пожалуйста, обновите страницу и войдите в систему заново.');
// Перенаправляем на главную страницу для повторной авторизации
router.push({ name: 'home' });
return;
}
// Если ошибка лимита Let's Encrypt
if (error.response?.status === 429 || error.response?.data?.rateLimit || errorMessage.includes('too many certificates') || errorMessage.includes('rate limit') || errorDetails.includes('too many certificates')) {
alert('⚠️ Превышен лимит Let\'s Encrypt!\n\nСлишком много сертификатов было выпущено для этого домена за последние 7 дней.\n\nРекомендации:\n1. Подождите до указанной даты\n2. Используйте существующий сертификат (если он есть)\n3. Проверьте статус SSL на странице\n\nЛимит: 5 сертификатов на домен за 168 часов (7 дней)');
return;
}
alert(`Ошибка получения SSL сертификата: ${errorMessage}`);
} finally {
isLoading.value = false;
}

View File

@@ -64,10 +64,10 @@
<!-- WEB SSH -->
<div class="web3-service-block">
<div class="service-header">
<h3>WEB SSH</h3>
<span class="service-badge webssh">Публикация через SSH-туннель</span>
<h3>VDS Сервер</h3>
<span class="service-badge webssh">Публикация на VDS сервере</span>
</div>
<p>Автоматическая публикация приложения в интернете через SSH-туннель.</p>
<p>Автоматическая публикация приложения в интернете.</p>
<div class="service-features">
<span class="feature"> Быстрое подключение</span>
<span class="feature"> Безопасно</span>

View File

@@ -21,8 +21,7 @@
/>
<div class="webssh-settings-block">
<button class="close-btn" @click="goBack">×</button>
<h2>WEB SSH: интеграция и настройки</h2>
<p class="desc">Автоматическая публикация приложения через SSH-туннель и NGINX.</p>
<h2>Настройка VDS Сервер</h2>
<WebSshForm />
</div>
</template>