import { authenticate } from "../middleware/auth.js"; import pricingService from "../services/pricing.js"; import Item from "../models/Item.js"; import Transaction from "../models/Transaction.js"; import User from "../models/User.js"; /** * Admin routes for price management and system operations * @param {FastifyInstance} fastify * @param {Object} options */ export default async function adminRoutes(fastify, options) { // Middleware to check if user is admin const isAdmin = async (request, reply) => { if (!request.user) { return reply.status(401).send({ success: false, message: "Authentication required", }); } // Check if user is admin (you can customize this check) // For now, checking if user has admin role or specific steamId const adminSteamIds = process.env.ADMIN_STEAM_IDS?.split(",") || []; if ( !request.user.isAdmin && !adminSteamIds.includes(request.user.steamId) ) { return reply.status(403).send({ success: false, message: "Admin access required", }); } }; // POST /admin/prices/update - Manually trigger price update fastify.post( "/prices/update", { preHandler: [authenticate, isAdmin], schema: { body: { type: "object", properties: { game: { type: "string", enum: ["cs2", "rust", "all"], default: "all", }, }, }, }, }, async (request, reply) => { try { const { game = "all" } = request.body; console.log( `🔄 Admin ${request.user.username} triggered price update for ${game}` ); let result; if (game === "all") { result = await pricingService.updateAllPrices(); } else { result = await pricingService.updateDatabasePrices(game); } return reply.send({ success: true, message: "Price update completed", data: result, }); } catch (error) { console.error("❌ Price update failed:", error.message); return reply.status(500).send({ success: false, message: "Failed to update prices", error: error.message, }); } } ); // GET /admin/prices/status - Get price update status fastify.get( "/prices/status", { preHandler: [authenticate, isAdmin], }, async (request, reply) => { try { const cs2LastUpdate = pricingService.getLastUpdate("cs2"); const rustLastUpdate = pricingService.getLastUpdate("rust"); const cs2Stats = await Item.aggregate([ { $match: { game: "cs2", status: "active" } }, { $group: { _id: null, total: { $sum: 1 }, withMarketPrice: { $sum: { $cond: [{ $ne: ["$marketPrice", null] }, 1, 0], }, }, avgMarketPrice: { $avg: "$marketPrice" }, minMarketPrice: { $min: "$marketPrice" }, maxMarketPrice: { $max: "$marketPrice" }, }, }, ]); const rustStats = await Item.aggregate([ { $match: { game: "rust", status: "active" } }, { $group: { _id: null, total: { $sum: 1 }, withMarketPrice: { $sum: { $cond: [{ $ne: ["$marketPrice", null] }, 1, 0], }, }, avgMarketPrice: { $avg: "$marketPrice" }, minMarketPrice: { $min: "$marketPrice" }, maxMarketPrice: { $max: "$marketPrice" }, }, }, ]); return reply.send({ success: true, status: { cs2: { lastUpdate: cs2LastUpdate, needsUpdate: pricingService.needsUpdate("cs2"), stats: cs2Stats[0] || { total: 0, withMarketPrice: 0, }, }, rust: { lastUpdate: rustLastUpdate, needsUpdate: pricingService.needsUpdate("rust"), stats: rustStats[0] || { total: 0, withMarketPrice: 0, }, }, }, }); } catch (error) { console.error("❌ Failed to get price status:", error.message); return reply.status(500).send({ success: false, message: "Failed to get price status", error: error.message, }); } } ); // GET /admin/prices/missing - Get items without market prices fastify.get( "/prices/missing", { preHandler: [authenticate, isAdmin], schema: { querystring: { type: "object", properties: { game: { type: "string", enum: ["cs2", "rust"] }, limit: { type: "integer", minimum: 1, maximum: 100, default: 50 }, }, }, }, }, async (request, reply) => { try { const { game, limit = 50 } = request.query; const query = { status: "active", $or: [{ marketPrice: null }, { marketPrice: { $exists: false } }], }; if (game) { query.game = game; } const items = await Item.find(query) .select("name game category rarity wear phase seller") .populate("seller", "username") .limit(limit) .sort({ listedAt: -1 }); return reply.send({ success: true, total: items.length, items, }); } catch (error) { console.error("❌ Failed to get missing prices:", error.message); return reply.status(500).send({ success: false, message: "Failed to get items with missing prices", error: error.message, }); } } ); // POST /admin/prices/estimate - Manually estimate price for an item fastify.post( "/prices/estimate", { preHandler: [authenticate, isAdmin], schema: { body: { type: "object", required: ["itemId"], properties: { itemId: { type: "string" }, }, }, }, }, async (request, reply) => { try { const { itemId } = request.body; const item = await Item.findById(itemId); if (!item) { return reply.status(404).send({ success: false, message: "Item not found", }); } const estimatedPrice = await pricingService.estimatePrice({ name: item.name, wear: item.wear, phase: item.phase, statTrak: item.statTrak, souvenir: item.souvenir, }); // Update the item item.marketPrice = estimatedPrice; item.priceUpdatedAt = new Date(); await item.save(); return reply.send({ success: true, message: "Price estimated and updated", item: { id: item._id, name: item.name, marketPrice: item.marketPrice, priceUpdatedAt: item.priceUpdatedAt, }, }); } catch (error) { console.error("❌ Failed to estimate price:", error.message); return reply.status(500).send({ success: false, message: "Failed to estimate price", error: error.message, }); } } ); // GET /admin/stats - Get overall system statistics fastify.get( "/stats", { preHandler: [authenticate, isAdmin], }, async (request, reply) => { try { const [totalItems, activeItems, soldItems, totalUsers, recentSales] = await Promise.all([ Item.countDocuments(), Item.countDocuments({ status: "active" }), Item.countDocuments({ status: "sold" }), fastify.mongoose.connection.db.collection("users").countDocuments(), Item.find({ status: "sold" }) .sort({ soldAt: -1 }) .limit(10) .select("name price soldAt game") .lean(), ]); // Calculate total value const totalValueResult = await Item.aggregate([ { $match: { status: "active" } }, { $group: { _id: null, total: { $sum: "$price" } } }, ]); const totalValue = totalValueResult[0]?.total || 0; // Calculate revenue (sum of sold items) const revenueResult = await Item.aggregate([ { $match: { status: "sold" } }, { $group: { _id: null, total: { $sum: "$price" } } }, ]); const totalRevenue = revenueResult[0]?.total || 0; return reply.send({ success: true, stats: { items: { total: totalItems, active: activeItems, sold: soldItems, }, users: { total: totalUsers, }, marketplace: { totalValue, totalRevenue, }, recentSales, }, }); } catch (error) { console.error("❌ Failed to get stats:", error.message); return reply.status(500).send({ success: false, message: "Failed to get system statistics", error: error.message, }); } } ); // POST /admin/items/bulk-update - Bulk update item fields fastify.post( "/items/bulk-update", { preHandler: [authenticate, isAdmin], schema: { body: { type: "object", required: ["filter", "update"], properties: { filter: { type: "object" }, update: { type: "object" }, }, }, }, }, async (request, reply) => { try { const { filter, update } = request.body; console.log(`🔄 Admin ${request.user.username} performing bulk update`); console.log("Filter:", JSON.stringify(filter)); console.log("Update:", JSON.stringify(update)); const result = await Item.updateMany(filter, update); return reply.send({ success: true, message: `Updated ${result.modifiedCount} items`, matched: result.matchedCount, modified: result.modifiedCount, }); } catch (error) { console.error("❌ Bulk update failed:", error.message); return reply.status(500).send({ success: false, message: "Bulk update failed", error: error.message, }); } } ); // DELETE /admin/items/:id - Delete an item (admin only) fastify.delete( "/items/:id", { preHandler: [authenticate, isAdmin], schema: { params: { type: "object", required: ["id"], properties: { id: { type: "string" }, }, }, }, }, async (request, reply) => { try { const { id } = request.params; const item = await Item.findByIdAndDelete(id); if (!item) { return reply.status(404).send({ success: false, message: "Item not found", }); } console.log( `🗑️ Admin ${request.user.username} deleted item: ${item.name}` ); return reply.send({ success: true, message: "Item deleted successfully", item: { id: item._id, name: item.name, }, }); } catch (error) { console.error("❌ Failed to delete item:", error.message); return reply.status(500).send({ success: false, message: "Failed to delete item", error: error.message, }); } } ); // POST /admin/prices/schedule - Configure automatic price updates fastify.post( "/prices/schedule", { preHandler: [authenticate, isAdmin], schema: { body: { type: "object", properties: { intervalMinutes: { type: "integer", minimum: 15, maximum: 1440, default: 60, }, }, }, }, }, async (request, reply) => { try { const { intervalMinutes = 60 } = request.body; const intervalMs = intervalMinutes * 60 * 1000; pricingService.scheduleUpdates(intervalMs); console.log( `⏰ Admin ${request.user.username} configured price updates every ${intervalMinutes} minutes` ); return reply.send({ success: true, message: `Scheduled price updates every ${intervalMinutes} minutes`, intervalMinutes, }); } catch (error) { console.error("❌ Failed to schedule updates:", error.message); return reply.status(500).send({ success: false, message: "Failed to schedule price updates", error: error.message, }); } } ); // GET /admin/financial/overview - Get financial overview with profit, fees, deposits, withdrawals fastify.get( "/financial/overview", { preHandler: [authenticate, isAdmin], schema: { querystring: { type: "object", properties: { startDate: { type: "string" }, endDate: { type: "string" }, period: { type: "string", enum: ["today", "week", "month", "year", "all"], default: "all", }, }, }, }, }, async (request, reply) => { try { const { startDate, endDate, period = "all" } = request.query; // Calculate date range let dateFilter = {}; const now = new Date(); if (startDate || endDate) { dateFilter.createdAt = {}; if (startDate) dateFilter.createdAt.$gte = new Date(startDate); if (endDate) dateFilter.createdAt.$lte = new Date(endDate); } else if (period !== "all") { dateFilter.createdAt = {}; switch (period) { case "today": dateFilter.createdAt.$gte = new Date(now.setHours(0, 0, 0, 0)); break; case "week": dateFilter.createdAt.$gte = new Date( now.setDate(now.getDate() - 7) ); break; case "month": dateFilter.createdAt.$gte = new Date( now.setMonth(now.getMonth() - 1) ); break; case "year": dateFilter.createdAt.$gte = new Date( now.setFullYear(now.getFullYear() - 1) ); break; } } // Get transaction statistics const [deposits, withdrawals, purchases, sales, fees] = await Promise.all([ // Total deposits Transaction.aggregate([ { $match: { type: "deposit", status: "completed", ...dateFilter }, }, { $group: { _id: null, total: { $sum: "$amount" }, count: { $sum: 1 }, }, }, ]), // Total withdrawals Transaction.aggregate([ { $match: { type: "withdrawal", status: "completed", ...dateFilter, }, }, { $group: { _id: null, total: { $sum: "$amount" }, count: { $sum: 1 }, }, }, ]), // Total purchases Transaction.aggregate([ { $match: { type: "purchase", status: "completed", ...dateFilter, }, }, { $group: { _id: null, total: { $sum: "$amount" }, count: { $sum: 1 }, }, }, ]), // Total sales Transaction.aggregate([ { $match: { type: "sale", status: "completed", ...dateFilter } }, { $group: { _id: null, total: { $sum: "$amount" }, count: { $sum: 1 }, }, }, ]), // Total fees collected Transaction.aggregate([ { $match: { status: "completed", fee: { $gt: 0 }, ...dateFilter }, }, { $group: { _id: null, total: { $sum: "$fee" }, count: { $sum: 1 }, }, }, ]), ]); const totalDeposits = deposits[0]?.total || 0; const totalWithdrawals = withdrawals[0]?.total || 0; const totalPurchases = purchases[0]?.total || 0; const totalSales = sales[0]?.total || 0; const totalFees = fees[0]?.total || 0; // Calculate profit (fees collected + margin on sales) const grossProfit = totalFees; const netProfit = grossProfit - (totalWithdrawals - totalDeposits); // Get transaction volume by day for charts const transactionsByDay = await Transaction.aggregate([ { $match: { status: "completed", ...dateFilter } }, { $group: { _id: { date: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" }, }, type: "$type", }, total: { $sum: "$amount" }, count: { $sum: 1 }, }, }, { $sort: { "_id.date": 1 } }, ]); return reply.send({ success: true, financial: { deposits: { total: totalDeposits, count: deposits[0]?.count || 0, }, withdrawals: { total: totalWithdrawals, count: withdrawals[0]?.count || 0, }, purchases: { total: totalPurchases, count: purchases[0]?.count || 0, }, sales: { total: totalSales, count: sales[0]?.count || 0, }, fees: { total: totalFees, count: fees[0]?.count || 0, }, profit: { gross: grossProfit, net: netProfit, }, balance: totalDeposits - totalWithdrawals, }, chartData: transactionsByDay, }); } catch (error) { console.error("❌ Failed to get financial overview:", error.message); return reply.status(500).send({ success: false, message: "Failed to get financial overview", error: error.message, }); } } ); // GET /admin/transactions - Get all transactions with filtering fastify.get( "/transactions", { preHandler: [authenticate, isAdmin], schema: { querystring: { type: "object", properties: { type: { type: "string" }, status: { type: "string" }, userId: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100, default: 50 }, skip: { type: "integer", minimum: 0, default: 0 }, startDate: { type: "string" }, endDate: { type: "string" }, }, }, }, }, async (request, reply) => { try { const { type, status, userId, limit = 50, skip = 0, startDate, endDate, } = request.query; const query = {}; if (type) query.type = type; if (status) query.status = status; if (userId) query.userId = userId; if (startDate || endDate) { query.createdAt = {}; if (startDate) query.createdAt.$gte = new Date(startDate); if (endDate) query.createdAt.$lte = new Date(endDate); } const [transactions, total] = await Promise.all([ Transaction.find(query) .sort({ createdAt: -1 }) .limit(limit) .skip(skip) .populate("userId", "username steamId avatar") .populate("itemId", "name image game rarity") .lean(), Transaction.countDocuments(query), ]); return reply.send({ success: true, transactions, pagination: { total, limit, skip, hasMore: skip + limit < total, }, }); } catch (error) { console.error("❌ Failed to get transactions:", error.message); return reply.status(500).send({ success: false, message: "Failed to get transactions", error: error.message, }); } } ); // GET /admin/items/all - Get all items with filtering (CS2/Rust separation) fastify.get( "/items/all", { preHandler: [authenticate, isAdmin], schema: { querystring: { type: "object", properties: { game: { type: "string" }, status: { type: "string" }, category: { type: "string" }, rarity: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 200, default: 100 }, skip: { type: "integer", minimum: 0, default: 0 }, search: { type: "string" }, sortBy: { type: "string", enum: ["price", "marketPrice", "listedAt", "views"], default: "listedAt", }, sortOrder: { type: "string", enum: ["asc", "desc"], default: "desc", }, }, }, }, }, async (request, reply) => { try { const { game, status, category, rarity, limit = 100, skip = 0, search, sortBy = "listedAt", sortOrder = "desc", } = request.query; const query = {}; if (game && (game === "cs2" || game === "rust")) query.game = game; if (status && ["active", "sold", "removed"].includes(status)) query.status = status; if (category) query.category = category; if (rarity) query.rarity = rarity; if (search) { query.name = { $regex: search, $options: "i" }; } const sort = {}; sort[sortBy] = sortOrder === "asc" ? 1 : -1; const [items, total] = await Promise.all([ Item.find(query) .sort(sort) .limit(limit) .skip(skip) .populate("seller", "username steamId") .populate("buyer", "username steamId") .lean(), Item.countDocuments(query), ]); return reply.send({ success: true, items, pagination: { total, limit, skip, hasMore: skip + limit < total, }, }); } catch (error) { console.error("❌ Failed to get items:", error.message); return reply.status(500).send({ success: false, message: "Failed to get items", error: error.message, }); } } ); // PUT /admin/items/:id/price - Override item price fastify.put( "/items/:id/price", { preHandler: [authenticate, isAdmin], schema: { params: { type: "object", required: ["id"], properties: { id: { type: "string" }, }, }, body: { type: "object", required: ["price"], properties: { price: { type: "number", minimum: 0 }, marketPrice: { type: "number", minimum: 0 }, }, }, }, }, async (request, reply) => { try { const { id } = request.params; const { price, marketPrice } = request.body; const item = await Item.findById(id); if (!item) { return reply.status(404).send({ success: false, message: "Item not found", }); } const updates = {}; if (price !== undefined) { updates.price = price; updates.priceOverride = true; // Mark as admin-overridden } if (marketPrice !== undefined) { updates.marketPrice = marketPrice; updates.priceUpdatedAt = new Date(); updates.priceOverride = true; // Mark as admin-overridden } const updatedItem = await Item.findByIdAndUpdate( id, { $set: updates }, { new: true } ).populate("seller", "username steamId"); console.log( `💰 Admin ${request.user.username} updated prices for item: ${item.name} (Price: $${price}, Market: $${marketPrice})` ); return reply.send({ success: true, message: "Item prices updated successfully", item: updatedItem, }); } catch (error) { console.error("❌ Failed to update item price:", error.message); return reply.status(500).send({ success: false, message: "Failed to update item price", error: error.message, }); } } ); // GET /admin/users - Get user list with balances fastify.get( "/users", { preHandler: [authenticate, isAdmin], schema: { querystring: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 100, default: 50 }, skip: { type: "integer", minimum: 0, default: 0 }, search: { type: "string" }, sortBy: { type: "string", enum: ["balance", "createdAt"], default: "createdAt", }, sortOrder: { type: "string", enum: ["asc", "desc"], default: "desc", }, }, }, }, }, async (request, reply) => { try { const { limit = 50, skip = 0, search, sortBy = "createdAt", sortOrder = "desc", } = request.query; const query = {}; if (search) { query.$or = [ { username: { $regex: search, $options: "i" } }, { steamId: { $regex: search, $options: "i" } }, ]; } const sort = {}; sort[sortBy] = sortOrder === "asc" ? 1 : -1; const [users, total] = await Promise.all([ User.find(query) .select( "username steamId avatar balance staffLevel createdAt email.verified ban.banned" ) .sort(sort) .limit(limit) .skip(skip) .lean(), User.countDocuments(query), ]); return reply.send({ success: true, users, pagination: { total, limit, skip, hasMore: skip + limit < total, }, }); } catch (error) { console.error("❌ Failed to get users:", error.message); return reply.status(500).send({ success: false, message: "Failed to get users", error: error.message, }); } } ); // GET /admin/dashboard - Get comprehensive dashboard data fastify.get( "/dashboard", { preHandler: [authenticate, isAdmin], }, async (request, reply) => { try { const now = new Date(); const today = new Date(now.setHours(0, 0, 0, 0)); const thisWeek = new Date(now.setDate(now.getDate() - 7)); const thisMonth = new Date(now.setMonth(now.getMonth() - 1)); // Get counts const [ totalUsers, totalItems, activeItems, soldItems, cs2Items, rustItems, todayTransactions, weekTransactions, monthTransactions, totalFees, ] = await Promise.all([ User.countDocuments(), Item.countDocuments(), Item.countDocuments({ status: "active" }), Item.countDocuments({ status: "sold" }), Item.countDocuments({ game: "cs2", status: "active" }), Item.countDocuments({ game: "rust", status: "active" }), Transaction.countDocuments({ createdAt: { $gte: today }, status: "completed", }), Transaction.countDocuments({ createdAt: { $gte: thisWeek }, status: "completed", }), Transaction.countDocuments({ createdAt: { $gte: thisMonth }, status: "completed", }), Transaction.aggregate([ { $match: { status: "completed", fee: { $gt: 0 } } }, { $group: { _id: null, total: { $sum: "$fee" } } }, ]), ]); // Get recent activity const recentTransactions = await Transaction.find({ status: "completed", }) .sort({ createdAt: -1 }) .limit(10) .populate("userId", "username avatar") .populate("itemId", "name image") .lean(); // Get top sellers const topSellers = await Transaction.aggregate([ { $match: { type: "sale", status: "completed" } }, { $group: { _id: "$userId", totalSales: { $sum: "$amount" }, count: { $sum: 1 }, }, }, { $sort: { totalSales: -1 } }, { $limit: 5 }, ]); // Populate top sellers const topSellersWithDetails = await User.populate(topSellers, { path: "_id", select: "username avatar steamId", }); return reply.send({ success: true, dashboard: { overview: { totalUsers, totalItems, activeItems, soldItems, cs2Items, rustItems, }, transactions: { today: todayTransactions, week: weekTransactions, month: monthTransactions, }, revenue: { totalFees: totalFees[0]?.total || 0, }, recentActivity: recentTransactions, topSellers: topSellersWithDetails, }, }); } catch (error) { console.error("❌ Failed to get dashboard:", error.message); return reply.status(500).send({ success: false, message: "Failed to get dashboard data", error: error.message, }); } } ); }