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

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

@@ -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 "$@"

View File

@@ -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;
}

View File

@@ -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;"]

View File

@@ -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() {

View File

@@ -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>

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>

View 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
};
}

View File

@@ -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 [];
}

View File

@@ -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)
};
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}`);