Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import userRoutes from "./routes/user.routes.js";
import productRoutes from "./routes/product.routes.js";
import cartRoutes from "./routes/cartRoutes.js";
import authRoutes from "./routes/auth.js";
import auctionRoutes from "./routes/auctionRoutes";

import { signAccessToken, signRefreshToken, verifyRefreshToken } from "./controllers/auth.js";
import { authenticate, AuthRequest } from "./middleware/authMiddleware.js";
Expand Down Expand Up @@ -198,6 +199,7 @@ app.use("/api/health", healthRoutes);
app.use("/api/products", productRoutes);
app.use("/api/users", userRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/auctions", auctionRoutes);
app.use("/api", authRoutes);

// ------------------- DEFAULT -------------------
Expand Down
225 changes: 225 additions & 0 deletions backend/src/controllers/auction.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { Request, Response } from "express";
import Auction from "../models/auction.model";
import Bid from "../models/bid.model";
import Product from "../models/product.model";
import mongoose from "mongoose";

//new auction
export const createAuction = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: "Unauthorized" });
return;
}
const { productId, startPrice, minIncrement, durationHours } = req.body;
if (!productId || !startPrice) {
res.status(400).json({ message: "Product ID and start price are required" });
return;
}
//if product exists
const product = await Product.findById(productId);
if (!product) {
res.status(404).json({ message: "Product not found" });
return;
}
const existingAuction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(productId), status: "active" });
if (existingAuction) {
res.status(400).json({ message: "An active auction already exists for this product" });
return;
}
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + (durationHours || 48));

const auction = new Auction({
productId: new mongoose.Types.ObjectId(productId),
itemName: product.name,
itemDescription: product.description,
startPrice,
currentHighestBid: startPrice,
minIncrement: minIncrement || 100,
seller: new mongoose.Types.ObjectId(userId),
expiresAt,
status: "active",
});
await auction.save();
res.status(201).json(auction);
} catch (error: any) {
console.error("Create auction error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

export const placeBid = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: "Unauthorized" });
return;
}

const { id } = req.params;
const { amount } = req.body;

if (!amount || amount <= 0) {
res.status(400).json({ message: "Valid bid amount is required" });
return;
}
let auction = await Auction.findById(id);
if (!auction) {
auction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(id), status: "active" });
}

if (!auction) {
res.status(404).json({ message: "Auction not found or not active" });
return;
}

if (auction.status !== "active") {
res.status(400).json({ message: "Auction is not active" });
return;
}

// Check if expired
if (new Date() > auction.expiresAt) {
auction.status = "expired";
await auction.save();
res.status(400).json({ message: "Auction has expired" });
return;
}
if (auction.seller.toString() === userId) {
res.status(400).json({ message: "You cannot bid on your own auction" });
return;
}
const minBid = auction.currentHighestBid + (auction.minIncrement || 100);
if (amount < minBid) {
res.status(400).json({ message: `Bid must be at least ₹${minBid}` });
return;
}
const bid = new Bid({
auctionId: auction._id,
bidder: new mongoose.Types.ObjectId(userId),
amount,
});

await bid.save();
auction.currentHighestBid = amount;
auction.highestBidder = new mongoose.Types.ObjectId(userId);
await auction.save();

res.status(201).json({
message: "Bid placed successfully",
bid,
auction: {
currentHighestBid: auction.currentHighestBid,
highestBidder: auction.highestBidder,
},
});
} catch (error: any) {
console.error("Place bid error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};
export const acceptHighestBid = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: "Unauthorized" });
return;
}

const { id } = req.params;
let auction = await Auction.findById(id);
if (!auction) {
auction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(id), status: "active" });
}

if (!auction) {
res.status(404).json({ message: "Auction not found" });
return;
}

if (auction.seller.toString() !== userId) {
res.status(403).json({ message: "Only the seller can accept bids" });
return;
}

if (!auction.highestBidder) {
res.status(400).json({ message: "No bids to accept" });
return;
}
// Mark auction as sold
auction.status = "sold";
auction.soldTo = auction.highestBidder;
auction.soldPrice = auction.currentHighestBid;
await auction.save();

