first commit
This commit is contained in:
474
routes/marketplace.example.js
Normal file
474
routes/marketplace.example.js
Normal file
@@ -0,0 +1,474 @@
|
||||
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",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user