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

This commit is contained in:
2025-10-03 18:48:11 +03:00
parent ad7b8e9716
commit 67cf473455
42 changed files with 5515 additions and 1180 deletions

View File

@@ -16,84 +16,67 @@
<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>
<button v-if="isConnected" @click="resetConnection" class="disconnect-btn">Сбросить статус</button>
</div>
<!-- Форма настроек -->
<form @submit.prevent="handleSubmit" class="tunnel-form">
<form @submit.prevent="handleSubmit" class="vds-form">
<div class="form-section">
<h3>Настройки VDS</h3>
<div class="form-group">
<label for="vdsIp">IP адрес VDS сервера *</label>
<input id="vdsIp" v-model="form.vdsIp" type="text" placeholder="192.168.1.100" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="domain">Домен *</label>
<input id="domain" v-model="form.domain" type="text" placeholder="example.com" required :disabled="isConnected" />
<small class="form-help">Домен должен указывать на IP VDS сервера (A запись)</small>
<input id="domain" v-model="form.domain" type="text" placeholder="example.com" required :disabled="isConnected" @blur="checkDomainDNS" />
<small class="form-help">Домен должен указывать на IP VDS сервера (A запись). IP адрес будет определен автоматически.</small>
<div v-if="domainStatus" class="domain-status" :class="domainStatus.type">
{{ domainStatus.message }}
</div>
</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" />
<small class="form-help">Email для получения SSL сертификата от Let's Encrypt</small>
</div>
<div class="form-group">
<label for="ubuntuUser">Логин Ubuntu *</label>
<input id="ubuntuUser" v-model="form.ubuntuUser" type="text" placeholder="ubuntu" required :disabled="isConnected" />
<small class="form-help">Обычно: ubuntu, root, или ваш пользователь на VDS</small>
</div>
<div class="form-group">
<label for="ubuntuPassword">Пароль Ubuntu *</label>
<input id="ubuntuPassword" v-model="form.ubuntuPassword" type="password" placeholder="Введите пароль" required :disabled="isConnected" />
</div>
<!-- Пароль Ubuntu убран - доступ только через SSH ключи -->
<div class="form-group">
<label for="dockerUser">Логин Docker *</label>
<input id="dockerUser" v-model="form.dockerUser" type="text" placeholder="docker" required :disabled="isConnected" />
<small class="form-help">Пользователь для Docker (будет создан автоматически)</small>
</div>
<div class="form-group">
<label for="dockerPassword">Пароль Docker *</label>
<input id="dockerPassword" v-model="form.dockerPassword" type="password" placeholder="Введите пароль" required :disabled="isConnected" />
<!-- Пароль Docker убран - доступ только через SSH ключи -->
<div class="security-notice">
<h4>🔐 Безопасность</h4>
<p>Пользователи Ubuntu и Docker будут созданы <strong>без паролей</strong>. Доступ будет осуществляться только через SSH ключи, что обеспечивает максимальную безопасность.</p>
</div>
</div>
<div class="form-section">
<h3>Настройки SSH сервера</h3>
<h3>SSH подключение к VDS</h3>
<div class="form-group">
<label for="sshUser">SSH Пользователь *</label>
<input id="sshUser" v-model="form.sshUser" type="text" placeholder="root" required :disabled="isConnected" />
<label for="sshHost">SSH хост *</label>
<input id="sshHost" v-model="form.sshHost" type="text" placeholder="" required :disabled="isConnected" />
<small class="form-help">SSH хост сервера (может отличаться от домена)</small>
</div>
<div class="form-group">
<label for="sshKey">SSH Приватный ключ *</label>
<div class="key-container">
<textarea id="sshKey" v-model="form.sshKey" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----" rows="6" required :disabled="isConnected" :type="showSshKey ? 'text' : 'password'"></textarea>
<button type="button" @click="toggleSshKey" class="toggle-key-btn" :disabled="isConnected">
{{ showSshKey ? 'Скрыть' : 'Показать' }}
</button>
</div>
<label for="sshPort">SSH порт *</label>
<input id="sshPort" v-model="form.sshPort" type="number" placeholder="" required :disabled="isConnected" />
<small class="form-help">SSH порт сервера (обычно 22, но может быть другой)</small>
</div>
<div class="form-group">
<label for="encryptionKey">Ключ шифрования *</label>
<div class="encryption-key-container">
<textarea id="encryptionKey" v-model="form.encryptionKey" placeholder="Ключ шифрования будет загружен автоматически..." rows="4" required :disabled="isConnected" :type="showEncryptionKey ? 'text' : 'password'"></textarea>
<button type="button" @click="toggleEncryptionKey" class="toggle-key-btn" :disabled="isConnected">
{{ showEncryptionKey ? 'Скрыть' : 'Показать' }}
</button>
</div>
</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>
<label for="sshUser">SSH пользователь *</label>
<input id="sshUser" v-model="form.sshUser" type="text" placeholder="" required :disabled="isConnected" />
<small class="form-help">Пользователь для SSH подключения к VDS</small>
</div>
<div class="form-group">
<label for="sshPassword">SSH пароль *</label>
<input id="sshPassword" v-model="form.sshPassword" type="password" placeholder="" required :disabled="isConnected" />
<small class="form-help">Пароль для SSH подключения к VDS</small>
</div>
</div>
<!-- Ключ шифрования убран - будет генерироваться автоматически на VDS -->
<div class="form-actions">
<button type="submit" :disabled="isLoading || isConnected" class="publish-btn">
{{ isLoading ? 'Настройка...' : 'Опубликовать' }}
@@ -101,13 +84,55 @@
<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>
<!-- Real-time лог операций WebSSH -->
<div class="operation-log">
<div class="log-header">
<h3>Real-time лог операций WebSSH</h3>
<div class="log-controls">
<button
@click="startListening"
:disabled="isListening"
class="control-btn start-btn"
title="Начать прослушивание"
>
▶️ Начать
</button>
<button
@click="stopListening"
:disabled="!isListening"
class="control-btn stop-btn"
title="Остановить прослушивание"
>
⏹️ Остановить
</button>
<button
@click="clearLogs"
class="control-btn clear-btn"
title="Очистить логи"
>
🗑️ Очистить
</button>
<span class="connection-status" :class="{ connected: isConnected }">
{{ isConnected ? '🟢 Подключено' : '🔴 Отключено' }}
</span>
</div>
</div>
<div class="log-container" ref="logContainer">
<div v-if="logs.length === 0" class="no-logs">
<p>Нет логов. Нажмите "Начать" для прослушивания real-time логов WebSSH агента.</p>
</div>
<div v-else>
<div
v-for="log in logs"
:key="log.id"
class="log-entry"
:class="log.type"
>
<span class="log-icon">{{ getLogIcon(log.type) }}</span>
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
<span class="log-message" v-html="log.message"></span>
</div>
</div>
</div>
</div>
@@ -117,101 +142,111 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useWebSshService } from '../services/webSshService';
import { useWebSshLogs } from '../composables/useWebSshLogs';
const webSshService = useWebSshService();
// Используем композабл для real-time логов
const {
logs,
isConnected,
isListening,
addLog,
startListening,
stopListening,
clearLogs,
formatTime,
getLogColor,
getLogIcon
} = useWebSshLogs();
const isLoading = ref(false);
const isConnected = ref(false);
const connectionStatus = ref('Не подключено');
const logs = ref([]);
const showSshKey = ref(false);
const showEncryptionKey = ref(false);
const domainStatus = ref(null);
const form = reactive({
vdsIp: '',
domain: '',
email: '',
ubuntuUser: 'ubuntu',
ubuntuPassword: '',
dockerUser: 'docker',
dockerPassword: '',
sshHost: '',
sshPort: '',
sshUser: '',
sshKey: '',
encryptionKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
sshPassword: ''
});
// Автоматически загружаем SSH ключ и ключ шифрования при загрузке компонента
// Ключ шифрования будет генерироваться автоматически на VDS
onMounted(async () => {
try {
// Пытаемся получить SSH ключ с хостового сервера
const response = await fetch('http://localhost:3001/ssh-key');
if (response.ok) {
const data = await response.json();
if (data.sshKey) {
form.sshKey = data.sshKey;
addLog('info', 'SSH ключ автоматически загружен');
} else {
addLog('error', 'SSH ключ не найден. Запустите ./setup.sh для создания ключей');
}
} else {
addLog('error', 'SSH ключ не найден. Запустите ./setup.sh для создания ключей');
}
} catch (error) {
addLog('error', 'SSH ключ не найден. Запустите ./setup.sh для создания ключей');
}
// Загружаем ключ шифрования
try {
const response = await fetch('http://localhost:3001/encryption-key');
if (response.ok) {
const data = await response.json();
if (data.encryptionKey) {
form.encryptionKey = data.encryptionKey;
addLog('info', 'Ключ шифрования автоматически загружен');
} else {
addLog('error', 'Ключ шифрования не найден');
}
} else {
addLog('error', 'Ключ шифрования не найден');
}
} catch (error) {
addLog('error', 'Ошибка загрузки ключа шифрования');
}
// Инициализация компонента
});
function validatePrivateKey(key) {
if (!key) return false;
const trimmed = key.trim();
if (!trimmed.startsWith('-----BEGIN OPENSSH PRIVATE KEY-----')) return false;
if (!trimmed.endsWith('-----END OPENSSH PRIVATE KEY-----')) return false;
if (trimmed.split('\n').length < 3) return false;
return true;
}
const handleSubmit = async () => {
if (!validateForm()) return;
if (!validatePrivateKey(form.sshKey)) {
addLog('error', 'Проверьте формат приватного ключа!');
// Функции переключения видимости ключей
const toggleSshKey = () => {
showSshKey.value = !showSshKey.value;
};
// Функция переключения ключа шифрования убрана
// Функция проверки DNS для домена
const checkDomainDNS = async () => {
if (!form.domain || form.domain.trim() === '') {
domainStatus.value = null;
return;
}
isLoading.value = true;
addLog('info', 'Запуск публикации...');
try {
const result = await webSshService.createTunnel(form);
domainStatus.value = { type: 'loading', message: 'Проверка DNS...' };
const response = await fetch(`http://localhost:8000/api/dns-check/${form.domain}`);
const data = await response.json();
if (data.success) {
domainStatus.value = {
type: 'success',
message: `✅ Домен найден: ${data.ip}`
};
addLog('success', `DNS: ${form.domain} → ${data.ip}`);
} else {
domainStatus.value = {
type: 'error',
message: `❌ ${data.message}`
};
addLog('error', `DNS ошибка: ${data.message}`);
}
} catch (error) {
domainStatus.value = {
type: 'error',
message: `❌ Ошибка проверки DNS: ${error.message}`
};
addLog('error', `DNS ошибка: ${error.message}`);
}
};
const handleSubmit = async () => {
if (!validateForm()) return;
isLoading.value = true;
addLog('info', 'Запуск настройки VDS...');
try {
const result = await webSshService.setupVDS(form);
if (result.success) {
isConnected.value = true;
connectionStatus.value = `Подключено к ${form.domain}`;
addLog('success', 'SSH туннель успешно создан и настроен');
addLog('info', `Ваше приложение доступно по адресу: https://${form.domain}`);
connectionStatus.value = `VDS настроен: ${form.domain}`;
addLog('success', 'VDS успешно настроена');
addLog('info', `Ваше приложение будет доступно по адресу: https://${form.domain}`);
// Сохраняем статус VDS как настроенного
localStorage.setItem('vds-config', JSON.stringify({ isConfigured: true }));
localStorage.setItem('vds-config', JSON.stringify({
isConfigured: true,
domain: form.domain
}));
// Отправляем событие об изменении статуса VDS
window.dispatchEvent(new CustomEvent('vds-status-changed', {
detail: { isConfigured: true }
}));
} else {
addLog('error', result.message || 'Ошибка при создании туннеля');
addLog('error', result.message || 'Ошибка при настройке VDS');
}
} catch (error) {
addLog('error', `Ошибка: ${error.message}`);
@@ -219,18 +254,21 @@ const handleSubmit = async () => {
isLoading.value = false;
}
};
const disconnectTunnel = async () => {
const resetConnection = async () => {
isLoading.value = true;
addLog('info', 'Отключаю SSH туннель...');
addLog('info', 'Сброс статуса подключения...');
try {
const result = await webSshService.disconnectTunnel();
if (result.success) {
isConnected.value = false;
connectionStatus.value = 'Не подключено';
addLog('success', 'SSH туннель отключен');
} else {
addLog('error', result.message || 'Ошибка при отключении туннеля');
}
isConnected.value = false;
connectionStatus.value = 'Не подключено';
addLog('success', 'Статус сброшен');
// Очищаем статус VDS
localStorage.removeItem('vds-config');
// Отправляем событие об изменении статуса VDS
window.dispatchEvent(new CustomEvent('vds-status-changed', {
detail: { isConfigured: false }
}));
} catch (error) {
addLog('error', `Ошибка: ${error.message}`);
} finally {
@@ -238,59 +276,66 @@ const disconnectTunnel = async () => {
}
};
const validateForm = () => {
if (!form.vdsIp || !form.domain || !form.email || !form.ubuntuUser || !form.ubuntuPassword || !form.dockerUser || !form.dockerPassword || !form.sshUser || !form.sshKey || !form.encryptionKey) {
if (!form.domain || !form.email || !form.ubuntuUser || !form.dockerUser || !form.sshHost || !form.sshPort || !form.sshUser || !form.sshPassword) {
addLog('error', 'Заполните все обязательные поля');
return false;
}
if (!form.email.includes('@')) {
addLog('error', 'Введите корректный email');
// Валидация домена
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
if (!domainRegex.test(form.domain)) {
addLog('error', 'Введите корректный домен (например: example.com)');
return false;
}
if (!form.sshKey.includes('-----BEGIN') || !form.sshKey.includes('-----END')) {
addLog('error', 'SSH ключ должен быть в формате OpenSSH');
// Валидация email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(form.email)) {
addLog('error', 'Введите корректный email адрес');
return false;
}
// Валидация логинов
if (form.ubuntuUser.length < 3 || form.ubuntuUser.length > 32) {
addLog('error', 'Логин Ubuntu должен быть от 3 до 32 символов');
return false;
}
if (!/^[a-z][a-z0-9_-]*$/.test(form.ubuntuUser)) {
addLog('error', 'Логин Ubuntu должен начинаться с буквы и содержать только строчные буквы, цифры, _ и -');
return false;
}
if (form.dockerUser.length < 3 || form.dockerUser.length > 32) {
addLog('error', 'Логин Docker должен быть от 3 до 32 символов');
return false;
}
if (!/^[a-z][a-z0-9_-]*$/.test(form.dockerUser)) {
addLog('error', 'Логин Docker должен начинаться с буквы и содержать только строчные буквы, цифры, _ и -');
return false;
}
// Валидация паролей убрана - доступ только через SSH ключи
// Валидация ключа шифрования убрана - будет генерироваться автоматически
return true;
};
const resetForm = () => {
Object.assign(form, {
vdsIp: '',
domain: '',
email: '',
ubuntuUser: 'ubuntu',
ubuntuPassword: '',
dockerUser: 'docker',
dockerPassword: '',
sshHost: '',
sshPort: '',
sshUser: '',
sshKey: '',
encryptionKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
sshPassword: ''
});
logs.value = [];
showSshKey.value = false;
showEncryptionKey.value = false;
};
const addLog = (type, message) => {
logs.value.push({
type,
message,
timestamp: new Date()
});
};
// Методы для переключения видимости ключей
const toggleSshKey = () => {
showSshKey.value = !showSshKey.value;
};
const toggleEncryptionKey = () => {
showEncryptionKey.value = !showEncryptionKey.value;
};
const formatTime = (timestamp) => {
return timestamp.toLocaleTimeString();
domainStatus.value = null;
};
// Функции addLog и formatTime теперь предоставляются композаблом useWebSshLogs
</script>
<style scoped>
@@ -343,7 +388,7 @@ const formatTime = (timestamp) => {
}
/* Форма */
.tunnel-form {
.vds-form {
display: flex;
flex-direction: column;
gap: 2rem;
@@ -413,16 +458,6 @@ const formatTime = (timestamp) => {
font-size: 0.9rem;
}
/* Дополнительные настройки */
.advanced-section {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
/* Кнопки */
.form-actions {
@@ -498,22 +533,109 @@ const formatTime = (timestamp) => {
font-weight: 600;
}
/* Заголовок логов с элементами управления */
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-border);
}
.log-header h3 {
margin: 0;
color: var(--color-text);
font-size: 1.1rem;
}
.log-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-btn {
padding: 0.25rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background);
color: var(--color-text);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover:not(:disabled) {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.start-btn:hover:not(:disabled) {
background: #27ae60;
border-color: #27ae60;
}
.stop-btn:hover:not(:disabled) {
background: #e74c3c;
border-color: #e74c3c;
}
.clear-btn:hover:not(:disabled) {
background: #f39c12;
border-color: #f39c12;
}
.connection-status {
font-size: 0.85rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: #e74c3c;
color: white;
}
.connection-status.connected {
background: #27ae60;
}
.log-container {
max-height: 300px;
max-height: 400px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
.no-logs {
text-align: center;
color: var(--color-text-secondary);
font-style: italic;
padding: 2rem;
}
.log-entry {
display: flex;
gap: 1rem;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
font-size: 0.9rem;
}
.log-icon {
font-size: 0.8rem;
min-width: 20px;
text-align: center;
flex-shrink: 0;
}
.log-entry:last-child {
border-bottom: none;
}
@@ -639,4 +761,57 @@ textarea[type="password"]:focus::placeholder {
background: #6c757d;
cursor: not-allowed;
}
/* Статус проверки домена */
.domain-status {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
}
.domain-status.loading {
background: #e3f2fd;
color: #1976d2;
border: 1px solid #bbdefb;
}
.domain-status.success {
background: #e8f5e8;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.domain-status.error {
background: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
/* Информационный блок о безопасности */
.security-notice {
background: #e8f5e8;
border: 1px solid #4caf50;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.security-notice h4 {
margin: 0 0 0.5rem 0;
color: #2e7d32;
font-size: 1rem;
}
.security-notice p {
margin: 0;
color: #2e7d32;
font-size: 0.9rem;
line-height: 1.4;
}
.security-notice strong {
color: #1b5e20;
}
</style>