475 lines
13 KiB
JavaScript
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",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
}
|