feat: editable settings page with .env write and container restart
- Add settings form with all config fields (Bot, Commission, Wallets, WireGuard) - POST handler writes .env file and restarts container via process.exit(0) - Secrets (ENCRYPTION_KEY, ADMIN_SECRET, GITEA_TOKEN, WG_PRIVATE_KEY, WG_PRESHARED_KEY) are never sent to browser - masked placeholders used instead - PRESERVE_KEYS enforced: secret keys cannot be overwritten via form - Values sanitized: newlines stripped before writing to .env - start.sh loads .env file before node to override Docker env_file cache - Extract shared escapeHtml utility to escape.js (used by 6 view files) - Update paymentWallets view to link to Settings page instead of .env - Add .env volume mount for settings panel read/write - Fix registerRoutes() not being called in index.js (bot menu buttons)
This commit is contained in:
@@ -16,6 +16,7 @@ services:
|
|||||||
- ./db:/app/db/ # Синхронизация базы данных (persistence)
|
- ./db:/app/db/ # Синхронизация базы данных (persistence)
|
||||||
- ./uploads:/app/uploads/ # Uploaded product photos
|
- ./uploads:/app/uploads/ # Uploaded product photos
|
||||||
- ./wg/start.sh:/app/start.sh # Монтируем start.sh (генерирует wg0.conf из env)
|
- ./wg/start.sh:/app/start.sh # Монтируем start.sh (генерирует wg0.conf из env)
|
||||||
|
- ./.env:/app/.env:rw # Settings panel read/write
|
||||||
cap_add: # Минимальные привилегии, необходимые только для WireGuard
|
cap_add: # Минимальные привилегии, необходимые только для WireGuard
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
sysctls:
|
sysctls:
|
||||||
|
|||||||
@@ -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; }
|
.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 {
|
.settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
|||||||
@@ -1,22 +1,95 @@
|
|||||||
import { Router } from 'express';
|
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 config from '../../config/config.js';
|
||||||
import { renderSettings } from '../views/settings.js';
|
import { renderSettings } from '../views/settings.js';
|
||||||
|
|
||||||
const router = Router();
|
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) => {
|
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 = {
|
const data = {
|
||||||
botToken: config.BOT_TOKEN,
|
botToken: config.BOT_TOKEN,
|
||||||
adminIds: config.ADMIN_IDS,
|
adminIds: config.ADMIN_IDS,
|
||||||
superAdminIds: config.SUPER_ADMIN_IDS,
|
superAdminIds: config.SUPER_ADMIN_IDS,
|
||||||
|
supportLink: config.SUPPORT_LINK || '',
|
||||||
commissionEnabled: config.COMMISSION_ENABLED,
|
commissionEnabled: config.COMMISSION_ENABLED,
|
||||||
commissionPercent: config.COMMISSION_PERCENT,
|
commissionPercent: config.COMMISSION_PERCENT,
|
||||||
commissionWallets: config.COMMISSION_WALLETS,
|
commissionWallets: config.COMMISSION_WALLETS,
|
||||||
wgEnabled: process.env.WG_ENABLED === 'true',
|
wgEnabled: process.env.WG_ENABLED === 'true',
|
||||||
wgEndpoint: process.env.WG_ENDPOINT || '',
|
wgEndpoint: process.env.WG_ENDPOINT || '',
|
||||||
wgAddress: process.env.WG_ADDRESS || '',
|
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));
|
res.send(renderSettings(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
export default router;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { layout, table } from './layout.js';
|
import { layout, table } from './layout.js';
|
||||||
|
import { escapeHtml } from './escape.js';
|
||||||
|
|
||||||
export function renderAuditLog(entries) {
|
export function renderAuditLog(entries) {
|
||||||
const headers = ['ID', 'Action', 'Admin ID', 'Details', 'Date'];
|
const headers = ['ID', 'Action', 'Admin ID', 'Details', 'Date'];
|
||||||
@@ -14,7 +15,3 @@ export function renderAuditLog(entries) {
|
|||||||
.replace('<tbody></tbody>', `<tbody>${rows}</tbody>`);
|
.replace('<tbody></tbody>', `<tbody>${rows}</tbody>`);
|
||||||
return layout('Audit Log', content, 'audit');
|
return layout('Audit Log', content, 'audit');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { layout, flash } from './layout.js';
|
import { layout, flash } from './layout.js';
|
||||||
|
import { escapeHtml } from './escape.js';
|
||||||
|
|
||||||
export function renderCategoryList(categories, locations, subcategories) {
|
export function renderCategoryList(categories, locations, subcategories) {
|
||||||
const locOptions = locations.map(l =>
|
const locOptions = locations.map(l =>
|
||||||
@@ -76,7 +77,3 @@ export function renderCategoryList(categories, locations, subcategories) {
|
|||||||
</table>`;
|
</table>`;
|
||||||
return layout('Categories', content, 'categories');
|
return layout('Categories', content, 'categories');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|||||||
3
src/admin/views/escape.js
Normal file
3
src/admin/views/escape.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function escapeHtml(str) {
|
||||||
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { layout, flash } from './layout.js';
|
import { layout, flash } from './layout.js';
|
||||||
|
import { escapeHtml } from './escape.js';
|
||||||
|
|
||||||
export function renderLocationList(locations) {
|
export function renderLocationList(locations) {
|
||||||
const rows = locations.map(l => `<tr>
|
const rows = locations.map(l => `<tr>
|
||||||
@@ -32,7 +33,3 @@ export function renderLocationList(locations) {
|
|||||||
</table>`;
|
</table>`;
|
||||||
return layout('Locations', content, 'locations');
|
return layout('Locations', content, 'locations');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { layout } from './layout.js';
|
import { layout } from './layout.js';
|
||||||
|
import { escapeHtml } from './escape.js';
|
||||||
|
|
||||||
export function renderPaymentWallets(data) {
|
export function renderPaymentWallets(data) {
|
||||||
const walletRows = Object.entries(data.wallets).map(([type, addr]) =>
|
const walletRows = Object.entries(data.wallets).map(([type, addr]) =>
|
||||||
@@ -17,7 +18,7 @@ export function renderPaymentWallets(data) {
|
|||||||
|
|
||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
<h2>Commission Wallet Addresses</h2>
|
<h2>Commission Wallet Addresses</h2>
|
||||||
<p class="muted">These are the shop's payment wallets. Edit them in the <code>.env</code> file.</p>
|
<p class="muted">Edit wallet addresses on the <a href="/settings">Settings</a> page.</p>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Currency</th><th>Address</th></tr></thead>
|
<thead><tr><th>Currency</th><th>Address</th></tr></thead>
|
||||||
<tbody>${walletRows}</tbody>
|
<tbody>${walletRows}</tbody>
|
||||||
@@ -26,7 +27,3 @@ export function renderPaymentWallets(data) {
|
|||||||
`;
|
`;
|
||||||
return layout('Payment Wallets', content, 'payment-wallets');
|
return layout('Payment Wallets', content, 'payment-wallets');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { layout, flash } from './layout.js';
|
import { layout, flash } from './layout.js';
|
||||||
|
import { escapeHtml } from './escape.js';
|
||||||
|
|
||||||
export function renderProductList(products, categories, subcategories) {
|
export function renderProductList(products, categories, subcategories) {
|
||||||
const catOptions = categories.map(c =>
|
const catOptions = categories.map(c =>
|
||||||
@@ -104,7 +105,3 @@ export function renderProductEdit(product, categories, subcategories) {
|
|||||||
</script>`;
|
</script>`;
|
||||||
return layout('Edit Product', content, 'products');
|
return layout('Edit Product', content, 'products');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,45 +1,95 @@
|
|||||||
import { layout } from './layout.js';
|
import { layout, flash } from './layout.js';
|
||||||
|
import { escapeHtml } from './escape.js';
|
||||||
|
|
||||||
|
const PLACEHOLDER = '••••••••';
|
||||||
|
const SECRET_KEYS = new Set(['ENCRYPTION_KEY', 'ADMIN_SECRET', 'GITEA_TOKEN']);
|
||||||
|
|
||||||
|
function maskSecret(val) {
|
||||||
|
return val ? PLACEHOLDER : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function renderSettings(data) {
|
export function renderSettings(data) {
|
||||||
const maskToken = (t) => t ? t.slice(0, 10) + '***' : 'Not set';
|
|
||||||
|
|
||||||
const idList = (ids) => ids.length
|
|
||||||
? ids.map(id => `<code>${escapeHtml(id)}</code>`).join(', ')
|
|
||||||
: '<em>None</em>';
|
|
||||||
|
|
||||||
const walletRows = Object.entries(data.commissionWallets).map(([type, addr]) =>
|
|
||||||
`<tr><td><strong>${type}</strong></td><td><code>${escapeHtml(addr || 'Not set')}</code></td></tr>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const content = `
|
const content = `
|
||||||
<div class="detail-card">
|
${data.message ? flash(data.message, data.saved ? 'info' : 'error') : ''}
|
||||||
<h2>Bot Configuration</h2>
|
<form method="POST" action="/settings">
|
||||||
<p><strong>Bot Token:</strong> <code>${maskToken(data.botToken)}</code></p>
|
<div class="settings-grid">
|
||||||
<p><strong>Admin IDs:</strong> ${idList(data.adminIds)}</p>
|
<div class="detail-card">
|
||||||
<p><strong>Super Admin IDs:</strong> ${idList(data.superAdminIds)}</p>
|
<h2>Bot Configuration</h2>
|
||||||
</div>
|
<div class="form">
|
||||||
|
<label>Bot Token</label>
|
||||||
|
<input type="password" name="BOT_TOKEN" value="${escapeHtml(data.botToken)}" autocomplete="off" placeholder="Enter new token to change">
|
||||||
|
<label>Admin IDs (comma-separated)</label>
|
||||||
|
<input type="text" name="ADMIN_IDS" value="${escapeHtml(data.adminIds.join(','))}">
|
||||||
|
<label>Super Admin IDs (comma-separated)</label>
|
||||||
|
<input type="text" name="SUPER_ADMIN_IDS" value="${escapeHtml(data.superAdminIds.join(','))}">
|
||||||
|
<label>Support Link</label>
|
||||||
|
<input type="text" name="SUPPORT_LINK" value="${escapeHtml(data.supportLink)}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
<h2>Commission Settings</h2>
|
<h2>Commission Settings</h2>
|
||||||
<p><strong>Commission:</strong> <span class="badge badge-${data.commissionEnabled ? 'active' : 'banned'}">${data.commissionEnabled ? 'ON' : 'OFF'}</span></p>
|
<div class="form">
|
||||||
<p><strong>Percentage:</strong> ${data.commissionPercent}%</p>
|
<label class="checkbox-label">
|
||||||
<h3>Commission Wallets</h3>
|
<input type="checkbox" name="COMMISSION_ENABLED" value="true" ${data.commissionEnabled ? 'checked' : ''}>
|
||||||
<table>
|
Commission Enabled
|
||||||
<thead><tr><th>Type</th><th>Address</th></tr></thead>
|
</label>
|
||||||
<tbody>${walletRows}</tbody>
|
<label>Commission Percent</label>
|
||||||
</table>
|
<input type="number" name="COMMISSION_PERCENT" value="${data.commissionPercent}" min="0" max="100" step="0.1">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
<h2>WireGuard</h2>
|
<h2>Commission Wallets</h2>
|
||||||
<p><strong>Enabled:</strong> <span class="badge badge-${data.wgEnabled ? 'active' : 'banned'}">${data.wgEnabled ? 'ON' : 'OFF'}</span></p>
|
<div class="form">
|
||||||
<p><strong>Endpoint:</strong> <code>${escapeHtml(data.wgEndpoint || 'Not set')}</code></p>
|
<label>BTC</label>
|
||||||
<p><strong>Address:</strong> <code>${escapeHtml(data.wgAddress || 'Not set')}</code></p>
|
<input type="text" name="COMMISSION_WALLET_BTC" value="${escapeHtml(data.commissionWallets.BTC)}">
|
||||||
</div>
|
<label>LTC</label>
|
||||||
|
<input type="text" name="COMMISSION_WALLET_LTC" value="${escapeHtml(data.commissionWallets.LTC)}">
|
||||||
|
<label>USDT</label>
|
||||||
|
<input type="text" name="COMMISSION_WALLET_USDT" value="${escapeHtml(data.commissionWallets.USDT)}">
|
||||||
|
<label>USDC</label>
|
||||||
|
<input type="text" name="COMMISSION_WALLET_USDC" value="${escapeHtml(data.commissionWallets.USDC)}">
|
||||||
|
<label>ETH</label>
|
||||||
|
<input type="text" name="COMMISSION_WALLET_ETH" value="${escapeHtml(data.commissionWallets.ETH)}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-card">
|
||||||
|
<h2>WireGuard</h2>
|
||||||
|
<div class="form">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="WG_ENABLED" value="true" ${data.wgEnabled ? 'checked' : ''}>
|
||||||
|
WireGuard Enabled
|
||||||
|
</label>
|
||||||
|
<label>Endpoint</label>
|
||||||
|
<input type="text" name="WG_ENDPOINT" value="${escapeHtml(data.wgEndpoint)}">
|
||||||
|
<label>Address</label>
|
||||||
|
<input type="text" name="WG_ADDRESS" value="${escapeHtml(data.wgAddress)}">
|
||||||
|
<label>Private Key</label>
|
||||||
|
<input type="password" name="WG_PRIVATE_KEY" placeholder="Enter new key to change" autocomplete="off">
|
||||||
|
<label>Public Key</label>
|
||||||
|
<input type="text" name="WG_PUBLIC_KEY" value="${escapeHtml(data.wgPublicKey)}">
|
||||||
|
<label>Preshared Key</label>
|
||||||
|
<input type="password" name="WG_PRESHARED_KEY" placeholder="Enter new key to change" autocomplete="off">
|
||||||
|
<label>DNS</label>
|
||||||
|
<input type="text" name="WG_DNS" value="${escapeHtml(data.wgDns)}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="ENCRYPTION_KEY" value="${maskSecret(data.encryptionKey)}">
|
||||||
|
<input type="hidden" name="ADMIN_SECRET" value="${maskSecret(data.adminSecret)}">
|
||||||
|
<input type="hidden" name="GITEA_TOKEN" value="${maskSecret(data.giteaToken)}">
|
||||||
|
<input type="hidden" name="ADMIN_PORT" value="${escapeHtml(data.adminPort)}">
|
||||||
|
<input type="hidden" name="ADMIN_URL" value="${escapeHtml(data.adminUrl)}">
|
||||||
|
<input type="hidden" name="CATALOG_PATH" value="${escapeHtml(data.catalogPath)}">
|
||||||
|
<input type="hidden" name="GITEA_API_URL" value="${escapeHtml(data.giteaApiUrl)}">
|
||||||
|
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<button type="submit" class="btn btn-success">Save Settings & Restart</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
`;
|
`;
|
||||||
return layout('Settings', content, 'settings');
|
return layout('Settings', content, 'settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { runMigrations, cleanUpInvalidForeignKeys } from './migrations/runner.js';
|
import { runMigrations, cleanUpInvalidForeignKeys } from './migrations/runner.js';
|
||||||
import './router/routes.js';
|
import { registerRoutes } from './router/routes.js';
|
||||||
import bot from './context/bot.js';
|
import bot from './context/bot.js';
|
||||||
import ErrorHandler from './utils/errorHandler.js';
|
import ErrorHandler from './utils/errorHandler.js';
|
||||||
import logger from './utils/logger.js';
|
import logger from './utils/logger.js';
|
||||||
@@ -14,6 +14,7 @@ import { initStates } from './services/stateService.js';
|
|||||||
await runMigrations();
|
await runMigrations();
|
||||||
await cleanUpInvalidForeignKeys();
|
await cleanUpInvalidForeignKeys();
|
||||||
await initStates();
|
await initStates();
|
||||||
|
registerRoutes();
|
||||||
|
|
||||||
const logDebug = (action, functionName) => {
|
const logDebug = (action, functionName) => {
|
||||||
logger.debug({ action, functionName }, 'Button Press');
|
logger.debug({ action, functionName }, 'Button Press');
|
||||||
|
|||||||
11
wg/start.sh
11
wg/start.sh
@@ -1,5 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Load .env file into environment (overrides Docker env_file cached values)
|
||||||
|
if [ -f /app/.env ]; then
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
key=$(echo "$key" | xargs)
|
||||||
|
case "$key" in
|
||||||
|
''|'#'*) continue ;;
|
||||||
|
esac
|
||||||
|
export "$key=$value"
|
||||||
|
done < /app/.env
|
||||||
|
fi
|
||||||
|
|
||||||
# Функция для отображения разделителя
|
# Функция для отображения разделителя
|
||||||
print_separator() {
|
print_separator() {
|
||||||
echo "════════════════════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════════════════════"
|
||||||
|
|||||||
Reference in New Issue
Block a user