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

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

@@ -0,0 +1 @@
{"AccountTag":"a67861072a144cdd746e9c9bdd8476fe","TunnelSecret":"NCu/3BUoqAbF5kwXfs3rTjU9QUiVvXv7OM27BrUd/50Kf/wthq2rIH0G+Eu76LK8JQon/UQBUbQPoRPRY3qbtA==","TunnelID":"a765a217-5312-48f8-9bb7-5a7ef56602b8"}

7
.cloudflared/config.yml Normal file
View 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
View 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
View 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"]

View File

@@ -3,7 +3,19 @@ FROM node:20-bullseye
WORKDIR /app WORKDIR /app
# Устанавливаем зависимости, включая Python для node-gyp # Устанавливаем зависимости, включая 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 для установки зависимостей # Копируем package.json и yarn.lock для установки зависимостей
COPY 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 tagsInitRoutes = require('./routes/tagsInit');
const tagsRoutes = require('./routes/tags'); const tagsRoutes = require('./routes/tags');
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
const cloudflareRoutes = require('./routes/cloudflare');
// Проверка и создание директорий для хранения данных контрактов // Проверка и создание директорий для хранения данных контрактов
const ensureDirectoriesExist = () => { const ensureDirectoriesExist = () => {
@@ -190,6 +191,7 @@ app.use('/api/tags', tagsInitRoutes);
app.use('/api/tags', tagsRoutes); app.use('/api/tags', tagsRoutes);
app.use('/api/identities', identitiesRoutes); app.use('/api/identities', identitiesRoutes);
app.use('/api/rag', ragRoutes); // Подключаем роут app.use('/api/rag', ragRoutes); // Подключаем роут
app.use('/api/cloudflare', cloudflareRoutes);
const nonceStore = new Map(); // или любая другая реализация хранилища nonce 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", "@openzeppelin/contracts": "5.2.0",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.8.4", "axios": "^1.8.4",
"cloudflare": "^4.4.1",
"connect-pg-simple": "^10.0.0", "connect-pg-simple": "^10.0.0",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"cors": "^2.8.5", "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" strip-ansi "^6.0.1"
wrap-ansi "^7.0.0" 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: color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"

17
cloudflared-agent.js Normal file
View 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

Binary file not shown.

2
cloudflared.env Normal file
View File

@@ -0,0 +1,2 @@
TUNNEL_TOKEN=eyJhIjoiYTY3ODYxMDcyYTE0NGNkZDc0NmU5YzliZGQ4NDc2ZmUiLCJ0IjoiZmYwMDVkMTUtZjc4OC00NDI2LTg1NjAtNWRlZjI0MmEyYTE0IiwicyI6Ik5tVTFNakkzWXpJdE5XWTFOUzAwT1RCaExXSTFOamN0TWpnMU1EQTRaak5sTmpJeSJ9
DOMAIN=dapp.hb3-accelerator.com

View File

@@ -1,5 +1,3 @@
# version: '3.8'
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
@@ -13,13 +11,14 @@ services:
POSTGRES_USER: ${DB_USER:-dapp_user} POSTGRES_USER: ${DB_USER:-dapp_user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password} POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password}
ports: ports:
- "5432:5432" - '5432:5432'
healthcheck: 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 interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
ollama: ollama:
image: ollama/ollama:latest image: ollama/ollama:latest
container_name: dapp-ollama container_name: dapp-ollama
@@ -27,9 +26,8 @@ services:
volumes: volumes:
- ollama_data:/root/.ollama - ollama_data:/root/.ollama
ports: ports:
- "11434:11434" - '11434:11434'
command: serve command: serve
backend: backend:
build: build:
context: ./backend context: ./backend
@@ -44,6 +42,8 @@ services:
volumes: volumes:
- ./backend:/app - ./backend:/app
- ./frontend/dist:/app/frontend_dist:ro - ./frontend/dist:/app/frontend_dist:ro
- ./cloudflared.env:/cloudflared.env
- /var/run/docker.sock:/var/run/docker.sock
environment: environment:
- NODE_ENV=${NODE_ENV:-development} - NODE_ENV=${NODE_ENV:-development}
- PORT=${PORT:-8000} - PORT=${PORT:-8000}
@@ -52,18 +52,16 @@ services:
- DB_NAME=${DB_NAME:-dapp_db} - DB_NAME=${DB_NAME:-dapp_db}
- DB_USER=${DB_USER:-dapp_user} - DB_USER=${DB_USER:-dapp_user}
- DB_PASSWORD=${DB_PASSWORD:-dapp_password} - 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_BASE_URL=http://ollama:11434
- OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:7b} - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:7b}
- OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-qwen2.5:7b} - OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-qwen2.5:7b}
- FRONTEND_URL=http://localhost:5173 - FRONTEND_URL=http://localhost:5173
ports: ports:
- "8000:8000" - '8000:8000'
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - host.docker.internal:host-gateway
# command: sh -c "yarn run dev" # Временно комментируем эту строку
# command: nodemon server.js # Запускаем через nodemon
frontend: frontend:
build: build:
context: ./frontend context: ./frontend
@@ -76,9 +74,8 @@ services:
- ./frontend:/app - ./frontend:/app
- frontend_node_modules:/app/node_modules - frontend_node_modules:/app/node_modules
ports: ports:
- "5173:5173" - '5173:5173'
command: yarn run dev -- --host 0.0.0.0 command: yarn run dev -- --host 0.0.0.0
ollama-setup: ollama-setup:
image: curlimages/curl:latest image: curlimages/curl:latest
container_name: dapp-ollama-setup 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' curl -X POST http://ollama:11434/api/pull -d '{\"name\":\"${OLLAMA_MODEL:-qwen2.5:7b}\"}' -H 'Content-Type: application/json'
echo 'Done!' 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: volumes:
postgres_data: postgres_data: null
ollama_data: ollama_data: null
backend_node_modules: backend_node_modules: null
frontend_node_modules: frontend_node_modules: null

View File

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

View File

@@ -4,7 +4,8 @@ import HomeView from '../views/HomeView.vue';
const SettingsAiView = () => import('../views/settings/AiSettingsView.vue'); const SettingsAiView = () => import('../views/settings/AiSettingsView.vue');
const SettingsBlockchainView = () => import('../views/settings/BlockchainSettingsView.vue'); const SettingsBlockchainView = () => import('../views/settings/BlockchainSettingsView.vue');
const SettingsSecurityView = () => import('../views/settings/SecuritySettingsView.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 axios from 'axios';
import { setToStorage } from '../utils/storage.js'; import { setToStorage } from '../utils/storage.js';
@@ -52,6 +53,11 @@ const routes = [
name: 'settings-interface', name: 'settings-interface',
component: SettingsInterfaceView, component: SettingsInterfaceView,
}, },
{
path: 'interface/cloudflare-details',
name: 'settings-interface-cloudflare-details',
component: SettingsInterfaceCloudflareDetailsView,
},
{ {
path: 'telegram', path: 'telegram',
name: 'settings-telegram', name: 'settings-telegram',

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

View File

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

View File

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

View File

@@ -39,6 +39,7 @@ export default defineConfig({
server: { server: {
port: 5173, port: 5173,
host: '0.0.0.0', host: '0.0.0.0',
allowedHosts: ['dapp-frontend', 'localhost', '127.0.0.1', 'hb3-accelerator.com', 'dapp.hb3-accelerator.com'],
proxy: { proxy: {
'/api': { '/api': {
target: 'http://dapp-backend:8000', target: 'http://dapp-backend:8000',

84
start-cloudflared.sh Executable file
View 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"

View File

@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1