diff --git a/.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json b/.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json deleted file mode 100755 index 09721f9..0000000 --- a/.cloudflared/a765a217-5312-48f8-9bb7-5a7ef56602b8.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index a82b2cb..0000000 --- a/.cloudflared/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -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.cloudflared b/Dockerfile.cloudflared deleted file mode 100644 index 2d7c6fe..0000000 --- a/Dockerfile.cloudflared +++ /dev/null @@ -1,9 +0,0 @@ -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/cloudflaredEnv.js b/backend/cloudflaredEnv.js index e4d7803..0c11453 100644 --- a/backend/cloudflaredEnv.js +++ b/backend/cloudflaredEnv.js @@ -3,11 +3,13 @@ const path = require('path'); function writeCloudflaredEnv({ tunnelToken, domain }) { console.log('[writeCloudflaredEnv] tunnelToken:', tunnelToken, 'domain:', domain); - const envPath = '/cloudflared.env'; + const envPath = path.join(__dirname, '../cloudflared.env'); let content = ''; if (tunnelToken) content += `TUNNEL_TOKEN=${tunnelToken}\n`; if (domain) content += `DOMAIN=${domain}\n`; + console.log('[writeCloudflaredEnv] Writing to:', envPath, 'content:', content); fs.writeFileSync(envPath, content, 'utf8'); + console.log('[writeCloudflaredEnv] File written successfully'); } module.exports = { writeCloudflaredEnv }; \ No newline at end of file diff --git a/backend/routes/cloudflare.js b/backend/routes/cloudflare.js index 0ab0ae2..c530042 100644 --- a/backend/routes/cloudflare.js +++ b/backend/routes/cloudflare.js @@ -4,6 +4,7 @@ let Cloudflare; try { Cloudflare = require('cloudflare'); } catch (e) { + console.warn('[Cloudflare] Cloudflare package not available:', e.message); Cloudflare = null; } const db = require('../db'); @@ -19,10 +20,17 @@ const tunnelName = 'hb3-accelerator'; // или из настроек // --- Вспомогательные функции --- async function getSettings() { + try { const { rows } = await db.query('SELECT * FROM cloudflare_settings ORDER BY id DESC LIMIT 1'); return rows[0] || {}; + } catch (e) { + console.error('[Cloudflare] Error getting settings:', e); + return {}; + } } + async function upsertSettings(fields) { + try { const current = await getSettings(); if (current.id) { const updates = []; @@ -39,8 +47,13 @@ async function upsertSettings(fields) { 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); + } + } catch (e) { + console.error('[Cloudflare] Error upserting settings:', e); + throw e; } } + function generateDockerCompose(tunnelToken) { return `version: '3.8' services: @@ -52,6 +65,7 @@ services: restart: unless-stopped `; } + function runDockerCompose() { return new Promise((resolve, reject) => { exec(`docker-compose -f ${dockerComposePath} up -d cloudflared`, (err, stdout, stderr) => { @@ -60,6 +74,7 @@ function runDockerCompose() { }); }); } + function checkCloudflaredStatus() { return new Promise((resolve) => { exec('docker ps --filter "name=cloudflared" --format "{{.Status}}"', (err, stdout) => { @@ -119,15 +134,22 @@ router.post('/account-id', async (req, res) => { router.post('/domain', async (req, res) => { const steps = []; try { + console.log('[Cloudflare /domain] Starting domain connection process'); + // 1. Сохраняем домен, если он пришёл с фронта const { domain: domainFromBody } = req.body; if (domainFromBody) { + console.log('[Cloudflare /domain] Saving domain:', domainFromBody); await upsertSettings({ domain: domainFromBody }); } + // 2. Получаем актуальные настройки const settings = await getSettings(); + console.log('[Cloudflare /domain] Current settings:', { ...settings, api_token: settings.api_token ? '[HIDDEN]' : 'null' }); + const { api_token, domain, account_id, tunnel_id, tunnel_token } = settings; if (!api_token || !domain || !account_id) { + console.error('[Cloudflare /domain] Missing required parameters:', { api_token: !!api_token, domain: !!domain, account_id: !!account_id }); return res.json({ success: false, error: 'Не все параметры Cloudflare заданы (api_token, domain, account_id)' }); } let tunnelId = tunnel_id; @@ -169,7 +191,7 @@ router.post('/domain', async (req, res) => { { config: { ingress: [ - { hostname: domain, service: 'http://dapp-frontend:5173' }, + { hostname: domain, service: 'http://localhost:5173' }, { service: 'http_status:404' } ] } @@ -185,13 +207,172 @@ router.post('/domain', async (req, res) => { steps.push({ step: 'create_route', status: 'error', message: 'Ошибка создания маршрута: ' + errorMsg }); return res.json({ success: false, steps, error: errorMsg }); } - // 4. Перезапуск cloudflared через cloudflared-agent + + // 3.5. Автоматическое создание DNS записей для туннеля try { - await axios.post('http://cloudflared-agent:9000/cloudflared/restart'); + console.log('[Cloudflare /domain] Creating DNS records automatically...'); + + // Получаем зону для домена + const zonesResp = await axios.get('https://api.cloudflare.com/client/v4/zones', { + headers: { Authorization: `Bearer ${api_token}` }, + params: { name: domain } + }); + + const zones = zonesResp.data.result; + if (!zones || zones.length === 0) { + steps.push({ step: 'create_dns', status: 'error', message: 'Домен не найден в Cloudflare аккаунте для создания DNS записей' }); + console.log('[Cloudflare /domain] Domain not found in Cloudflare account, skipping DNS creation'); + } else { + const zoneId = zones[0].id; + + // Получаем существующие DNS записи + const recordsResp = await axios.get(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, { + headers: { Authorization: `Bearer ${api_token}` } + }); + + const existingRecords = recordsResp.data.result || []; + + // Проверяем, есть ли уже запись для основного домена, указывающая на туннель + const tunnelCnamePattern = new RegExp(`${tunnelId}\.cfargotunnel\.com`); + const hasMainRecord = existingRecords.some(record => + record.name === domain && + ( + (record.type === 'CNAME' && tunnelCnamePattern.test(record.content)) || + (record.type === 'CNAME' && record.content.includes('cfargotunnel.com')) + ) + ); + + if (!hasMainRecord) { + // Удаляем конфликтующие записи для основного домена (A, AAAA, CNAME) + const conflictingRecords = existingRecords.filter(record => + record.name === domain && ['A', 'AAAA', 'CNAME'].includes(record.type) + ); + + for (const conflictRecord of conflictingRecords) { + try { + await axios.delete( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${conflictRecord.id}`, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + console.log('[Cloudflare /domain] Removed conflicting record:', conflictRecord.type, conflictRecord.name, conflictRecord.content); + } catch (delErr) { + console.warn('[Cloudflare /domain] Failed to delete conflicting record:', delErr.message); + } + } + + // Создаем CNAME запись для основного домена + const cnameRecord = { + type: 'CNAME', + name: domain, + content: `${tunnelId}.cfargotunnel.com`, + ttl: 1, + proxied: true + }; + + const createResp = await axios.post( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, + cnameRecord, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + + console.log('[Cloudflare /domain] Main CNAME record created:', createResp.data.result); + steps.push({ step: 'create_dns', status: 'ok', message: `DNS запись создана: ${domain} -> ${tunnelId}.cfargotunnel.com (проксирована)` }); + } else { + console.log('[Cloudflare /domain] Main record already exists and points to tunnel'); + steps.push({ step: 'create_dns', status: 'ok', message: 'DNS запись для основного домена уже существует и настроена правильно' }); + } + + // Создаем www поддомен только для корневых доменов (не для поддоменов) + const domainParts = domain.split('.'); + const isRootDomain = domainParts.length === 2; // example.com, а не subdomain.example.com + + if (isRootDomain) { + // Обновляем список записей после возможных изменений + const updatedRecordsResp = await axios.get(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, { + headers: { Authorization: `Bearer ${api_token}` } + }); + const updatedRecords = updatedRecordsResp.data.result || []; + + // Проверяем, есть ли уже запись для www поддомена + const hasWwwRecord = updatedRecords.some(record => + record.name === `www.${domain}` && + ( + (record.type === 'CNAME' && tunnelCnamePattern.test(record.content)) || + (record.type === 'CNAME' && record.content.includes('cfargotunnel.com')) + ) + ); + + if (!hasWwwRecord) { + // Удаляем конфликтующие записи для www поддомена + const conflictingWwwRecords = updatedRecords.filter(record => + record.name === `www.${domain}` && ['A', 'AAAA', 'CNAME'].includes(record.type) + ); + + for (const conflictRecord of conflictingWwwRecords) { + try { + await axios.delete( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${conflictRecord.id}`, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + console.log('[Cloudflare /domain] Removed conflicting www record:', conflictRecord.type, conflictRecord.name, conflictRecord.content); + } catch (delErr) { + console.warn('[Cloudflare /domain] Failed to delete conflicting www record:', delErr.message); + } + } + + // Создаем CNAME запись для www поддомена + const wwwCnameRecord = { + type: 'CNAME', + name: `www.${domain}`, + content: `${tunnelId}.cfargotunnel.com`, + ttl: 1, + proxied: true + }; + + const createWwwResp = await axios.post( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, + wwwCnameRecord, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + + console.log('[Cloudflare /domain] WWW CNAME record created:', createWwwResp.data.result); + steps.push({ step: 'create_dns_www', status: 'ok', message: `DNS запись создана: www.${domain} -> ${tunnelId}.cfargotunnel.com (проксирована)` }); + } else { + console.log('[Cloudflare /domain] WWW record already exists and points to tunnel'); + steps.push({ step: 'create_dns_www', status: 'ok', message: 'DNS запись для www поддомена уже существует и настроена правильно' }); + } + } else { + console.log('[Cloudflare /domain] Skipping www subdomain creation for non-root domain'); + } + } + } catch (e) { + console.error('[Cloudflare /domain] Error creating DNS records:', e); + steps.push({ step: 'create_dns', status: 'error', message: 'Ошибка создания DNS записей: ' + (e.response?.data?.errors?.[0]?.message || e.message) }); + // Не прерываем процесс, DNS можно настроить вручную + } + // 4. Перезапуск cloudflared через docker compose + try { + console.log('[Cloudflare /domain] Restarting cloudflared via docker compose...'); + const { exec } = require('child_process'); + + await new Promise((resolve, reject) => { + exec('cd /app && docker compose restart cloudflared', (err, stdout, stderr) => { + if (err) { + console.error('[Cloudflare /domain] Docker compose restart error:', stderr || err.message); + reject(new Error(stderr || err.message)); + } else { + console.log('[Cloudflare /domain] Docker compose restart success:', stdout); + resolve(stdout); + } + }); + }); + steps.push({ step: 'restart_cloudflared', status: 'ok', message: 'cloudflared перезапущен.' }); } catch (e) { + console.error('[Cloudflare /domain] Error restarting cloudflared:', e.message); steps.push({ step: 'restart_cloudflared', status: 'error', message: 'Ошибка перезапуска cloudflared: ' + e.message }); - return res.json({ success: false, steps, error: e.message }); + // Не возвращаем ошибку, так как туннель создан + console.log('[Cloudflare /domain] Continuing despite restart error...'); } // 5. Возврат app_url res.json({ @@ -283,28 +464,28 @@ router.get('/status', async (req, res) => { domainMsg = 'Ошибка Cloudflare API: ' + e.message; } } - if (settings.api_token && settings.tunnel_token && Cloudflare) { + if (settings.api_token && settings.tunnel_id && settings.account_id) { 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; + console.log('[Cloudflare /status] Checking tunnel status...'); const tunnelsResp = await axios.get( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/cfd_tunnel`, + `https://api.cloudflare.com/client/v4/accounts/${settings.account_id}/cfd_tunnel`, { headers: { Authorization: `Bearer ${settings.api_token}` } } ); - const tunnels = tunnelsResp.data.result; - const foundTunnel = tunnels.find(t => settings.tunnel_token.includes(t.id)); + const tunnels = tunnelsResp.data.result || []; + console.log('[Cloudflare /status] Found tunnels:', tunnels.map(t => ({ id: t.id, name: t.name, status: t.status }))); + + const foundTunnel = tunnels.find(t => t.id === settings.tunnel_id); if (foundTunnel) { tunnelStatus = foundTunnel.status || 'active'; tunnelMsg = `Туннель найден: ${foundTunnel.name || foundTunnel.id}, статус: ${foundTunnel.status}`; + console.log('[Cloudflare /status] Tunnel found:', foundTunnel); } else { tunnelStatus = 'not_found'; tunnelMsg = 'Туннель не найден в Cloudflare аккаунте'; + console.log('[Cloudflare /status] Tunnel not found. Looking for tunnel_id:', settings.tunnel_id); } } catch (e) { + console.error('[Cloudflare /status] Error checking tunnel:', e); tunnelStatus = 'error'; tunnelMsg = 'Ошибка Cloudflare API (туннель): ' + e.message; } @@ -320,4 +501,200 @@ router.get('/status', async (req, res) => { }); }); +// --- DNS Управление --- + +// Получить список DNS записей для домена +router.get('/dns-records', async (req, res) => { + try { + const settings = await getSettings(); + const { api_token, domain } = settings; + + if (!api_token || !domain) { + return res.json({ + success: false, + message: 'API Token и домен должны быть настроены' + }); + } + + // Получаем зону для домена + const zonesResp = await axios.get('https://api.cloudflare.com/client/v4/zones', { + headers: { Authorization: `Bearer ${api_token}` }, + params: { name: domain } + }); + + const zones = zonesResp.data.result; + if (!zones || zones.length === 0) { + return res.json({ + success: false, + message: 'Домен не найден в Cloudflare аккаунте' + }); + } + + const zoneId = zones[0].id; + + // Получаем DNS записи для зоны + const recordsResp = await axios.get(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, { + headers: { Authorization: `Bearer ${api_token}` } + }); + + const records = recordsResp.data.result || []; + + res.json({ + success: true, + records: records.map(record => ({ + id: record.id, + type: record.type, + name: record.name, + content: record.content, + ttl: record.ttl, + proxied: record.proxied, + zone_id: record.zone_id, + zone_name: record.zone_name, + created_on: record.created_on, + modified_on: record.modified_on + })), + zone_id: zoneId + }); + } catch (e) { + console.error('[Cloudflare /dns-records] Error:', e); + res.json({ + success: false, + message: 'Ошибка получения DNS записей: ' + (e.response?.data?.errors?.[0]?.message || e.message) + }); + } +}); + +// Создать/обновить DNS запись +router.post('/dns-records', async (req, res) => { + try { + const settings = await getSettings(); + const { api_token, domain } = settings; + const { type, name, content, ttl = 1, proxied = false, recordId } = req.body; + + if (!api_token || !domain) { + return res.json({ + success: false, + message: 'API Token и домен должны быть настроены' + }); + } + + if (!type || !name || !content) { + return res.json({ + success: false, + message: 'Обязательные поля: type, name, content' + }); + } + + // Получаем зону для домена + const zonesResp = await axios.get('https://api.cloudflare.com/client/v4/zones', { + headers: { Authorization: `Bearer ${api_token}` }, + params: { name: domain } + }); + + const zones = zonesResp.data.result; + if (!zones || zones.length === 0) { + return res.json({ + success: false, + message: 'Домен не найден в Cloudflare аккаунте' + }); + } + + const zoneId = zones[0].id; + const recordData = { type, name, content, ttl }; + + // Добавляем proxied только для типов записей, которые поддерживают прокси + if (['A', 'AAAA', 'CNAME'].includes(type)) { + recordData.proxied = proxied; + } + + let result; + if (recordId) { + // Обновляем существующую запись + const updateResp = await axios.put( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`, + recordData, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + result = updateResp.data.result; + } else { + // Создаем новую запись + const createResp = await axios.post( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, + recordData, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + result = createResp.data.result; + } + + res.json({ + success: true, + message: recordId ? 'DNS запись обновлена' : 'DNS запись создана', + record: { + id: result.id, + type: result.type, + name: result.name, + content: result.content, + ttl: result.ttl, + proxied: result.proxied, + zone_id: result.zone_id + } + }); + } catch (e) { + console.error('[Cloudflare /dns-records POST] Error:', e); + res.json({ + success: false, + message: 'Ошибка создания/обновления DNS записи: ' + (e.response?.data?.errors?.[0]?.message || e.message) + }); + } +}); + +// Удалить DNS запись +router.delete('/dns-records/:recordId', async (req, res) => { + try { + const settings = await getSettings(); + const { api_token, domain } = settings; + const { recordId } = req.params; + + if (!api_token || !domain) { + return res.json({ + success: false, + message: 'API Token и домен должны быть настроены' + }); + } + + // Получаем зону для домена + const zonesResp = await axios.get('https://api.cloudflare.com/client/v4/zones', { + headers: { Authorization: `Bearer ${api_token}` }, + params: { name: domain } + }); + + const zones = zonesResp.data.result; + if (!zones || zones.length === 0) { + return res.json({ + success: false, + message: 'Домен не найден в Cloudflare аккаунте' + }); + } + + const zoneId = zones[0].id; + + // Удаляем DNS запись + await axios.delete( + `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`, + { headers: { Authorization: `Bearer ${api_token}` } } + ); + + res.json({ + success: true, + message: 'DNS запись удалена' + }); + } catch (e) { + console.error('[Cloudflare /dns-records DELETE] Error:', e); + res.json({ + success: false, + message: 'Ошибка удаления DNS записи: ' + (e.response?.data?.errors?.[0]?.message || e.message) + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/dle.js b/backend/routes/dle.js index 276e528..4064701 100644 --- a/backend/routes/dle.js +++ b/backend/routes/dle.js @@ -51,9 +51,9 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => /** * @route GET /api/dle * @desc Получить список всех DLE - * @access Private (только для авторизованных пользователей) + * @access Public (доступно всем пользователям, включая гостевых) */ -router.get('/', auth.requireAuth, async (req, res, next) => { +router.get('/', async (req, res, next) => { try { const dles = await dleService.getAllDLEs(); res.json({ diff --git a/backend/routes/settings.js b/backend/routes/settings.js index e8dabc7..af1ddc8 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -19,10 +19,37 @@ const dbSettingsService = require('../services/dbSettingsService'); logger.info(`Ethers version: ${ethers.version || 'unknown'}`); // Получение RPC настроек -router.get('/rpc', requireAdmin, async (req, res, next) => { +router.get('/rpc', async (req, res, next) => { try { + let isAdmin = false; + + // Проверяем, авторизован ли пользователь и является ли он админом + if (req.session && req.session.authenticated) { + if (req.session.address) { + const authService = require('../services/auth-service'); + isAdmin = await authService.checkAdminTokens(req.session.address); + } else { + isAdmin = req.session.isAdmin || false; + } + } + const rpcConfigs = await rpcProviderService.getAllRpcProviders(); - res.json({ success: true, data: rpcConfigs }); + + if (isAdmin) { + // Для админов возвращаем полные данные + res.json({ success: true, data: rpcConfigs }); + } else { + // Для обычных пользователей и гостей возвращаем ограниченные данные для ОТОБРАЖЕНИЯ, + // но с полными данными для функциональности (тестирование RPC) + const limitedConfigs = rpcConfigs.map(config => ({ + network_id: config.network_id, + rpc_url: config.rpc_url, // Передаем реальный URL для функциональности + rpc_url_display: 'Скрыто', // Для отображения в UI + chain_id: config.chain_id, + _isLimited: true + })); + res.json({ success: true, data: limitedConfigs }); + } } catch (error) { logger.error('Ошибка при получении RPC настроек:', error); next(error); @@ -67,9 +94,11 @@ router.delete('/rpc/:networkId', requireAdmin, async (req, res, next) => { }); // Получение токенов для аутентификации -router.get('/auth-tokens', requireAdmin, async (req, res, next) => { +router.get('/auth-tokens', async (req, res, next) => { try { const authTokens = await authTokenService.getAllAuthTokens(); + + // Возвращаем полные данные для всех пользователей (включая гостевых) res.json({ success: true, data: authTokens }); } catch (error) { logger.error('Ошибка при получении токенов аутентификации:', error); @@ -120,7 +149,7 @@ router.delete('/auth-token/:address/:network', requireAdmin, async (req, res, ne }); // Тестирование RPC соединения -router.post('/rpc-test', requireAdmin, async (req, res, next) => { +router.post('/rpc-test', async (req, res, next) => { try { const { rpcUrl, networkId } = req.body; @@ -174,11 +203,28 @@ router.post('/rpc-test', requireAdmin, async (req, res, next) => { }); // Получить настройки AI-провайдера -router.get('/ai-settings/:provider', requireAdmin, async (req, res, next) => { +router.get('/ai-settings/:provider', async (req, res, next) => { try { - const { provider } = req.params; - const settings = await aiProviderSettingsService.getProviderSettings(provider); - res.json({ success: true, settings }); + let isAdmin = false; + + // Проверяем, авторизован ли пользователь и является ли он админом + if (req.session && req.session.authenticated) { + if (req.session.address) { + const authService = require('../services/auth-service'); + isAdmin = await authService.checkAdminTokens(req.session.address); + } else { + isAdmin = req.session.isAdmin || false; + } + } + + if (isAdmin) { + const { provider } = req.params; + const settings = await aiProviderSettingsService.getProviderSettings(provider); + res.json({ success: true, settings }); + } else { + // Для обычных пользователей и гостей возвращаем пустые настройки + res.json({ success: true, settings: null }); + } } catch (error) { logger.error('Ошибка при получении AI-настроек:', error); next(error); @@ -389,7 +435,7 @@ router.get('/ai-provider-models', requireAdmin, async (req, res, next) => { }); // Получить настройки базы данных -router.get('/db-settings', requireAdmin, async (req, res) => { +router.get('/db-settings', async (req, res) => { try { const settings = await dbSettingsService.getSettings(); res.json({ success: true, settings }); diff --git a/cloudflared-linux-amd64.deb b/cloudflared-linux-amd64.deb deleted file mode 100644 index e6dc494..0000000 Binary files a/cloudflared-linux-amd64.deb and /dev/null differ diff --git a/cloudflared.env b/cloudflared.env index d641c1f..694f32e 100644 --- a/cloudflared.env +++ b/cloudflared.env @@ -1,2 +1,2 @@ -TUNNEL_TOKEN=eyJhIjoiYTY3ODYxMDcyYTE0NGNkZDc0NmU5YzliZGQ4NDc2ZmUiLCJ0IjoiZmYwMDVkMTUtZjc4OC00NDI2LTg1NjAtNWRlZjI0MmEyYTE0IiwicyI6Ik5tVTFNakkzWXpJdE5XWTFOUzAwT1RCaExXSTFOamN0TWpnMU1EQTRaak5sTmpJeSJ9 -DOMAIN=dapp.hb3-accelerator.com +TUNNEL_TOKEN=eyJhIjoiYTY3ODYxMDcyYTE0NGNkZDc0NmU5YzliZGQ4NDc2ZmUiLCJ0IjoiMjc2NGQyOTgtNjZiZC00NDVmLTg1NGQtOWJjYThjNDgxOGNjIiwicyI6IjlCMit6UVJEMmtLeEdWb1YxWGcxMFhSKzk0WUFPazRmalVxNXliNFkzb3R3cHFsL0U3RFM4RGdMdXNZenRIemt2a2dCb3ZRdEdkOFJMdXhFSkp1VUdRPT0ifQ== +DOMAIN=hb3-accelerator.com diff --git a/docker-compose.yml b/docker-compose.yml index 85da063..181cb56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,40 +92,11 @@ 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 + # cloudflared теперь запускается на хосте через start-dapp.sh + # Это решает проблемы с Docker networking + v2rayN в WSL2 + # Чтобы запустить только Docker сервисы: docker compose up postgres backend frontend + # Чтобы запустить весь стек: make up или ./start-dapp.sh + # Туннельные сервисы удалены - переход на Pinata IPFS для Web3 хостинга volumes: postgres_data: null ollama_data: null diff --git a/frontend/src/components/CloudflareDnsManager.vue b/frontend/src/components/CloudflareDnsManager.vue new file mode 100644 index 0000000..e384579 --- /dev/null +++ b/frontend/src/components/CloudflareDnsManager.vue @@ -0,0 +1,559 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/DleManagement.vue b/frontend/src/components/DleManagement.vue index cb7697f..68a3523 100644 --- a/frontend/src/components/DleManagement.vue +++ b/frontend/src/components/DleManagement.vue @@ -216,10 +216,14 @@ + + \ No newline at end of file diff --git a/frontend/src/components/RpcTestModal.vue b/frontend/src/components/RpcTestModal.vue new file mode 100644 index 0000000..f50c63e --- /dev/null +++ b/frontend/src/components/RpcTestModal.vue @@ -0,0 +1,226 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/ContactsView.vue b/frontend/src/views/ContactsView.vue index e21f023..901344d 100644 --- a/frontend/src/views/ContactsView.vue +++ b/frontend/src/views/ContactsView.vue @@ -4,8 +4,72 @@ Контакты +{{ newContacts.length }} - + + +
+
+

