feat: Интеграция DePay для криптовалютных платежей (USDT/Polygon)
- Добавлены endpoints для DePay: callback, success, rate, history - Заменена система проверки чеков на CryptoPayment - Переименована модель Check в CryptoPayment в Prisma схеме - Обновлен billing.tsx для работы с DePay виджетом - Все секреты вынесены в .env файлы - Интеграция с CoinGecko API для курса USDT/RUB - Добавлена RSA верификация webhook от DePay
This commit is contained in:
@@ -70,3 +70,15 @@ MINIO_BUCKET_PREFIX=ospab
|
||||
MINIO_REGION_DEFAULT=ru-central-1
|
||||
|
||||
MINIO_MC_ALIAS=minio
|
||||
|
||||
# === DePay Crypto Payment Configuration ===
|
||||
DEPAY_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA33QBvp1NDq3vZn8k4I+z
|
||||
e0U90iklattb4C2EHFDXs8Vmssimt63I55KofEV2/e7cJKQVHTrg1OpHFgivTXf8
|
||||
GeFd5Bxx6W+vGHed3YZnVYHj0hP0rqUbweZyvD58EOkmYQ55d2zf03NTf1LmI1K4
|
||||
MrBn+icWm500n4eWNtFta2l5g+/gDLRByLiIn4qobyHIsLr2FVqZiUYcMkx0BepZ
|
||||
nNrI+VGuEyb/i+Eqi58j4x/Y7uoK3NV9lF/DWp95dPU9uCO1sW7Y6NNzKFrN4OOT
|
||||
hURT672kfH2iFkFW2cP7WsRxq1ZU/gW33Wed5kqTEhpOQjSQi83s0heYSAT5gkrY
|
||||
rwIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
DEPAY_FALLBACK_RATE=95
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Rename Check table to CryptoPayment
|
||||
RENAME TABLE `check` TO `crypto_payment`;
|
||||
|
||||
-- Add new columns for crypto payments
|
||||
ALTER TABLE `crypto_payment`
|
||||
ADD COLUMN `transactionHash` VARCHAR(191) NULL,
|
||||
ADD COLUMN `blockchain` VARCHAR(191) DEFAULT 'polygon',
|
||||
ADD COLUMN `token` VARCHAR(191) DEFAULT 'USDT',
|
||||
ADD COLUMN `cryptoAmount` DECIMAL(20, 8) NULL,
|
||||
ADD COLUMN `exchangeRate` DECIMAL(10, 2) NULL COMMENT 'USDT to RUB rate',
|
||||
ADD COLUMN `paymentProvider` VARCHAR(191) DEFAULT 'depay',
|
||||
MODIFY COLUMN `status` VARCHAR(191) DEFAULT 'pending' COMMENT 'pending, completed, failed',
|
||||
MODIFY COLUMN `fileUrl` VARCHAR(191) NULL COMMENT 'Not used for crypto payments';
|
||||
|
||||
-- Update foreign key name if needed
|
||||
ALTER TABLE `crypto_payment`
|
||||
DROP FOREIGN KEY `check_userId_fkey`,
|
||||
ADD CONSTRAINT `crypto_payment_userId_fkey`
|
||||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -23,7 +23,7 @@ model User {
|
||||
isAdmin Boolean @default(false)
|
||||
tickets Ticket[] @relation("UserTickets")
|
||||
responses Response[] @relation("OperatorResponses")
|
||||
checks Check[] @relation("UserChecks")
|
||||
cryptoPayments CryptoPayment[] @relation("UserCryptoPayments")
|
||||
balance Float @default(0)
|
||||
notifications Notification[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
@@ -46,16 +46,22 @@ model User {
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Check {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
amount Float
|
||||
status String @default("pending") // pending, approved, rejected
|
||||
fileUrl String
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation("UserChecks", fields: [userId], references: [id])
|
||||
model CryptoPayment {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
amount Float // Amount in RUB credited to balance
|
||||
cryptoAmount Float? // Amount in USDT paid
|
||||
exchangeRate Float? // USDT to RUB exchange rate at payment time
|
||||
status String @default("pending") // pending, completed, failed
|
||||
transactionHash String? // Blockchain transaction hash
|
||||
blockchain String @default("polygon") // polygon, ethereum, bsc, etc.
|
||||
token String @default("USDT") // USDT, USDC, ETH, etc.
|
||||
paymentProvider String @default("depay") // depay
|
||||
fileUrl String? // Not used for crypto payments (legacy from Check)
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation("UserCryptoPayments", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("check")
|
||||
@@map("crypto_payment")
|
||||
}
|
||||
|
||||
model Service {
|
||||
|
||||
@@ -10,7 +10,7 @@ import authRoutes from './modules/auth/auth.routes';
|
||||
import oauthRoutes from './modules/auth/oauth.routes';
|
||||
import adminRoutes from './modules/admin/admin.routes';
|
||||
import ticketRoutes from './modules/ticket/ticket.routes';
|
||||
import checkRoutes from './modules/check/check.routes';
|
||||
import depayRoutes from './modules/payment/depay.routes';
|
||||
import blogRoutes from './modules/blog/blog.routes';
|
||||
import notificationRoutes from './modules/notification/notification.routes';
|
||||
import userRoutes from './modules/user/user.routes';
|
||||
@@ -329,7 +329,7 @@ app.use('/api/auth', authLimiter, authRoutes);
|
||||
app.use('/api/auth', authLimiter, oauthRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/ticket', ticketRoutes);
|
||||
app.use('/api/check', checkRoutes);
|
||||
app.use('/payment/depay', depayRoutes);
|
||||
app.use('/api/blog', blogRoutes);
|
||||
app.use('/api/notifications', notificationRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function checkFileAccessMiddleware(req: Request, res: Response, nex
|
||||
}
|
||||
|
||||
// Для обычных пользователей - проверяем владение чеком
|
||||
const check = await prisma.check.findFirst({
|
||||
const check = await prisma.cryptoPayment.findFirst({
|
||||
where: {
|
||||
fileUrl: {
|
||||
contains: filename
|
||||
|
||||
@@ -290,7 +290,7 @@ export async function confirmAccountDeletion(userId: number, code: string): Prom
|
||||
logger.log(`Удалено тикетов: ${tickets.count}`);
|
||||
|
||||
// 3. Удаляем чеки
|
||||
const checks = await tx.check.deleteMany({
|
||||
const checks = await tx.cryptoPayment.deleteMany({
|
||||
where: { userId }
|
||||
});
|
||||
logger.log(`Удалено чеков: ${checks.count}`);
|
||||
|
||||
@@ -89,7 +89,7 @@ export class AdminController {
|
||||
buckets: {
|
||||
orderBy: { createdAt: 'desc' }
|
||||
},
|
||||
checks: {
|
||||
cryptoPayments: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10
|
||||
},
|
||||
@@ -111,7 +111,7 @@ export class AdminController {
|
||||
const safeUser = {
|
||||
...user,
|
||||
balance: typeof user.balance === 'number' ? user.balance : Number(user.balance ?? 0),
|
||||
buckets: user.buckets?.map((bucket: any) => ({
|
||||
buckets: (user as any).buckets?.map((bucket: any) => ({
|
||||
...bucket,
|
||||
usedBytes: typeof bucket.usedBytes === 'bigint' ? Number(bucket.usedBytes) : Number(bucket.usedBytes ?? 0),
|
||||
objectCount: typeof bucket.objectCount === 'number' ? bucket.objectCount : Number(bucket.objectCount ?? 0),
|
||||
@@ -305,7 +305,7 @@ export class AdminController {
|
||||
prisma.storageBucket.count(),
|
||||
prisma.storageBucket.count({ where: { public: true } }),
|
||||
prisma.user.aggregate({ _sum: { balance: true } }),
|
||||
prisma.check.count({ where: { status: 'pending' } }),
|
||||
prisma.cryptoPayment.count({ where: { status: 'pending' } }),
|
||||
prisma.ticket.count({ where: { status: 'open' } }),
|
||||
prisma.storageBucket.aggregate({
|
||||
_sum: {
|
||||
@@ -441,7 +441,7 @@ export class AdminController {
|
||||
await tx.storageBucket.deleteMany({ where: { userId } });
|
||||
|
||||
await tx.ticket.deleteMany({ where: { userId } });
|
||||
await tx.check.deleteMany({ where: { userId } });
|
||||
await tx.cryptoPayment.deleteMany({ where: { userId } });
|
||||
await tx.transaction.deleteMany({ where: { userId } });
|
||||
await tx.post.deleteMany({ where: { authorId: userId } });
|
||||
await tx.comment.deleteMany({ where: { userId } });
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function uploadCheck(req: MulterRequest, res: Response) {
|
||||
// Сохраняем путь к файлу
|
||||
const fileUrl = `/uploads/checks/${file.filename}`;
|
||||
|
||||
const check = await prisma.check.create({
|
||||
const check = await prisma.cryptoPayment.create({
|
||||
data: { userId, amount: Number(amount), fileUrl }
|
||||
});
|
||||
res.json(check);
|
||||
@@ -30,7 +30,7 @@ export async function uploadCheck(req: MulterRequest, res: Response) {
|
||||
export async function getChecks(req: Request, res: Response) {
|
||||
const isOperator = Number(req.user?.operator) === 1;
|
||||
if (!isOperator) return res.status(403).json({ error: 'Нет прав' });
|
||||
const checks = await prisma.check.findMany({
|
||||
const checks = await prisma.cryptoPayment.findMany({
|
||||
include: { user: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
@@ -50,7 +50,7 @@ export async function approveCheck(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
// Найти чек
|
||||
const check = await prisma.check.findUnique({
|
||||
const check = await prisma.cryptoPayment.findUnique({
|
||||
where: { id: checkId },
|
||||
include: { user: true }
|
||||
});
|
||||
@@ -67,7 +67,7 @@ export async function approveCheck(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
// Обновить статус чека
|
||||
await prisma.check.update({
|
||||
await prisma.cryptoPayment.update({
|
||||
where: { id: checkId },
|
||||
data: { status: 'approved' }
|
||||
});
|
||||
@@ -103,7 +103,7 @@ export async function rejectCheck(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
// Найти чек
|
||||
const check = await prisma.check.findUnique({
|
||||
const check = await prisma.cryptoPayment.findUnique({
|
||||
where: { id: checkId },
|
||||
include: { user: true }
|
||||
});
|
||||
@@ -120,7 +120,7 @@ export async function rejectCheck(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
// Обновить статус чека
|
||||
await prisma.check.update({
|
||||
await prisma.cryptoPayment.update({
|
||||
where: { id: checkId },
|
||||
data: {
|
||||
status: 'rejected',
|
||||
@@ -142,7 +142,7 @@ export async function getUserChecks(req: Request, res: Response) {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Не авторизован' });
|
||||
|
||||
const checks = await prisma.check.findMany({
|
||||
const checks = await prisma.cryptoPayment.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50 // Последние 50 чеков
|
||||
@@ -161,7 +161,7 @@ export async function viewCheck(req: Request, res: Response) {
|
||||
const userId = req.user?.id;
|
||||
const isOperator = Number(req.user?.operator) === 1;
|
||||
|
||||
const check = await prisma.check.findUnique({ where: { id: checkId } });
|
||||
const check = await prisma.cryptoPayment.findUnique({ where: { id: checkId } });
|
||||
|
||||
if (!check) {
|
||||
return res.status(404).json({ error: 'Чек не найден' });
|
||||
@@ -190,7 +190,7 @@ export async function getCheckFile(req: Request, res: Response) {
|
||||
// Операторы имеют доступ ко всем файлам
|
||||
if (!isOperator) {
|
||||
// Для обычных пользователей проверяем владение
|
||||
const check = await prisma.check.findFirst({
|
||||
const check = await prisma.cryptoPayment.findFirst({
|
||||
where: {
|
||||
fileUrl: {
|
||||
contains: filename
|
||||
|
||||
65
ospabhost/backend/src/modules/payment/crypto.routes.ts
Normal file
65
ospabhost/backend/src/modules/payment/crypto.routes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { prisma } from '../../prisma/client';
|
||||
import { authMiddleware } from '../auth/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/crypto/payments
|
||||
* Get user's crypto payment history
|
||||
*/
|
||||
router.get('/payments', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = (req as any).user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const payments = await prisma.cryptoPayment.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50, // Last 50 payments
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: payments });
|
||||
} catch (error) {
|
||||
console.error('[Crypto] Error fetching payments:', error);
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch payments';
|
||||
return res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/crypto/payments/:id
|
||||
* Get specific payment details
|
||||
*/
|
||||
router.get('/payments/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = (req as any).user?.id;
|
||||
const paymentId = parseInt(req.params.id);
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const payment = await prisma.cryptoPayment.findFirst({
|
||||
where: {
|
||||
id: paymentId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!payment) {
|
||||
return res.status(404).json({ error: 'Payment not found' });
|
||||
}
|
||||
|
||||
return res.json({ success: true, data: payment });
|
||||
} catch (error) {
|
||||
console.error('[Crypto] Error fetching payment:', error);
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch payment';
|
||||
return res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
231
ospabhost/backend/src/modules/payment/depay.routes.ts
Normal file
231
ospabhost/backend/src/modules/payment/depay.routes.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { prisma } from '../../prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import { authMiddleware } from '../auth/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// DePay configuration from environment variables
|
||||
const DEPAY_PUBLIC_KEY = process.env.DEPAY_PUBLIC_KEY || '';
|
||||
const DEPAY_FALLBACK_RATE = parseFloat(process.env.DEPAY_FALLBACK_RATE || '95');
|
||||
|
||||
// Function to get USDT to RUB exchange rate
|
||||
async function getUsdtToRubRate(): Promise<number> {
|
||||
try {
|
||||
// Use CoinGecko API to get USDT price in RUB
|
||||
const response = await fetch(
|
||||
'https://api.coingecko.com/api/v3/simple/price?ids=tether&vs_currencies=rub'
|
||||
);
|
||||
const data = await response.json() as { tether?: { rub?: number } };
|
||||
const rate = data.tether?.rub || DEPAY_FALLBACK_RATE;
|
||||
console.log('[DePay] Current USDT/RUB rate:', rate);
|
||||
return rate;
|
||||
} catch (error) {
|
||||
console.error('[DePay] Error fetching exchange rate:', error);
|
||||
return DEPAY_FALLBACK_RATE;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify webhook signature from DePay
|
||||
function verifyWebhookSignature(
|
||||
payload: string,
|
||||
signature: string
|
||||
): boolean {
|
||||
try {
|
||||
const verifier = crypto.createVerify('SHA256');
|
||||
verifier.update(payload);
|
||||
const isValid = verifier.verify(DEPAY_PUBLIC_KEY, signature, 'base64');
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
console.error('[DePay] Signature verification error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/depay/callback
|
||||
* Webhook endpoint called by DePay after successful payment
|
||||
*/
|
||||
router.post('/callback', async (req: Request, res: Response) => {
|
||||
try {
|
||||
console.log('[DePay] Callback received:', JSON.stringify(req.body, null, 2));
|
||||
|
||||
// Verify webhook signature
|
||||
const signature = req.headers['x-signature'] as string;
|
||||
const rawBody = JSON.stringify(req.body);
|
||||
|
||||
if (signature && !verifyWebhookSignature(rawBody, signature)) {
|
||||
console.error('[DePay] Invalid webhook signature');
|
||||
return res.status(401).json({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
const {
|
||||
transaction,
|
||||
payment,
|
||||
user: paymentUser,
|
||||
} = req.body;
|
||||
|
||||
// Extract payment data
|
||||
const transactionHash = transaction?.id || transaction?.hash;
|
||||
const blockchain = transaction?.blockchain || 'polygon';
|
||||
const tokenAddress = payment?.token;
|
||||
const cryptoAmount = parseFloat(payment?.amount) || 0;
|
||||
const userId = parseInt(paymentUser?.id) || null;
|
||||
|
||||
if (!userId) {
|
||||
console.error('[DePay] No user ID in callback');
|
||||
return res.status(400).json({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
if (!cryptoAmount || cryptoAmount <= 0) {
|
||||
console.error('[DePay] Invalid crypto amount');
|
||||
return res.status(400).json({ error: 'Invalid amount' });
|
||||
}
|
||||
|
||||
// Get current exchange rate
|
||||
const exchangeRate = await getUsdtToRubRate();
|
||||
const amountInRub = cryptoAmount * exchangeRate;
|
||||
|
||||
console.log('[DePay] Payment details:', {
|
||||
userId,
|
||||
cryptoAmount,
|
||||
exchangeRate,
|
||||
amountInRub,
|
||||
transactionHash,
|
||||
});
|
||||
|
||||
// Create crypto payment record
|
||||
const cryptoPayment = await prisma.cryptoPayment.create({
|
||||
data: {
|
||||
userId,
|
||||
amount: amountInRub,
|
||||
cryptoAmount,
|
||||
exchangeRate,
|
||||
status: 'completed',
|
||||
transactionHash,
|
||||
blockchain,
|
||||
token: 'USDT',
|
||||
paymentProvider: 'depay',
|
||||
},
|
||||
});
|
||||
|
||||
// Get user balance before update
|
||||
const userBefore = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { balance: true },
|
||||
});
|
||||
const balanceBefore = userBefore?.balance || 0;
|
||||
const balanceAfter = balanceBefore + amountInRub;
|
||||
|
||||
// Update user balance
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
balance: {
|
||||
increment: amountInRub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create transaction record
|
||||
await prisma.transaction.create({
|
||||
data: {
|
||||
userId,
|
||||
amount: amountInRub,
|
||||
type: 'deposit',
|
||||
description: `Crypto payment via DePay: ${cryptoAmount} USDT`,
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[DePay] ✅ Payment processed successfully:', {
|
||||
paymentId: cryptoPayment.id,
|
||||
amountInRub,
|
||||
});
|
||||
|
||||
// Send success notification
|
||||
try {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId,
|
||||
type: 'balance',
|
||||
title: 'Balance Topped Up',
|
||||
message: `Your balance has been topped up by ${amountInRub.toFixed(2)} ₽ (${cryptoAmount} USDT)`,
|
||||
isRead: false,
|
||||
},
|
||||
});
|
||||
} catch (notifError) {
|
||||
console.error('[DePay] Error creating notification:', notifError);
|
||||
}
|
||||
|
||||
return res.json({ success: true, paymentId: cryptoPayment.id });
|
||||
} catch (error) {
|
||||
console.error('[DePay] Callback error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Internal server error';
|
||||
return res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /payment/depay/success
|
||||
* Redirect endpoint after successful payment (shown to user)
|
||||
*/
|
||||
router.get('/success', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { transaction, user: userId } = req.query;
|
||||
|
||||
console.log('[DePay] Success redirect:', { transaction, userId });
|
||||
|
||||
// Redirect to frontend with success message
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://ospab.host';
|
||||
return res.redirect(
|
||||
`${frontendUrl}/dashboard/billing?payment=success&tx=${transaction}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[DePay] Success redirect error:', error);
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://ospab.host';
|
||||
return res.redirect(`${frontendUrl}/dashboard/billing?payment=error`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /payment/depay/rate
|
||||
* Get current USDT to RUB exchange rate
|
||||
*/
|
||||
router.get('/rate', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const rate = await getUsdtToRubRate();
|
||||
return res.json({ rate, currency: 'RUB', crypto: 'USDT' });
|
||||
} catch (error) {
|
||||
console.error('[DePay] Rate fetch error:', error);
|
||||
return res.status(500).json({ error: 'Failed to fetch exchange rate' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /payment/depay/history
|
||||
* Get user's crypto payment history (requires authentication)
|
||||
*/
|
||||
router.get('/history', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = (req as any).user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const payments = await prisma.cryptoPayment.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
return res.json({ payments });
|
||||
} catch (error) {
|
||||
console.error('[DePay] History fetch error:', error);
|
||||
return res.status(500).json({ error: 'Failed to fetch payment history' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -408,7 +408,7 @@ export const exportUserData = async (req: Request, res: Response) => {
|
||||
buckets: true,
|
||||
|
||||
tickets: true,
|
||||
checks: true,
|
||||
cryptoPayments: true,
|
||||
transactions: true,
|
||||
notifications: true,
|
||||
apiKeys: {
|
||||
|
||||
Reference in New Issue
Block a user