res.status(200).json({
message: "Bid accepted successfully",
auction,
});
} catch (error: any) {
console.error("Accept bid error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

// Get auction by product ID
export const getAuctionByProductId = async (req: Request, res: Response): Promise<void> => {
try {
const { productId } = req.params;

const auction = await Auction.findOne({
productId: new mongoose.Types.ObjectId(productId),
status: "active",
}).populate("seller", "name email").populate("highestBidder", "name email");

if (!auction) {
res.status(404).json({ message: "No active auction found for this product" });
return;
}

//time left
const now = new Date();
const expiresAt = new Date(auction.expiresAt);
const timeLeftMs = expiresAt.getTime() - now.getTime();
const timeLeftHours = Math.max(0, Math.floor(timeLeftMs / (1000 * 60 * 60)));
const timeLeftMinutes = Math.max(0, Math.floor((timeLeftMs % (1000 * 60 * 60)) / (1000 * 60)));

res.status(200).json({
...auction.toObject(),
timeLeft: timeLeftMs > 0 ? `${timeLeftHours}h ${timeLeftMinutes}m` : "Expired",
isExpired: timeLeftMs <= 0,
});
} catch (error: any) {
console.error("Get auction error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

// Get all bids for an auction
export const getAuctionBids = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;

let auction = await Auction.findById(id);
if (!auction) {
auction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(id) });
}

if (!auction) {
res.status(404).json({ message: "Auction not found" });
return;
}

const bids = await Bid.find({ auctionId: auction._id })
.populate("bidder", "name email")
.sort({ createdAt: -1 });

res.status(200).json(bids);
} catch (error: any) {
console.error("Get bids error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

10 changes: 10 additions & 0 deletions backend/src/models/auction.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import mongoose from "mongoose";

const auctionSchema = new mongoose.Schema({
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
itemName: {
type: String,
required: true,
Expand All @@ -15,6 +20,11 @@ const auctionSchema = new mongoose.Schema({
required: true,
min: 0,
},
minIncrement: {
type: Number,
default: 100,
min: 1,
},
currentHighestBid: {
type: Number,
default: function () { return this.startPrice; },
Expand Down
17 changes: 10 additions & 7 deletions backend/src/routes/auctionRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import {
createAuction,
placeBid,
acceptHighestBid,
getAuctionByProductId,
getAuctionBids,
} from "../controllers/auction.controller";
import { authenticate, authorizeRole } from "../middleware/authMiddleware";
import { authenticate } from "../middleware/authMiddleware";

const router = express.Router();

// Create a new auction (only sellers)
router.post("/", authenticate, authorizeRole("seller"), createAuction);
router.get("/product/:productId", getAuctionByProductId);
router.get("/:id/bids", getAuctionBids);
router.post("/", authenticate, createAuction);

// Place a bid (only buyers)
router.post("/:id/bid", authenticate, authorizeRole("buyer"), placeBid);
// Place a bid (authenticated)
router.post("/:id/bid", authenticate, placeBid);

// Seller accepts the highest bid (only sellers)
router.post("/:id/accept", authenticate, authorizeRole("seller"), acceptHighestBid);
// Seller accepts the highest bid (authenticated)
router.post("/:id/accept", authenticate, acceptHighestBid);

export default router;
17 changes: 10 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CartProvider } from "./components/CartContext";
import Cart from "./pages/Cart";
import Payment from "./pages/Payment";
import Dashboard from "./pages/Dashboard";
import { NotificationProvider } from "./components/NotificationContext";
import RequestReset from "./pages/RequestReset";
import ResetPassword from "./pages/ResetPassword";

Expand All @@ -22,10 +23,11 @@ const App = () => (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
<BrowserRouter>
<Navbar />
<div className="pt-[50px]">
<CartProvider>
<NotificationProvider>
<CartProvider>
<BrowserRouter>
<Navbar />
<div className="pt-[50px]">
<Routes>
<Route path="/" element={<Index />} />
<Route path="/signup" element={<SignUp />} />
Expand All @@ -42,9 +44,10 @@ const App = () => (
<Route path="/request-reset" element={<RequestReset />} />
<Route path="/reset-password/:token" element={<ResetPassword />} />
</Routes>
</CartProvider>
</div>
</BrowserRouter>
</div>
</BrowserRouter>
</CartProvider>
</NotificationProvider>
</TooltipProvider>
</QueryClientProvider>
);
Expand Down
Loading
Loading