Контакты

+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ИмяEmailTelegramКошелекДата созданияДействие
•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• + +
+ +
+ + Полные данные контактов доступны только администраторам +
+
@@ -15,12 +79,14 @@ import { useRouter } from 'vue-router'; import BaseLayout from '../components/BaseLayout.vue'; import ContactTable from '../components/ContactTable.vue'; import { useContactsAndMessagesWebSocket } from '../composables/useContactsWebSocket'; +import { useAuthContext } from '@/composables/useAuth'; const { contacts, newContacts, newMessages, markContactsAsRead, markMessagesAsReadForUser, markContactAsRead } = useContactsAndMessagesWebSocket(); const router = useRouter(); +const { isAdmin } = useAuthContext(); function goBack() { if (window.history.length > 1) { @@ -48,4 +114,138 @@ function goBack() { font-size: 0.95em; margin-left: 7px; } + +/* Стили для таблицы-заглушки */ +.contact-table-placeholder { + background: #fff; + border-radius: 16px; + box-shadow: 0 4px 32px rgba(0,0,0,0.12); + padding: 32px 24px 24px 24px; + width: 100%; + margin-top: 40px; + position: relative; + overflow-x: auto; +} + +.contact-table-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.contact-table-header h2 { + margin: 0; + color: #333; +} + +.close-btn { + position: absolute; + top: 18px; + right: 18px; + background: none; + border: none; + font-size: 2rem; + cursor: pointer; + color: #bbb; + transition: color 0.2s; +} + +.close-btn:hover { + color: #333; +} + +.filters-form-placeholder { + margin-bottom: 24px; + padding: 16px; + background: #f8f9fa; + border-radius: 8px; +} + +.form-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: end; +} + +.form-item { + display: flex; + flex-direction: column; + min-width: 150px; +} + +.form-item label { + font-size: 0.9rem; + color: #666; + margin-bottom: 4px; +} + +.form-item input, +.form-item select { + padding: 8px 12px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 0.9rem; + background: #f8f9fa; + color: #6c757d; + cursor: not-allowed; +} + +.btn-disabled { + padding: 8px 16px; + border: 1px solid #dee2e6; + border-radius: 4px; + background: #f8f9fa; + color: #6c757d; + cursor: not-allowed; + height: fit-content; +} + +.contact-table-masked { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.contact-table-masked th, +.contact-table-masked td { + padding: 12px 8px; + text-align: left; + border-bottom: 1px solid #e9ecef; +} + +.contact-table-masked th { + background: #f8f9fa; + font-weight: 600; + color: #495057; +} + +.contact-table-masked td { + color: #adb5bd; + font-family: monospace; +} + +.details-btn-disabled { + padding: 6px 12px; + border: 1px solid #dee2e6; + border-radius: 4px; + background: #f8f9fa; + color: #6c757d; + font-size: 0.8rem; + cursor: not-allowed; +} + +.access-notice { + margin-top: 20px; + padding: 12px 16px; + background: #e3f2fd; + border: 1px solid #bbdefb; + border-radius: 8px; + color: #1976d2; + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; +} \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 0d07880..06b1285 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -6,6 +6,18 @@ :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')" > + + diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index aea2ed6..d402106 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -8,10 +8,6 @@ >

Настройки

-
Загрузка данных пользователя...
-
-

Для доступа к настройкам необходимо .

-
diff --git a/frontend/src/views/contacts/ContactDeleteConfirm.vue b/frontend/src/views/contacts/ContactDeleteConfirm.vue index fa841bc..bd11cc7 100644 --- a/frontend/src/views/contacts/ContactDeleteConfirm.vue +++ b/frontend/src/views/contacts/ContactDeleteConfirm.vue @@ -10,9 +10,10 @@

Кошелек: {{ contact.wallet || '-' }}

Дата создания: {{ formatDate(contact.created_at) }}

- +
+
Нет прав для удаления контакта
{{ error }}
@@ -22,6 +23,7 @@ import { ref, onMounted } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import contactsService from '../../services/contactsService.js'; +import { useAuthContext } from '@/composables/useAuth'; const route = useRoute(); const router = useRouter(); @@ -29,6 +31,7 @@ const contact = ref(null); const isLoading = ref(true); const isDeleting = ref(false); const error = ref(''); +const { isAdmin } = useAuthContext(); function formatDate(date) { if (!date) return '-'; diff --git a/frontend/src/views/contacts/ContactDetailsView.vue b/frontend/src/views/contacts/ContactDetailsView.vue index 7875206..921b141 100644 --- a/frontend/src/views/contacts/ContactDetailsView.vue +++ b/frontend/src/views/contacts/ContactDetailsView.vue @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/frontend/src/views/settings/BlockchainSettingsView.vue b/frontend/src/views/settings/BlockchainSettingsView.vue index 87d256b..790117f 100644 --- a/frontend/src/views/settings/BlockchainSettingsView.vue +++ b/frontend/src/views/settings/BlockchainSettingsView.vue @@ -145,9 +145,9 @@ - + - +
@@ -188,13 +188,18 @@
Добавленные RPC конфигурации:
ID Сети: {{ rpc.networkId }} - URL: {{ rpc.rpcUrl }} + URL: {{ rpc.rpcUrlDisplay || rpc.rpcUrl }}
-
@@ -233,7 +238,7 @@
- + @@ -291,7 +296,7 @@
- @@ -313,6 +318,13 @@
+ + + @@ -1309,4 +1348,12 @@ h3 { .close-btn:hover { color: #333; } + +.btn[disabled], .btn:disabled { + background: #e0e0e0 !important; + color: #aaa !important; + border-color: #ccc !important; + cursor: not-allowed !important; + opacity: 1 !important; +} \ No newline at end of file diff --git a/frontend/src/views/settings/Interface/CloudflareDetailsView.vue b/frontend/src/views/settings/Interface/CloudflareDetailsView.vue index e5bdc85..8d4804f 100644 --- a/frontend/src/views/settings/Interface/CloudflareDetailsView.vue +++ b/frontend/src/views/settings/Interface/CloudflareDetailsView.vue @@ -52,12 +52,28 @@ sudo apt install cloudflared {{ step.message }} + + +
+ +
+ + +