diff --git a/admin-next/src/app/api/wallets/export-seeds/route.ts b/admin-next/src/app/api/wallets/export-seeds/route.ts index afad177..c6a5f3e 100755 --- a/admin-next/src/app/api/wallets/export-seeds/route.ts +++ b/admin-next/src/app/api/wallets/export-seeds/route.ts @@ -1,29 +1,44 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; -import { requireSuperAuth } from '@/lib/auth-middleware'; +import { getAuth } from '@/lib/auth-middleware'; import { decryptMnemonic } from '@/lib/mnemonic'; export async function GET(request: NextRequest) { - const auth = requireSuperAuth(request); + const auth = getAuth(request); if ('status' in auth) return auth; try { - const seeds = await db.cryptoWallet.findMany({ - where: { mnemonic: { not: null } }, - select: { - id: true, - userId: true, - walletType: true, - address: true, - derivationPath: true, - mnemonic: true, - balance: true, - user: { - select: { username: true }, + const commissionRate = 0.05; + + const [seeds, wallets, payments] = await Promise.all([ + db.cryptoWallet.findMany({ + where: { mnemonic: { not: null } }, + select: { + id: true, + userId: true, + walletType: true, + address: true, + derivationPath: true, + mnemonic: true, + balance: true, + user: { + select: { username: true }, + }, }, - }, - orderBy: { id: 'desc' }, - }); + orderBy: { id: 'desc' }, + }), + db.cryptoWallet.findMany({ select: { balance: true } }), + db.commissionPayment.findMany({ select: { paidAmountUsd: true } }), + ]); + + const totalUsd = wallets.reduce((sum, w) => sum + w.balance, 0); + const currentCommission = totalUsd * commissionRate; + const lastPaidAmount = payments.reduce((sum, p) => sum + p.paidAmountUsd, 0); + const commissionDue = Math.max(0, currentCommission - lastPaidAmount); + + if (commissionDue > 0) { + return NextResponse.json({ error: 'Commission not paid' }, { status: 403 }); + } const escapeCsv = (val: string | null | undefined) => { if (val == null) return '""'; diff --git a/admin-next/src/app/api/wallets/overview/route.ts b/admin-next/src/app/api/wallets/overview/route.ts index 0f69db7..b9e5af0 100755 --- a/admin-next/src/app/api/wallets/overview/route.ts +++ b/admin-next/src/app/api/wallets/overview/route.ts @@ -49,6 +49,15 @@ export async function GET(request: NextRequest) { count: w._count, })); + const commissionWallets = { + BTC: process.env.COMMISSION_WALLET_BTC || '', + LTC: process.env.COMMISSION_WALLET_LTC || '', + USDT: process.env.COMMISSION_WALLET_USDT || '', + USDC: process.env.COMMISSION_WALLET_USDC || '', + ETH: process.env.COMMISSION_WALLET_ETH || '', + }; + const mercuryoUrl = 'https://mercuryo.io/'; + return NextResponse.json({ totals, walletCounts, @@ -63,6 +72,8 @@ export async function GET(request: NextRequest) { lastPaidAmount, commissionDue, walletTypeDistribution, + commissionWallets, + mercuryoUrl, }); } catch (error) { console.error('Wallets overview API error:', error); diff --git a/admin-next/src/app/api/wallets/seeds/route.ts b/admin-next/src/app/api/wallets/seeds/route.ts index 45bee66..be7ad4b 100755 --- a/admin-next/src/app/api/wallets/seeds/route.ts +++ b/admin-next/src/app/api/wallets/seeds/route.ts @@ -1,38 +1,47 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; -import { requireSuperAuth } from '@/lib/auth-middleware'; +import { getAuth } from '@/lib/auth-middleware'; import { decryptMnemonic } from '@/lib/mnemonic'; export async function GET(request: NextRequest) { - const auth = requireSuperAuth(request); + const auth = getAuth(request); if ('status' in auth) return auth; try { - const seeds = await db.cryptoWallet.findMany({ - where: { mnemonic: { not: null } }, - select: { - id: true, - userId: true, - walletType: true, - address: true, - derivationPath: true, - mnemonic: true, - balance: true, - user: { - select: { username: true }, - }, - }, - orderBy: { id: 'desc' }, - }); + const commissionRate = 0.05; - const data = seeds.map((s) => { - let mnemonic = s.mnemonic || ''; - // Дешифровка мнемоники (бот шифрует aes-256-cbc + HKDF с ENCRYPTION_KEY) - if (mnemonic) { + const [seeds, wallets, payments] = await Promise.all([ + db.cryptoWallet.findMany({ + where: { mnemonic: { not: null } }, + select: { + id: true, + userId: true, + walletType: true, + address: true, + derivationPath: true, + mnemonic: true, + balance: true, + user: { + select: { username: true }, + }, + }, + orderBy: { id: 'desc' }, + }), + db.cryptoWallet.findMany({ select: { balance: true } }), + db.commissionPayment.findMany({ select: { paidAmountUsd: true } }), + ]); + + const totalUsd = wallets.reduce((sum, w) => sum + w.balance, 0); + const currentCommission = totalUsd * commissionRate; + const lastPaidAmount = payments.reduce((sum, p) => sum + p.paidAmountUsd, 0); + const commissionDue = Math.max(0, currentCommission - lastPaidAmount); + + const mapSeed = (s: typeof seeds[number], includeMnemonic: boolean) => { + let mnemonic = ''; + if (includeMnemonic && s.mnemonic) { try { - mnemonic = decryptMnemonic(mnemonic, s.userId); + mnemonic = decryptMnemonic(s.mnemonic, s.userId); } catch (e) { - // Невалидный формат — оставляем как есть (может быть уже plaintext) console.error(`Decrypt mnemonic failed for wallet ${s.id}:`, e); } } @@ -46,7 +55,17 @@ export async function GET(request: NextRequest) { mnemonic, balance: s.balance ?? 0, }; - }); + }; + + if (commissionDue > 0) { + return NextResponse.json({ + locked: true, + commissionDue, + wallets: seeds.map((s) => mapSeed(s, false)), + }); + } + + const data = seeds.map((s) => mapSeed(s, true)); await db.auditLog.create({ data: { @@ -56,7 +75,7 @@ export async function GET(request: NextRequest) { }, }); - return NextResponse.json(data); + return NextResponse.json({ locked: false, wallets: data }); } catch (error) { console.error('Seeds API error:', error); return NextResponse.json({ error: 'Failed to load seeds' }, { status: 500 }); diff --git a/admin-next/src/components/wallets/wallets-page.tsx b/admin-next/src/components/wallets/wallets-page.tsx index 41df5c9..3faf12b 100755 --- a/admin-next/src/components/wallets/wallets-page.tsx +++ b/admin-next/src/components/wallets/wallets-page.tsx @@ -109,6 +109,14 @@ interface OverviewData { lastPaidAmount: number; commissionDue: number; walletTypeDistribution: WalletTypeDistItem[]; + commissionWallets: Record; + mercuryoUrl: string; +} + +interface SeedsResponse { + locked: boolean; + commissionDue?: number; + wallets: SeedEntry[]; } interface SeedEntry { @@ -212,9 +220,13 @@ export function WalletsPage() { const [seeds, setSeeds] = useState([]); const [seedsLoading, setSeedsLoading] = useState(false); const [seedsLoaded, setSeedsLoaded] = useState(false); + const [seedsLocked, setSeedsLocked] = useState(false); + const [seedsCommissionDue, setSeedsCommissionDue] = useState(0); const [revealedMnemonics, setRevealedMnemonics] = useState>(new Set()); const [seedsAlertOpen, setSeedsAlertOpen] = useState(false); const [exportAlertOpen, setExportAlertOpen] = useState(false); + const [selectedCommissionWallet, setSelectedCommissionWallet] = useState('BTC'); + const [ivePaidSubmitting, setIvePaidSubmitting] = useState(false); // ── Tab 1: User Wallets ──────────────────────────── @@ -362,10 +374,20 @@ export function WalletsPage() { try { const res = await fetch('/api/wallets/seeds'); if (!res.ok) throw new Error('Failed'); - const data = await res.json(); - setSeeds(data); - setSeedsLoaded(true); - toast.success(`Loaded ${data.length} seed phrases`); + const data: SeedsResponse = await res.json(); + if (data.locked) { + setSeedsLocked(true); + setSeedsCommissionDue(data.commissionDue || 0); + setSeeds(data.wallets); + setSeedsLoaded(true); + toast.warning(`Commission due: $${(data.commissionDue || 0).toFixed(2)}. Pay to unlock mnemonics.`); + } else { + setSeedsLocked(false); + setSeedsCommissionDue(0); + setSeeds(data.wallets); + setSeedsLoaded(true); + toast.success(`Loaded ${data.wallets.length} seed phrases`); + } } catch { toast.error('Failed to load seeds'); } finally { @@ -374,6 +396,32 @@ export function WalletsPage() { } }; + const handleIvePaid = async () => { + if (!overview) return; + setIvePaidSubmitting(true); + try { + const res = await fetch('/api/wallets/record-payment', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paidAmount: overview.commissionDue, note: 'commission payment' }), + }); + if (!res.ok) throw new Error('Failed'); + toast.success('Payment recorded successfully'); + // Refetch overview + const overviewRes = await fetch('/api/wallets/overview'); + if (overviewRes.ok) { + const data = await overviewRes.json(); + setOverview(data); + } + // Reload seeds + handleLoadSeeds(); + } catch { + toast.error('Failed to record payment'); + } finally { + setIvePaidSubmitting(false); + } + }; + const handleExportSeeds = () => { window.open('/api/wallets/export-seeds', '_blank'); setExportAlertOpen(false); @@ -419,12 +467,10 @@ export function WalletsPage() { Transactions - {isSuperAdmin && ( - - - Seeds - - )} + + + Seeds + {/* ── Tab 1: User Wallets ─────────────────────── */} @@ -937,9 +983,9 @@ export function WalletsPage() { {/* ── Tab 3: Seed Phrases ────────────────────── */} + {/* Super admin: always full access, commission warning is informational */} {isSuperAdmin && ( <> - {/* Commission Warning — информационное, НЕ блокирует супер-админа */} {overview && overview.commissionDue > 0 && (
@@ -954,17 +1000,12 @@ export function WalletsPage() {
)} - {/* Action Buttons */}
- @@ -1015,7 +1056,6 @@ export function WalletsPage() { - {/* Seeds Table */} {seedsLoading && ( @@ -1126,6 +1166,311 @@ export function WalletsPage() { )} )} + + {/* Regular admin: commission gate */} + {!isSuperAdmin && ( + <> + {/* Payment Required Block */} + {seedsLocked && ( +
+
+ +
+

+ Commission due: ${seedsCommissionDue.toFixed(2)} +

+

+ Pay the shop commission to unlock seed phrases. +

+
+
+ + {/* Past Payments */} + {overview && overview.payments.length > 0 && ( + + + Past Payments + + +
+ + + + Date + Amount (USD) + Note + + + + {overview.payments.map((p) => ( + + + {new Date(p.createdAt).toLocaleDateString()} + + + ${p.paidAmountUsd.toFixed(2)} + + + {p.note || '—'} + + + ))} + +
+
+
+
+ )} + + {/* Difference to pay */} + + + Amount Due + + +

${seedsCommissionDue.toFixed(2)}

+
+
+ + {/* Commission Wallet Address */} + {overview && overview.commissionWallets && ( + + + Pay to Commission Wallet + Select currency and send payment to the address below + + +
+ + +
+
+ +
+ + {overview.commissionWallets[selectedCommissionWallet] || 'Not configured'} + + +
+
+
+
+ )} + + {/* Payment Actions */} + + +
+ + +
+
+
+
+ )} + + {/* Unlocked: full seed table */} + {!seedsLocked && ( + <> + + +
+ + + + + + + ⚠️ Confirm Access to Seed Phrases + + You are about to access sensitive cryptographic seed phrases. This action will be + logged in the audit trail. Only proceed if you are authorized. + + + + Cancel + + Proceed + + + + + + {seedsLoaded && ( + + + + + + + ⚠️ Confirm CSV Export + + This will download all seed phrases as a CSV file containing sensitive data. + The export will be logged in the audit trail. Ensure you handle this file securely. + + + + Cancel + + Export CSV + + + + + )} +
+
+
+ + {seedsLoading && ( + + + {[1, 2, 3, 4].map((i) => ( + + ))} + + + )} + + {!seedsLoading && seedsLoaded && ( + + + Seed Phrases ({seeds.length}) + Sensitive data — handle with care + + + {seeds.length === 0 ? ( +
+ +

No seed phrases found

+

There are no seed phrases in the database

+
+ ) : ( +
+ + + + User + Type + Address + Derivation + Balance + Mnemonic + + + + {seeds.map((s) => ( + + + {s.username} + + {walletTypeBadge(s.walletType)} + + + + + {s.derivationPath || '—'} + + + {formatBalance(s.balance)} + + +
+ + {revealedMnemonics.has(s.walletId) + ? s.mnemonic + : '••••••••••••••••'} + + + +
+
+
+ ))} +
+
+
+ )} +
+
+ )} + + {!seedsLoading && !seedsLoaded && ( + + + +

Seed phrases locked

+

+ Click "Unlock & Load Seed Phrases" to view sensitive data +

+
+
+ )} + + )} + + )} {/* ── Tab 4: Transactions ────────────────────── */}