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

This commit is contained in:
2025-09-30 22:58:08 +03:00
parent 738a615809
commit ad7b8e9716
16 changed files with 1854 additions and 54 deletions

View File

@@ -21,29 +21,60 @@
<!-- Форма настроек -->
<form @submit.prevent="handleSubmit" class="tunnel-form">
<div class="form-section">
<h3>Настройки домена</h3>
<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>
</div>
<div class="form-group">
<label for="email">Email для SSL *</label>
<input id="email" v-model="form.email" type="email" placeholder="admin@example.com" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="ubuntuUser">Логин Ubuntu *</label>
<input id="ubuntuUser" v-model="form.ubuntuUser" type="text" placeholder="ubuntu" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="ubuntuPassword">Пароль Ubuntu *</label>
<input id="ubuntuPassword" v-model="form.ubuntuPassword" type="password" placeholder="Введите пароль" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="dockerUser">Логин Docker *</label>
<input id="dockerUser" v-model="form.dockerUser" type="text" placeholder="docker" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="dockerPassword">Пароль Docker *</label>
<input id="dockerPassword" v-model="form.dockerPassword" type="password" placeholder="Введите пароль" required :disabled="isConnected" />
</div>
</div>
<div class="form-section">
<h3>Настройки SSH сервера</h3>
<div class="form-group">
<label for="sshHost">SSH Host/IP *</label>
<input id="sshHost" v-model="form.sshHost" type="text" placeholder="192.168.1.100 или server.example.com" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="sshUser">SSH Пользователь *</label>
<input id="sshUser" v-model="form.sshUser" type="text" placeholder="root" required :disabled="isConnected" />
</div>
<div class="form-group">
<label for="sshKey">SSH Приватный ключ *</label>
<textarea id="sshKey" v-model="form.sshKey" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----" rows="6" required :disabled="isConnected"></textarea>
<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>
</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">
@@ -84,23 +115,70 @@
</template>
<script setup>
import { ref, reactive } from 'vue';
import { ref, reactive, onMounted } from 'vue';
import { useWebSshService } from '../services/webSshService';
const webSshService = useWebSshService();
const isLoading = ref(false);
const isConnected = ref(false);
const connectionStatus = ref('Не подключено');
const logs = ref([]);
const showSshKey = ref(false);
const showEncryptionKey = ref(false);
const form = reactive({
vdsIp: '',
domain: '',
email: '',
sshHost: '',
ubuntuUser: 'ubuntu',
ubuntuPassword: '',
dockerUser: 'docker',
dockerPassword: '',
sshUser: '',
sshKey: '',
encryptionKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
});
// Автоматически загружаем SSH ключ и ключ шифрования при загрузке компонента
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();
@@ -124,6 +202,14 @@ const handleSubmit = async () => {
connectionStatus.value = `Подключено к ${form.domain}`;
addLog('success', 'SSH туннель успешно создан и настроен');
addLog('info', `Ваше приложение доступно по адресу: https://${form.domain}`);
// Сохраняем статус VDS как настроенного
localStorage.setItem('vds-config', JSON.stringify({ isConfigured: true }));
// Отправляем событие об изменении статуса VDS
window.dispatchEvent(new CustomEvent('vds-status-changed', {
detail: { isConfigured: true }
}));
} else {
addLog('error', result.message || 'Ошибка при создании туннеля');
}
@@ -152,7 +238,7 @@ const disconnectTunnel = async () => {
}
};
const validateForm = () => {
if (!form.domain || !form.email || !form.sshHost || !form.sshUser || !form.sshKey) {
if (!form.vdsIp || !form.domain || !form.email || !form.ubuntuUser || !form.ubuntuPassword || !form.dockerUser || !form.dockerPassword || !form.sshUser || !form.sshKey || !form.encryptionKey) {
addLog('error', 'Заполните все обязательные поля');
return false;
}
@@ -168,16 +254,23 @@ const validateForm = () => {
};
const resetForm = () => {
Object.assign(form, {
vdsIp: '',
domain: '',
email: '',
sshHost: '',
ubuntuUser: 'ubuntu',
ubuntuPassword: '',
dockerUser: 'docker',
dockerPassword: '',
sshUser: '',
sshKey: '',
encryptionKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
});
logs.value = [];
showSshKey.value = false;
showEncryptionKey.value = false;
};
const addLog = (type, message) => {
logs.value.push({
@@ -186,6 +279,15 @@ const addLog = (type, message) => {
timestamp: new Date()
});
};
// Методы для переключения видимости ключей
const toggleSshKey = () => {
showSshKey.value = !showSshKey.value;
};
const toggleEncryptionKey = () => {
showEncryptionKey.value = !showEncryptionKey.value;
};
const formatTime = (timestamp) => {
return timestamp.toLocaleTimeString();
};
@@ -442,6 +544,14 @@ const formatTime = (timestamp) => {
font-weight: 600;
}
/* Стили для контейнера ключа шифрования */
.encryption-key-container {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
@@ -462,5 +572,71 @@ const formatTime = (timestamp) => {
align-items: flex-start;
gap: 0.5rem;
}
}
/* Маскировка приватных ключей */
textarea[type="password"] {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.2;
letter-spacing: 0.5px;
color: transparent;
text-shadow: 0 0 8px #000;
background: repeating-linear-gradient(
0deg,
#333 0px,
#333 1px,
#444 1px,
#444 2px
);
border: 1px solid #555;
}
textarea[type="password"]:focus {
color: transparent;
text-shadow: 0 0 8px #000;
background: repeating-linear-gradient(
0deg,
#333 0px,
#333 1px,
#444 1px,
#444 2px
);
}
/* Показывать содержимое при фокусе для редактирования */
textarea[type="password"]:focus::placeholder {
color: #666;
text-shadow: none;
}
/* Контейнеры для ключей */
.key-container {
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toggle-key-btn {
background: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
cursor: pointer;
margin-top: 0.5rem;
align-self: flex-start;
}
.toggle-key-btn:hover {
background: #0056b3;
}
.toggle-key-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
</style>

View File

@@ -259,6 +259,11 @@ const routes = [
name: 'management-settings',
component: () => import('../views/smartcontracts/SettingsView.vue')
},
{
path: '/vds-mock',
name: 'vds-mock',
component: () => import('../views/VdsMockView.vue')
},
];
const router = createRouter({

View File

@@ -17,6 +17,109 @@
const LOCAL_AGENT_URL = 'http://localhost:12345';
// Функция для генерации nginx конфигурации
function getNginxConfig(domain, serverPort) {
return `# Rate limiting для защиты от DDoS
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_limit_per_ip:10m rate=5r/s;
# Блокировка известных сканеров и вредоносных ботов
map $http_user_agent $bad_bot {
default 0;
~*bot 1;
~*crawler 1;
~*spider 1;
~*scanner 1;
~*sqlmap 1;
~*nikto 1;
~*dirb 1;
~*gobuster 1;
~*wfuzz 1;
~*burp 1;
~*zap 1;
~*nessus 1;
~*openvas 1;
}
server {
listen 80;
server_name ${domain};
# Блокировка подозрительных ботов
if ($bad_bot = 1) {
return 403;
}
# Защита от path traversal
if ($request_uri ~* "(\\\\.\\\\.|\\\\.\\\\./|\\\\.\\\\.\\\\.\\\\|\\\\.\\\\.%2f|\\\\.\\\\.%5c)") {
return 404;
}
# Защита от опасных расширений
location ~* \\\\.(zip|rar|7z|tar|gz|bz2|xz|sql|sqlite|db|bak|backup|old|csv|php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|tmp|temp|swp|swo|~)$ {
return 404;
}
# Защита от доступа к чувствительным файлам
location ~* /(\\\\.htaccess|\\\\.htpasswd|\\\\.env|\\\\.git|\\\\.svn|\\\\.DS_Store|Thumbs\\\\.db|web\\\\.config|robots\\\\.txt|sitemap\\\\.xml)$ {
deny all;
return 404;
}
# Основной location для фронтенда
location / {
# Rate limiting для основных страниц
limit_req zone=req_limit_per_ip burst=20 nodelay;
proxy_pass http://localhost:${serverPort};
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 $scheme;
# Базовые заголовки безопасности
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 Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
# API проксирование к backend через туннель
location /api/ {
# Rate limiting для API (более строгое)
limit_req zone=api_limit_per_ip burst=10 nodelay;
proxy_pass http://localhost: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;
proxy_set_header X-Forwarded-Proto $scheme;
# Заголовки безопасности для API
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
# WebSocket поддержка
location /ws {
proxy_pass http://localhost: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 $scheme;
}
# Скрытие информации о сервере
server_tokens off;
}`;
}
class WebSshService {
constructor() {
this.isAgentRunning = false;
@@ -164,10 +267,52 @@ EOF
}
/**
* Создание SSH туннеля
* Проверка DNS записей домена
*/
async createTunnel(config) {
async checkDomainDNS(domain, vdsIp) {
try {
console.log(`Проверка DNS записей для домена ${domain}...`);
// Простая проверка через fetch (может не работать в браузере из-за CORS)
// В реальной реализации нужно использовать backend API
const response = await fetch(`https://dns.google/resolve?name=${domain}&type=A`);
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;
}
} else {
console.log(`DNS запись для домена ${domain} не найдена`);
return false;
}
} catch (error) {
console.warn(`Не удалось проверить DNS: ${error.message}. Продолжаем настройку...`);
return true; // Продолжаем даже если DNS проверка не удалась
}
}
/**
* Настройка существующей VDS для туннелей
*/
async setupVDS(config) {
try {
// Проверяем DNS записи домена
if (config.domain && config.vdsIp) {
const dnsValid = await this.checkDomainDNS(config.domain, config.vdsIp);
if (!dnsValid) {
return {
success: false,
message: 'DNS записи не готовы. Убедитесь, что домен указывает на IP VDS сервера.'
};
}
}
// Проверяем, что агент запущен
const agentStatus = await this.checkAgentStatus();
if (!agentStatus.running) {
@@ -178,21 +323,23 @@ EOF
}
}
// Отправляем конфигурацию туннеля агенту
const response = await fetch(`${LOCAL_AGENT_URL}/tunnel/create`, {
// Отправляем конфигурацию VDS агенту
const response = await fetch(`${LOCAL_AGENT_URL}/vds/setup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
vdsIp: config.vdsIp,
domain: config.domain,
email: config.email,
sshHost: config.sshHost,
ubuntuUser: config.ubuntuUser,
ubuntuPassword: config.ubuntuPassword,
dockerUser: config.dockerUser,
dockerPassword: config.dockerPassword,
sshUser: config.sshUser,
sshKey: config.sshKey,
localPort: config.localPort || 5173,
serverPort: config.serverPort || 9000,
sshPort: config.sshPort || 22
encryptionKey: config.encryptionKey
})
});
@@ -203,7 +350,7 @@ EOF
this.connectionStatus = {
connected: true,
domain: config.domain,
tunnelId: result.tunnelId
vdsIp: config.vdsIp
};
}
@@ -212,11 +359,11 @@ EOF
const error = await response.json();
return {
success: false,
message: error.message || 'Ошибка при создании туннеля'
message: error.message || 'Ошибка при настройке VDS'
};
}
} catch (error) {
// console.error('Ошибка при создании туннеля:', error);
// console.error('Ошибка при настройке VDS:', error);
return {
success: false,
message: `Ошибка подключения к агенту: ${error.message}`
@@ -300,6 +447,125 @@ EOF
}
}
/**
* Настройка почтового сервера
*/
async setupMailServer(ssh, config, domain) {
const mailDomain = config.mailDomain || `mail.${domain}`;
const adminEmail = config.adminEmail || config.email;
const adminPassword = config.adminPassword || 'Admin123!';
// Настройка Postfix
const postfixConfig = `
# Основные настройки
myhostname = ${mailDomain}
mydomain = ${domain}
myorigin = $mydomain
inet_interfaces = all
inet_protocols = ipv4
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
home_mailbox = Maildir/
# SSL настройки
smtpd_use_tls = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/${domain}/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/${domain}/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = no
smtp_tls_security_level = may
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom
# Аутентификация
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination
`;
await ssh.execCommand(`echo '${postfixConfig}' > /etc/postfix/main.cf`);
// Настройка Dovecot
const dovecotConfig = `
# Основные настройки
protocols = imap pop3 lmtp
mail_location = maildir:~/Maildir
namespace inbox {
inbox = yes
}
passdb {
driver = pam
}
userdb {
driver = passwd
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
}
ssl = required
ssl_cert = </etc/letsencrypt/live/${domain}/fullchain.pem
ssl_key = </etc/letsencrypt/live/${domain}/privkey.pem
`;
await ssh.execCommand(`echo '${dovecotConfig}' > /etc/dovecot/dovecot.conf`);
// Создание пользователя для почты
await ssh.execCommand(`useradd -m -s /bin/bash ${adminEmail.split('@')[0]}`);
await ssh.execCommand(`echo '${adminEmail.split('@')[0]}:${adminPassword}' | chpasswd`);
// Настройка Roundcube если включен
if (config.enableWebmail) {
const roundcubeConfig = `
server {
listen 80;
server_name ${mailDomain};
location / {
proxy_pass http://localhost/roundcube/;
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 $scheme;
}
}
`;
await ssh.execCommand(`echo '${roundcubeConfig}' > /etc/nginx/sites-available/${mailDomain}`);
await ssh.execCommand(`ln -sf /etc/nginx/sites-available/${mailDomain} /etc/nginx/sites-enabled/`);
}
// Получение SSL для почтового домена
await ssh.execCommand(`certbot --nginx -d ${mailDomain} --non-interactive --agree-tos --email ${adminEmail}`);
// Запуск сервисов
await ssh.execCommand('systemctl enable postfix dovecot');
await ssh.execCommand('systemctl restart postfix dovecot nginx');
// Создание DNS записей
const dnsRecords = `
# Добавьте эти DNS записи в ваш домен:
# MX запись: ${domain} -> ${mailDomain} (приоритет 10)
# A запись: ${mailDomain} -> IP вашего сервера
# SPF запись: v=spf1 mx a ip4:IP_СЕРВЕРА ~all
# DKIM запись: (будет создан автоматически)
`;
console.log('DNS записи для настройки:', dnsRecords);
}
/**
* Получение кода агента для установки
*/
@@ -352,24 +618,111 @@ app.post('/tunnel/create', async (req, res) => {
port: sshPort
});
// Установка NGINX и certbot
await ssh.execCommand('apt-get update && apt-get install -y nginx certbot python3-certbot-nginx');
// Установка NGINX, certbot и почтовых сервисов
const installPackages = 'apt-get update && apt-get install -y nginx certbot python3-certbot-nginx';
const mailPackages = config.enableMail ? 'postfix dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-managesieved dovecot-sieve dovecot-mysql mysql-server roundcube roundcube-mysql' : '';
await ssh.execCommand(\`\${installPackages} \${mailPackages}\`);
// Создание конфигурации NGINX
const nginxConfig = \`
// Создание конфигурации NGINX с полной защитой
const nginxConfig = \`# Rate limiting для защиты от DDoS
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_limit_per_ip:10m rate=5r/s;
# Блокировка известных сканеров и вредоносных ботов
map $http_user_agent $bad_bot {
default 0;
~*bot 1;
~*crawler 1;
~*spider 1;
~*scanner 1;
~*sqlmap 1;
~*nikto 1;
~*dirb 1;
~*gobuster 1;
~*wfuzz 1;
~*burp 1;
~*zap 1;
~*nessus 1;
~*openvas 1;
}
server {
listen 80;
server_name \${domain};
# Блокировка подозрительных ботов
if ($bad_bot = 1) {
return 403;
}
# Защита от path traversal
if ($request_uri ~* "(\\\\.\\\\.|\\\\.\\\\./|\\\\.\\\\.\\\\.\\\\|\\\\.\\\\.%2f|\\\\.\\\\.%5c)") {
return 404;
}
# Защита от опасных расширений
location ~* \\\\.(zip|rar|7z|tar|gz|bz2|xz|sql|sqlite|db|bak|backup|old|csv|php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|tmp|temp|swp|swo|~)$ {
return 404;
}
# Защита от доступа к чувствительным файлам
location ~* /(\\\\.htaccess|\\\\.htpasswd|\\\\.env|\\\\.git|\\\\.svn|\\\\.DS_Store|Thumbs\\\\.db|web\\\\.config|robots\\\\.txt|sitemap\\\\.xml)$ {
deny all;
return 404;
}
# Основной location для фронтенда
location / {
# Rate limiting для основных страниц
limit_req zone=req_limit_per_ip burst=20 nodelay;
proxy_pass http://localhost:\${serverPort};
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 $scheme;
# Базовые заголовки безопасности
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 Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
}
\`;
# API проксирование к backend через туннель
location /api/ {
# Rate limiting для API (более строгое)
limit_req zone=api_limit_per_ip burst=10 nodelay;
proxy_pass http://localhost: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;
proxy_set_header X-Forwarded-Proto $scheme;
# Заголовки безопасности для API
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
# WebSocket поддержка
location /ws {
proxy_pass http://localhost: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 $scheme;
}
# Скрытие информации о сервере
server_tokens off;
}\`;
await ssh.execCommand(\`echo '\${nginxConfig}' > /etc/nginx/sites-available/\${domain}\`);
await ssh.execCommand(\`ln -sf /etc/nginx/sites-available/\${domain} /etc/nginx/sites-enabled/\`);
@@ -378,28 +731,61 @@ server {
// Получение SSL сертификата
await ssh.execCommand(\`certbot --nginx -d \${domain} --non-interactive --agree-tos --email \${email}\`);
// Настройка почты если включена
if (config.enableMail) {
await setupMailServer(ssh, config, domain);
}
ssh.dispose();
// Создание SSH туннеля
// Создание SSH туннелей для frontend и backend
const tunnelId = Date.now().toString();
const sshArgs = [
// SSH туннель для frontend (порт 9000)
const frontendSshArgs = [
'-i', keyPath,
'-p', sshPort.toString(),
'-R', \`\${serverPort}:localhost:\${localPort}\`,
'-N',
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'ServerAliveInterval=60',
'-o', 'ServerAliveCountMax=3',
\`\${sshUser}@\${sshHost}\`
];
const sshProcess = spawn('ssh', sshArgs);
// SSH туннель для backend (порт 8000)
const backendSshArgs = [
'-i', keyPath,
'-p', sshPort.toString(),
'-R', '8000:localhost:8000',
'-N',
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'ServerAliveInterval=60',
'-o', 'ServerAliveCountMax=3',
\`\${sshUser}@\${sshHost}\`
];
sshProcess.on('error', (error) => {
// console.error('SSH процесс ошибка:', error);
// Запускаем оба SSH туннеля
const frontendSshProcess = spawn('ssh', frontendSshArgs);
const backendSshProcess = spawn('ssh', backendSshArgs);
frontendSshProcess.on('error', (error) => {
// console.error('Frontend SSH процесс ошибка:', error);
});
sshProcess.on('close', (code) => {
// console.log('SSH процесс завершен с кодом:', code);
backendSshProcess.on('error', (error) => {
// console.error('Backend SSH процесс ошибка:', error);
});
frontendSshProcess.on('close', (code) => {
// console.log('Frontend SSH процесс завершен с кодом:', code);
tunnelState.connected = false;
});
backendSshProcess.on('close', (code) => {
// console.log('Backend SSH процесс завершен с кодом:', code);
tunnelState.connected = false;
});
@@ -408,7 +794,8 @@ server {
connected: true,
domain,
tunnelId,
sshProcess
frontendSshProcess,
backendSshProcess
};
res.json({
@@ -427,18 +814,22 @@ server {
}
});
// Отключение туннеля
// Отключение туннелей
app.post('/tunnel/disconnect', (req, res) => {
try {
if (tunnelState.sshProcess) {
tunnelState.sshProcess.kill();
if (tunnelState.frontendSshProcess) {
tunnelState.frontendSshProcess.kill();
}
if (tunnelState.backendSshProcess) {
tunnelState.backendSshProcess.kill();
}
tunnelState = {
connected: false,
domain: null,
tunnelId: null,
sshProcess: null
frontendSshProcess: null,
backendSshProcess: null
};
res.json({
@@ -478,7 +869,7 @@ export function useWebSshService() {
return {
checkAgentStatus: () => service.checkAgentStatus(),
installAndStartAgent: () => service.installAndStartAgent(),
createTunnel: (config) => service.createTunnel(config),
setupVDS: (config) => service.setupVDS(config),
disconnectTunnel: () => service.disconnectTunnel(),
getStatus: () => service.getStatus()
};

View File

@@ -46,6 +46,13 @@
Подробнее
</button>
</div>
<!-- Блок Веб3 приложение -->
<div class="crm-web3-block">
<h2>Веб3 приложение</h2>
<button class="details-btn" @click="goToWeb3App">
Подробнее
</button>
</div>
</div>
</BaseLayout>
</template>
@@ -217,6 +224,10 @@ function goToContent() {
function goToManagement() {
router.push({ name: 'management' });
}
function goToWeb3App() {
router.push({ name: 'vds-mock' });
}
</script>
<style scoped>
@@ -358,4 +369,33 @@ strong {
.crm-management-block .details-btn {
margin-top: 0;
}
.crm-web3-block {
margin: 32px 0 24px 0;
padding: 24px;
background: #f5f5f5;
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

@@ -0,0 +1,393 @@
<!--
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
-->
<template>
<BaseLayout
:is-authenticated="isAuthenticated"
:identities="identities"
:token-balances="tokenBalances"
:is-loading-tokens="isLoadingTokens"
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="vds-mock-container">
<div class="mock-header">
<h1>VDS Сервер - Не настроен</h1>
<div class="mock-status">
<div class="status-indicator offline"></div>
<span>Офлайн</span>
</div>
</div>
<!-- Мок интерфейс -->
<div class="mock-content">
<div class="mock-card">
<h2>Статус сервера</h2>
<div class="mock-metrics">
<div class="mock-metric">
<span class="label">CPU:</span>
<span class="value mock">--%</span>
</div>
<div class="mock-metric">
<span class="label">RAM:</span>
<span class="value mock">--%</span>
</div>
<div class="mock-metric">
<span class="label">Диск:</span>
<span class="value mock">--%</span>
</div>
<div class="mock-metric">
<span class="label">Uptime:</span>
<span class="value mock">--</span>
</div>
</div>
</div>
<div class="mock-card">
<h2>Управление сервисами</h2>
<div class="mock-services">
<div class="mock-service">
<span class="service-name">DLE Application</span>
<span class="service-status mock">Недоступно</span>
</div>
<div class="mock-service">
<span class="service-name">PostgreSQL</span>
<span class="service-status mock">Недоступно</span>
</div>
<div class="mock-service">
<span class="service-name">Nginx</span>
<span class="service-status mock">Недоступно</span>
</div>
<div class="mock-service">
<span class="service-name">Docker</span>
<span class="service-status mock">Недоступно</span>
</div>
</div>
</div>
<div class="mock-card">
<h2>Логи системы</h2>
<div class="mock-logs">
<pre>VDS сервер не настроен
Для активации перейдите в настройки и настройте VDS сервер</pre>
</div>
</div>
<div class="mock-card">
<h2>Деплой приложения</h2>
<div class="mock-deploy">
<p>Деплой недоступен - VDS сервер не настроен</p>
<button class="mock-btn" disabled>Деплой недоступен</button>
</div>
</div>
<div class="mock-card">
<h2>Управление бэкапами</h2>
<div class="mock-backups">
<p>Бэкапы недоступны - VDS сервер не настроен</p>
<div class="mock-backup-list">
<div class="mock-backup-item">
<span class="backup-name">Нет бэкапов</span>
<span class="backup-date">--</span>
<span class="backup-size">--</span>
</div>
</div>
</div>
</div>
<!-- Призыв к действию -->
<div class="call-to-action">
<h2>Настройте VDS сервер</h2>
<p>Для использования всех функций управления VDS сервером необходимо его настроить.</p>
<button class="setup-btn" @click="goToSetup">
Перейти к настройке VDS
</button>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import BaseLayout from '../components/BaseLayout.vue';
// Props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
// Emits
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const goToSetup = () => {
router.push({ name: 'webssh-settings' });
};
</script>
<style scoped>
.vds-mock-container {
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
opacity: 0.7;
}
.mock-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.mock-header h1 {
margin: 0;
color: #6c757d;
font-size: 2rem;
font-weight: 600;
}
.mock-status {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
background: #f8d7da;
color: #721c24;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-indicator.offline {
background: #dc3545;
}
.mock-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.mock-card {
background: #f5f5f5;
border-radius: 10px;
padding: 24px;
border: 2px solid #dee2e6;
}
.mock-card h2 {
margin: 0 0 20px 0;
font-size: 1.4rem;
font-weight: 600;
color: #6c757d;
}
.mock-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.mock-metric {
display: flex;
justify-content: space-between;
padding: 12px;
background: #e9ecef;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.mock-metric .label {
font-weight: 600;
color: #6c757d;
}
.mock-metric .value.mock {
font-weight: 700;
color: #6c757d;
}
.mock-services {
display: flex;
flex-direction: column;
gap: 16px;
}
.mock-service {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #e9ecef;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.service-name {
font-weight: 600;
color: #6c757d;
}
.service-status.mock {
font-size: 0.9rem;
font-weight: 500;
padding: 2px 8px;
border-radius: 12px;
background: #f8d7da;
color: #721c24;
}
.mock-logs {
background: #2d3748;
color: #6c757d;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
max-height: 200px;
overflow-y: auto;
border: 1px solid #dee2e6;
}
.mock-deploy {
text-align: center;
padding: 20px;
}
.mock-deploy p {
color: #6c757d;
margin-bottom: 16px;
}
.mock-btn {
padding: 12px 24px;
background: #6c757d;
color: white;
border: none;
border-radius: 8px;
cursor: not-allowed;
font-size: 1rem;
font-weight: 600;
opacity: 0.6;
}
.mock-backups {
text-align: center;
padding: 20px;
}
.mock-backups p {
color: #6c757d;
margin-bottom: 16px;
}
.mock-backup-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.mock-backup-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #e9ecef;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.backup-name {
font-weight: 600;
color: #6c757d;
}
.backup-date, .backup-size {
font-size: 0.9rem;
color: #6c757d;
}
.call-to-action {
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border-radius: 10px;
padding: 32px;
text-align: center;
border: 2px solid #bbdefb;
}
.call-to-action h2 {
margin: 0 0 16px 0;
color: var(--color-primary);
font-size: 1.6rem;
font-weight: 600;
}
.call-to-action p {
color: #6c757d;
margin-bottom: 24px;
font-size: 1.1rem;
}
.setup-btn {
padding: 16px 32px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.setup-btn:hover {
background: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
/* Адаптивность */
@media (max-width: 768px) {
.mock-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.mock-metrics {
grid-template-columns: 1fr;
}
.mock-service, .mock-backup-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>

View File

@@ -18,8 +18,6 @@
:identities="identities"
:token-balances="tokenBalances"
:is-loading-tokens="isLoadingTokens"
:telegram-auth="telegramAuth"
:email-auth="emailAuth"
/>
<div class="webssh-settings-block">
<button class="close-btn" @click="goBack">×</button>
@@ -37,6 +35,18 @@ import Header from '@/components/Header.vue';
import Sidebar from '@/components/Sidebar.vue';
import { useAuthContext } from '@/composables/useAuth';
// Определяем пропсы, которые мы принимаем
defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Array,
isLoadingTokens: Boolean,
formattedLastUpdate: String
});
// Определяем события, которые мы эмитим
defineEmits(['authActionCompleted']);
const router = useRouter();
const goBack = () => router.push('/settings/interface');
const showSidebar = ref(false);
@@ -49,18 +59,6 @@ const isAuthenticated = auth.isAuthenticated.value;
const identities = auth.identities?.value || [];
const tokenBalances = auth.tokenBalances?.value || [];
const isLoadingTokens = false;
// Дефолтные объекты для Sidebar
const telegramAuth = {
showVerification: false,
botLink: '',
verificationCode: '',
error: ''
};
const emailAuth = {
showForm: false,
showVerification: false
};
</script>
<style scoped>