first commit

This commit is contained in:
2026-01-10 04:57:43 +00:00
parent 16a76a2cd6
commit 232968de1e
131 changed files with 43262 additions and 0 deletions

442
routes/market.js Normal file
View File

@@ -0,0 +1,442 @@
import Fastify from 'fastify';
import Item from '../models/Item.js';
import { authenticate, optionalAuthenticate } from '../middleware/auth.js';
/**
* Market routes for browsing and purchasing items
* @param {FastifyInstance} fastify
* @param {Object} options
*/
export default async function marketRoutes(fastify, options) {
// GET /market/items - Browse marketplace items
fastify.get('/items', {
preHandler: optionalAuthenticate,
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', minimum: 1, default: 1 },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 24 },
search: { type: 'string' },
game: { type: 'string', enum: ['cs2', 'rust'] },
category: { type: 'string' },
rarity: { type: 'string' },
wear: { type: 'string' },
minPrice: { type: 'number', minimum: 0 },
maxPrice: { type: 'number', minimum: 0 },
sortBy: { type: 'string', enum: ['price_asc', 'price_desc', 'name_asc', 'name_desc', 'date_new', 'date_old'], default: 'date_new' },
statTrak: { type: 'boolean' },
souvenir: { type: 'boolean' },
}
}
}
}, async (request, reply) => {
try {
const {
page = 1,
limit = 24,
search,
game,
category,
rarity,
wear,
minPrice,
maxPrice,
sortBy = 'date_new',
statTrak,
souvenir,
} = request.query;
// Build query
const query = { status: 'active' };
if (search) {
query.$or = [
{ name: { $regex: search, $options: 'i' } },
{ description: { $regex: search, $options: 'i' } }
];
}
if (game) query.game = game;
if (category && category !== 'all') query.category = category;
if (rarity) query.rarity = rarity;
if (wear) query.wear = wear;
if (statTrak !== undefined) query.statTrak = statTrak;
if (souvenir !== undefined) query.souvenir = souvenir;
if (minPrice !== undefined || maxPrice !== undefined) {
query.price = {};
if (minPrice !== undefined) query.price.$gte = minPrice;
if (maxPrice !== undefined) query.price.$lte = maxPrice;
}
// Build sort
let sort = {};
switch (sortBy) {
case 'price_asc':
sort = { price: 1 };
break;
case 'price_desc':
sort = { price: -1 };
break;
case 'name_asc':
sort = { name: 1 };
break;
case 'name_desc':
sort = { name: -1 };
break;
case 'date_new':
sort = { listedAt: -1 };
break;
case 'date_old':
sort = { listedAt: 1 };
break;
default:
sort = { listedAt: -1 };
}
// Execute query with pagination
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
Item.find(query)
.sort(sort)
.skip(skip)
.limit(limit)
.populate('seller', 'username avatar steamId')
.lean(),
Item.countDocuments(query)
]);
return reply.send({
success: true,
items,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
});
} catch (error) {
fastify.log.error(error);
return reply.status(500).send({
success: false,
message: 'Failed to fetch items',
});
}
});
// GET /market/featured - Get featured items
fastify.get('/featured', {
preHandler: optionalAuthenticate,
}, async (request, reply) => {
try {
const items = await Item.find({
status: 'active',
featured: true
})
.sort({ listedAt: -1 })
.limit(12)
.populate('seller', 'username avatar steamId')
.lean();
return reply.send({
success: true,
items,
});
} catch (error) {
fastify.log.error(error);
return reply.status(500).send({
success: false,
message: 'Failed to fetch featured items',
});
}
});
// GET /market/recent-sales - Get recent sales
fastify.get('/recent-sales', {
preHandler: optionalAuthenticate,
schema: {
querystring: {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
}
}
}
}, async (request, reply) => {
try {
const { limit = 10 } = request.query;
const sales = await Item.find({ status: 'sold' })
.sort({ soldAt: -1 })
.limit(limit)
.populate('seller', 'username avatar steamId')
.populate('buyer', 'username avatar steamId')
.lean();
// Format for frontend
const formattedSales = sales.map(sale => ({
id: sale._id,
itemName: sale.name,
itemImage: sale.image,
wear: sale.wear,
price: sale.price,
soldAt: sale.soldAt,
seller: sale.seller,
buyer: sale.buyer,
}));
return reply.send({
success: true,
sales: formattedSales,
});
} catch (error) {
fastify.log.error(error);
return reply.status(500).send({
success: false,
message: 'Failed to fetch recent sales',
});
}
});
// GET /market/items/:id - Get single item details
fastify.get('/items/:id', {
preHandler: optionalAuthenticate,
schema: {
params: {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string' }
}
}
}
}, async (request, reply) => {
try {
const { id } = request.params;
const item = await Item.findById(id)
.populate('seller', 'username avatar steamId')
.lean();
if (!item) {
return reply.status(404).send({
success: false,
message: 'Item not found',
});
}
// Increment views (don't await)
Item.findByIdAndUpdate(id, { $inc: { views: 1 } }).exec();
return reply.send({
success: true,
item,
});
} catch (error) {
fastify.log.error(error);
return reply.status(500).send({
success: false,
message: 'Failed to fetch item',
});
}
});
// POST /market/purchase/:id - Purchase an item
fastify.post('/purchase/:id', {
preHandler: authenticate,
schema: {
params: {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string' }
}
}
}
}, async (request, reply) => {
try {
const { id } = request.params;
const userId = request.user.userId;
// Find item
const item = await Item.findById(id);
if (!item) {
return reply.status(404).send({
success: false,
message: 'Item not found',
});
}
if (item.status !== 'active') {
return reply.status(400).send({
success: false,
message: 'Item is not available for purchase',
});
}
// Check if user is trying to buy their own item
if (item.seller.toString() === userId) {
return reply.status(400).send({
success: false,
message: 'You cannot purchase your own item',
});
}
// Get user to check balance
const User = fastify.mongoose.model('User');
const user = await User.findById(userId);
if (!user) {
return reply.status(404).send({
success: false,
message: 'User not found',
});
}
if (user.balance < item.price) {
return reply.status(400).send({
success: false,
message: 'Insufficient balance',
});
}
// Check if user has trade URL set
if (!user.tradeUrl) {
return reply.status(400).send({
success: false,
message: 'Please set your trade URL in profile settings',
});
}
// Process purchase (in a transaction would be better)
// Deduct from buyer
user.balance -= item.price;
await user.save();
// Add to seller
const seller = await User.findById(item.seller);
if (seller) {
seller.balance += item.price;
await seller.save();
}
// Mark item as sold
await item.markAsSold(userId);
// TODO: Send trade offer via Steam bot
// TODO: Send notifications via WebSocket
// Broadcast to WebSocket clients
if (fastify.websocketManager) {
fastify.websocketManager.broadcastPublic('item_sold', {
itemId: item._id,
itemName: item.name,
price: item.price,
});
// Send notification to buyer
fastify.websocketManager.sendToUser(user.steamId, {
type: 'item_purchased',
data: {
itemId: item._id,
itemName: item.name,
price: item.price,
}
});
// Send notification to seller
if (seller) {
fastify.websocketManager.sendToUser(seller.steamId, {
type: 'item_sold',
data: {
itemId: item._id,
itemName: item.name,
price: item.price,
buyer: user.username,
}
});
}
// Update balance for buyer
fastify.websocketManager.sendToUser(user.steamId, {
type: 'balance_update',
data: {
balance: user.balance,
}
});
// Update balance for seller
if (seller) {
fastify.websocketManager.sendToUser(seller.steamId, {
type: 'balance_update',
data: {
balance: seller.balance,
}
});
}
}
return reply.send({
success: true,
message: 'Purchase successful! You will receive a trade offer shortly.',
item,
newBalance: user.balance,
});
} catch (error) {
fastify.log.error(error);
return reply.status(500).send({
success: false,
message: 'Failed to process purchase',
});
}
});
// GET /market/stats - Get marketplace statistics
fastify.get('/stats', {
preHandler: optionalAuthenticate,
}, async (request, reply) => {
try {
const [
totalActive,
totalSold,
totalValue,
averagePrice,
] = await Promise.all([
Item.countDocuments({ status: 'active' }),
Item.countDocuments({ status: 'sold' }),
Item.aggregate([
{ $match: { status: 'active' } },
{ $group: { _id: null, total: { $sum: '$price' } } }
]),
Item.aggregate([
{ $match: { status: 'active' } },
{ $group: { _id: null, avg: { $avg: '$price' } } }
]),
]);
return reply.send({
success: true,
stats: {
totalActive,
totalSold,
totalValue: totalValue[0]?.total || 0,
averagePrice: averagePrice[0]?.avg || 0,
}
});
} catch (error) {
fastify.log.error(error);
return reply.status(500).send({
success: false,
message: 'Failed to fetch statistics',
});
}
});
}