first commit

This commit is contained in:
2026-01-10 04:57:43 +00:00
parent 16a76a2cd6
commit 232968de1e
131 changed files with 43262 additions and 0 deletions

824
routes/user.js Normal file
View 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",
});
}
}
);
}