Files
TurboTrades/routes/marketplace.example.js
2026-01-10 04:57:43 +00:00

475 lines
13 KiB
JavaScript

import { authenticate, optionalAuthenticate } from "../middleware/auth.js";
import { wsManager } from "../utils/websocket.js";
/**
* Example marketplace routes demonstrating WebSocket broadcasting
* This shows how to integrate real-time updates for listings, prices, etc.
* @param {FastifyInstance} fastify - Fastify instance
*/
export default async function marketplaceRoutes(fastify, options) {
// Get all listings (public endpoint with optional auth for user-specific data)
fastify.get(
"/marketplace/listings",
{
preHandler: optionalAuthenticate,
schema: {
querystring: {
type: "object",
properties: {
game: { type: "string", enum: ["cs2", "rust"] },
minPrice: { type: "number" },
maxPrice: { type: "number" },
search: { type: "string" },
page: { type: "number", default: 1 },
limit: { type: "number", default: 20, maximum: 100 },
},
},
},
},
async (request, reply) => {
try {
const { game, minPrice, maxPrice, search, page = 1, limit = 20 } = request.query;
// TODO: Implement actual database query
// This is a placeholder showing the structure
const listings = {
items: [
{
id: "listing_123",
itemName: "AK-47 | Redline",
game: "cs2",
price: 45.99,
seller: {
steamId: "76561198012345678",
username: "TraderPro",
},
condition: "Field-Tested",
float: 0.23,
createdAt: new Date(),
},
],
pagination: {
page,
limit,
total: 1,
pages: 1,
},
};
return reply.send({
success: true,
listings: listings.items,
pagination: listings.pagination,
});
} catch (error) {
console.error("Error fetching listings:", error);
return reply.status(500).send({
error: "InternalServerError",
message: "Failed to fetch listings",
});
}
}
);
// Create new listing (authenticated users only)
fastify.post(
"/marketplace/listings",
{
preHandler: authenticate,
schema: {
body: {
type: "object",
required: ["itemName", "game", "price"],
properties: {
itemName: { type: "string" },
game: { type: "string", enum: ["cs2", "rust"] },
price: { type: "number", minimum: 0.01 },
description: { type: "string", maxLength: 500 },
assetId: { type: "string" },
condition: { type: "string" },
float: { type: "number" },
},
},
},
},
async (request, reply) => {
try {
const { itemName, game, price, description, assetId, condition, float } = request.body;
const user = request.user;
// Verify user has verified email (optional security check)
if (!user.email?.verified) {
return reply.status(403).send({
error: "Forbidden",
message: "Email verification required to create listings",
});
}
// Verify user has trade URL set
if (!user.tradeUrl) {
return reply.status(400).send({
error: "ValidationError",
message: "Trade URL must be set before creating listings",
});
}
// TODO: Implement actual listing creation in database
const newListing = {
id: `listing_${Date.now()}`,
itemName,
game,
price,
description,
assetId,
condition,
float,
seller: {
id: user._id,
steamId: user.steamId,
username: user.username,
avatar: user.avatar,
},
status: "active",
createdAt: new Date(),
};
// Broadcast new listing to all connected clients
wsManager.broadcastPublic("new_listing", {
listing: newListing,
message: `New ${game.toUpperCase()} item listed: ${itemName}`,
});
console.log(`📢 Broadcasted new listing: ${itemName} by ${user.username}`);
return reply.status(201).send({
success: true,
message: "Listing created successfully",
listing: newListing,
});
} catch (error) {
console.error("Error creating listing:", error);
return reply.status(500).send({
error: "InternalServerError",
message: "Failed to create listing",
});
}
}
);
// Update listing price (seller only)
fastify.patch(
"/marketplace/listings/:listingId/price",
{
preHandler: authenticate,
schema: {
params: {
type: "object",
required: ["listingId"],
properties: {
listingId: { type: "string" },
},
},
body: {
type: "object",
required: ["price"],
properties: {
price: { type: "number", minimum: 0.01 },
},
},
},
},
async (request, reply) => {
try {
const { listingId } = request.params;
const { price } = request.body;
const user = request.user;
// TODO: Fetch listing from database and verify ownership
// This is a placeholder
const listing = {
id: listingId,
itemName: "AK-47 | Redline",
game: "cs2",
oldPrice: 45.99,
seller: {
id: user._id.toString(),
steamId: user.steamId,
username: user.username,
},
};
// Verify user owns the listing
if (listing.seller.id !== user._id.toString()) {
return reply.status(403).send({
error: "Forbidden",
message: "You can only update your own listings",
});
}
// Update price (TODO: Update in database)
const updatedListing = {
...listing,
price,
updatedAt: new Date(),
};
// Broadcast price update to all clients
wsManager.broadcastPublic("price_update", {
listingId,
itemName: listing.itemName,
oldPrice: listing.oldPrice,
newPrice: price,
percentChange: ((price - listing.oldPrice) / listing.oldPrice * 100).toFixed(2),
});
console.log(`📢 Broadcasted price update for listing ${listingId}`);
return reply.send({
success: true,
message: "Price updated successfully",
listing: updatedListing,
});
} catch (error) {
console.error("Error updating listing price:", error);
return reply.status(500).send({
error: "InternalServerError",
message: "Failed to update listing price",
});
}
}
);
// Purchase item
fastify.post(
"/marketplace/listings/:listingId/purchase",
{
preHandler: authenticate,
schema: {
params: {
type: "object",
required: ["listingId"],
properties: {
listingId: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { listingId } = request.params;
const buyer = request.user;
// TODO: Fetch listing from database
const listing = {
id: listingId,
itemName: "AK-47 | Redline",
game: "cs2",
price: 45.99,
seller: {
id: "different_user_id",
steamId: "76561198012345678",
username: "TraderPro",
},
status: "active",
};
// Prevent self-purchase
if (listing.seller.id === buyer._id.toString()) {
return reply.status(400).send({
error: "ValidationError",
message: "You cannot purchase your own listing",
});
}
// Check if listing is still active
if (listing.status !== "active") {
return reply.status(400).send({
error: "ValidationError",
message: "This listing is no longer available",
});
}
// Check buyer balance
if (buyer.balance < listing.price) {
return reply.status(400).send({
error: "InsufficientFunds",
message: `Insufficient balance. Required: $${listing.price}, Available: $${buyer.balance}`,
});
}
// TODO: Process transaction in database
// - Deduct from buyer balance
// - Add to seller balance
// - Create transaction record
// - Update listing status to "sold"
// - Create trade offer via Steam API
const transaction = {
id: `tx_${Date.now()}`,
listingId,
itemName: listing.itemName,
price: listing.price,
buyer: {
id: buyer._id,
steamId: buyer.steamId,
username: buyer.username,
},
seller: listing.seller,
status: "processing",
createdAt: new Date(),
};
// Notify seller via WebSocket
wsManager.sendToUser(listing.seller.id, {
type: "item_sold",
data: {
transaction,
message: `Your ${listing.itemName} has been sold for $${listing.price}!`,
},
});
// Notify buyer
wsManager.sendToUser(buyer._id.toString(), {
type: "purchase_confirmed",
data: {
transaction,
message: `Purchase confirmed! Trade offer will be sent shortly.`,
},
});
// Broadcast listing removal to all clients
wsManager.broadcastPublic("listing_sold", {
listingId,
itemName: listing.itemName,
price: listing.price,
});
console.log(`💰 Item sold: ${listing.itemName} - Buyer: ${buyer.username}, Seller: ${listing.seller.username}`);
return reply.send({
success: true,
message: "Purchase successful! Trade offer will be sent to your Steam account.",
transaction,
});
} catch (error) {
console.error("Error processing purchase:", error);
return reply.status(500).send({
error: "InternalServerError",
message: "Failed to process purchase",
});
}
}
);
// Delete listing (seller or admin)
fastify.delete(
"/marketplace/listings/:listingId",
{
preHandler: authenticate,
schema: {
params: {
type: "object",
required: ["listingId"],
properties: {
listingId: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { listingId } = request.params;
const user = request.user;
// TODO: Fetch listing from database
const listing = {
id: listingId,
itemName: "AK-47 | Redline",
seller: {
id: user._id.toString(),
steamId: user.steamId,
username: user.username,
},
};
// Check permissions (owner or admin)
if (listing.seller.id !== user._id.toString() && user.staffLevel < 3) {
return reply.status(403).send({
error: "Forbidden",
message: "You can only delete your own listings",
});
}
// TODO: Delete from database
// Broadcast listing removal
wsManager.broadcastPublic("listing_removed", {
listingId,
itemName: listing.itemName,
reason: user.staffLevel >= 3 ? "Removed by admin" : "Removed by seller",
});
console.log(`🗑️ Listing deleted: ${listingId} by ${user.username}`);
return reply.send({
success: true,
message: "Listing deleted successfully",
});
} catch (error) {
console.error("Error deleting listing:", error);
return reply.status(500).send({
error: "InternalServerError",
message: "Failed to delete listing",
});
}
}
);
// Get user's own listings
fastify.get(
"/marketplace/my-listings",
{
preHandler: authenticate,
schema: {
querystring: {
type: "object",
properties: {
status: { type: "string", enum: ["active", "sold", "cancelled"] },
page: { type: "number", default: 1 },
limit: { type: "number", default: 20, maximum: 100 },
},
},
},
},
async (request, reply) => {
try {
const { status, page = 1, limit = 20 } = request.query;
const user = request.user;
// TODO: Fetch user's listings from database
const listings = {
items: [],
pagination: {
page,
limit,
total: 0,
pages: 0,
},
};
return reply.send({
success: true,
listings: listings.items,
pagination: listings.pagination,
});
} catch (error) {
console.error("Error fetching user listings:", error);
return reply.status(500).send({
error: "InternalServerError",
message: "Failed to fetch your listings",
});
}
}
);
}