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

This commit is contained in:
2025-06-29 02:07:50 +03:00
parent b2dcaaab42
commit 0c37942282
28 changed files with 10118 additions and 168 deletions

View File

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

View File

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

View 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()
);

View File

@@ -0,0 +1 @@
ALTER TABLE cloudflare_settings ADD COLUMN account_id TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE cloudflare_settings ADD COLUMN tunnel_id TEXT;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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