825 lines
22 KiB
JavaScript
825 lines
22 KiB
JavaScript
import { authenticate, requireVerifiedEmail } from "../middleware/auth.js";
|
|
import User from "../models/User.js";
|
|
import speakeasy from "speakeasy";
|
|
import qrcode from "qrcode";
|
|
import { sendVerificationEmail, send2FASetupEmail } from "../utils/email.js";
|
|
|
|
/**
|
|
* User routes for profile and settings management
|
|
* @param {FastifyInstance} fastify - Fastify instance
|
|
*/
|
|
export default async function userRoutes(fastify, options) {
|
|
// Get user profile
|
|
fastify.get(
|
|
"/profile",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
const user = request.user.toObject();
|
|
|
|
// Remove sensitive data
|
|
delete user.twoFactor.secret;
|
|
delete user.email.emailToken;
|
|
|
|
return reply.send({
|
|
success: true,
|
|
user: user,
|
|
});
|
|
}
|
|
);
|
|
|
|
// Update trade URL (PATCH method)
|
|
fastify.patch(
|
|
"/trade-url",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["tradeUrl"],
|
|
properties: {
|
|
tradeUrl: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { tradeUrl } = request.body;
|
|
|
|
// Basic validation for Steam trade URL
|
|
const tradeUrlRegex =
|
|
/^https?:\/\/steamcommunity\.com\/tradeoffer\/new\/\?partner=\d+&token=[a-zA-Z0-9_-]+$/;
|
|
|
|
if (!tradeUrlRegex.test(tradeUrl)) {
|
|
return reply.status(400).send({
|
|
error: "ValidationError",
|
|
message: "Invalid Steam trade URL format",
|
|
});
|
|
}
|
|
|
|
request.user.tradeUrl = tradeUrl;
|
|
await request.user.save();
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Trade URL updated successfully",
|
|
tradeUrl: request.user.tradeUrl,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating trade URL:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to update trade URL",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update trade URL (PUT method) - same as PATCH for convenience
|
|
fastify.put(
|
|
"/trade-url",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["tradeUrl"],
|
|
properties: {
|
|
tradeUrl: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { tradeUrl } = request.body;
|
|
|
|
// Basic validation for Steam trade URL
|
|
const tradeUrlRegex =
|
|
/^https?:\/\/steamcommunity\.com\/tradeoffer\/new\/\?partner=\d+&token=[a-zA-Z0-9_-]+$/;
|
|
|
|
if (!tradeUrlRegex.test(tradeUrl)) {
|
|
return reply.status(400).send({
|
|
error: "ValidationError",
|
|
message: "Invalid Steam trade URL format",
|
|
});
|
|
}
|
|
|
|
request.user.tradeUrl = tradeUrl;
|
|
await request.user.save();
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Trade URL updated successfully",
|
|
tradeUrl: request.user.tradeUrl,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating trade URL:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to update trade URL",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update email
|
|
fastify.patch(
|
|
"/email",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["email"],
|
|
properties: {
|
|
email: { type: "string", format: "email" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { email } = request.body;
|
|
|
|
// Basic email validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
return reply.status(400).send({
|
|
error: "ValidationError",
|
|
message: "Invalid email format",
|
|
});
|
|
}
|
|
|
|
// Generate verification token
|
|
const emailToken =
|
|
Math.random().toString(36).substring(2, 15) +
|
|
Math.random().toString(36).substring(2, 15);
|
|
|
|
request.user.email = {
|
|
address: email,
|
|
verified: false,
|
|
emailToken: emailToken,
|
|
};
|
|
|
|
await request.user.save();
|
|
|
|
// Send verification email
|
|
try {
|
|
await sendVerificationEmail(email, request.user.username, emailToken);
|
|
console.log(`📧 Verification email sent to ${email}`);
|
|
} catch (error) {
|
|
console.error("Failed to send verification email:", error);
|
|
// Don't fail the request if email sending fails
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message:
|
|
"Email updated. Please check your inbox for verification link.",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating email:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to update email",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Verify email
|
|
fastify.get("/verify-email/:token", async (request, reply) => {
|
|
try {
|
|
const { token } = request.params;
|
|
|
|
const User = (await import("../models/User.js")).default;
|
|
const user = await User.findOne({ "email.emailToken": token });
|
|
|
|
if (!user) {
|
|
return reply.status(404).send({
|
|
error: "NotFound",
|
|
message: "Invalid verification token",
|
|
});
|
|
}
|
|
|
|
user.email.verified = true;
|
|
user.email.emailToken = null;
|
|
await user.save();
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Email verified successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error verifying email:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to verify email",
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get user balance
|
|
fastify.get(
|
|
"/balance",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
return reply.send({
|
|
success: true,
|
|
balance: request.user.balance || 0,
|
|
});
|
|
}
|
|
);
|
|
|
|
// Get user stats
|
|
fastify.get(
|
|
"/stats",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
// TODO: Implement actual stats calculations from orders/trades
|
|
const stats = {
|
|
balance: request.user.balance || 0,
|
|
totalSpent: 0,
|
|
totalEarned: 0,
|
|
totalTrades: 0,
|
|
accountAge: Date.now() - new Date(request.user.createdAt).getTime(),
|
|
verified: {
|
|
email: request.user.email?.verified || false,
|
|
twoFactor: request.user.twoFactor?.enabled || false,
|
|
},
|
|
};
|
|
|
|
return reply.send({
|
|
success: true,
|
|
stats: stats,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching user stats:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to fetch user stats",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update intercom ID
|
|
fastify.patch(
|
|
"/intercom",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["intercom"],
|
|
properties: {
|
|
intercom: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { intercom } = request.body;
|
|
|
|
request.user.intercom = intercom;
|
|
await request.user.save();
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Intercom ID updated successfully",
|
|
intercom: request.user.intercom,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating intercom:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to update intercom ID",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get public user profile (for viewing other users)
|
|
fastify.get("/:steamId", async (request, reply) => {
|
|
try {
|
|
const { steamId } = request.params;
|
|
|
|
const User = (await import("../models/User.js")).default;
|
|
const user = await User.findOne({ steamId }).select(
|
|
"username steamId avatar account_creation communityvisibilitystate staffLevel createdAt"
|
|
);
|
|
|
|
if (!user) {
|
|
return reply.status(404).send({
|
|
error: "NotFound",
|
|
message: "User not found",
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
user: user,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching user:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to fetch user",
|
|
});
|
|
}
|
|
});
|
|
|
|
// ==================== 2FA ROUTES ====================
|
|
|
|
// Setup 2FA - Generate QR code and secret
|
|
fastify.post(
|
|
"/2fa/setup",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
// Check if 2FA is already enabled
|
|
if (request.user.twoFactor?.enabled) {
|
|
return reply.status(400).send({
|
|
error: "BadRequest",
|
|
message: "Two-factor authentication is already enabled",
|
|
});
|
|
}
|
|
|
|
// Generate secret
|
|
const secret = speakeasy.generateSecret({
|
|
name: `TurboTrades (${request.user.username})`,
|
|
issuer: "TurboTrades",
|
|
});
|
|
|
|
// Generate revocation code (for recovery)
|
|
const revocationCode = Math.random()
|
|
.toString(36)
|
|
.substring(2, 10)
|
|
.toUpperCase();
|
|
|
|
// Generate QR code
|
|
const qrCodeUrl = await qrcode.toDataURL(secret.otpauth_url);
|
|
|
|
// Save to user (but don't enable yet - need verification)
|
|
request.user.twoFactor = {
|
|
enabled: false,
|
|
secret: secret.base32,
|
|
qrCode: qrCodeUrl,
|
|
revocationCode: revocationCode,
|
|
};
|
|
await request.user.save();
|
|
|
|
return reply.send({
|
|
success: true,
|
|
secret: secret.base32,
|
|
qrCode: qrCodeUrl,
|
|
revocationCode: revocationCode,
|
|
message:
|
|
"Scan the QR code with your authenticator app and verify with a code",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error setting up 2FA:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to setup 2FA",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Verify 2FA code and enable 2FA
|
|
fastify.post(
|
|
"/2fa/verify",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["token"],
|
|
properties: {
|
|
token: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { token } = request.body;
|
|
|
|
console.log(
|
|
"🔐 2FA Verify - Starting verification for user:",
|
|
request.user._id
|
|
);
|
|
|
|
// Refresh user data to get the latest 2FA secret
|
|
const freshUser = await User.findById(request.user._id);
|
|
|
|
if (!freshUser) {
|
|
console.error("❌ 2FA Verify - User not found:", request.user._id);
|
|
return reply.status(401).send({
|
|
error: "Unauthorized",
|
|
message: "User not found",
|
|
});
|
|
}
|
|
|
|
console.log("✅ 2FA Verify - User found:", freshUser.username);
|
|
console.log(" Has 2FA secret:", !!freshUser.twoFactor?.secret);
|
|
|
|
if (!freshUser.twoFactor?.secret) {
|
|
console.error(
|
|
"❌ 2FA Verify - No 2FA secret found for user:",
|
|
freshUser.username
|
|
);
|
|
return reply.status(400).send({
|
|
error: "BadRequest",
|
|
message: "2FA setup not initiated. Call /2fa/setup first",
|
|
});
|
|
}
|
|
|
|
console.log("🔍 2FA Verify - Verifying token...");
|
|
|
|
// Verify the token
|
|
const verified = speakeasy.totp.verify({
|
|
secret: freshUser.twoFactor.secret,
|
|
encoding: "base32",
|
|
token: token,
|
|
window: 2, // Allow 2 time steps before/after
|
|
});
|
|
|
|
console.log(" Token verification result:", verified);
|
|
|
|
if (!verified) {
|
|
console.error("❌ 2FA Verify - Invalid token provided");
|
|
return reply.status(400).send({
|
|
error: "InvalidToken",
|
|
message: "Invalid 2FA code",
|
|
});
|
|
}
|
|
|
|
console.log("✅ 2FA Verify - Token valid, enabling 2FA...");
|
|
|
|
// Enable 2FA
|
|
freshUser.twoFactor.enabled = true;
|
|
await freshUser.save();
|
|
|
|
console.log("✅ 2FA Verify - 2FA enabled in database");
|
|
|
|
// Send confirmation email
|
|
if (freshUser.email?.address) {
|
|
try {
|
|
await send2FASetupEmail(
|
|
freshUser.email.address,
|
|
freshUser.username
|
|
);
|
|
console.log("✅ 2FA Verify - Confirmation email sent");
|
|
} catch (error) {
|
|
console.error(
|
|
"⚠️ 2FA Verify - Failed to send confirmation email:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`✅ 2FA enabled for user: ${freshUser.username}`);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Two-factor authentication enabled successfully",
|
|
revocationCode: freshUser.twoFactor.revocationCode,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ Error verifying 2FA:", error);
|
|
console.error(" Error name:", error.name);
|
|
console.error(" Error message:", error.message);
|
|
console.error(" Error stack:", error.stack);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to verify 2FA",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Disable 2FA
|
|
fastify.post(
|
|
"/2fa/disable",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["password"],
|
|
properties: {
|
|
password: { type: "string" }, // Can be 2FA code or revocation code
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { password } = request.body;
|
|
|
|
if (!request.user.twoFactor?.enabled) {
|
|
return reply.status(400).send({
|
|
error: "BadRequest",
|
|
message: "Two-factor authentication is not enabled",
|
|
});
|
|
}
|
|
|
|
// Check if password is revocation code or 2FA token
|
|
const isRevocationCode =
|
|
password === request.user.twoFactor.revocationCode;
|
|
const isValidToken = speakeasy.totp.verify({
|
|
secret: request.user.twoFactor.secret,
|
|
encoding: "base32",
|
|
token: password,
|
|
window: 2,
|
|
});
|
|
|
|
if (!isRevocationCode && !isValidToken) {
|
|
return reply.status(400).send({
|
|
error: "InvalidCredentials",
|
|
message: "Invalid 2FA code or revocation code",
|
|
});
|
|
}
|
|
|
|
// Disable 2FA
|
|
request.user.twoFactor = {
|
|
enabled: false,
|
|
secret: null,
|
|
qrCode: null,
|
|
revocationCode: null,
|
|
};
|
|
await request.user.save();
|
|
|
|
console.log(`⚠️ 2FA disabled for user: ${request.user.username}`);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Two-factor authentication disabled successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error disabling 2FA:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to disable 2FA",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// ==================== SESSION MANAGEMENT ROUTES ====================
|
|
|
|
// Get active sessions
|
|
fastify.get(
|
|
"/sessions",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const Session = (await import("../models/Session.js")).default;
|
|
const sessions = await Session.getActiveSessions(request.user._id);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
sessions: sessions.map((s) => ({
|
|
id: s._id,
|
|
ip: s.ip,
|
|
device: s.device,
|
|
browser: s.browser,
|
|
os: s.os,
|
|
location: s.location,
|
|
lastActivity: s.lastActivity,
|
|
createdAt: s.createdAt,
|
|
isCurrent: s.token === request.token, // Mark current session
|
|
})),
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching sessions:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to fetch sessions",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Revoke a specific session
|
|
fastify.delete(
|
|
"/sessions/:sessionId",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { sessionId } = request.params;
|
|
const Session = (await import("../models/Session.js")).default;
|
|
|
|
const session = await Session.findOne({
|
|
_id: sessionId,
|
|
userId: request.user._id,
|
|
});
|
|
|
|
if (!session) {
|
|
return reply.status(404).send({
|
|
error: "NotFound",
|
|
message: "Session not found",
|
|
});
|
|
}
|
|
|
|
await session.deactivate();
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Session revoked successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error revoking session:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to revoke session",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Revoke all sessions except current
|
|
fastify.post(
|
|
"/sessions/revoke-all",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const Session = (await import("../models/Session.js")).default;
|
|
|
|
// Find current session
|
|
const currentSession = await Session.findOne({
|
|
token: request.token,
|
|
userId: request.user._id,
|
|
});
|
|
|
|
if (currentSession) {
|
|
await Session.revokeAllExcept(request.user._id, currentSession._id);
|
|
} else {
|
|
await Session.revokeAll(request.user._id);
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "All other sessions revoked successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error revoking all sessions:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to revoke sessions",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// ==================== TRANSACTION ROUTES ====================
|
|
|
|
// Get user's transaction history
|
|
fastify.get(
|
|
"/transactions",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const Transaction = (await import("../models/Transaction.js")).default;
|
|
|
|
console.log("📊 Fetching transactions for user:", request.user._id);
|
|
|
|
// Get query parameters
|
|
const { limit = 50, skip = 0, type, status } = request.query;
|
|
|
|
const transactions = await Transaction.getUserTransactions(
|
|
request.user._id,
|
|
{
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip),
|
|
type,
|
|
status,
|
|
}
|
|
);
|
|
|
|
console.log(`✅ Found ${transactions.length} transactions`);
|
|
|
|
// Get user stats
|
|
const stats = await Transaction.getUserStats(request.user._id);
|
|
|
|
console.log("📈 Stats:", stats);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
transactions: transactions.map((t) => ({
|
|
id: t._id,
|
|
type: t.type,
|
|
status: t.status,
|
|
amount: t.amount,
|
|
currency: t.currency,
|
|
description: t.description,
|
|
balanceBefore: t.balanceBefore,
|
|
balanceAfter: t.balanceAfter,
|
|
sessionIdShort: t.sessionIdShort,
|
|
device: t.sessionId?.device || null,
|
|
browser: t.sessionId?.browser || null,
|
|
os: t.sessionId?.os || null,
|
|
ip: t.sessionId?.ip || null,
|
|
itemName: t.itemName,
|
|
itemImage: t.itemImage,
|
|
paymentMethod: t.paymentMethod,
|
|
fee: t.fee,
|
|
direction: t.direction,
|
|
createdAt: t.createdAt,
|
|
completedAt: t.completedAt,
|
|
})),
|
|
stats: stats,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ Error fetching transactions:", error);
|
|
console.error("User ID:", request.user?._id);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to fetch transactions",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get single transaction details
|
|
fastify.get(
|
|
"/transactions/:transactionId",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { transactionId } = request.params;
|
|
const Transaction = (await import("../models/Transaction.js")).default;
|
|
|
|
const transaction = await Transaction.findOne({
|
|
_id: transactionId,
|
|
userId: request.user._id,
|
|
}).populate("sessionId", "device browser os ip");
|
|
|
|
if (!transaction) {
|
|
return reply.status(404).send({
|
|
error: "NotFound",
|
|
message: "Transaction not found",
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
transaction: {
|
|
id: transaction._id,
|
|
type: transaction.type,
|
|
status: transaction.status,
|
|
amount: transaction.amount,
|
|
currency: transaction.currency,
|
|
description: transaction.description,
|
|
balanceBefore: transaction.balanceBefore,
|
|
balanceAfter: transaction.balanceAfter,
|
|
sessionIdShort: transaction.sessionIdShort,
|
|
session: transaction.sessionId,
|
|
device: transaction.device,
|
|
ip: transaction.ip,
|
|
itemName: transaction.itemName,
|
|
paymentMethod: transaction.paymentMethod,
|
|
fee: transaction.fee,
|
|
feePercentage: transaction.feePercentage,
|
|
direction: transaction.direction,
|
|
metadata: transaction.metadata,
|
|
createdAt: transaction.createdAt,
|
|
completedAt: transaction.completedAt,
|
|
failedAt: transaction.failedAt,
|
|
cancelledAt: transaction.cancelledAt,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching transaction:", error);
|
|
return reply.status(500).send({
|
|
error: "InternalServerError",
|
|
message: "Failed to fetch transaction",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
}
|