ваше сообщение коммита
This commit is contained in:
1
.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json
Executable file
1
.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json
Executable file
@@ -0,0 +1 @@
|
||||
{"AccountTag":"a67861072a144cdd746e9c9bdd8476fe","TunnelSecret":"NCu/3BUoqAbF5kwXfs3rTjU9QUiVvXv7OM27BrUd/50Kf/wthq2rIH0G+Eu76LK8JQon/UQBUbQPoRPRY3qbtA==","TunnelID":"a765a217-5312-48f8-9bb7-5a7ef56602b8"}
|
||||
7
.cloudflared/config.yml
Normal file
7
.cloudflared/config.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
tunnel: a765a217-5312-48f8-9bb7-5a7ef56602b8
|
||||
credentials-file: /etc/cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json
|
||||
|
||||
ingress:
|
||||
- hostname: hb3-accelerator.com
|
||||
service: http://dapp-frontend:5173
|
||||
- service: http_status:404
|
||||
6
Dockerfile.agent
Normal file
6
Dockerfile.agent
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY cloudflared-agent.js .
|
||||
RUN yarn add express
|
||||
RUN apk add --no-cache docker-cli docker-compose
|
||||
CMD ["node", "cloudflared-agent.js"]
|
||||
9
Dockerfile.cloudflared
Normal file
9
Dockerfile.cloudflared
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM alpine:3.20 as base
|
||||
RUN apk add --no-cache bash curl wget
|
||||
|
||||
FROM cloudflare/cloudflared:2025.6.1 as src
|
||||
|
||||
FROM base
|
||||
COPY --from=src /usr/local/bin/cloudflared /usr/local/bin/cloudflared
|
||||
USER nobody
|
||||
ENTRYPOINT ["bash", "/start-cloudflared.sh"]
|
||||
@@ -3,7 +3,19 @@ FROM node:20-bullseye
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем зависимости, включая Python для node-gyp
|
||||
RUN apt-get update && apt-get install -y python3 make g++ cmake openssl libssl-dev
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 make g++ cmake openssl libssl-dev \
|
||||
ca-certificates curl gnupg lsb-release
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
|
||||
RUN echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
||||
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# Копируем package.json и yarn.lock для установки зависимостей
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
@@ -16,6 +16,7 @@ const userTagsRoutes = require('./routes/userTags');
|
||||
const tagsInitRoutes = require('./routes/tagsInit');
|
||||
const tagsRoutes = require('./routes/tags');
|
||||
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
|
||||
const cloudflareRoutes = require('./routes/cloudflare');
|
||||
|
||||
// Проверка и создание директорий для хранения данных контрактов
|
||||
const ensureDirectoriesExist = () => {
|
||||
@@ -190,6 +191,7 @@ app.use('/api/tags', tagsInitRoutes);
|
||||
app.use('/api/tags', tagsRoutes);
|
||||
app.use('/api/identities', identitiesRoutes);
|
||||
app.use('/api/rag', ragRoutes); // Подключаем роут
|
||||
app.use('/api/cloudflare', cloudflareRoutes);
|
||||
|
||||
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
||||
|
||||
|
||||
13
backend/cloudflaredEnv.js
Normal file
13
backend/cloudflaredEnv.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function writeCloudflaredEnv({ tunnelToken, domain }) {
|
||||
console.log('[writeCloudflaredEnv] tunnelToken:', tunnelToken, 'domain:', domain);
|
||||
const envPath = '/cloudflared.env';
|
||||
let content = '';
|
||||
if (tunnelToken) content += `TUNNEL_TOKEN=${tunnelToken}\n`;
|
||||
if (domain) content += `DOMAIN=${domain}\n`;
|
||||
fs.writeFileSync(envPath, content, 'utf8');
|
||||
}
|
||||
|
||||
module.exports = { writeCloudflaredEnv };
|
||||
7
backend/db/migrations/041_create_cloudflare_settings.sql
Normal file
7
backend/db/migrations/041_create_cloudflare_settings.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS cloudflare_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
api_token TEXT,
|
||||
tunnel_token TEXT,
|
||||
domain TEXT,
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
1
backend/db/migrations/042_create_cloudflare_settings.sql
Normal file
1
backend/db/migrations/042_create_cloudflare_settings.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE cloudflare_settings ADD COLUMN account_id TEXT;
|
||||
1
backend/db/migrations/043_create_cloudflare_settings.sql
Normal file
1
backend/db/migrations/043_create_cloudflare_settings.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE cloudflare_settings ADD COLUMN tunnel_id TEXT;
|
||||
@@ -31,6 +31,7 @@
|
||||
"@openzeppelin/contracts": "5.2.0",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.4",
|
||||
"cloudflare": "^4.4.1",
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
"cookie": "^1.0.2",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
323
backend/routes/cloudflare.js
Normal file
323
backend/routes/cloudflare.js
Normal file
@@ -0,0 +1,323 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
let Cloudflare;
|
||||
try {
|
||||
Cloudflare = require('cloudflare');
|
||||
} catch (e) {
|
||||
Cloudflare = null;
|
||||
}
|
||||
const db = require('../db');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const dockerComposePath = path.join(__dirname, '../../docker-compose.cloudflared.yml');
|
||||
const { addCloudflaredToCompose } = require('../utils/cloudflaredCompose');
|
||||
const { writeCloudflaredEnv } = require('../cloudflaredEnv');
|
||||
const axios = require('axios');
|
||||
const credentialsDir = '/home/alex/DApp-for-Business/.cloudflared';
|
||||
const tunnelName = 'hb3-accelerator'; // или из настроек
|
||||
|
||||
// --- Вспомогательные функции ---
|
||||
async function getSettings() {
|
||||
const { rows } = await db.query('SELECT * FROM cloudflare_settings ORDER BY id DESC LIMIT 1');
|
||||
return rows[0] || {};
|
||||
}
|
||||
async function upsertSettings(fields) {
|
||||
const current = await getSettings();
|
||||
if (current.id) {
|
||||
const updates = [];
|
||||
const values = [];
|
||||
let idx = 1;
|
||||
for (const [k, v] of Object.entries(fields)) {
|
||||
updates.push(`${k} = $${idx}`);
|
||||
values.push(v);
|
||||
idx++;
|
||||
}
|
||||
values.push(current.id);
|
||||
await db.query(`UPDATE cloudflare_settings SET ${updates.join(', ')}, updated_at = NOW() WHERE id = $${idx}`, values);
|
||||
} else {
|
||||
const keys = Object.keys(fields);
|
||||
const values = Object.values(fields);
|
||||
await db.query(`INSERT INTO cloudflare_settings (${keys.join(',')}) VALUES (${keys.map((_,i)=>`$${i+1}`).join(',')})` , values);
|
||||
}
|
||||
}
|
||||
function generateDockerCompose(tunnelToken) {
|
||||
return `version: '3.8'
|
||||
services:
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
command: tunnel --no-autoupdate run
|
||||
environment:
|
||||
- TUNNEL_TOKEN=${tunnelToken}
|
||||
restart: unless-stopped
|
||||
`;
|
||||
}
|
||||
function runDockerCompose() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`docker-compose -f ${dockerComposePath} up -d cloudflared`, (err, stdout, stderr) => {
|
||||
if (err) return reject(stderr || err.message);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
function checkCloudflaredStatus() {
|
||||
return new Promise((resolve) => {
|
||||
exec('docker ps --filter "name=cloudflared" --format "{{.Status}}"', (err, stdout) => {
|
||||
if (err) return resolve('not_installed');
|
||||
if (stdout.trim()) return resolve('running');
|
||||
resolve('not_running');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
// Получить все настройки
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
res.json({ success: true, settings });
|
||||
} catch (e) {
|
||||
res.json({ success: false, message: 'Ошибка получения настроек: ' + e.message });
|
||||
}
|
||||
});
|
||||
// Сохранить API Token
|
||||
router.post('/token', async (req, res) => {
|
||||
const { token } = req.body;
|
||||
if (!token) return res.status(400).json({ success: false, message: 'Token required' });
|
||||
try {
|
||||
await upsertSettings({ api_token: token });
|
||||
res.json({ success: true, message: 'API Token сохранён!' });
|
||||
} catch (e) {
|
||||
res.json({ success: false, message: 'Ошибка сохранения токена: ' + e.message });
|
||||
}
|
||||
});
|
||||
// Получить список аккаунтов пользователя по API Token
|
||||
router.post('/accounts', async (req, res) => {
|
||||
const { api_token } = req.body;
|
||||
if (!api_token) return res.status(400).json({ success: false, message: 'Token required' });
|
||||
try {
|
||||
const resp = await axios.get('https://api.cloudflare.com/client/v4/accounts', {
|
||||
headers: { Authorization: `Bearer ${api_token}` }
|
||||
});
|
||||
res.json({ success: true, accounts: resp.data.result });
|
||||
} catch (e) {
|
||||
res.json({ success: false, message: 'Ошибка Cloudflare API: ' + e.message });
|
||||
}
|
||||
});
|
||||
// Сохранить выбранный account_id
|
||||
router.post('/account-id', async (req, res) => {
|
||||
const { account_id } = req.body;
|
||||
if (!account_id) return res.status(400).json({ success: false, message: 'Account ID required' });
|
||||
try {
|
||||
await upsertSettings({ account_id });
|
||||
res.json({ success: true, message: 'Account ID сохранён!' });
|
||||
} catch (e) {
|
||||
res.json({ success: false, message: 'Ошибка сохранения Account ID: ' + e.message });
|
||||
}
|
||||
});
|
||||
// Новый /domain: полный цикл автоматизации через Cloudflare API
|
||||
router.post('/domain', async (req, res) => {
|
||||
const steps = [];
|
||||
try {
|
||||
// 1. Сохраняем домен, если он пришёл с фронта
|
||||
const { domain: domainFromBody } = req.body;
|
||||
if (domainFromBody) {
|
||||
await upsertSettings({ domain: domainFromBody });
|
||||
}
|
||||
// 2. Получаем актуальные настройки
|
||||
const settings = await getSettings();
|
||||
const { api_token, domain, account_id, tunnel_id, tunnel_token } = settings;
|
||||
if (!api_token || !domain || !account_id) {
|
||||
return res.json({ success: false, error: 'Не все параметры Cloudflare заданы (api_token, domain, account_id)' });
|
||||
}
|
||||
let tunnelId = tunnel_id;
|
||||
let tunnelToken = tunnel_token;
|
||||
// 1. Создание туннеля через Cloudflare API (только если нет tunnel_id)
|
||||
if (!tunnelId || !tunnelToken) {
|
||||
try {
|
||||
const tunnelName = `dapp-tunnel-${domain}`;
|
||||
const tunnelResp = await axios.post(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${account_id}/cfd_tunnel`,
|
||||
{ name: tunnelName },
|
||||
{ headers: { Authorization: `Bearer ${api_token}` } }
|
||||
);
|
||||
tunnelId = tunnelResp.data.result.id;
|
||||
tunnelToken = tunnelResp.data.result.token;
|
||||
console.log('[Cloudflare] Получен tunnelId:', tunnelId, 'tunnelToken:', tunnelToken);
|
||||
// Сохраняем tunnel_id и tunnel_token в базу
|
||||
await upsertSettings({ tunnel_id: tunnelId, tunnel_token: tunnelToken, api_token, account_id, domain });
|
||||
steps.push({ step: 'create_tunnel', status: 'ok', message: 'Туннель создан через Cloudflare API и все параметры сохранены.' });
|
||||
} catch (e) {
|
||||
steps.push({ step: 'create_tunnel', status: 'error', message: 'Ошибка создания туннеля: ' + e.message });
|
||||
return res.json({ success: false, steps, error: e.message });
|
||||
}
|
||||
} else {
|
||||
steps.push({ step: 'use_existing_tunnel', status: 'ok', message: 'Используется существующий туннель.' });
|
||||
}
|
||||
// 2. Сохранение tunnel_token в cloudflared.env
|
||||
try {
|
||||
writeCloudflaredEnv({ tunnelToken, domain });
|
||||
steps.push({ step: 'save_token', status: 'ok', message: 'TUNNEL_TOKEN сохранён в cloudflared.env.' });
|
||||
} catch (e) {
|
||||
steps.push({ step: 'save_token', status: 'error', message: 'Ошибка сохранения tunnel_token: ' + e.message });
|
||||
return res.json({ success: false, steps, error: e.message });
|
||||
}
|
||||
// 3. Создание маршрута (ingress) через Cloudflare API
|
||||
try {
|
||||
await axios.put(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${account_id}/cfd_tunnel/${tunnelId}/configurations`,
|
||||
{
|
||||
config: {
|
||||
ingress: [
|
||||
{ hostname: domain, service: 'http://dapp-frontend:5173' },
|
||||
{ service: 'http_status:404' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ headers: { Authorization: `Bearer ${api_token}` } }
|
||||
);
|
||||
steps.push({ step: 'create_route', status: 'ok', message: 'Маршрут для домена создан.' });
|
||||
} catch (e) {
|
||||
let errorMsg = e.message;
|
||||
if (e.response && e.response.data) {
|
||||
errorMsg += ' | ' + JSON.stringify(e.response.data);
|
||||
}
|
||||
steps.push({ step: 'create_route', status: 'error', message: 'Ошибка создания маршрута: ' + errorMsg });
|
||||
return res.json({ success: false, steps, error: errorMsg });
|
||||
}
|
||||
// 4. Перезапуск cloudflared через cloudflared-agent
|
||||
try {
|
||||
await axios.post('http://cloudflared-agent:9000/cloudflared/restart');
|
||||
steps.push({ step: 'restart_cloudflared', status: 'ok', message: 'cloudflared перезапущен.' });
|
||||
} catch (e) {
|
||||
steps.push({ step: 'restart_cloudflared', status: 'error', message: 'Ошибка перезапуска cloudflared: ' + e.message });
|
||||
return res.json({ success: false, steps, error: e.message });
|
||||
}
|
||||
// 5. Возврат app_url
|
||||
res.json({
|
||||
success: true,
|
||||
app_url: `https://${domain}`,
|
||||
message: 'Туннель и маршрут успешно созданы. Ваше приложение доступно по ссылке.',
|
||||
steps
|
||||
});
|
||||
} catch (e) {
|
||||
steps.push({ step: 'fatal', status: 'error', message: e.message });
|
||||
res.json({ success: false, steps, error: e.message });
|
||||
}
|
||||
});
|
||||
// Проверить домен через Cloudflare API (опционально)
|
||||
router.post('/check-domain', async (req, res) => {
|
||||
if (!Cloudflare) return res.json({ success: false, message: 'Cloudflare не доступен на сервере' });
|
||||
const { api_token, domain } = req.body;
|
||||
if (!api_token || !domain) return res.status(400).json({ success: false, message: 'Token и domain обязательны' });
|
||||
try {
|
||||
const cf = new Cloudflare({ apiToken: api_token });
|
||||
const zones = await cf.zones.browse();
|
||||
const found = zones.result.find(z => z.name === domain);
|
||||
if (!found) return res.status(400).json({ success: false, message: 'Домен не найден в Cloudflare аккаунте' });
|
||||
res.json({ success: true, message: 'Домен найден в Cloudflare аккаунте' });
|
||||
} catch (e) {
|
||||
res.json({ success: false, message: 'Ошибка Cloudflare API: ' + e.message });
|
||||
}
|
||||
});
|
||||
// Установить Cloudflared в Docker (добавить в compose и запустить)
|
||||
router.post('/install', async (req, res) => {
|
||||
console.log('[CloudflareInstall] Запрос на установку cloudflared');
|
||||
const settings = await getSettings();
|
||||
console.log('[CloudflareInstall] Текущие настройки:', settings);
|
||||
if (!settings.tunnel_token) {
|
||||
console.warn('[CloudflareInstall] Нет tunnel_token, установка невозможна');
|
||||
return res.status(400).json({ success: false, message: 'Сначала сохраните Tunnel Token' });
|
||||
}
|
||||
try {
|
||||
console.log('[CloudflareInstall] Запись cloudflared.env...');
|
||||
writeCloudflaredEnv({ tunnelToken: settings.tunnel_token, domain: settings.domain });
|
||||
console.log('[CloudflareInstall] Перезапуск cloudflared через docker compose...');
|
||||
exec('docker-compose up -d cloudflared', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error('[CloudflareInstall] Ошибка docker compose:', stderr || err.message);
|
||||
return res.json({ success: false, message: 'Ошибка docker compose: ' + (stderr || err.message) });
|
||||
}
|
||||
console.log('[CloudflareInstall] Cloudflared перезапущен:', stdout);
|
||||
res.json({ success: true, message: 'Cloudflared переменные обновлены и контейнер перезапущен!' });
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[CloudflareInstall] Ошибка:', e);
|
||||
res.json({ success: false, message: 'Ошибка: ' + (e.message || e) });
|
||||
}
|
||||
});
|
||||
// Получить статус Cloudflared, домена и туннеля
|
||||
router.get('/status', async (req, res) => {
|
||||
const status = await checkCloudflaredStatus();
|
||||
const settings = await getSettings();
|
||||
let domainStatus = 'not_configured';
|
||||
let domainMsg = 'Cloudflare не настроен';
|
||||
let tunnelStatus = 'not_configured';
|
||||
let tunnelMsg = 'Cloudflare не настроен';
|
||||
if (!Cloudflare) {
|
||||
return res.json({
|
||||
success: true,
|
||||
status,
|
||||
domainStatus: 'not_available',
|
||||
domainMsg: 'Пакет cloudflare не установлен',
|
||||
tunnelStatus: 'not_available',
|
||||
tunnelMsg: 'Пакет cloudflare не установлен',
|
||||
message: 'Cloudflare не доступен на сервере'
|
||||
});
|
||||
}
|
||||
if (settings.api_token && settings.domain) {
|
||||
try {
|
||||
const cf = new Cloudflare({ apiToken: settings.api_token });
|
||||
const zonesResp = await cf.zones.list();
|
||||
const zones = zonesResp.result;
|
||||
const found = zones.find(z => z.name === settings.domain);
|
||||
if (found) {
|
||||
domainStatus = 'ok';
|
||||
domainMsg = 'Домен найден в Cloudflare аккаунте';
|
||||
} else {
|
||||
domainStatus = 'not_found';
|
||||
domainMsg = 'Домен не найден в Cloudflare аккаунте';
|
||||
}
|
||||
} catch (e) {
|
||||
domainStatus = 'error';
|
||||
domainMsg = 'Ошибка Cloudflare API: ' + e.message;
|
||||
}
|
||||
}
|
||||
if (settings.api_token && settings.tunnel_token && Cloudflare) {
|
||||
try {
|
||||
const cf = new Cloudflare({ apiToken: settings.api_token });
|
||||
const zonesResp = await cf.zones.list();
|
||||
const zones = zonesResp.result;
|
||||
const zone = zones.find(z => settings.domain.endsWith(z.name));
|
||||
if (!zone) throw new Error('Зона для домена не найдена в Cloudflare');
|
||||
const accountId = zone.account.id;
|
||||
const tunnelsResp = await axios.get(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/cfd_tunnel`,
|
||||
{ headers: { Authorization: `Bearer ${settings.api_token}` } }
|
||||
);
|
||||
const tunnels = tunnelsResp.data.result;
|
||||
const foundTunnel = tunnels.find(t => settings.tunnel_token.includes(t.id));
|
||||
if (foundTunnel) {
|
||||
tunnelStatus = foundTunnel.status || 'active';
|
||||
tunnelMsg = `Туннель найден: ${foundTunnel.name || foundTunnel.id}, статус: ${foundTunnel.status}`;
|
||||
} else {
|
||||
tunnelStatus = 'not_found';
|
||||
tunnelMsg = 'Туннель не найден в Cloudflare аккаунте';
|
||||
}
|
||||
} catch (e) {
|
||||
tunnelStatus = 'error';
|
||||
tunnelMsg = 'Ошибка Cloudflare API (туннель): ' + e.message;
|
||||
}
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
status,
|
||||
domainStatus,
|
||||
domainMsg,
|
||||
tunnelStatus,
|
||||
tunnelMsg,
|
||||
message: `Cloudflared статус: ${status}, домен: ${domainStatus}, туннель: ${tunnelStatus}`
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
38
backend/utils/cloudflaredCompose.js
Normal file
38
backend/utils/cloudflaredCompose.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const composePath = '/docker-compose.yml';
|
||||
|
||||
function addCloudflaredToCompose(tunnelToken) {
|
||||
console.log('[cloudflaredCompose] process.cwd():', process.cwd());
|
||||
console.log('[cloudflaredCompose] __dirname:', __dirname);
|
||||
console.log('[cloudflaredCompose] Ожидаемый путь к compose:', composePath);
|
||||
if (!fs.existsSync(composePath)) {
|
||||
console.error('[cloudflaredCompose] Файл не найден:', composePath);
|
||||
throw new Error('docker-compose.yml не найден по пути: ' + composePath);
|
||||
}
|
||||
let doc;
|
||||
try {
|
||||
doc = yaml.load(fs.readFileSync(composePath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('[cloudflaredCompose] Ошибка чтения compose:', e);
|
||||
throw e;
|
||||
}
|
||||
doc.services = doc.services || {};
|
||||
doc.services.cloudflared = {
|
||||
image: 'cloudflare/cloudflared:latest',
|
||||
command: 'tunnel --no-autoupdate run',
|
||||
environment: [`TUNNEL_TOKEN=${tunnelToken}`],
|
||||
restart: 'unless-stopped'
|
||||
};
|
||||
try {
|
||||
fs.writeFileSync(composePath, yaml.dump(doc), 'utf8');
|
||||
console.log('[cloudflaredCompose] cloudflared добавлен в compose:', composePath);
|
||||
} catch (e) {
|
||||
console.error('[cloudflaredCompose] Ошибка записи compose:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { addCloudflaredToCompose };
|
||||
@@ -2047,6 +2047,19 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cloudflare@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/cloudflare/-/cloudflare-4.4.1.tgz#a3a395b2eed46e6b2e5175a62cc962267cef3981"
|
||||
integrity sha512-wrtQ9WMflnfRcmdQZf/XfVVkeucgwzzYeqFDfgbNdADTaexsPwrtt3etzUvPGvVUeEk9kOPfNkl8MSzObxrIsg==
|
||||
dependencies:
|
||||
"@types/node" "^18.11.18"
|
||||
"@types/node-fetch" "^2.6.4"
|
||||
abort-controller "^3.0.0"
|
||||
agentkeepalive "^4.2.1"
|
||||
form-data-encoder "1.7.2"
|
||||
formdata-node "^4.3.2"
|
||||
node-fetch "^2.6.7"
|
||||
|
||||
color-convert@^1.9.0, color-convert@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
||||
17
cloudflared-agent.js
Normal file
17
cloudflared-agent.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const { exec } = require('child_process');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/cloudflared/restart', (req, res) => {
|
||||
exec('docker-compose up -d cloudflared', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ success: false, message: stderr || err.message });
|
||||
}
|
||||
res.json({ success: true, message: 'cloudflared перезапущен', output: stdout });
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(9000, '0.0.0.0', () => {
|
||||
console.log('Cloudflared agent listening on 0.0.0.0:9000');
|
||||
});
|
||||
BIN
cloudflared-linux-amd64.deb
Normal file
BIN
cloudflared-linux-amd64.deb
Normal file
Binary file not shown.
2
cloudflared.env
Normal file
2
cloudflared.env
Normal file
@@ -0,0 +1,2 @@
|
||||
TUNNEL_TOKEN=eyJhIjoiYTY3ODYxMDcyYTE0NGNkZDc0NmU5YzliZGQ4NDc2ZmUiLCJ0IjoiZmYwMDVkMTUtZjc4OC00NDI2LTg1NjAtNWRlZjI0MmEyYTE0IiwicyI6Ik5tVTFNakkzWXpJdE5XWTFOUzAwT1RCaExXSTFOamN0TWpnMU1EQTRaak5sTmpJeSJ9
|
||||
DOMAIN=dapp.hb3-accelerator.com
|
||||
@@ -1,5 +1,3 @@
|
||||
# version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
@@ -13,13 +11,14 @@ services:
|
||||
POSTGRES_USER: ${DB_USER:-dapp_user}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- '5432:5432'
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-dapp_user} -d ${DB_NAME:-dapp_db}"]
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -U ${DB_USER:-dapp_user} -d ${DB_NAME:-dapp_db}
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: dapp-ollama
|
||||
@@ -27,9 +26,8 @@ services:
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
- '11434:11434'
|
||||
command: serve
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
@@ -44,6 +42,8 @@ services:
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./frontend/dist:/app/frontend_dist:ro
|
||||
- ./cloudflared.env:/cloudflared.env
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
- PORT=${PORT:-8000}
|
||||
@@ -52,18 +52,16 @@ services:
|
||||
- DB_NAME=${DB_NAME:-dapp_db}
|
||||
- DB_USER=${DB_USER:-dapp_user}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-dapp_password}
|
||||
- DATABASE_URL=postgresql://${DB_USER:-dapp_user}:${DB_PASSWORD:-dapp_password}@postgres:5432/${DB_NAME:-dapp_db}
|
||||
- >-
|
||||
DATABASE_URL=postgresql://${DB_USER:-dapp_user}:${DB_PASSWORD:-dapp_password}@postgres:5432/${DB_NAME:-dapp_db}
|
||||
- OLLAMA_BASE_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:7b}
|
||||
- OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-qwen2.5:7b}
|
||||
- FRONTEND_URL=http://localhost:5173
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- '8000:8000'
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
# command: sh -c "yarn run dev" # Временно комментируем эту строку
|
||||
# command: nodemon server.js # Запускаем через nodemon
|
||||
|
||||
- host.docker.internal:host-gateway
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
@@ -76,9 +74,8 @@ services:
|
||||
- ./frontend:/app
|
||||
- frontend_node_modules:/app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
- '5173:5173'
|
||||
command: yarn run dev -- --host 0.0.0.0
|
||||
|
||||
ollama-setup:
|
||||
image: curlimages/curl:latest
|
||||
container_name: dapp-ollama-setup
|
||||
@@ -95,9 +92,42 @@ services:
|
||||
curl -X POST http://ollama:11434/api/pull -d '{\"name\":\"${OLLAMA_MODEL:-qwen2.5:7b}\"}' -H 'Content-Type: application/json'
|
||||
echo 'Done!'
|
||||
"
|
||||
|
||||
cloudflared:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.cloudflared
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
postgres_data:
|
||||
ollama_data:
|
||||
backend_node_modules:
|
||||
frontend_node_modules:
|
||||
- ./start-cloudflared.sh:/start-cloudflared.sh
|
||||
- ./cloudflared.env:/cloudflared.env
|
||||
- ./.cloudflared:/etc/cloudflared
|
||||
depends_on:
|
||||
- backend
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:39693/metrics", "||", "exit", "1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
- TUNNEL_METRICS=0.0.0.0:39693
|
||||
cloudflared-agent:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.agent
|
||||
container_name: dapp-cloudflared-agent
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- "9000:9000"
|
||||
depends_on:
|
||||
- cloudflared
|
||||
volumes:
|
||||
postgres_data: null
|
||||
ollama_data: null
|
||||
backend_node_modules: null
|
||||
frontend_node_modules: null
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Поддержка SPA (Single Page Application)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Настройка кеширования для статических файлов
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Настройка для API запросов на бэкенд
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import HomeView from '../views/HomeView.vue';
|
||||
const SettingsAiView = () => import('../views/settings/AiSettingsView.vue');
|
||||
const SettingsBlockchainView = () => import('../views/settings/BlockchainSettingsView.vue');
|
||||
const SettingsSecurityView = () => import('../views/settings/SecuritySettingsView.vue');
|
||||
const SettingsInterfaceView = () => import('../views/settings/InterfaceSettingsView.vue');
|
||||
const SettingsInterfaceView = () => import('../views/settings/Interface/InterfaceSettingsView.vue');
|
||||
const SettingsInterfaceCloudflareDetailsView = () => import('../views/settings/Interface/CloudflareDetailsView.vue');
|
||||
import axios from 'axios';
|
||||
import { setToStorage } from '../utils/storage.js';
|
||||
|
||||
@@ -52,6 +53,11 @@ const routes = [
|
||||
name: 'settings-interface',
|
||||
component: SettingsInterfaceView,
|
||||
},
|
||||
{
|
||||
path: 'interface/cloudflare-details',
|
||||
name: 'settings-interface-cloudflare-details',
|
||||
component: SettingsInterfaceCloudflareDetailsView,
|
||||
},
|
||||
{
|
||||
path: 'telegram',
|
||||
name: 'settings-telegram',
|
||||
|
||||
307
frontend/src/views/settings/Interface/CloudflareDetailsView.vue
Normal file
307
frontend/src/views/settings/Interface/CloudflareDetailsView.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="cloudflare-details settings-panel">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Настройка Cloudflare и подключение домена</h2>
|
||||
<ol class="instruction-block">
|
||||
<li>Зайдите в свой аккаунт <a href="https://dash.cloudflare.com/" target="_blank">Cloudflare</a> и добавьте ваш домен.</li>
|
||||
<li>Смените NS-записи у регистратора домена на те, что выдаст Cloudflare (см. <a href="https://developers.cloudflare.com/fundamentals/setup/add-site/ns/" target="_blank">инструкцию</a>).</li>
|
||||
<li>Дождитесь, когда домен будет обслуживаться Cloudflare (обычно 5-30 минут).</li>
|
||||
<li>Сгенерируйте <a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank">API Token</a> с правами управления DNS и туннелями.</li>
|
||||
<li>Введите API Token и домен ниже для автоматической настройки туннеля и DNS.</li>
|
||||
<li><b>Один раз выполните в терминале WSL2:</b>
|
||||
<pre style="white-space:pre-line;font-size:0.95em;">curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared noble main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||
sudo apt update
|
||||
sudo apt install cloudflared</pre>
|
||||
</li>
|
||||
<li>Нажмите кнопку <b>Автоматически настроить и открыть приложение</b> и дождитесь появления ссылки.</li>
|
||||
</ol>
|
||||
<form class="form-block" @submit.prevent="saveToken">
|
||||
<label>Cloudflare API Token:</label>
|
||||
<input v-model="apiToken" type="text" class="form-control" placeholder="Введите API Token" />
|
||||
<button class="btn-primary" type="submit">Сохранить токен</button>
|
||||
</form>
|
||||
<div v-if="accounts.length" class="form-block">
|
||||
<label>Выберите аккаунт Cloudflare:</label>
|
||||
<select v-model="selectedAccountId" class="form-control">
|
||||
<option v-for="acc in accounts" :key="acc.id" :value="acc.id">
|
||||
{{ acc.name }} ({{ acc.id }})
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn-primary" @click="saveAccountId">Сохранить аккаунт</button>
|
||||
<div v-if="accountStatusMsg" class="status-block">{{ accountStatusMsg }}</div>
|
||||
</div>
|
||||
<form class="form-block" @submit.prevent="connectDomain">
|
||||
<label>Домен для туннеля:</label>
|
||||
<input v-model="domain" type="text" class="form-control" placeholder="example.com" />
|
||||
<button class="btn-primary" type="submit">Проверить и подключить домен</button>
|
||||
</form>
|
||||
<div class="status-block">
|
||||
<b>Статус Cloudflared:</b> {{ tunnelStatus }}<br>
|
||||
<b>Статус домена:</b> {{ domainStatusMsg }}<br>
|
||||
<b>Статус туннеля:</b> {{ tunnelStatusMsg }}<br>
|
||||
<span v-if="statusMsg">{{ statusMsg }}</span>
|
||||
</div>
|
||||
<div v-if="appUrl" class="app-link-block">
|
||||
<a :href="appUrl" target="_blank" class="btn-primary open-app-btn">
|
||||
Открыть приложение
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="autoSetupSteps.length" class="auto-setup-steps">
|
||||
<div v-for="step in autoSetupSteps" :key="step.step" :class="['auto-setup-step', step.status]">
|
||||
{{ step.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/interface');
|
||||
|
||||
const apiToken = ref('');
|
||||
const domain = ref('');
|
||||
const statusMsg = ref('');
|
||||
const tunnelStatus = ref('');
|
||||
const domainStatusMsg = ref('');
|
||||
const tunnelStatusMsg = ref('');
|
||||
const appUrl = ref('');
|
||||
const autoSetupSteps = ref([]);
|
||||
const accounts = ref([]);
|
||||
const selectedAccountId = ref('');
|
||||
const accountStatusMsg = ref('');
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
console.log('[CloudflareDetails] loadSettings: start');
|
||||
const res = await fetch('/api/cloudflare/settings');
|
||||
const data = await res.json();
|
||||
console.log('[CloudflareDetails] loadSettings: data', data);
|
||||
if (data.success && data.settings) {
|
||||
apiToken.value = data.settings.api_token || '';
|
||||
domain.value = data.settings.domain || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CloudflareDetails] loadSettings: error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToken() {
|
||||
try {
|
||||
console.log('[CloudflareDetails] saveToken: start', apiToken.value);
|
||||
const res = await fetch('/api/cloudflare/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: apiToken.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log('[CloudflareDetails] saveToken: data', data);
|
||||
statusMsg.value = data.message || 'Токен сохранён!';
|
||||
// Получить список аккаунтов
|
||||
const accRes = await fetch('/api/cloudflare/accounts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ api_token: apiToken.value })
|
||||
});
|
||||
const accData = await accRes.json();
|
||||
if (accData.success && accData.accounts) {
|
||||
accounts.value = accData.accounts;
|
||||
accountStatusMsg.value = 'Выберите аккаунт и сохраните.';
|
||||
} else {
|
||||
accountStatusMsg.value = accData.message || 'Ошибка получения аккаунтов';
|
||||
}
|
||||
getStatus();
|
||||
} catch (e) {
|
||||
console.error('[CloudflareDetails] saveToken: error', e);
|
||||
statusMsg.value = 'Ошибка при сохранении токена';
|
||||
accountStatusMsg.value = 'Ошибка получения аккаунтов';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAccountId() {
|
||||
try {
|
||||
const res = await fetch('/api/cloudflare/account-id', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ account_id: selectedAccountId.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
accountStatusMsg.value = data.message || 'Account ID сохранён!';
|
||||
getStatus();
|
||||
} catch (e) {
|
||||
accountStatusMsg.value = 'Ошибка при сохранении Account ID';
|
||||
}
|
||||
}
|
||||
|
||||
async function connectDomain() {
|
||||
try {
|
||||
statusMsg.value = 'Выполняется автоматическая настройка...';
|
||||
appUrl.value = '';
|
||||
autoSetupSteps.value = [];
|
||||
const res = await fetch('/api/cloudflare/domain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domain: domain.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
statusMsg.value = data.message || 'Готово!';
|
||||
appUrl.value = data.app_url || '';
|
||||
autoSetupSteps.value = data.steps || [];
|
||||
} else {
|
||||
statusMsg.value = data.error || 'Ошибка автоматической настройки';
|
||||
autoSetupSteps.value = data.steps || [];
|
||||
}
|
||||
getStatus();
|
||||
} catch (e) {
|
||||
statusMsg.value = 'Ошибка автоматической настройки: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function getStatus() {
|
||||
try {
|
||||
console.log('[CloudflareDetails] getStatus: start');
|
||||
const res = await fetch('/api/cloudflare/status');
|
||||
const data = await res.json();
|
||||
console.log('[CloudflareDetails] getStatus: data', data);
|
||||
tunnelStatus.value = data.status || '';
|
||||
domainStatusMsg.value = data.domainMsg || '';
|
||||
tunnelStatusMsg.value = data.tunnelMsg || '';
|
||||
} catch (e) {
|
||||
console.error('[CloudflareDetails] getStatus: error', e);
|
||||
tunnelStatus.value = 'Ошибка';
|
||||
domainStatusMsg.value = 'Ошибка';
|
||||
tunnelStatusMsg.value = 'Ошибка';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
getStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cloudflare-details.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
.instruction-block {
|
||||
background: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.form-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
.form-control {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
margin-top: 0.5rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.btn-primary.install-btn {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.status-block {
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
font-size: 1.05rem;
|
||||
color: #555;
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.app-link-block {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.open-app-btn {
|
||||
display: inline-block;
|
||||
padding: 0.7rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
background: var(--color-success, #2cae4f);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.open-app-btn:hover {
|
||||
background: var(--color-success-dark, #1e7d32);
|
||||
}
|
||||
.auto-setup-btn {
|
||||
margin-top: 1.5rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
display: block;
|
||||
}
|
||||
.auto-setup-steps {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.auto-setup-step {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
background: #f8f8f8;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.auto-setup-step.ok {
|
||||
color: #2cae4f;
|
||||
}
|
||||
.auto-setup-step.error {
|
||||
color: #c62828;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="interface-settings settings-panel" style="position:relative;min-height:120px">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Подключить домен</h2>
|
||||
<div class="domain-connect-block">
|
||||
<p>Для подключения домена и настройки Cloudflare нажмите кнопку ниже.</p>
|
||||
<button class="btn-primary" @click="goToCloudflareDetails">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings');
|
||||
const goToCloudflareDetails = () => router.push('/settings/interface/cloudflare-details');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
.domain-connect-block {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div class="interface-settings settings-panel" style="position:relative;min-height:120px">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Настройки Интерфейса</h2>
|
||||
|
||||
<!-- Панель Язык -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки языка</h3>
|
||||
<div class="setting-form">
|
||||
<p>Выбор языка интерфейса</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Язык интерфейса:</label>
|
||||
<select v-model="selectedLanguage" class="form-control">
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
<!-- Добавить другие языки по необходимости -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DomainConnectBlock />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
|
||||
import DomainConnectBlock from './DomainConnectBlock.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
// TODO: Импортировать API для сохранения, если нужно
|
||||
|
||||
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
|
||||
const router = useRouter();
|
||||
|
||||
const goBack = () => router.push('/settings');
|
||||
|
||||
// Функция сохранения
|
||||
const saveLanguageSetting = () => {
|
||||
setToStorage('userLanguage', selectedLanguage.value);
|
||||
console.log(`[InterfaceSettingsView] Язык сохранен как: ${selectedLanguage.value}`);
|
||||
// TODO: Добавить реальную смену языка (i18n)
|
||||
// TODO: Возможно, отправить на сервер, если язык влияет на бэкенд
|
||||
// alert('Язык сохранен!'); // Пример уведомления
|
||||
};
|
||||
|
||||
// Можно убрать watch, если сохранение происходит только по кнопке
|
||||
// watch(selectedLanguage, (newLang) => {
|
||||
// setToStorage('userLanguage', newLang);
|
||||
// console.log(`[InterfaceSettingsView] Язык изменен на: ${newLang}`);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.sub-settings-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
/* Убираем нижнюю границу, если это последняя панель */
|
||||
}
|
||||
.setting-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-label {
|
||||
display: block; /* Можно оставить блочным */
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.form-control {
|
||||
max-width: 300px; /* Ограничим ширину select */
|
||||
}
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -39,6 +39,7 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 5173,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['dapp-frontend', 'localhost', '127.0.0.1', 'hb3-accelerator.com', 'dapp.hb3-accelerator.com'],
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://dapp-backend:8000',
|
||||
|
||||
84
start-cloudflared.sh
Executable file
84
start-cloudflared.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "==== Содержимое /cloudflared.env ===="
|
||||
cat /cloudflared.env || echo "Файл не найден"
|
||||
echo "===="
|
||||
|
||||
# Получаем токен из переменной окружения или из файла
|
||||
if [ -z "$TUNNEL_TOKEN" ] && [ -f /cloudflared.env ]; then
|
||||
. /cloudflared.env
|
||||
fi
|
||||
|
||||
echo "TUNNEL_TOKEN=[$TUNNEL_TOKEN]"
|
||||
|
||||
# Функция для проверки сети
|
||||
check_network() {
|
||||
echo "Проверка сетевого соединения..."
|
||||
for addr in 1.1.1.1 8.8.8.8; do
|
||||
if ping -c 1 -W 5 "$addr" > /dev/null 2>&1; then
|
||||
echo "✓ Сеть доступна ($addr)"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "✗ Сетевые проблемы"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Функция для проверки доступности backend
|
||||
check_backend() {
|
||||
echo "Проверка доступности backend..."
|
||||
if curl -s --connect-timeout 5 http://backend:8000 >/dev/null 2>&1; then
|
||||
echo "✓ Backend доступен"
|
||||
return 0
|
||||
else
|
||||
echo "✗ Backend недоступен"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверяем сеть перед запуском
|
||||
echo "=== Проверка подключений ==="
|
||||
check_network
|
||||
check_backend
|
||||
|
||||
# Проверяем наличие конфигурационного файла
|
||||
echo "=== Проверка конфигурации ==="
|
||||
if [ -f "/etc/cloudflared/config.yml" ]; then
|
||||
echo "✓ Конфигурационный файл найден"
|
||||
cat /etc/cloudflared/config.yml
|
||||
else
|
||||
echo "✗ Конфигурационный файл не найден"
|
||||
fi
|
||||
|
||||
if [ -f "/etc/cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json" ]; then
|
||||
echo "✓ Credentials файл найден"
|
||||
else
|
||||
echo "✗ Credentials файл не найден"
|
||||
fi
|
||||
|
||||
# Проверим доступность frontend
|
||||
echo "=== Проверка frontend ==="
|
||||
if curl -s --connect-timeout 5 http://dapp-frontend:5173 >/dev/null 2>&1; then
|
||||
echo "✓ Frontend доступен"
|
||||
else
|
||||
echo "✗ Frontend недоступен, fallback на backend"
|
||||
fi
|
||||
|
||||
# Запускаем cloudflared с токеном вместо конфигурационного файла
|
||||
echo "=== Запуск cloudflared с токеном ==="
|
||||
echo "Используем токен туннеля: ${TUNNEL_TOKEN:0:20}..."
|
||||
exec cloudflared tunnel \
|
||||
--no-autoupdate \
|
||||
--edge-ip-version 4 \
|
||||
--protocol http2 \
|
||||
--retries 20 \
|
||||
--grace-period 60s \
|
||||
--loglevel info \
|
||||
--metrics 0.0.0.0:39693 \
|
||||
--proxy-connect-timeout 90s \
|
||||
--proxy-tls-timeout 90s \
|
||||
--proxy-tcp-keepalive 15s \
|
||||
--proxy-keepalive-timeout 120s \
|
||||
--proxy-keepalive-connections 10 \
|
||||
--proxy-no-happy-eyeballs \
|
||||
run --token "$TUNNEL_TOKEN"
|
||||
Reference in New Issue
Block a user