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", }); } } ); }