- Add user management system with all CRUD operations - Add promotion statistics dashboard with export - Simplify Trading & Market settings UI - Fix promotion schema (dates now optional) - Add missing API endpoints and PATCH support - Add comprehensive documentation - Fix critical bugs (deletePromotion, duplicate endpoints) All features tested and production-ready.
23 KiB
Complete Admin Features Implementation Plan
🎯 Project Goal
Fully implement all admin panel features so they actually work and enforce the configured settings across the entire TurboTrades platform.
📋 Phase 1: Maintenance Mode & Core Infrastructure (Priority: CRITICAL)
1.1 Maintenance Mode Enforcement
Files to Create/Modify:
middleware/maintenance.js(exists, needs enhancement)index.js(register middleware globally)frontend/src/views/MaintenancePage.vue(new)
Implementation:
// Global hook in index.js
fastify.addHook('preHandler', async (request, reply) => {
// Skip for certain routes (health, auth callback)
const skipRoutes = ['/health', '/api/health', '/auth/steam/callback'];
if (skipRoutes.includes(request.url)) return;
const config = await SiteConfig.getConfig();
if (config.maintenance.enabled) {
// Check scheduled times
if (config.maintenance.scheduledStart && config.maintenance.scheduledEnd) {
const now = new Date();
if (now < config.maintenance.scheduledStart || now > config.maintenance.scheduledEnd) {
return; // Outside maintenance window
}
}
// Allow admins and whitelisted users
if (request.user?.isAdmin) return;
if (config.maintenance.allowedSteamIds?.includes(request.user?.steamId)) return;
// Block everyone else
return reply.status(503).send({
success: false,
error: 'Maintenance Mode',
message: config.maintenance.message || 'Site is under maintenance',
scheduledEnd: config.maintenance.scheduledEnd
});
}
});
Frontend:
- Create maintenance page component
- Show maintenance message
- Show countdown if scheduled end time exists
- Admin bypass indicator
Testing:
- Toggle maintenance ON → non-admin users blocked ✓
- Admin users can still access ✓
- Whitelisted Steam IDs can access ✓
- Scheduled maintenance activates/deactivates ✓
📋 Phase 2: Market Settings Integration (Priority: HIGH)
2.1 Market Enable/Disable
Files to Modify:
routes/market.jsfrontend/src/views/MarketPage.vue
Implementation:
// Add to ALL market routes
const checkMarketEnabled = async (request, reply) => {
const config = await SiteConfig.getConfig();
if (!config.market.enabled) {
return reply.status(503).send({
success: false,
message: 'Marketplace is currently disabled'
});
}
};
// Apply to routes
fastify.get('/listings', { preHandler: [checkMarketEnabled] }, ...);
fastify.post('/listings', { preHandler: [authenticate, checkMarketEnabled] }, ...);
Frontend:
- Show "Market Disabled" message when trying to access
- Disable market navigation when disabled
- Admin can still access for configuration
2.2 Price Limits Enforcement
Implementation:
// In listing creation
const config = await SiteConfig.getConfig();
if (price < config.market.minListingPrice) {
return reply.status(400).send({
success: false,
message: `Price must be at least $${config.market.minListingPrice}`
});
}
if (price > config.market.maxListingPrice) {
return reply.status(400).send({
success: false,
message: `Price cannot exceed $${config.market.maxListingPrice}`
});
}
Frontend Validation:
- Add min/max attributes to price inputs
- Show real-time validation
- Fetch limits from
/api/config/status
2.3 Commission Application
Implementation:
// When item is sold
const config = await SiteConfig.getConfig();
const commission = salePrice * config.market.commission;
const sellerProceeds = salePrice - commission;
// Update seller balance
seller.balance += sellerProceeds;
await seller.save();
// Record commission
await Transaction.create({
user: seller._id,
type: 'market_sale',
amount: sellerProceeds,
description: `Sold ${item.name}`,
metadata: {
itemId: item._id,
salePrice: salePrice,
commission: commission,
commissionRate: config.market.commission
}
});
// Record platform revenue
await Transaction.create({
type: 'platform_commission',
amount: commission,
description: `Commission from sale of ${item.name}`,
metadata: {
itemId: item._id,
salePrice: salePrice,
seller: seller._id
}
});
2.4 Auto Price Updates
Files to Create:
services/priceUpdater.jsjobs/updatePrices.js
Implementation:
// services/priceUpdater.js
class PriceUpdater {
async updateAllPrices() {
const config = await SiteConfig.getConfig();
if (!config.market.autoUpdatePrices) return;
// Fetch latest prices from external API (Steam, CSGOFloat, etc)
const items = await Item.find({ status: 'listed' });
for (const item of items) {
const newPrice = await this.fetchLatestPrice(item);
if (newPrice && Math.abs(newPrice - item.price) > 0.01) {
item.marketPrice = newPrice;
// Optionally update listing price within bounds
await item.save();
}
}
}
async start() {
const config = await SiteConfig.getConfig();
const interval = config.market.priceUpdateInterval || 3600000; // 1 hour default
setInterval(() => this.updateAllPrices(), interval);
}
}
📋 Phase 3: Trading System Implementation (Priority: HIGH)
3.1 Deposit System
Files to Create:
routes/trading.js(new)models/Deposit.js(new)services/steamBot.js(enhance existing)
Database Schema:
// models/Deposit.js
const depositSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
items: [{
assetId: String,
name: String,
marketHashName: String,
iconUrl: String,
rarity: String,
value: Number
}],
totalValue: { type: Number, required: true },
tradeOfferId: String,
botId: String,
status: {
type: String,
enum: ['pending', 'accepted', 'declined', 'cancelled', 'completed', 'failed'],
default: 'pending'
},
promotion: {
id: String,
code: String,
bonusAmount: Number,
bonusPercentage: Number
},
bonusApplied: { type: Number, default: 0 },
finalAmount: Number,
createdAt: { type: Date, default: Date.now },
completedAt: Date
});
API Endpoints:
// POST /api/trading/deposit/initiate
// - Check if deposits enabled
// - Check minimum deposit
// - Validate items
// - Check for active promotions
// - Create trade offer via Steam bot
// - Return trade offer URL
// POST /api/trading/deposit/cancel
// - Cancel pending deposit
// - Cancel trade offer
// GET /api/trading/deposit/history
// - Get user's deposit history
Implementation:
// POST /api/trading/deposit/initiate
fastify.post('/deposit/initiate', {
preHandler: [authenticate],
schema: {
body: {
type: 'object',
required: ['items'],
properties: {
items: { type: 'array' },
promoCode: { type: 'string' }
}
}
}
}, async (request, reply) => {
const config = await SiteConfig.getConfig();
// Check if deposits enabled
if (!config.trading.enabled || !config.trading.depositEnabled) {
return reply.status(503).send({
success: false,
message: 'Deposits are currently disabled'
});
}
const { items, promoCode } = request.body;
// Calculate total value
let totalValue = items.reduce((sum, item) => sum + item.value, 0);
// Check minimum deposit
if (totalValue < config.trading.minDeposit) {
return reply.status(400).send({
success: false,
message: `Minimum deposit is $${config.trading.minDeposit}`
});
}
// Check max items
if (items.length > config.trading.maxItemsPerTrade) {
return reply.status(400).send({
success: false,
message: `Maximum ${config.trading.maxItemsPerTrade} items per trade`
});
}
// Check for active promotion
let promotion = null;
let bonusAmount = 0;
if (promoCode) {
const promoResult = await validateAndApplyPromotion(
request.user,
promoCode,
totalValue,
config
);
if (promoResult.valid) {
promotion = promoResult.promotion;
bonusAmount = promoResult.bonusAmount;
}
}
const finalAmount = totalValue + bonusAmount;
// Create deposit record
const deposit = await Deposit.create({
user: request.user._id,
items,
totalValue,
bonusApplied: bonusAmount,
finalAmount,
promotion: promotion ? {
id: promotion.id,
code: promotion.code,
bonusAmount: promotion.bonusAmount,
bonusPercentage: promotion.bonusPercentage
} : null,
status: 'pending'
});
// Create trade offer via Steam bot
const tradeOffer = await steamBot.createDepositOffer(
request.user.tradeUrl,
items,
deposit._id
);
deposit.tradeOfferId = tradeOffer.id;
deposit.botId = tradeOffer.botId;
await deposit.save();
return reply.send({
success: true,
deposit: {
id: deposit._id,
totalValue,
bonusAmount,
finalAmount,
tradeUrl: tradeOffer.url
}
});
});
3.2 Withdrawal System
Files to Create:
models/Withdrawal.js- Routes in
routes/trading.js
Database Schema:
const withdrawalSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
items: [{
itemId: { type: Schema.Types.ObjectId, ref: 'Item' },
name: String,
value: Number
}],
totalValue: { type: Number, required: true },
fee: { type: Number, required: true },
feePercentage: Number,
amountDeducted: Number,
tradeOfferId: String,
botId: String,
status: {
type: String,
enum: ['pending', 'processing', 'sent', 'accepted', 'declined', 'cancelled'],
default: 'pending'
},
createdAt: { type: Date, default: Date.now },
processedAt: Date
});
Implementation:
// POST /api/trading/withdraw/request
fastify.post('/withdraw/request', {
preHandler: [authenticate]
}, async (request, reply) => {
const config = await SiteConfig.getConfig();
// Check if withdrawals enabled
if (!config.trading.enabled || !config.trading.withdrawEnabled) {
return reply.status(503).send({
success: false,
message: 'Withdrawals are currently disabled'
});
}
// Check trade URL
if (!request.user.tradeUrl) {
return reply.status(400).send({
success: false,
message: 'Please set your trade URL first'
});
}
const { itemIds } = request.body;
// Fetch items from user's inventory
const items = await Item.find({
_id: { $in: itemIds },
owner: request.user._id,
status: 'owned'
});
if (items.length !== itemIds.length) {
return reply.status(400).send({
success: false,
message: 'Some items not found or not owned by you'
});
}
// Calculate total value
const totalValue = items.reduce((sum, item) => sum + item.value, 0);
// Check minimum withdrawal
if (totalValue < config.trading.minWithdraw) {
return reply.status(400).send({
success: false,
message: `Minimum withdrawal is $${config.trading.minWithdraw}`
});
}
// Calculate fee
const feePercentage = config.trading.withdrawFee;
const fee = totalValue * feePercentage;
const amountDeducted = totalValue + fee;
// Check user balance (if paying fee from balance)
if (request.user.balance < fee) {
return reply.status(400).send({
success: false,
message: `Insufficient balance for withdrawal fee ($${fee.toFixed(2)})`
});
}
// Create withdrawal
const withdrawal = await Withdrawal.create({
user: request.user._id,
items: items.map(item => ({
itemId: item._id,
name: item.name,
value: item.value
})),
totalValue,
fee,
feePercentage,
amountDeducted,
status: 'pending'
});
// Deduct fee from balance
request.user.balance -= fee;
await request.user.save();
// Mark items as withdrawing
await Item.updateMany(
{ _id: { $in: itemIds } },
{ status: 'withdrawing', withdrawalId: withdrawal._id }
);
// Create transaction record
await Transaction.create({
user: request.user._id,
type: 'withdrawal_fee',
amount: -fee,
description: `Withdrawal fee for ${items.length} items`,
metadata: {
withdrawalId: withdrawal._id,
itemCount: items.length,
totalValue
}
});
return reply.send({
success: true,
withdrawal: {
id: withdrawal._id,
totalValue,
fee,
status: 'pending',
message: 'Withdrawal request created. Admin will process it soon.'
}
});
});
📋 Phase 4: Promotions System (Priority: HIGH)
4.1 Promotion Validation & Application
Files to Create:
services/promotionService.js
Implementation:
// services/promotionService.js
class PromotionService {
async validatePromoCode(code, user, amount) {
const config = await SiteConfig.getConfig();
// Find promotion
const promotion = config.promotions.find(p =>
p.code === code && p.enabled
);
if (!promotion) {
return { valid: false, message: 'Invalid promo code' };
}
// Check dates
const now = new Date();
if (promotion.startDate && now < new Date(promotion.startDate)) {
return { valid: false, message: 'Promotion not started yet' };
}
if (promotion.endDate && now > new Date(promotion.endDate)) {
return { valid: false, message: 'Promotion has expired' };
}
// Check new users only
if (promotion.newUsersOnly) {
const firstDeposit = await Deposit.findOne({ user: user._id, status: 'completed' });
if (firstDeposit) {
return { valid: false, message: 'Promotion is for new users only' };
}
}
// Check minimum deposit
if (amount < promotion.minDeposit) {
return {
valid: false,
message: `Minimum deposit for this promo is $${promotion.minDeposit}`
};
}
// Check usage limits
const usageCount = await PromoUsage.countDocuments({
promotionId: promotion.id,
user: user._id
});
if (promotion.maxUsesPerUser && usageCount >= promotion.maxUsesPerUser) {
return { valid: false, message: 'You have used this promotion too many times' };
}
const totalUsage = await PromoUsage.countDocuments({
promotionId: promotion.id
});
if (promotion.maxTotalUses && totalUsage >= promotion.maxTotalUses) {
return { valid: false, message: 'Promotion usage limit reached' };
}
// Calculate bonus
let bonusAmount = 0;
if (promotion.bonusPercentage > 0) {
bonusAmount = amount * (promotion.bonusPercentage / 100);
}
if (promotion.bonusAmount > 0) {
bonusAmount = promotion.bonusAmount;
}
// Apply max bonus cap
if (promotion.maxBonus > 0 && bonusAmount > promotion.maxBonus) {
bonusAmount = promotion.maxBonus;
}
return {
valid: true,
promotion,
bonusAmount
};
}
async recordUsage(promotion, user, deposit) {
await PromoUsage.create({
promotionId: promotion.id,
promotionCode: promotion.code,
user: user._id,
depositId: deposit._id,
bonusAmount: deposit.bonusApplied,
usedAt: new Date()
});
}
}
4.2 Frontend Promo Code Input
Files to Modify:
frontend/src/views/DepositPage.vue
Implementation:
<template>
<div class="deposit-page">
<!-- ... existing deposit UI ... -->
<div class="promo-section">
<label>Promo Code (Optional)</label>
<div class="promo-input-group">
<input
v-model="promoCode"
placeholder="Enter promo code"
@input="validatePromo"
/>
<button @click="applyPromo" :disabled="!promoCode">
Apply
</button>
</div>
<div v-if="promoResult.valid" class="promo-success">
✓ {{ promoResult.promotion.name }}: +${{ promoResult.bonusAmount.toFixed(2) }} bonus!
</div>
<div v-if="promoResult.error" class="promo-error">
{{ promoResult.error }}
</div>
</div>
<!-- Show total with bonus -->
<div class="deposit-summary">
<div>Deposit Value: ${{ depositValue.toFixed(2) }}</div>
<div v-if="promoResult.bonusAmount > 0" class="bonus-line">
Bonus: +${{ promoResult.bonusAmount.toFixed(2) }}
</div>
<div class="total-line">
Total: ${{ (depositValue + promoResult.bonusAmount).toFixed(2) }}
</div>
</div>
</div>
</template>
<script setup>
const promoCode = ref('');
const promoResult = ref({ valid: false, bonusAmount: 0 });
const validatePromo = async () => {
if (!promoCode.value) {
promoResult.value = { valid: false, bonusAmount: 0 };
return;
}
try {
const response = await axios.post('/api/config/promotions/validate', {
code: promoCode.value,
amount: depositValue.value
});
if (response.data.valid) {
promoResult.value = response.data;
} else {
promoResult.value = { valid: false, error: response.data.message };
}
} catch (error) {
promoResult.value = { valid: false, error: 'Failed to validate promo code' };
}
};
</script>
📋 Phase 5: Frontend Integration (Priority: MEDIUM)
5.1 Config Status Endpoint
Files to Modify:
routes/config.js
Implementation:
// GET /api/config/status - Public endpoint
fastify.get('/status', async (request, reply) => {
const config = await SiteConfig.getConfig();
return reply.send({
success: true,
maintenance: {
enabled: config.maintenance.enabled,
message: config.maintenance.message,
scheduledEnd: config.maintenance.scheduledEnd
},
trading: {
enabled: config.trading.enabled,
depositEnabled: config.trading.depositEnabled,
withdrawEnabled: config.trading.withdrawEnabled,
minDeposit: config.trading.minDeposit,
minWithdraw: config.trading.minWithdraw,
withdrawFee: config.trading.withdrawFee
},
market: {
enabled: config.market.enabled,
minListingPrice: config.market.minListingPrice,
maxListingPrice: config.market.maxListingPrice,
commission: config.market.commission
}
});
});
5.2 Frontend Status Store
Files to Create:
frontend/src/stores/config.js
Implementation:
import { defineStore } from 'pinia';
import axios from '@/utils/axios';
export const useConfigStore = defineStore('config', {
state: () => ({
status: {
maintenance: { enabled: false },
trading: { enabled: true },
market: { enabled: true }
},
loaded: false
}),
getters: {
isMaintenanceMode: (state) => state.status.maintenance.enabled,
isTradingEnabled: (state) => state.status.trading.enabled,
isMarketEnabled: (state) => state.status.market.enabled,
canDeposit: (state) => state.status.trading.depositEnabled,
canWithdraw: (state) => state.status.trading.withdrawEnabled
},
actions: {
async fetchStatus() {
try {
const response = await axios.get('/api/config/status');
if (response.data.success) {
this.status = response.data;
this.loaded = true;
}
} catch (error) {
console.error('Failed to fetch config status:', error);
}
}
}
});
5.3 Route Guards
Files to Modify:
frontend/src/router/index.js
Implementation:
router.beforeEach(async (to, from, next) => {
const configStore = useConfigStore();
if (!configStore.loaded) {
await configStore.fetchStatus();
}
// Block market access if disabled
if (to.path.startsWith('/market') && !configStore.isMarketEnabled) {
toast.warning('Marketplace is currently disabled');
next({ name: 'Home' });
return;
}
// Block deposit if disabled
if (to.path === '/deposit' && !configStore.canDeposit) {
toast.warning('Deposits are currently disabled');
next({ name: 'Home' });
return;
}
// Block withdraw if disabled
if (to.path === '/withdraw' && !configStore.canWithdraw) {
toast.warning('Withdrawals are currently disabled');
next({ name: 'Home' });
return;
}
next();
});
📋 Phase 6: Testing & Polish (Priority: MEDIUM)
6.1 Admin Testing Checklist
- Toggle maintenance ON → site blocks non-admins
- Toggle maintenance OFF → site accessible again
- Scheduled maintenance activates at correct time
- Disable deposits → deposit page blocked
- Disable withdrawals → withdraw page blocked
- Disable market → market page blocked
- Change min deposit → enforced on deposit attempts
- Change min withdraw → enforced on withdraw attempts
- Change commission → applied to new sales
- Apply promo code → bonus calculated correctly
- Promo usage limit → blocked after max uses
- New user only promo → blocked for existing users
6.2 Error Handling
- Graceful degradation if config fetch fails
- Clear error messages for users
- Admin notifications for system issues
- Fallback to default values if config missing
6.3 Performance Optimization
- Cache config in memory (refresh every 5 minutes)
- Avoid fetching config on every request
- Use Redis for config caching in production
- Batch price updates
📋 Phase 7: Documentation & Deployment (Priority: LOW)
7.1 API Documentation
- Document all new endpoints
- Add request/response examples
- Error code reference
- Rate limiting info
7.2 Admin Guide
- How to enable/disable features
- How to create promotions
- How to schedule maintenance
- Common troubleshooting
7.3 Deployment Checklist
- Environment variables configured
- Database migrations run
- Steam bot configured
- Price API keys set
- Monitoring enabled
- Backup strategy in place
🚀 Implementation Order
Week 1: Critical Infrastructure
- Day 1-2: Maintenance mode enforcement
- Day 3-4: Market settings integration
- Day 5: Config status endpoint & frontend store
Week 2: Trading System
- Day 1-3: Deposit system implementation
- Day 4-5: Withdrawal system implementation
Week 3: Promotions & Polish
- Day 1-2: Promotions validation & application
- Day 3-4: Frontend integration & UI
- Day 5: Testing & bug fixes
Week 4: Advanced Features
- Day 1-2: Auto price updates
- Day 3: Scheduled maintenance automation
- Day 4-5: Documentation & final testing
📊 Success Metrics
Functionality
- All admin toggles actually work
- Settings are enforced across the platform
- Promotions apply correctly
- Commission is calculated and recorded
- Limits are respected
Performance
- Config fetch < 50ms
- No performance degradation from checks
- Cache hit rate > 95%
User Experience
- Clear error messages
- No unexpected blocking
- Smooth transitions
- Proper feedback
🎯 Final Deliverables
-
Backend
- Trading routes fully implemented
- All admin settings enforced
- Promotion system working
- Proper error handling
- Comprehensive logging
-
Frontend
- Deposit/withdraw pages functional
- Promo code input working
- Real-time validation
- Config-aware navigation
- Clear status indicators
-
Admin Panel
- All settings actually work
- Real-time effect on site
- Usage statistics visible
- Easy testing/debugging
-
Documentation
- API documentation complete
- Admin guide written
- Deployment guide ready
- Troubleshooting reference
Estimated Total Time: 3-4 weeks of focused development Complexity: Medium-High Risk Level: Medium (Steam bot integration, real money handling) Priority: High (Core functionality for platform)