diff --git a/docker-compose.yml b/docker-compose.yml index 232fc4b..523aa8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - ./db:/app/db/ # Синхронизация базы данных (persistence) - ./uploads:/app/uploads/ # Uploaded product photos - ./wg/start.sh:/app/start.sh # Монтируем start.sh (генерирует wg0.conf из env) + - ./.env:/app/.env:rw # Settings panel read/write cap_add: # Минимальные привилегии, необходимые только для WireGuard - NET_ADMIN sysctls: diff --git a/src/admin/public/style.css b/src/admin/public/style.css index c20484d..6b90e8f 100644 --- a/src/admin/public/style.css +++ b/src/admin/public/style.css @@ -208,6 +208,20 @@ pre { font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-width .muted { color: var(--muted); font-size: 0.85rem; } +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 1.2rem; + height: 1.2rem; + accent-color: var(--primary); +} + .settings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); diff --git a/src/admin/routes/settings.js b/src/admin/routes/settings.js index 4ecf7cd..034c0f5 100644 --- a/src/admin/routes/settings.js +++ b/src/admin/routes/settings.js @@ -1,22 +1,95 @@ import { Router } from 'express'; +import { readFileSync, writeFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; import config from '../../config/config.js'; import { renderSettings } from '../views/settings.js'; const router = Router(); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = join(__dirname, '..', '..', '..'); +const envPath = join(projectRoot, '.env'); + +const PLACEHOLDER = '••••••••'; + +const PRESERVE_KEYS = new Set([ + 'ENCRYPTION_KEY', 'ADMIN_SECRET', 'GITEA_TOKEN', + 'WG_PRIVATE_KEY', 'WG_PRESHARED_KEY' +]); router.get('/', (req, res) => { + const saved = req.query.saved === '1'; + const error = req.query.error === '1'; + const message = saved ? 'Settings saved. Container restarting...' : + error ? 'Failed to save settings.' : null; + const data = { botToken: config.BOT_TOKEN, adminIds: config.ADMIN_IDS, superAdminIds: config.SUPER_ADMIN_IDS, + supportLink: config.SUPPORT_LINK || '', commissionEnabled: config.COMMISSION_ENABLED, commissionPercent: config.COMMISSION_PERCENT, commissionWallets: config.COMMISSION_WALLETS, wgEnabled: process.env.WG_ENABLED === 'true', wgEndpoint: process.env.WG_ENDPOINT || '', wgAddress: process.env.WG_ADDRESS || '', + wgPublicKey: process.env.WG_PUBLIC_KEY || '', + wgDns: process.env.WG_DNS || '', + adminPort: process.env.ADMIN_PORT || '', + adminUrl: process.env.ADMIN_URL || '', + catalogPath: process.env.CATALOG_PATH || '', + giteaApiUrl: process.env.GITEA_API_URL || '', + encryptionKey: process.env.ENCRYPTION_KEY || '', + adminSecret: process.env.ADMIN_SECRET || '', + giteaToken: process.env.GITEA_TOKEN || '', + saved, + message, }; res.send(renderSettings(data)); }); -export default router; +router.post('/', (req, res) => { + try { + const body = req.body; + + const checkboxKeys = ['COMMISSION_ENABLED', 'WG_ENABLED']; + for (const k of checkboxKeys) { + if (!(k in body)) body[k] = 'false'; + } + + const original = readFileSync(envPath, 'utf-8'); + const lines = original.split('\n'); + + const updated = lines.map(line => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) return line; + + const eqIdx = line.indexOf('='); + if (eqIdx === -1) return line; + + const key = line.slice(0, eqIdx).trim(); + if (key in body) { + if (PRESERVE_KEYS.has(key)) return line; + + let val = String(body[key]).replace(/[\r\n]+/g, ''); + if (val === PLACEHOLDER) return line; + + if (val === '') return `${key}=`; + return `${key}=${val}`; + } + return line; + }); + + writeFileSync(envPath, updated.join('\n'), 'utf-8'); + + setTimeout(() => process.exit(0), 500); + + res.redirect('/settings?saved=1'); + } catch (err) { + console.error('Settings save error:', err); + res.redirect('/settings?error=1'); + } +}); + +export default router; \ No newline at end of file diff --git a/src/admin/views/audit.js b/src/admin/views/audit.js index 350d563..6efbaa4 100644 --- a/src/admin/views/audit.js +++ b/src/admin/views/audit.js @@ -1,4 +1,5 @@ import { layout, table } from './layout.js'; +import { escapeHtml } from './escape.js'; export function renderAuditLog(entries) { const headers = ['ID', 'Action', 'Admin ID', 'Details', 'Date']; @@ -14,7 +15,3 @@ export function renderAuditLog(entries) { .replace('
', `${rows}`); return layout('Audit Log', content, 'audit'); } - -function escapeHtml(str) { - return str.replace(/&/g, '&').replace(//g, '>'); -} diff --git a/src/admin/views/categories.js b/src/admin/views/categories.js index 6ca15b9..a93368f 100644 --- a/src/admin/views/categories.js +++ b/src/admin/views/categories.js @@ -1,4 +1,5 @@ import { layout, flash } from './layout.js'; +import { escapeHtml } from './escape.js'; export function renderCategoryList(categories, locations, subcategories) { const locOptions = locations.map(l => @@ -76,7 +77,3 @@ export function renderCategoryList(categories, locations, subcategories) { `; return layout('Categories', content, 'categories'); } - -function escapeHtml(str) { - return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} diff --git a/src/admin/views/escape.js b/src/admin/views/escape.js new file mode 100644 index 0000000..f48939b --- /dev/null +++ b/src/admin/views/escape.js @@ -0,0 +1,3 @@ +export function escapeHtml(str) { + return String(str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} \ No newline at end of file diff --git a/src/admin/views/locations.js b/src/admin/views/locations.js index 80004ba..99e905c 100644 --- a/src/admin/views/locations.js +++ b/src/admin/views/locations.js @@ -1,4 +1,5 @@ import { layout, flash } from './layout.js'; +import { escapeHtml } from './escape.js'; export function renderLocationList(locations) { const rows = locations.map(l => `These are the shop's payment wallets. Edit them in the .env file.
Edit wallet addresses on the Settings page.
| Currency | Address |
|---|---|
| ${type} | ${escapeHtml(addr || 'Not set')} |