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, }); } } ); }