first commit
This commit is contained in:
824
routes/user.js
Normal file
824
routes/user.js
Normal file
@@ -0,0 +1,824 @@
|
||||
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",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user