All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
1358 lines
37 KiB
JavaScript
1358 lines
37 KiB
JavaScript
import { authenticate, isAdmin } 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 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,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// ============================================
|
|
// 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,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
}
|