fix: seed phrase modal, USDT address bug, CSV balance column, remove CSRF
- Fix seed phrase reveal modal: use classList instead of style.display
to properly toggle d-none/d-flex on Bootstrap elements
- Fix USDT/USDC wallet creation bug: was using ETH address (index 0)
instead of correct derivation path address (index 1/2)
- Add Balance column to CSV seed export
- Remove CSRF tokens from wallets.ejs (incompatible with Tor/onion)
- Super admin CSV export: no commission check required
- Audit logging for CSV seed exports
- Use window.addEventListener('load') for seed modal JS to ensure
Bootstrap is loaded before initializing bootstrap.Modal
This commit is contained in:
@@ -139,22 +139,8 @@ router.post('/record-payment', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/export-seeds', async (req, res) => {
|
router.post('/export-seeds', requireSuperAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const walletStats = await getWalletStats();
|
|
||||||
const commissionRate = config.COMMISSION_PERCENT / 100;
|
|
||||||
const currentCommission = walletStats.totalUsd * commissionRate;
|
|
||||||
const lastPayment = await db.getAsync(
|
|
||||||
`SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 1`
|
|
||||||
);
|
|
||||||
const lastPaidAmount = lastPayment ? lastPayment.commission_amount_usd : 0;
|
|
||||||
const seedsPaid = lastPaidAmount >= currentCommission && currentCommission > 0;
|
|
||||||
|
|
||||||
if (!seedsPaid) {
|
|
||||||
logger.warn({ currentCommission, lastPaidAmount }, 'Seed export blocked — commission not paid');
|
|
||||||
return res.status(403).send('Seed export is locked until commission is paid. Due: $' + Math.max(0, currentCommission - lastPaidAmount).toFixed(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
const walletsWithSeeds = await db.allAsync(
|
const walletsWithSeeds = await db.allAsync(
|
||||||
`SELECT w.*, u.telegram_id, u.username
|
`SELECT w.*, u.telegram_id, u.username
|
||||||
FROM crypto_wallets w
|
FROM crypto_wallets w
|
||||||
@@ -163,11 +149,11 @@ router.post('/export-seeds', async (req, res) => {
|
|||||||
ORDER BY u.id, w.wallet_type`
|
ORDER BY u.id, w.wallet_type`
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows = [['User', 'Telegram ID', 'Type', 'Address', 'Derivation', 'Mnemonic']];
|
const rows = [['User', 'Telegram ID', 'Type', 'Address', 'Balance', 'Derivation', 'Mnemonic']];
|
||||||
for (const w of walletsWithSeeds) {
|
for (const w of walletsWithSeeds) {
|
||||||
let mnemonic = '';
|
let mnemonic = '';
|
||||||
try { mnemonic = decrypt(w.mnemonic, w.user_id); } catch { mnemonic = '[decrypt error]'; }
|
try { mnemonic = decrypt(w.mnemonic, w.user_id); } catch { mnemonic = '[decrypt error]'; }
|
||||||
rows.push([w.username || w.telegram_id, w.telegram_id, w.wallet_type, w.address, w.derivation_path, mnemonic]);
|
rows.push([w.username || w.telegram_id, w.telegram_id, w.wallet_type, w.address, w.balance || 0, w.derivation_path, mnemonic]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const csv = rows.map(r => r.map(c => {
|
const csv = rows.map(r => r.map(c => {
|
||||||
@@ -175,6 +161,10 @@ router.post('/export-seeds', async (req, res) => {
|
|||||||
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
|
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
|
||||||
}).join(',')).join('\n');
|
}).join(',')).join('\n');
|
||||||
|
|
||||||
|
await logAudit('csv_seed_export', req.admin?.role || 'unknown', {
|
||||||
|
walletCount: walletsWithSeeds.length,
|
||||||
|
});
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/csv');
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename=wallets_seeds.csv');
|
res.setHeader('Content-Disposition', 'attachment; filename=wallets_seeds.csv');
|
||||||
res.send(csv);
|
res.send(csv);
|
||||||
|
|||||||
@@ -265,24 +265,9 @@
|
|||||||
<div class="alert alert-success mb-2">
|
<div class="alert alert-success mb-2">
|
||||||
<strong>🔑 Super Admin Access:</strong> Click the <strong>Seed</strong> button next to any wallet above to view the seed phrase as text and QR code.
|
<strong>🔑 Super Admin Access:</strong> Click the <strong>Seed</strong> button next to any wallet above to view the seed phrase as text and QR code.
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted mb-2">You can also export all seed phrases as CSV.</p>
|
|
||||||
<% if (seedsUnlocked) { %>
|
|
||||||
<form method="POST" action="/wallets/export-seeds" class="mb-3">
|
<form method="POST" action="/wallets/export-seeds" class="mb-3">
|
||||||
<button type="submit" class="btn btn-danger">📥 Export All Seeds as CSV</button>
|
<button type="submit" class="btn btn-danger">📥 Export All Seeds as CSV</button>
|
||||||
</form>
|
</form>
|
||||||
<% } else { %>
|
|
||||||
<div class="alert alert-warning mb-2">
|
|
||||||
<strong>CSV Export Locked:</strong> Commission must be paid to unlock bulk CSV export.
|
|
||||||
<% if (stats.commissionDue > 0) { %>
|
|
||||||
<p class="mb-0 mt-1">Commission owed: <strong>$<%= Number(stats.commissionDue).toFixed(2) %></strong></p>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
<% if (stats.seedsPaid) { %>
|
|
||||||
<a href="/wallets?seeds=1&user=<%= selectedUser ? selectedUser.id : '' %>" class="btn btn-warning">🔓 Unlock CSV Export</a>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="btn btn-secondary disabled">🔒 CSV export requires commission payment</span>
|
|
||||||
<% } %>
|
|
||||||
<% } %>
|
|
||||||
<% } else if (seedsUnlocked) { %>
|
<% } else if (seedsUnlocked) { %>
|
||||||
<p class="text-muted mb-2">Seed phrases are unlocked. Download via CSV export.</p>
|
<p class="text-muted mb-2">Seed phrases are unlocked. Download via CSV export.</p>
|
||||||
<form method="POST" action="/wallets/export-seeds" class="mb-3">
|
<form method="POST" action="/wallets/export-seeds" class="mb-3">
|
||||||
@@ -379,12 +364,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-danger" onclick="hideSeedPhrase(); bootstrap.Modal.getInstance(document.getElementById('seedModal')).hide();">🔒 Close & Hide Seed</button>
|
<button type="button" class="btn btn-danger" onclick="hideSeedPhrase(); var m=bootstrap.Modal.getInstance(document.getElementById('seedModal')); if(m) m.hide();">🔒 Close & Hide Seed</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var search = document.getElementById('user-search');
|
var search = document.getElementById('user-search');
|
||||||
var items = document.querySelectorAll('#user-list .list-group-item');
|
var items = document.querySelectorAll('#user-list .list-group-item');
|
||||||
@@ -442,7 +429,6 @@
|
|||||||
// --- Auto-refresh wallet balances ---
|
// --- Auto-refresh wallet balances ---
|
||||||
var refreshInterval = null;
|
var refreshInterval = null;
|
||||||
var currentUserId = <%= selectedUser ? selectedUser.id : 'null' %>;
|
var currentUserId = <%= selectedUser ? selectedUser.id : 'null' %>;
|
||||||
var csrfToken = <%- JSON.stringify(csrfToken) %>;
|
|
||||||
var REFRESH_INTERVAL_MS = 30000;
|
var REFRESH_INTERVAL_MS = 30000;
|
||||||
|
|
||||||
function formatBalance(val) {
|
function formatBalance(val) {
|
||||||
@@ -520,9 +506,7 @@
|
|||||||
function fetchBalances() {
|
function fetchBalances() {
|
||||||
if (!currentUserId) return;
|
if (!currentUserId) return;
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
fetch('/wallets/refresh-balances/' + currentUserId, {
|
fetch('/wallets/refresh-balances/' + currentUserId)
|
||||||
headers: { 'x-csrf-token': csrfToken }
|
|
||||||
})
|
|
||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
@@ -566,15 +550,18 @@
|
|||||||
if (currentUserId) {
|
if (currentUserId) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// --- Seed Phrase Reveal (Super Admin Only) ---
|
// --- Seed Phrase Reveal (Super Admin Only) ---
|
||||||
|
// Must wait for full page load (including Bootstrap) before using bootstrap.Modal
|
||||||
|
window.addEventListener('load', function() {
|
||||||
var isSuperAdmin = <%= isSuperAdmin ? 'true' : 'false' %>;
|
var isSuperAdmin = <%= isSuperAdmin ? 'true' : 'false' %>;
|
||||||
var seedRevealTimer = null;
|
if (!isSuperAdmin) return;
|
||||||
|
|
||||||
var seedAutoHideInterval = null;
|
var seedAutoHideInterval = null;
|
||||||
var currentSeedWalletId = null;
|
var currentSeedWalletId = null;
|
||||||
|
|
||||||
function openSeedModal(walletId, walletType, userId) {
|
function openSeedModal(walletId, walletType, userId) {
|
||||||
if (!isSuperAdmin) return;
|
|
||||||
|
|
||||||
currentSeedWalletId = walletId;
|
currentSeedWalletId = walletId;
|
||||||
document.getElementById('seedModalWalletType').textContent = walletType;
|
document.getElementById('seedModalWalletType').textContent = walletType;
|
||||||
document.getElementById('seedModalWalletId').textContent = walletId;
|
document.getElementById('seedModalWalletId').textContent = walletId;
|
||||||
@@ -583,7 +570,7 @@
|
|||||||
// Reset state
|
// Reset state
|
||||||
document.getElementById('seedPhraseText').textContent = '';
|
document.getElementById('seedPhraseText').textContent = '';
|
||||||
document.getElementById('seedPhraseMask').classList.remove('d-none');
|
document.getElementById('seedPhraseMask').classList.remove('d-none');
|
||||||
document.getElementById('seedPhraseMask').style.display = '';
|
document.getElementById('seedPhraseMask').classList.remove('d-none');
|
||||||
document.getElementById('seedModalAddress').textContent = '';
|
document.getElementById('seedModalAddress').textContent = '';
|
||||||
document.getElementById('seedModalDerivation').textContent = '';
|
document.getElementById('seedModalDerivation').textContent = '';
|
||||||
document.getElementById('seedQrImage').classList.add('d-none');
|
document.getElementById('seedQrImage').classList.add('d-none');
|
||||||
@@ -591,13 +578,13 @@
|
|||||||
document.getElementById('seedQrError').classList.add('d-none');
|
document.getElementById('seedQrError').classList.add('d-none');
|
||||||
document.getElementById('seedAutoHideNotice').classList.add('d-none');
|
document.getElementById('seedAutoHideNotice').classList.add('d-none');
|
||||||
|
|
||||||
var modal = new bootstrap.Modal(document.getElementById('seedModal'));
|
var modalEl = document.getElementById('seedModal');
|
||||||
|
if (!modalEl) return;
|
||||||
|
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
// Fetch seed phrase
|
// Fetch seed phrase
|
||||||
fetch('/wallets/seed/' + walletId, {
|
fetch('/wallets/seed/' + walletId)
|
||||||
headers: { 'x-csrf-token': csrfToken }
|
|
||||||
})
|
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 403) throw new Error('Super admin access required');
|
if (res.status === 403) throw new Error('Super admin access required');
|
||||||
@@ -609,9 +596,7 @@
|
|||||||
document.getElementById('seedModalAddress').textContent = data.address || '';
|
document.getElementById('seedModalAddress').textContent = data.address || '';
|
||||||
document.getElementById('seedModalDerivation').textContent = data.derivationPath || '';
|
document.getElementById('seedModalDerivation').textContent = data.derivationPath || '';
|
||||||
document.getElementById('seedModalUsername').textContent = data.username || 'User #' + data.userId;
|
document.getElementById('seedModalUsername').textContent = data.username || 'User #' + data.userId;
|
||||||
// Store seed phrase in memory but don't display yet
|
|
||||||
window._currentSeedPhrase = data.mnemonic || '';
|
window._currentSeedPhrase = data.mnemonic || '';
|
||||||
// Load QR code
|
|
||||||
var qrImg = document.getElementById('seedQrImage');
|
var qrImg = document.getElementById('seedQrImage');
|
||||||
qrImg.src = '/wallets/seed-qr/' + walletId + '?_t=' + Date.now();
|
qrImg.src = '/wallets/seed-qr/' + walletId + '?_t=' + Date.now();
|
||||||
qrImg.onload = function() {
|
qrImg.onload = function() {
|
||||||
@@ -631,10 +616,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function revealSeedPhrase() {
|
window.openSeedModal = openSeedModal;
|
||||||
|
|
||||||
|
window.revealSeedPhrase = function() {
|
||||||
if (!window._currentSeedPhrase) return;
|
if (!window._currentSeedPhrase) return;
|
||||||
document.getElementById('seedPhraseText').textContent = window._currentSeedPhrase;
|
document.getElementById('seedPhraseText').textContent = window._currentSeedPhrase;
|
||||||
document.getElementById('seedPhraseMask').style.display = 'none';
|
document.getElementById('seedPhraseMask').classList.add('d-none');
|
||||||
document.getElementById('seedAutoHideNotice').classList.remove('d-none');
|
document.getElementById('seedAutoHideNotice').classList.remove('d-none');
|
||||||
|
|
||||||
var secondsLeft = 60;
|
var secondsLeft = 60;
|
||||||
@@ -646,31 +633,21 @@
|
|||||||
secondsLeft--;
|
secondsLeft--;
|
||||||
document.getElementById('seedAutoHideTimer').textContent = secondsLeft;
|
document.getElementById('seedAutoHideTimer').textContent = secondsLeft;
|
||||||
document.getElementById('seedCountdown').textContent = secondsLeft;
|
document.getElementById('seedCountdown').textContent = secondsLeft;
|
||||||
if (secondsLeft <= 0) {
|
if (secondsLeft <= 0) { window.hideSeedPhrase(); }
|
||||||
hideSeedPhrase();
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
};
|
||||||
|
|
||||||
function hideSeedPhrase() {
|
window.hideSeedPhrase = function() {
|
||||||
document.getElementById('seedPhraseText').textContent = '••••••••••••••••••••••••••••••••••';
|
document.getElementById('seedPhraseText').textContent = '••••••••••••••••••••••••••••••••••';
|
||||||
document.getElementById('seedPhraseMask').style.display = '';
|
|
||||||
document.getElementById('seedPhraseMask').classList.remove('d-none');
|
document.getElementById('seedPhraseMask').classList.remove('d-none');
|
||||||
document.getElementById('seedAutoHideNotice').classList.add('d-none');
|
document.getElementById('seedAutoHideNotice').classList.add('d-none');
|
||||||
if (seedAutoHideInterval) {
|
if (seedAutoHideInterval) { clearInterval(seedAutoHideInterval); seedAutoHideInterval = null; }
|
||||||
clearInterval(seedAutoHideInterval);
|
|
||||||
seedAutoHideInterval = null;
|
|
||||||
}
|
|
||||||
window._currentSeedPhrase = null;
|
window._currentSeedPhrase = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
function copySeedPhrase() {
|
window.copySeedPhrase = function() {
|
||||||
var text = window._currentSeedPhrase || document.getElementById('seedPhraseText').textContent;
|
var text = window._currentSeedPhrase || document.getElementById('seedPhraseText').textContent;
|
||||||
if (!text || text.indexOf('•') !== -1) return;
|
if (!text || text.indexOf('•') !== -1) return;
|
||||||
copyToClipboard(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToClipboard(text) {
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
var btn = event.currentTarget || event.target;
|
var btn = event.currentTarget || event.target;
|
||||||
var original = btn.textContent;
|
var original = btn.textContent;
|
||||||
@@ -686,9 +663,9 @@
|
|||||||
try { document.execCommand('copy'); } catch(ex) {}
|
try { document.execCommand('copy'); } catch(ex) {}
|
||||||
document.body.removeChild(ta);
|
document.body.removeChild(ta);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function downloadQr() {
|
window.downloadQr = function() {
|
||||||
var img = document.getElementById('seedQrImage');
|
var img = document.getElementById('seedQrImage');
|
||||||
if (!img.src || img.classList.contains('d-none')) return;
|
if (!img.src || img.classList.contains('d-none')) return;
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
@@ -697,23 +674,23 @@
|
|||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Attach click handlers to seed reveal buttons
|
// Attach click handlers to seed reveal buttons
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
var btn = e.target.closest('.seed-reveal-btn');
|
var btn = e.target.closest('.seed-reveal-btn');
|
||||||
if (!btn || !isSuperAdmin) return;
|
if (!btn) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openSeedModal(btn.dataset.walletId, btn.dataset.walletType, btn.dataset.userId);
|
openSeedModal(btn.dataset.walletId, btn.dataset.walletType, btn.dataset.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up on modal close
|
// Clean up on modal close
|
||||||
document.getElementById('seedModal')?.addEventListener('hidden.bs.modal', function() {
|
var seedModalEl = document.getElementById('seedModal');
|
||||||
hideSeedPhrase();
|
if (seedModalEl) {
|
||||||
if (seedAutoHideInterval) {
|
seedModalEl.addEventListener('hidden.bs.modal', function() {
|
||||||
clearInterval(seedAutoHideInterval);
|
window.hideSeedPhrase();
|
||||||
seedAutoHideInterval = null;
|
if (seedAutoHideInterval) { clearInterval(seedAutoHideInterval); seedAutoHideInterval = null; }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ class WalletService {
|
|||||||
throw new Error('Failed to generate wallets');
|
throw new Error('Failed to generate wallets');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем наличие базового типа кошелька
|
// Проверяем наличие сгенерированного кошелька нужного типа
|
||||||
const baseType = walletType === 'USDT' || walletType === 'USDC' ? 'ETH' : walletType;
|
const walletKey = walletType.toUpperCase();
|
||||||
if (!wallets[baseType.toUpperCase()]) {
|
if (!wallets[walletKey]) {
|
||||||
throw new Error(`Unsupported wallet type: ${walletType}`);
|
throw new Error(`Unsupported wallet type: ${walletType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,24 +116,20 @@ class WalletService {
|
|||||||
|
|
||||||
const encryptedMnemonic = encrypt(mnemonic, userId);
|
const encryptedMnemonic = encrypt(mnemonic, userId);
|
||||||
|
|
||||||
// Определяем путь деривации
|
// Определяем путь деривации и адрес
|
||||||
let derivationPath;
|
let derivationPath;
|
||||||
|
let address;
|
||||||
if (walletType === 'USDT') {
|
if (walletType === 'USDT') {
|
||||||
derivationPath = "m/44'/60'/0'/0/1"; // Путь для USDT
|
derivationPath = "m/44'/60'/0'/0/1"; // Путь для USDT
|
||||||
|
address = wallets['USDT'].address;
|
||||||
} else if (walletType === 'USDC') {
|
} else if (walletType === 'USDC') {
|
||||||
derivationPath = "m/44'/60'/0'/0/2"; // Путь для USDC
|
derivationPath = "m/44'/60'/0'/0/2"; // Путь для USDC
|
||||||
|
address = wallets['USDC'].address;
|
||||||
} else {
|
} else {
|
||||||
derivationPath = wallets[walletType.toUpperCase()].path;
|
derivationPath = wallets[walletType.toUpperCase()].path;
|
||||||
|
address = wallets[walletType.toUpperCase()].address;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем адрес для базового типа
|
|
||||||
const walletData = wallets[baseType.toUpperCase()];
|
|
||||||
if (!walletData || !walletData.address) {
|
|
||||||
logger.error({ baseType, walletKeys: Object.keys(wallets) }, 'Wallet generation failed');
|
|
||||||
throw new Error('Failed to generate wallet address');
|
|
||||||
}
|
|
||||||
const address = walletData.address;
|
|
||||||
|
|
||||||
// Вставляем новый кошелек в базу данных
|
// Вставляем новый кошелек в базу данных
|
||||||
await db.runAsync(
|
await db.runAsync(
|
||||||
`INSERT INTO crypto_wallets
|
`INSERT INTO crypto_wallets
|
||||||
|
|||||||
Reference in New Issue
Block a user