Files
TurboTrades/routes/admin.js
iDefineHD aca0aca310
All checks were successful
Build Frontend / Build Frontend (push) Successful in 8s
system now uses seperate pricing.
2026-01-11 03:33:29 +00:00

1354 lines
37 KiB
JavaScript

import { authenticate } from "../middleware/auth.js";
import pricingService from "../services/pricing.js";
import Item from "../models/Item.js";
import MarketPrice from "../models/MarketPrice.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 has admin staff level (3 or higher)
if (!request.user.staffLevel || request.user.staffLevel < 3) {
return reply.status(403).send({
success: false,
message: "Admin access required",
requiredLevel: 3,
currentLevel: request.user.staffLevel || 0,
});
}
};
// 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,
});
}
}
);
// ============================================
// MARKETPRICE MANAGEMENT
// ============================================
// GET /admin/marketprices - Get paginated list of MarketPrice items
fastify.get(
"/marketprices",
{
preHandler: [authenticate, isAdmin],
schema: {
querystring: {
type: "object",
properties: {
page: { type: "integer", minimum: 1, default: 1 },
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
game: { type: "string", enum: ["cs2", "rust", ""] },
search: { type: "string" },
sort: { type: "string", default: "name-asc" },
},
},
},
},
async (request, reply) => {
try {
const {
page = 1,
limit = 50,
game = "",
search = "",
sort = "name-asc",
} = request.query;
// Build query
const query = {};
if (game) {
query.game = game;
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ marketHashName: { $regex: search, $options: "i" } },
];
}
// Build sort
let sortObj = {};
switch (sort) {
case "name-asc":
sortObj = { name: 1 };
break;
case "name-desc":
sortObj = { name: -1 };
break;
case "price-asc":
sortObj = { price: 1 };
break;
case "price-desc":
sortObj = { price: -1 };
break;
case "updated-desc":
sortObj = { lastUpdated: -1 };
break;
default:
sortObj = { name: 1 };
}
// Get total count
const total = await MarketPrice.countDocuments(query);
// Get items
const items = await MarketPrice.find(query)
.sort(sortObj)
.limit(limit)
.skip((page - 1) * limit)
.lean();
return reply.send({
success: true,
items,
page,
limit,
total,
pages: Math.ceil(total / limit),
});
} catch (error) {
console.error("❌ Failed to get market prices:", error);
return reply.status(500).send({
success: false,
message: "Failed to get market prices",
error: error.message,
});
}
}
);
// GET /admin/marketprices/stats - Get MarketPrice statistics
fastify.get(
"/marketprices/stats",
{
preHandler: [authenticate, isAdmin],
},
async (request, reply) => {
try {
const total = await MarketPrice.countDocuments();
const cs2 = await MarketPrice.countDocuments({ game: "cs2" });
const rust = await MarketPrice.countDocuments({ game: "rust" });
// Get most recent update
const lastItem = await MarketPrice.findOne()
.sort({ lastUpdated: -1 })
.select("lastUpdated")
.lean();
return reply.send({
success: true,
stats: {
total,
cs2,
rust,
lastUpdated: lastItem?.lastUpdated || null,
},
});
} catch (error) {
console.error("❌ Failed to get market price stats:", error);
return reply.status(500).send({
success: false,
message: "Failed to get stats",
error: error.message,
});
}
}
);
// PATCH /admin/marketprices/:id - Update a MarketPrice item
fastify.patch(
"/marketprices/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
properties: {
price: { type: "number", minimum: 0 },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const { price } = request.body;
const item = await MarketPrice.findById(id);
if (!item) {
return reply.status(404).send({
success: false,
message: "Item not found",
});
}
item.price = price;
item.priceType = "manual"; // Mark as manually updated
item.lastUpdated = new Date();
await item.save();
console.log(
`⚙️ Admin ${request.user.username} updated price for ${item.name}: $${price}`
);
return reply.send({
success: true,
message: "Price updated successfully",
item,
});
} catch (error) {
console.error("❌ Failed to update market price:", error);
return reply.status(500).send({
success: false,
message: "Failed to update price",
error: error.message,
});
}
}
);
// DELETE /admin/marketprices/:id - Delete a MarketPrice item
fastify.delete(
"/marketprices/: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 MarketPrice.findByIdAndDelete(id);
if (!item) {
return reply.status(404).send({
success: false,
message: "Item not found",
});
}
console.log(
`⚙️ Admin ${request.user.username} deleted market price for ${item.name}`
);
return reply.send({
success: true,
message: "Item deleted successfully",
});
} catch (error) {
console.error("❌ Failed to delete market price:", error);
return reply.status(500).send({
success: false,
message: "Failed to delete item",
error: error.message,
});
}
}
);
}