Files
TurboTrades/routes/admin-management.js
iDefineHD 7a32454b83
All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
system now uses seperate pricing.
2026-01-11 03:24:54 +00:00

1571 lines
44 KiB
JavaScript

import { authenticate } from "../middleware/auth.js";
import User from "../models/User.js";
import Transaction from "../models/Transaction.js";
import SiteConfig from "../models/SiteConfig.js";
import PromoUsage from "../models/PromoUsage.js";
import { v4 as uuidv4 } from "uuid";
/**
* Admin management routes for user administration and site configuration
* @param {FastifyInstance} fastify
* @param {Object} options
*/
export default async function adminManagementRoutes(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",
});
}
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",
});
}
};
// ============================================
// USER MANAGEMENT ROUTES
// ============================================
// GET /admin/users/search - Search for users
fastify.get(
"/users/search",
{
preHandler: [authenticate, isAdmin],
schema: {
querystring: {
type: "object",
properties: {
query: { type: "string" },
limit: { type: "integer", minimum: 1, maximum: 50, default: 20 },
},
},
},
},
async (request, reply) => {
try {
const { query, limit = 20 } = request.query;
let searchQuery = {};
if (query) {
// Search by username, steamId, or email
searchQuery = {
$or: [
{ username: { $regex: query, $options: "i" } },
{ steamId: { $regex: query, $options: "i" } },
{ "email.address": { $regex: query, $options: "i" } },
],
};
}
const users = await User.find(searchQuery)
.select("-twoFactor.secret")
.limit(limit)
.sort({ createdAt: -1 });
return reply.send({
success: true,
users,
count: users.length,
});
} catch (error) {
console.error("❌ User search failed:", error);
return reply.status(500).send({
success: false,
message: "Failed to search users",
error: error.message,
});
}
}
);
// GET /admin/users/:id - Get user details by ID
fastify.get(
"/users/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const user = await User.findById(id).select("-twoFactor.secret");
if (!user) {
return reply.status(404).send({
success: false,
message: "User not found",
});
}
// Get user's transaction stats
const transactionStats = await Transaction.aggregate([
{ $match: { user: user._id } },
{
$group: {
_id: "$type",
count: { $sum: 1 },
total: { $sum: "$amount" },
},
},
]);
const totalTransactions = await Transaction.countDocuments({
user: user._id,
});
return reply.send({
success: true,
user,
stats: {
totalTransactions,
byType: transactionStats,
},
});
} catch (error) {
console.error("❌ Failed to get user:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve user",
error: error.message,
});
}
}
);
// GET /admin/users/:id/stats - Get user statistics
fastify.get(
"/users/:id/stats",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const user = await User.findById(id);
if (!user) {
return reply.status(404).send({
success: false,
message: "User not found",
});
}
// Get transaction statistics
const transactionStats = await Transaction.aggregate([
{ $match: { user: user._id } },
{
$group: {
_id: "$type",
count: { $sum: 1 },
total: { $sum: "$amount" },
},
},
]);
const totalTransactions = await Transaction.countDocuments({
user: user._id,
});
const totalAmount = transactionStats.reduce(
(sum, stat) => sum + stat.total,
0
);
// Get trade count (if Trade model exists)
let totalTrades = 0;
try {
const Trade = fastify.mongoose.model("Trade");
totalTrades = await Trade.countDocuments({
$or: [{ sender: user._id }, { receiver: user._id }],
});
} catch (err) {
// Trade model might not exist yet
console.log("Trade model not found, skipping trade stats");
}
return reply.send({
success: true,
stats: {
totalTransactions,
totalAmount,
totalTrades,
byType: transactionStats,
},
});
} catch (error) {
console.error("❌ Failed to get user stats:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve user statistics",
error: error.message,
});
}
}
);
// POST /admin/users/:id/balance - Add or remove balance from user
// Also PATCH for frontend compatibility
// Also support PATCH for consistency with frontend
const balanceHandler = {
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
required: ["amount", "reason"],
properties: {
amount: { type: "number" },
reason: { type: "string", minLength: 3 },
type: {
type: "string",
enum: ["add", "remove", "credit", "debit"],
default: "add",
},
},
},
},
handler: async (request, reply) => {
try {
const { id } = request.params;
let { amount, reason, type = "add" } = request.body;
// Normalize type - support both add/remove and credit/debit
if (type === "credit") type = "add";
if (type === "debit") type = "remove";
const user = await User.findById(id);
if (!user) {
return reply.status(404).send({
success: false,
message: "User not found",
});
}
const adjustmentAmount =
type === "add" ? Math.abs(amount) : -Math.abs(amount);
const newBalance = user.balance + adjustmentAmount;
if (newBalance < 0) {
return reply.status(400).send({
success: false,
message: "Insufficient balance for removal",
});
}
// Update user balance
user.balance = newBalance;
await user.save();
// Create transaction record
const transaction = await Transaction.create({
user: user._id,
type: type === "add" ? "bonus" : "adjustment",
amount: adjustmentAmount,
status: "completed",
description: `Admin adjustment by ${request.user.username}: ${reason}`,
metadata: {
adminId: request.user._id,
adminUsername: request.user.username,
reason,
previousBalance: user.balance - adjustmentAmount,
newBalance: user.balance,
},
});
console.log(
`💰 Admin ${request.user.username} adjusted balance for ${
user.username
}: ${
adjustmentAmount > 0 ? "+" : ""
}${adjustmentAmount} (Reason: ${reason})`
);
return reply.send({
success: true,
message: `Successfully ${
type === "add" ? "added" : "removed"
} $${Math.abs(amount).toFixed(2)}`,
user: {
id: user._id,
username: user.username,
balance: user.balance,
},
transaction: {
id: transaction._id,
amount: transaction.amount,
type: transaction.type,
},
});
} catch (error) {
console.error("❌ Failed to adjust balance:", error);
return reply.status(500).send({
success: false,
message: "Failed to adjust balance",
error: error.message,
});
}
},
};
fastify.post("/users/:id/balance", balanceHandler);
fastify.patch("/users/:id/balance", balanceHandler);
// POST /admin/users/:id/ban - Ban or unban a user
// Also PATCH for frontend compatibility
// Also support PATCH for consistency
const banHandler = {
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
required: ["banned", "reason"],
properties: {
banned: { type: "boolean" },
reason: { type: "string" },
duration: { type: "number", minimum: 0 },
},
},
},
handler: async (request, reply) => {
try {
const { id } = request.params;
const { banned, reason, duration = 0 } = request.body;
const user = await User.findById(id);
if (!user) {
return reply.status(404).send({
success: false,
message: "User not found",
});
}
// Don't allow banning other admins
if (user.staffLevel >= 3) {
return reply.status(403).send({
success: false,
message: "Cannot ban admin users",
});
}
let expires = null;
if (banned && duration > 0) {
expires = new Date(Date.now() + duration * 60 * 60 * 1000);
}
user.ban = {
banned,
reason: banned ? reason || "Banned by administrator" : null,
expires: banned ? expires : null,
};
await user.save();
console.log(
`🔨 Admin ${request.user.username} ${
banned ? "banned" : "unbanned"
} user ${user.username}${
banned && reason ? ` (Reason: ${reason})` : ""
}`
);
return reply.send({
success: true,
message: banned
? `User ${user.username} has been banned`
: `User ${user.username} has been unbanned`,
user: {
id: user._id,
username: user.username,
banned: user.ban.banned,
reason: user.ban.reason,
expires: user.ban.expires,
},
});
} catch (error) {
console.error("❌ Failed to ban/unban user:", error);
return reply.status(500).send({
success: false,
message: "Failed to update ban status",
error: error.message,
});
}
},
};
fastify.post("/users/:id/ban", banHandler);
fastify.patch("/users/:id/ban", banHandler);
// POST /admin/users/:id/staff-level - Update user staff level
// Also support PATCH for consistency
const staffLevelHandler = {
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
required: ["level"],
properties: {
level: { type: "integer", minimum: 0, maximum: 4 },
},
},
},
handler: async (request, reply) => {
try {
const { id } = request.params;
const { level } = request.body;
const user = await User.findById(id);
if (!user) {
return reply.status(404).send({
success: false,
message: "User not found",
});
}
// Only allow super admins (level 5) to promote to admin
if (level >= 3 && request.user.staffLevel < 5) {
return reply.status(403).send({
success: false,
message: "Only super admins can promote to admin level",
});
}
const previousLevel = user.staffLevel;
user.staffLevel = level;
await user.save();
console.log(
`👮 Admin ${request.user.username} changed ${user.username}'s staff level from ${previousLevel} to ${level}`
);
return reply.send({
success: true,
message: `Staff level updated to ${level}`,
user: {
id: user._id,
username: user.username,
staffLevel: user.staffLevel,
isAdmin: user.isAdmin,
},
});
} catch (error) {
console.error("❌ Failed to update staff level:", error);
return reply.status(500).send({
success: false,
message: "Failed to update staff level",
error: error.message,
});
}
},
};
fastify.post("/users/:id/staff-level", staffLevelHandler);
fastify.patch("/users/:id/staff-level", staffLevelHandler);
// GET /admin/users/:id/transactions - Get user transactions
fastify.get(
"/users/:id/transactions",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
querystring: {
type: "object",
properties: {
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
skip: { type: "integer", minimum: 0, default: 0 },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const { limit = 50, skip = 0 } = request.query;
const user = await User.findById(id);
if (!user) {
return reply.status(404).send({
success: false,
message: "User not found",
});
}
const transactions = await Transaction.find({ user: id })
.sort({ createdAt: -1 })
.limit(limit)
.skip(skip);
const total = await Transaction.countDocuments({ user: id });
return reply.send({
success: true,
transactions,
pagination: {
total,
limit,
skip,
hasMore: skip + limit < total,
},
});
} catch (error) {
console.error("❌ Failed to get user transactions:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve transactions",
error: error.message,
});
}
}
);
// ============================================
// SITE CONFIGURATION ROUTES
// ============================================
// GET /admin/config - Get site configuration
fastify.get(
"/config",
{
preHandler: [authenticate, isAdmin],
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
return reply.send({
success: true,
config,
});
} catch (error) {
console.error("❌ Failed to get config:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve configuration",
error: error.message,
});
}
}
);
// PATCH /admin/config/maintenance - Update maintenance mode
fastify.patch(
"/config/maintenance",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
properties: {
enabled: { type: "boolean" },
message: { type: "string" },
allowedSteamIds: { type: "array", items: { type: "string" } },
scheduledStart: { type: ["string", "null"] },
scheduledEnd: { type: ["string", "null"] },
},
},
},
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
if (request.body.enabled !== undefined) {
config.maintenance.enabled = request.body.enabled;
}
if (request.body.message) {
config.maintenance.message = request.body.message;
}
if (request.body.allowedSteamIds) {
config.maintenance.allowedSteamIds = request.body.allowedSteamIds;
}
if (request.body.scheduledStart !== undefined) {
config.maintenance.scheduledStart = request.body.scheduledStart
? new Date(request.body.scheduledStart)
: null;
}
if (request.body.scheduledEnd !== undefined) {
config.maintenance.scheduledEnd = request.body.scheduledEnd
? new Date(request.body.scheduledEnd)
: null;
}
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`⚙️ Admin ${request.user.username} updated maintenance mode: ${
config.maintenance.enabled ? "ENABLED" : "DISABLED"
}`
);
return reply.send({
success: true,
message: "Maintenance mode updated",
maintenance: config.maintenance,
});
} catch (error) {
console.error("❌ Failed to update maintenance mode:", error);
return reply.status(500).send({
success: false,
message: "Failed to update maintenance mode",
error: error.message,
});
}
}
);
// PATCH /admin/config/trading - Update trading settings
fastify.patch(
"/config/trading",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
properties: {
enabled: { type: "boolean" },
depositEnabled: { type: "boolean" },
withdrawEnabled: { type: "boolean" },
minDeposit: { type: "number", minimum: 0 },
minWithdraw: { type: "number", minimum: 0 },
withdrawFee: { type: "number", minimum: 0, maximum: 1 },
maxItemsPerTrade: { type: "integer", minimum: 1 },
},
},
},
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
Object.keys(request.body).forEach((key) => {
if (request.body[key] !== undefined) {
config.trading[key] = request.body[key];
}
});
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`⚙️ Admin ${request.user.username} updated trading settings`
);
return reply.send({
success: true,
message: "Trading settings updated",
trading: config.trading,
});
} catch (error) {
console.error("❌ Failed to update trading settings:", error);
return reply.status(500).send({
success: false,
message: "Failed to update trading settings",
error: error.message,
});
}
}
);
// PATCH /admin/config/market - Update market settings
fastify.patch(
"/config/market",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
properties: {
enabled: { type: "boolean" },
commission: { type: "number", minimum: 0, maximum: 1 },
minListingPrice: { type: "number", minimum: 0 },
maxListingPrice: { type: "number", minimum: 0 },
autoUpdatePrices: { type: "boolean" },
priceUpdateInterval: { type: "integer", minimum: 60000 },
},
},
},
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
Object.keys(request.body).forEach((key) => {
if (request.body[key] !== undefined) {
config.market[key] = request.body[key];
}
});
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`⚙️ Admin ${request.user.username} updated market settings`
);
return reply.send({
success: true,
message: "Market settings updated",
market: config.market,
});
} catch (error) {
console.error("❌ Failed to update market settings:", error);
return reply.status(500).send({
success: false,
message: "Failed to update market settings",
error: error.message,
});
}
}
);
// PATCH /admin/config/instantsell - Update instant sell settings
fastify.patch(
"/config/instantsell",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
properties: {
enabled: { type: "boolean" },
payoutRate: { type: "number", minimum: 0, maximum: 1 },
minItemValue: { type: "number", minimum: 0 },
maxItemValue: { type: "number", minimum: 0 },
cs2: {
type: "object",
properties: {
enabled: { type: "boolean" },
payoutRate: { type: "number", minimum: 0, maximum: 1 },
},
},
rust: {
type: "object",
properties: {
enabled: { type: "boolean" },
payoutRate: { type: "number", minimum: 0, maximum: 1 },
},
},
},
},
},
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
// Update top-level instant sell settings
["enabled", "payoutRate", "minItemValue", "maxItemValue"].forEach(
(key) => {
if (request.body[key] !== undefined) {
config.instantSell[key] = request.body[key];
}
}
);
// Update CS2 settings
if (request.body.cs2) {
Object.keys(request.body.cs2).forEach((key) => {
if (request.body.cs2[key] !== undefined) {
config.instantSell.cs2[key] = request.body.cs2[key];
}
});
}
// Update Rust settings
if (request.body.rust) {
Object.keys(request.body.rust).forEach((key) => {
if (request.body.rust[key] !== undefined) {
config.instantSell.rust[key] = request.body.rust[key];
}
});
}
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`⚙️ Admin ${request.user.username} updated instant sell settings:`,
{
payoutRate: config.instantSell.payoutRate,
cs2PayoutRate: config.instantSell.cs2?.payoutRate,
rustPayoutRate: config.instantSell.rust?.payoutRate,
}
);
return reply.send({
success: true,
message: "Instant sell settings updated",
instantSell: config.instantSell,
});
} catch (error) {
console.error("❌ Failed to update instant sell settings:", error);
return reply.status(500).send({
success: false,
message: "Failed to update instant sell settings",
error: error.message,
});
}
}
);
// ============================================
// ANNOUNCEMENTS
// ============================================
// POST /admin/announcements - Create announcement
fastify.post(
"/announcements",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
required: ["message"],
properties: {
type: {
type: "string",
enum: ["info", "warning", "success", "error"],
default: "info",
},
message: { type: "string", minLength: 1 },
enabled: { type: "boolean", default: true },
startDate: { type: ["string", "null"] },
endDate: { type: ["string", "null"] },
dismissible: { type: "boolean", default: true },
},
},
},
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
const announcement = {
id: uuidv4(),
type: request.body.type || "info",
message: request.body.message,
enabled:
request.body.enabled !== undefined ? request.body.enabled : true,
startDate: request.body.startDate
? new Date(request.body.startDate)
: null,
endDate: request.body.endDate ? new Date(request.body.endDate) : null,
dismissible:
request.body.dismissible !== undefined
? request.body.dismissible
: true,
createdBy: request.user.username,
createdAt: new Date(),
};
config.announcements.push(announcement);
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`📢 Admin ${
request.user.username
} created announcement: ${announcement.message.substring(0, 50)}`
);
return reply.send({
success: true,
message: "Announcement created",
announcement,
});
} catch (error) {
console.error("❌ Failed to create announcement:", error);
return reply.status(500).send({
success: false,
message: "Failed to create announcement",
error: error.message,
});
}
}
);
// DELETE /admin/announcements/:id - Delete announcement
fastify.delete(
"/announcements/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const config = await SiteConfig.getConfig();
const index = config.announcements.findIndex((a) => a.id === id);
if (index === -1) {
return reply.status(404).send({
success: false,
message: "Announcement not found",
});
}
config.announcements.splice(index, 1);
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`🗑️ Admin ${request.user.username} deleted announcement ${id}`
);
return reply.send({
success: true,
message: "Announcement deleted",
});
} catch (error) {
console.error("❌ Failed to delete announcement:", error);
return reply.status(500).send({
success: false,
message: "Failed to delete announcement",
error: error.message,
});
}
}
);
// PATCH /admin/announcements/:id - Update announcement
fastify.patch(
"/announcements/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
properties: {
type: {
type: "string",
enum: ["info", "warning", "success", "error"],
},
message: { type: "string" },
enabled: { type: "boolean" },
startDate: { type: ["string", "null"] },
endDate: { type: ["string", "null"] },
dismissible: { type: "boolean" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const config = await SiteConfig.getConfig();
const announcement = config.announcements.find((a) => a.id === id);
if (!announcement) {
return reply.status(404).send({
success: false,
message: "Announcement not found",
});
}
Object.keys(request.body).forEach((key) => {
if (request.body[key] !== undefined) {
if (key === "startDate" || key === "endDate") {
announcement[key] = request.body[key]
? new Date(request.body[key])
: null;
} else {
announcement[key] = request.body[key];
}
}
});
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`📝 Admin ${request.user.username} updated announcement ${id}`
);
return reply.send({
success: true,
message: "Announcement updated",
announcement,
});
} catch (error) {
console.error("❌ Failed to update announcement:", error);
return reply.status(500).send({
success: false,
message: "Failed to update announcement",
error: error.message,
});
}
}
);
// ============================================
// PROMOTIONS
// ============================================
// POST /admin/promotions - Create promotion
fastify.post(
"/promotions",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
required: ["name", "description", "type"],
properties: {
name: { type: "string", minLength: 1 },
description: { type: "string", minLength: 1 },
type: {
type: "string",
enum: ["deposit_bonus", "discount", "free_item", "custom"],
},
enabled: { type: "boolean", default: true },
startDate: { type: ["string", "null"] },
endDate: { type: ["string", "null"] },
bonusPercentage: { type: "number", minimum: 0, maximum: 100 },
bonusAmount: { type: "number", minimum: 0 },
minDeposit: { type: "number", minimum: 0 },
maxBonus: { type: "number", minimum: 0 },
discountPercentage: { type: "number", minimum: 0, maximum: 100 },
maxUsesPerUser: { type: "integer", minimum: 1 },
maxTotalUses: { type: ["integer", "null"], minimum: 1 },
newUsersOnly: { type: "boolean", default: false },
code: { type: "string" },
bannerImage: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
console.log(
"🎁 Creating promotion with data:",
JSON.stringify(request.body, null, 2)
);
const config = await SiteConfig.getConfig();
if (!config) {
console.error("❌ SiteConfig not found or could not be retrieved");
return reply.status(500).send({
success: false,
message: "Site configuration not found",
});
}
console.log("✅ SiteConfig retrieved successfully");
const promotion = {
id: uuidv4(),
name: request.body.name,
description: request.body.description,
type: request.body.type,
enabled:
request.body.enabled !== undefined ? request.body.enabled : true,
startDate: request.body.startDate
? new Date(request.body.startDate)
: null,
endDate: request.body.endDate ? new Date(request.body.endDate) : null,
bonusPercentage: request.body.bonusPercentage || 0,
bonusAmount: request.body.bonusAmount || 0,
minDeposit: request.body.minDeposit || 0,
maxBonus: request.body.maxBonus || 0,
discountPercentage: request.body.discountPercentage || 0,
maxUsesPerUser: request.body.maxUsesPerUser || 1,
maxTotalUses: request.body.maxTotalUses || null,
currentUses: 0,
newUsersOnly: request.body.newUsersOnly || false,
code: request.body.code || null,
bannerImage: request.body.bannerImage || null,
createdBy: request.user.username,
createdAt: new Date(),
};
console.log(
"📝 Promotion object created:",
JSON.stringify(promotion, null, 2)
);
config.promotions.push(promotion);
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
console.log("💾 Attempting to save to database...");
await config.save();
console.log("✅ Database save successful");
console.log(
`🎁 Admin ${request.user.username} created promotion: ${promotion.name}`
);
return reply.send({
success: true,
message: "Promotion created",
promotion,
});
} catch (error) {
console.error("❌ Failed to create promotion:", error.message);
console.error("❌ Error name:", error.name);
console.error("❌ Error stack:", error.stack);
console.error(
"❌ Request body:",
JSON.stringify(request.body, null, 2)
);
console.error("❌ Request user:", request.user?.username);
if (error.name === "ValidationError") {
console.error("❌ Mongoose validation errors:", error.errors);
}
if (error.validation) {
console.error("❌ Fastify validation errors:", error.validation);
}
return reply.status(500).send({
success: false,
message: "Failed to create promotion",
error: error.message,
errorName: error.name,
details:
error.validation ||
(error.errors ? Object.keys(error.errors) : null),
});
}
}
);
// GET /admin/promotions - Get all promotions
fastify.get(
"/promotions",
{
preHandler: [authenticate, isAdmin],
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
// Get usage stats for each promotion
const promotionsWithStats = await Promise.all(
config.promotions.map(async (promo) => {
const stats = await PromoUsage.getPromoStats(promo.id);
return {
...promo.toObject(),
stats,
};
})
);
return reply.send({
success: true,
promotions: promotionsWithStats,
});
} catch (error) {
console.error("❌ Failed to get promotions:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve promotions",
error: error.message,
});
}
}
);
// PATCH /admin/promotions/:id - Update promotion
fastify.patch(
"/promotions/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
properties: {
name: { type: "string" },
description: { type: "string" },
enabled: { type: "boolean" },
startDate: { type: ["string", "null"] },
endDate: { type: ["string", "null"] },
bonusPercentage: { type: "number" },
bonusAmount: { type: "number" },
minDeposit: { type: "number" },
maxBonus: { type: "number" },
discountPercentage: { type: "number" },
maxUsesPerUser: { type: "integer" },
maxTotalUses: { type: "integer" },
newUsersOnly: { type: "boolean" },
code: { type: "string" },
bannerImage: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const config = await SiteConfig.getConfig();
const promotion = config.promotions.find((p) => p.id === id);
if (!promotion) {
return reply.status(404).send({
success: false,
message: "Promotion not found",
});
}
Object.keys(request.body).forEach((key) => {
if (request.body[key] !== undefined) {
if (key === "startDate" || key === "endDate") {
promotion[key] = request.body[key]
? new Date(request.body[key])
: null;
} else {
promotion[key] = request.body[key];
}
}
});
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`📝 Admin ${request.user.username} updated promotion ${promotion.name}`
);
return reply.send({
success: true,
message: "Promotion updated",
promotion,
});
} catch (error) {
console.error("❌ Failed to update promotion:", error);
return reply.status(500).send({
success: false,
message: "Failed to update promotion",
error: error.message,
});
}
}
);
// DELETE /admin/promotions/:id - Delete promotion
fastify.delete(
"/promotions/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const config = await SiteConfig.getConfig();
const index = config.promotions.findIndex((p) => p.id === id);
if (index === -1) {
return reply.status(404).send({
success: false,
message: "Promotion not found",
});
}
const promoName = config.promotions[index].name;
config.promotions.splice(index, 1);
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`🗑️ Admin ${request.user.username} deleted promotion ${promoName}`
);
return reply.send({
success: true,
message: "Promotion deleted",
});
} catch (error) {
console.error("❌ Failed to delete promotion:", error);
return reply.status(500).send({
success: false,
message: "Failed to delete promotion",
error: error.message,
});
}
}
);
// GET /admin/promotions/:id/stats - Get promotion statistics only
fastify.get(
"/promotions/:id/stats",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const stats = await PromoUsage.getPromoStats(id);
return reply.send({
success: true,
stats,
});
} catch (error) {
console.error("❌ Failed to get promotion stats:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve promotion statistics",
error: error.message,
});
}
}
);
// GET /admin/promotions/:id/usage - Get promotion usage details with user info
fastify.get(
"/promotions/:id/usage",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
querystring: {
type: "object",
properties: {
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
skip: { type: "integer", minimum: 0, default: 0 },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const { limit = 50, skip = 0 } = request.query;
const usages = await PromoUsage.find({ promoId: id })
.populate("userId", "username steamId avatar")
.sort({ usedAt: -1 })
.limit(limit)
.skip(skip);
const total = await PromoUsage.countDocuments({ promoId: id });
const stats = await PromoUsage.getPromoStats(id);
return reply.send({
success: true,
usages,
stats,
pagination: {
total,
limit,
skip,
hasMore: skip + limit < total,
},
});
} catch (error) {
console.error("❌ Failed to get promotion usage:", error);
return reply.status(500).send({
success: false,
message: "Failed to retrieve promotion usage",
error: error.message,
});
}
}
);
// ============================================
// BULK OPERATIONS
// ============================================
// POST /admin/users/bulk-ban - Bulk ban users
fastify.post(
"/users/bulk-ban",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
required: ["userIds", "banned"],
properties: {
userIds: {
type: "array",
items: { type: "string" },
minItems: 1,
},
banned: { type: "boolean" },
reason: { type: "string" },
duration: { type: "integer", minimum: 0 },
},
},
},
},
async (request, reply) => {
try {
const { userIds, banned, reason, duration = 0 } = request.body;
let expires = null;
if (banned && duration > 0) {
expires = new Date(Date.now() + duration * 60 * 60 * 1000);
}
const result = await User.updateMany(
{
_id: { $in: userIds },
staffLevel: { $lt: 3 }, // Don't ban admins
},
{
$set: {
"ban.banned": banned,
"ban.reason": banned ? reason || "Banned by administrator" : null,
"ban.expires": banned ? expires : null,
},
}
);
console.log(
`🔨 Admin ${request.user.username} bulk ${
banned ? "banned" : "unbanned"
} ${result.modifiedCount} users`
);
return reply.send({
success: true,
message: `${result.modifiedCount} users ${
banned ? "banned" : "unbanned"
}`,
modifiedCount: result.modifiedCount,
});
} catch (error) {
console.error("❌ Failed to bulk ban users:", error);
return reply.status(500).send({
success: false,
message: "Failed to perform bulk ban",
error: error.message,
});
}
}
);
}