diff --git a/.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json b/.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json new file mode 100755 index 0000000..09721f9 --- /dev/null +++ b/.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json @@ -0,0 +1 @@ +{"AccountTag":"a67861072a144cdd746e9c9bdd8476fe","TunnelSecret":"NCu/3BUoqAbF5kwXfs3rTjU9QUiVvXv7OM27BrUd/50Kf/wthq2rIH0G+Eu76LK8JQon/UQBUbQPoRPRY3qbtA==","TunnelID":"a765a217-5312-48f8-9bb7-5a7ef56602b8"} \ No newline at end of file diff --git a/.cloudflared/config.yml b/.cloudflared/config.yml new file mode 100644 index 0000000..a82b2cb --- /dev/null +++ b/.cloudflared/config.yml @@ -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 diff --git a/Dockerfile.agent b/Dockerfile.agent new file mode 100644 index 0000000..79f83cf --- /dev/null +++ b/Dockerfile.agent @@ -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"] \ No newline at end of file diff --git a/Dockerfile.cloudflared b/Dockerfile.cloudflared new file mode 100644 index 0000000..2d7c6fe --- /dev/null +++ b/Dockerfile.cloudflared @@ -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"] \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 5cd9056..491727b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 ./ diff --git a/backend/app.js b/backend/app.js index 7aabf51..e37115a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -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 diff --git a/backend/cloudflaredEnv.js b/backend/cloudflaredEnv.js new file mode 100644 index 0000000..e4d7803 --- /dev/null +++ b/backend/cloudflaredEnv.js @@ -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 }; \ No newline at end of file diff --git a/backend/db/migrations/041_create_cloudflare_settings.sql b/backend/db/migrations/041_create_cloudflare_settings.sql new file mode 100644 index 0000000..385d791 --- /dev/null +++ b/backend/db/migrations/041_create_cloudflare_settings.sql @@ -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() +); \ No newline at end of file diff --git a/backend/db/migrations/042_create_cloudflare_settings.sql b/backend/db/migrations/042_create_cloudflare_settings.sql new file mode 100644 index 0000000..55d8132 --- /dev/null +++ b/backend/db/migrations/042_create_cloudflare_settings.sql @@ -0,0 +1 @@ + ALTER TABLE cloudflare_settings ADD COLUMN account_id TEXT; \ No newline at end of file diff --git a/backend/db/migrations/043_create_cloudflare_settings.sql b/backend/db/migrations/043_create_cloudflare_settings.sql new file mode 100644 index 0000000..ec412a7 --- /dev/null +++ b/backend/db/migrations/043_create_cloudflare_settings.sql @@ -0,0 +1 @@ +ALTER TABLE cloudflare_settings ADD COLUMN tunnel_id TEXT; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 51e0a68..94522c4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/routes/cloudflare.js b/backend/routes/cloudflare.js new file mode 100644 index 0000000..0ab0ae2 --- /dev/null +++ b/backend/routes/cloudflare.js @@ -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; \ No newline at end of file diff --git a/backend/utils/cloudflaredCompose.js b/backend/utils/cloudflaredCompose.js new file mode 100644 index 0000000..a3612a8 --- /dev/null +++ b/backend/utils/cloudflaredCompose.js @@ -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 }; \ No newline at end of file diff --git a/backend/yarn.lock b/backend/yarn.lock index 76d8d44..b81c799 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -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" diff --git a/cloudflared-agent.js b/cloudflared-agent.js new file mode 100644 index 0000000..811e83a --- /dev/null +++ b/cloudflared-agent.js @@ -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'); +}); \ No newline at end of file diff --git a/cloudflared-linux-amd64.deb b/cloudflared-linux-amd64.deb new file mode 100644 index 0000000..e6dc494 Binary files /dev/null and b/cloudflared-linux-amd64.deb differ diff --git a/cloudflared.env b/cloudflared.env new file mode 100644 index 0000000..d641c1f --- /dev/null +++ b/cloudflared.env @@ -0,0 +1,2 @@ +TUNNEL_TOKEN=eyJhIjoiYTY3ODYxMDcyYTE0NGNkZDc0NmU5YzliZGQ4NDc2ZmUiLCJ0IjoiZmYwMDVkMTUtZjc4OC00NDI2LTg1NjAtNWRlZjI0MmEyYTE0IiwicyI6Ik5tVTFNakkzWXpJdE5XWTFOUzAwT1RCaExXSTFOamN0TWpnMU1EQTRaak5sTmpJeSJ9 +DOMAIN=dapp.hb3-accelerator.com diff --git a/docker-compose.yml b/docker-compose.yml index ba8dc06..85da063 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: + - ./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: - ollama_data: - backend_node_modules: - frontend_node_modules: \ No newline at end of file + postgres_data: null + ollama_data: null + backend_node_modules: null + frontend_node_modules: null diff --git a/frontend/nginx.conf b/frontend/nginx.conf deleted file mode 100644 index d5c0c7c..0000000 --- a/frontend/nginx.conf +++ /dev/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; - } -} \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 8f1f0a4..6c497a2 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -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', diff --git a/frontend/src/views/settings/Interface/CloudflareDetailsView.vue b/frontend/src/views/settings/Interface/CloudflareDetailsView.vue new file mode 100644 index 0000000..e5bdc85 --- /dev/null +++ b/frontend/src/views/settings/Interface/CloudflareDetailsView.vue @@ -0,0 +1,307 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/Interface/InterfaceSettingsView.vue b/frontend/src/views/settings/Interface/InterfaceSettingsView.vue new file mode 100644 index 0000000..0eb5da4 --- /dev/null +++ b/frontend/src/views/settings/Interface/InterfaceSettingsView.vue @@ -0,0 +1,71 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/InterfaceSettingsView.vue b/frontend/src/views/settings/InterfaceSettingsView.vue deleted file mode 100644 index b3983b5..0000000 --- a/frontend/src/views/settings/InterfaceSettingsView.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 68ad2d1..e1c49cc 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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', diff --git a/start-cloudflared.sh b/start-cloudflared.sh new file mode 100755 index 0000000..2337058 --- /dev/null +++ b/start-cloudflared.sh @@ -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" \ No newline at end of file diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index fb57ccd..0000000 --- a/yarn.lock +++ /dev/null @@ -1,4 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - -