ваше сообщение коммита
This commit is contained in:
26
frontend/docker-entrypoint.sh
Normal file
26
frontend/docker-entrypoint.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Проверка и установка значений по умолчанию
|
||||
export DOMAIN=${DOMAIN:-localhost}
|
||||
export BACKEND_CONTAINER=${BACKEND_CONTAINER:-dapp-backend}
|
||||
|
||||
echo "🔧 Настройка nginx с параметрами:"
|
||||
echo " DOMAIN: $DOMAIN"
|
||||
echo " BACKEND_CONTAINER: $BACKEND_CONTAINER"
|
||||
|
||||
# Обработка переменных окружения для nginx конфигурации
|
||||
envsubst '${DOMAIN} ${BACKEND_CONTAINER}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
# Проверка синтаксиса nginx конфигурации
|
||||
echo "🔍 Проверка синтаксиса nginx конфигурации..."
|
||||
nginx -t
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Nginx конфигурация корректна"
|
||||
else
|
||||
echo "❌ Ошибка в nginx конфигурации!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 Запуск nginx..."
|
||||
exec "$@"
|
||||
@@ -32,9 +32,33 @@ http {
|
||||
~*MSIE\ [1-9]\. 1;
|
||||
}
|
||||
|
||||
# HTTP сервер - редирект на HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
server_name ${DOMAIN};
|
||||
|
||||
# Редирект всех HTTP запросов на HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS сервер
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name ${DOMAIN};
|
||||
|
||||
# SSL конфигурация (автоматически обновляется certbot)
|
||||
ssl_certificate /etc/ssl/certs/live/${DOMAIN}/fullchain.pem;
|
||||
ssl_certificate_key /etc/ssl/certs/live/${DOMAIN}/privkey.pem;
|
||||
|
||||
# Современные SSL настройки
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
@@ -66,6 +90,19 @@ http {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Healthcheck endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Certbot webroot для автоматического получения SSL сертификатов
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Основной location
|
||||
location / {
|
||||
# Rate limiting для основных страниц
|
||||
@@ -73,12 +110,12 @@ http {
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Базовые заголовки безопасности
|
||||
# Базовые заголовки безопасности для HTTPS
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:;" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss:;" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
}
|
||||
|
||||
@@ -97,7 +134,7 @@ http {
|
||||
# Rate limiting для API (более строгое)
|
||||
limit_req zone=api_limit_per_ip burst=10 nodelay;
|
||||
|
||||
proxy_pass http://dapp-backend:8000/api/;
|
||||
proxy_pass http://${BACKEND_CONTAINER}:8000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -109,6 +146,20 @@ http {
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
|
||||
# WebSocket поддержка (HTTPS)
|
||||
location /ws {
|
||||
proxy_pass http://${BACKEND_CONTAINER}:8000/ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# Скрытие информации о сервере
|
||||
server_tokens off;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
# Этап 1: Сборка frontend
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файлы зависимостей
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Собираем frontend
|
||||
RUN yarn build
|
||||
|
||||
# Этап 2: Nginx с готовым frontend
|
||||
FROM nginx:alpine
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY nginx-simple.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Устанавливаем curl для healthcheck
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# Копируем собранный frontend из первого этапа
|
||||
COPY --from=frontend-builder /app/dist/ /usr/share/nginx/html/
|
||||
|
||||
# Копируем конфигурацию nginx
|
||||
COPY nginx-simple.conf /etc/nginx/nginx.conf.template
|
||||
|
||||
# Копируем скрипт запуска
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -113,6 +113,7 @@ import tablesService from '../services/tablesService';
|
||||
import messagesService from '../services/messagesService';
|
||||
import { useTagsWebSocket } from '../composables/useTagsWebSocket';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import api from '../api/axios';
|
||||
const props = defineProps({
|
||||
contacts: { type: Array, default: () => [] },
|
||||
newContacts: { type: Array, default: () => [] },
|
||||
@@ -233,12 +234,11 @@ function buildQuery() {
|
||||
}
|
||||
|
||||
async function fetchContacts() {
|
||||
let url = '/api/users';
|
||||
let url = '/users';
|
||||
const query = buildQuery();
|
||||
if (query) url += '?' + query;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
contactsArray.value = data.contacts || [];
|
||||
const res = await api.get(url);
|
||||
contactsArray.value = res.data.contacts || [];
|
||||
}
|
||||
|
||||
function onAnyFilterChange() {
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<div class="button-with-close">
|
||||
<button
|
||||
v-if="
|
||||
!telegramAuth.showVerification &&
|
||||
!emailAuth.showForm &&
|
||||
!emailAuth.showVerification
|
||||
!telegramAuth?.showVerification &&
|
||||
!emailAuth?.showForm &&
|
||||
!emailAuth?.showVerification
|
||||
"
|
||||
class="auth-btn connect-wallet-btn"
|
||||
@click="handleWalletAuth"
|
||||
@@ -57,18 +57,18 @@
|
||||
|
||||
<!-- Блок информации о пользователе или формы подключения -->
|
||||
<template v-if="isAuthenticated">
|
||||
<div v-if="emailAuth.showForm || emailAuth.showVerification" class="auth-modal-panel">
|
||||
<div v-if="emailAuth && (emailAuth.showForm || emailAuth.showVerification)" class="auth-modal-panel">
|
||||
<EmailConnect @success="$emit('cancel-email-auth')">
|
||||
<template #actions>
|
||||
<button class="close-btn" @click="$emit('cancel-email-auth')">Отмена</button>
|
||||
</template>
|
||||
</EmailConnect>
|
||||
</div>
|
||||
<div v-else-if="telegramAuth.showVerification" class="auth-modal-panel">
|
||||
<div v-else-if="telegramAuth && telegramAuth.showVerification" class="auth-modal-panel">
|
||||
<TelegramConnect
|
||||
:bot-link="telegramAuth.botLink"
|
||||
:verification-code="telegramAuth.verificationCode"
|
||||
:error="telegramAuth.error"
|
||||
:bot-link="telegramAuth?.botLink"
|
||||
:verification-code="telegramAuth?.verificationCode"
|
||||
:error="telegramAuth?.error"
|
||||
@cancel="$emit('cancel-telegram-auth')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
224
frontend/src/composables/useWebSshLogs.js
Normal file
224
frontend/src/composables/useWebSshLogs.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
/**
|
||||
* Композабл для real-time логов WebSSH агента
|
||||
*/
|
||||
export function useWebSshLogs() {
|
||||
const logs = ref([]);
|
||||
const isConnected = ref(false);
|
||||
const isListening = ref(false);
|
||||
const maxLogs = 1000; // Максимальное количество логов в памяти
|
||||
let ws = null;
|
||||
|
||||
// Добавление нового лога
|
||||
const addLog = (type, message, timestamp = new Date()) => {
|
||||
const logEntry = {
|
||||
id: Date.now() + Math.random(),
|
||||
type, // 'info', 'success', 'error', 'warning', 'debug'
|
||||
message,
|
||||
timestamp
|
||||
};
|
||||
|
||||
logs.value.push(logEntry);
|
||||
|
||||
// Ограничиваем количество логов в памяти
|
||||
if (logs.value.length > maxLogs) {
|
||||
logs.value = logs.value.slice(-maxLogs);
|
||||
}
|
||||
|
||||
// Автоматическая прокрутка к последнему логу
|
||||
setTimeout(() => {
|
||||
const logContainer = document.querySelector('.log-container');
|
||||
if (logContainer) {
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Обработчик WebSocket сообщений
|
||||
const handleWebSshLog = (data) => {
|
||||
console.log('[WebSSH Logs] Получен лог:', data);
|
||||
|
||||
if (data.type === 'webssh_log') {
|
||||
addLog(data.logType || 'info', data.message, new Date(data.timestamp || Date.now()));
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик WebSocket сообщений о статусе агента
|
||||
const handleWebSshStatus = (data) => {
|
||||
console.log('[WebSSH Logs] Получен статус:', data);
|
||||
|
||||
if (data.type === 'webssh_status') {
|
||||
isConnected.value = data.connected;
|
||||
|
||||
if (data.message) {
|
||||
addLog(data.status === 'connected' ? 'success' : 'error', data.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик WebSocket сообщений о прогрессе
|
||||
const handleWebSshProgress = (data) => {
|
||||
console.log('[WebSSH Logs] Получен прогресс:', data);
|
||||
|
||||
if (data.type === 'webssh_progress') {
|
||||
const progressMessage = `[${data.stage}] ${data.message}`;
|
||||
addLog('info', progressMessage);
|
||||
|
||||
if (data.percentage) {
|
||||
addLog('debug', `Прогресс: ${data.percentage}%`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Начать прослушивание логов
|
||||
const startListening = () => {
|
||||
if (isListening.value) return;
|
||||
|
||||
console.log('[WebSSH Logs] Начинаем прослушивание логов...');
|
||||
|
||||
try {
|
||||
// Подключаемся к WebSSH Agent WebSocket
|
||||
// Всегда используем localhost:3000, так как порт проброшен в Docker
|
||||
const wsUrl = 'ws://localhost:3000';
|
||||
|
||||
console.log('[WebSSH Logs] Подключение к:', wsUrl);
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WebSSH Logs] Подключено к WebSSH Agent');
|
||||
isConnected.value = true;
|
||||
addLog('success', 'Подключено к WebSSH Agent');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[WebSSH Logs] Отключено от WebSSH Agent');
|
||||
isConnected.value = false;
|
||||
addLog('warning', 'Отключено от WebSSH Agent');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Обрабатываем разные типы сообщений
|
||||
switch (data.type) {
|
||||
case 'webssh_log':
|
||||
handleWebSshLog(data);
|
||||
break;
|
||||
case 'webssh_status':
|
||||
handleWebSshStatus(data);
|
||||
break;
|
||||
case 'webssh_progress':
|
||||
handleWebSshProgress(data);
|
||||
break;
|
||||
default:
|
||||
console.log('[WebSSH Logs] Неизвестный тип сообщения:', data.type);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebSSH Logs] Ошибка парсинга сообщения:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSSH Logs] Ошибка WebSocket:', error);
|
||||
addLog('error', 'Ошибка подключения к WebSSH Agent');
|
||||
};
|
||||
|
||||
isListening.value = true;
|
||||
addLog('info', 'Подключение к WebSSH логам...');
|
||||
} catch (error) {
|
||||
console.error('[WebSSH Logs] Ошибка создания WebSocket:', error);
|
||||
addLog('error', 'Не удалось подключиться к WebSSH Agent');
|
||||
}
|
||||
};
|
||||
|
||||
// Остановить прослушивание логов
|
||||
const stopListening = () => {
|
||||
if (!isListening.value) return;
|
||||
|
||||
console.log('[WebSSH Logs] Останавливаем прослушивание логов...');
|
||||
|
||||
// Отключаемся от WebSSH Agent
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
isListening.value = false;
|
||||
isConnected.value = false;
|
||||
addLog('info', 'Отключение от WebSSH логов');
|
||||
};
|
||||
|
||||
// Очистить логи
|
||||
const clearLogs = () => {
|
||||
logs.value = [];
|
||||
addLog('info', 'Логи очищены');
|
||||
};
|
||||
|
||||
// Форматирование времени
|
||||
const formatTime = (timestamp) => {
|
||||
return timestamp.toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// Получить цвет для типа лога
|
||||
const getLogColor = (type) => {
|
||||
switch (type) {
|
||||
case 'success': return '#27ae60';
|
||||
case 'error': return '#e74c3c';
|
||||
case 'warning': return '#f39c12';
|
||||
case 'debug': return '#95a5a6';
|
||||
default: return '#3498db';
|
||||
}
|
||||
};
|
||||
|
||||
// Получить иконку для типа лога
|
||||
const getLogIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'success': return '✅';
|
||||
case 'error': return '❌';
|
||||
case 'warning': return '⚠️';
|
||||
case 'debug': return '🔍';
|
||||
default: return 'ℹ️';
|
||||
}
|
||||
};
|
||||
|
||||
// Автоматическое подключение при монтировании компонента
|
||||
onMounted(() => {
|
||||
startListening();
|
||||
});
|
||||
|
||||
// Автоматическое отключение при размонтировании компонента
|
||||
onUnmounted(() => {
|
||||
stopListening();
|
||||
});
|
||||
|
||||
return {
|
||||
logs,
|
||||
isConnected,
|
||||
isListening,
|
||||
addLog,
|
||||
startListening,
|
||||
stopListening,
|
||||
clearLogs,
|
||||
formatTime,
|
||||
getLogColor,
|
||||
getLogIcon
|
||||
};
|
||||
}
|
||||
@@ -74,10 +74,9 @@ async removeTagFromContact(contactId, tagId) {
|
||||
};
|
||||
|
||||
export async function getContacts() {
|
||||
const res = await fetch('/users');
|
||||
const data = await res.json();
|
||||
if (data && data.success) {
|
||||
return data.contacts;
|
||||
const res = await api.get('/users');
|
||||
if (res.data && res.data.success) {
|
||||
return res.data.contacts;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
/**
|
||||
* Сервис для управления WEB SSH туннелем
|
||||
* Взаимодействует с локальным агентом на порту 12345
|
||||
* Взаимодействует с локальным агентом на порту 3000
|
||||
*/
|
||||
|
||||
const LOCAL_AGENT_URL = 'http://localhost:12345';
|
||||
const LOCAL_AGENT_URL = 'http://localhost:3000';
|
||||
|
||||
// Функция для генерации nginx конфигурации
|
||||
function getNginxConfig(domain, serverPort) {
|
||||
@@ -126,7 +126,7 @@ class WebSshService {
|
||||
this.connectionStatus = {
|
||||
connected: false,
|
||||
domain: null,
|
||||
tunnelId: null
|
||||
vdsConfigured: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,33 +267,26 @@ EOF
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка DNS записей домена
|
||||
* Получение IP адреса из DNS записей домена через backend API
|
||||
*/
|
||||
async checkDomainDNS(domain, vdsIp) {
|
||||
async getDomainIP(domain) {
|
||||
try {
|
||||
console.log(`Проверка DNS записей для домена ${domain}...`);
|
||||
console.log(`Получение IP адреса для домена ${domain}...`);
|
||||
|
||||
// Простая проверка через fetch (может не работать в браузере из-за CORS)
|
||||
// В реальной реализации нужно использовать backend API
|
||||
const response = await fetch(`https://dns.google/resolve?name=${domain}&type=A`);
|
||||
// Используем backend API для проверки DNS
|
||||
const response = await fetch(`http://localhost:8000/api/dns-check/${domain}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.Answer && data.Answer.length > 0) {
|
||||
const dnsIp = data.Answer[0].data;
|
||||
if (dnsIp === vdsIp) {
|
||||
console.log(`DNS запись корректна: ${domain} → ${vdsIp}`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`DNS запись неверна: ${domain} → ${dnsIp} (ожидается ${vdsIp})`);
|
||||
return false;
|
||||
}
|
||||
if (data.success) {
|
||||
console.log(`DNS запись найдена: ${domain} → ${data.ip}`);
|
||||
return { success: true, ip: data.ip };
|
||||
} else {
|
||||
console.log(`DNS запись для домена ${domain} не найдена`);
|
||||
return false;
|
||||
console.log(`DNS запись для домена ${domain} не найдена: ${data.message}`);
|
||||
return { success: false, error: data.message };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Не удалось проверить DNS: ${error.message}. Продолжаем настройку...`);
|
||||
return true; // Продолжаем даже если DNS проверка не удалась
|
||||
console.warn(`Не удалось получить IP из DNS: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,15 +295,17 @@ EOF
|
||||
*/
|
||||
async setupVDS(config) {
|
||||
try {
|
||||
// Проверяем DNS записи домена
|
||||
if (config.domain && config.vdsIp) {
|
||||
const dnsValid = await this.checkDomainDNS(config.domain, config.vdsIp);
|
||||
if (!dnsValid) {
|
||||
// Получаем IP адрес из DNS записей домена
|
||||
if (config.domain) {
|
||||
const dnsResult = await this.getDomainIP(config.domain);
|
||||
if (!dnsResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'DNS записи не готовы. Убедитесь, что домен указывает на IP VDS сервера.'
|
||||
message: `Домен ${config.domain} не настроен или недоступен: ${dnsResult.error}`
|
||||
};
|
||||
}
|
||||
// Добавляем полученный IP в конфигурацию
|
||||
config.vdsIp = dnsResult.ip;
|
||||
}
|
||||
|
||||
// Проверяем, что агент запущен
|
||||
@@ -323,6 +318,8 @@ EOF
|
||||
}
|
||||
}
|
||||
|
||||
// API ключ больше не нужен - агент защищен сетевым доступом
|
||||
|
||||
// Отправляем конфигурацию VDS агенту
|
||||
const response = await fetch(`${LOCAL_AGENT_URL}/vds/setup`, {
|
||||
method: 'POST',
|
||||
@@ -334,12 +331,12 @@ EOF
|
||||
domain: config.domain,
|
||||
email: config.email,
|
||||
ubuntuUser: config.ubuntuUser,
|
||||
ubuntuPassword: config.ubuntuPassword,
|
||||
dockerUser: config.dockerUser,
|
||||
dockerPassword: config.dockerPassword,
|
||||
sshUser: config.sshUser,
|
||||
sshKey: config.sshKey,
|
||||
encryptionKey: config.encryptionKey
|
||||
sshUser: 'root', // SSH пользователь для настройки ключей (root)
|
||||
sshHost: config.sshHost, // SSH хост для подключения
|
||||
sshPort: config.sshPort, // SSH порт для подключения
|
||||
sshConnectUser: config.sshUser, // SSH пользователь для подключения
|
||||
sshConnectPassword: config.sshPassword // SSH пароль для подключения
|
||||
})
|
||||
});
|
||||
|
||||
@@ -371,81 +368,7 @@ EOF
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение туннеля
|
||||
*/
|
||||
async disconnectTunnel() {
|
||||
try {
|
||||
const response = await fetch(`${LOCAL_AGENT_URL}/tunnel/disconnect`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tunnelId: this.connectionStatus.tunnelId
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.connectionStatus = {
|
||||
connected: false,
|
||||
domain: null,
|
||||
tunnelId: null
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Ошибка при отключении туннеля'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Ошибка при отключении туннеля:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Ошибка подключения к агенту: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статуса подключения
|
||||
*/
|
||||
async getStatus() {
|
||||
try {
|
||||
const response = await fetch(`${LOCAL_AGENT_URL}/tunnel/status`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.connectionStatus = result;
|
||||
return result;
|
||||
} else {
|
||||
return {
|
||||
connected: false,
|
||||
domain: null,
|
||||
tunnelId: null
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Ошибка при получении статуса:', error);
|
||||
return {
|
||||
connected: false,
|
||||
domain: null,
|
||||
tunnelId: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка почтового сервера
|
||||
@@ -869,9 +792,7 @@ export function useWebSshService() {
|
||||
return {
|
||||
checkAgentStatus: () => service.checkAgentStatus(),
|
||||
installAndStartAgent: () => service.installAndStartAgent(),
|
||||
setupVDS: (config) => service.setupVDS(config),
|
||||
disconnectTunnel: () => service.disconnectTunnel(),
|
||||
getStatus: () => service.getStatus()
|
||||
setupVDS: (config) => service.setupVDS(config)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -373,29 +373,19 @@ strong {
|
||||
.crm-web3-block {
|
||||
margin: 32px 0 24px 0;
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
opacity: 0.7;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
.crm-web3-block h2 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
.crm-web3-block .details-btn {
|
||||
margin-top: 0;
|
||||
background: #6c757d;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.crm-web3-block .details-btn:hover {
|
||||
background: #5a6268;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
@@ -20,13 +20,23 @@
|
||||
>
|
||||
<div class="vds-mock-container">
|
||||
<div class="mock-header">
|
||||
<h1>VDS Сервер - Не настроен</h1>
|
||||
<h1 v-if="vdsConfigured">VDS Сервер - Настроен</h1>
|
||||
<h1 v-else>VDS Сервер - Не настроен</h1>
|
||||
<div class="mock-status">
|
||||
<div class="status-indicator offline"></div>
|
||||
<span>Офлайн</span>
|
||||
<div class="status-indicator" :class="vdsConfigured ? 'online' : 'offline'"></div>
|
||||
<span v-if="vdsConfigured">Онлайн</span>
|
||||
<span v-else>Офлайн</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о домене -->
|
||||
<div v-if="vdsConfigured && vdsDomain" class="domain-info">
|
||||
<h3>🌐 Ваше приложение доступно по адресу:</h3>
|
||||
<a :href="`https://${vdsDomain}`" target="_blank" class="domain-link">
|
||||
https://{{ vdsDomain }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Мок интерфейс -->
|
||||
<div class="mock-content">
|
||||
<div class="mock-card">
|
||||
@@ -117,7 +127,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { defineProps, defineEmits, ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
|
||||
@@ -134,9 +144,32 @@ const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Состояние VDS
|
||||
const vdsConfigured = ref(false);
|
||||
const vdsDomain = ref(null);
|
||||
|
||||
// Проверка статуса VDS
|
||||
const checkVdsStatus = () => {
|
||||
try {
|
||||
const vdsConfig = localStorage.getItem('vds-config');
|
||||
if (vdsConfig) {
|
||||
const config = JSON.parse(vdsConfig);
|
||||
vdsConfigured.value = config.isConfigured || false;
|
||||
vdsDomain.value = config.domain || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке статуса VDS:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const goToSetup = () => {
|
||||
router.push({ name: 'webssh-settings' });
|
||||
};
|
||||
|
||||
// Жизненный цикл
|
||||
onMounted(() => {
|
||||
checkVdsStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -188,6 +221,43 @@ const goToSetup = () => {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.domain-info {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.domain-info h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.domain-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.domain-link:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mock-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -61,8 +61,11 @@ sudo bash webssh-agent/install.sh</code></pre>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useWebSshService } from '../../services/webSshService';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const webSshService = useWebSshService();
|
||||
const agentAvailable = ref(false);
|
||||
|
||||
@@ -138,14 +141,20 @@ const handleSubmit = async () => {
|
||||
addLog('info', 'Запуск публикации...');
|
||||
try {
|
||||
// Публикация через агента
|
||||
const result = await webSshService.createTunnel(form);
|
||||
const result = await webSshService.setupVDS(form);
|
||||
if (result.success) {
|
||||
isConnected.value = true;
|
||||
connectionStatus.value = `Подключено к ${form.domain}`;
|
||||
addLog('success', 'SSH туннель успешно создан и настроен');
|
||||
addLog('info', `Ваше приложение доступно по адресу: https://${form.domain}`);
|
||||
addLog('success', 'VDS успешно настроена');
|
||||
addLog('info', `Ваше приложение будет доступно по адресу: https://${form.domain}`);
|
||||
|
||||
// Перенаправляем на страницу VDS Mock через 3 секунды
|
||||
addLog('info', 'Перенаправление на страницу управления VDS через 3 секунды...');
|
||||
setTimeout(() => {
|
||||
router.push({ name: 'vds-mock' });
|
||||
}, 3000);
|
||||
} else {
|
||||
addLog('error', result.message || 'Ошибка при создании туннеля');
|
||||
addLog('error', result.message || 'Ошибка при настройке VDS');
|
||||
}
|
||||
} catch (error) {
|
||||
addLog('error', `Ошибка: ${error.message}`);
|
||||
|
||||
Reference in New Issue
Block a user