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
7 changes: 5 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import cartRoutes from "./routes/cartRoutes.js";
import authRoutes from "./routes/auth.js";
import auctionRoutes from "./routes/auctionRoutes";
import paymentRoutes from "./routes/paymentRoutes";
import notificationRoutes from "./routes/notificationRoutes";

import {
signAccessToken,
Expand All @@ -28,8 +29,7 @@ import {
import {
authenticate,
authorizeRole,
authorizeRoles,
AuthRequest
authorizeRoles
} from "./middleware/authMiddleware.js";

dotenv.config();
Expand Down Expand Up @@ -222,6 +222,9 @@ app.use("/api/auctions", authenticate, authorizeRoles("senior", "admin"), auctio
// Payments — senior + admin
app.use("/api/payments", authenticate, authorizeRoles("senior", "admin"), paymentRoutes);

// Notifications — all authenticated users
app.use("/api/notifications", authenticate, notificationRoutes);

// Auth
app.use("/api", authRoutes);

Expand Down
11 changes: 11 additions & 0 deletions backend/src/controllers/auction.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Auction from "../models/auction.model";
import Bid from "../models/bid.model";
import Product from "../models/product.model";
import mongoose from "mongoose";
import { notifyNewBid, notifyBidAccepted } from "../services/notification.service";

//new auction
export const createAuction = async (req: Request, res: Response): Promise<void> => {
Expand Down Expand Up @@ -107,6 +108,11 @@ export const placeBid = async (req: Request, res: Response): Promise<void> => {
auction.highestBidder = new mongoose.Types.ObjectId(userId);
await auction.save();

// Notify seller about new bid (async, don't wait for it)
notifyNewBid(auction._id, bid._id, amount).catch((error) => {
console.error("Failed to send new bid notification:", error);
});

res.status(201).json({
message: "Bid placed successfully",
bid,
Expand Down Expand Up @@ -154,6 +160,11 @@ export const acceptHighestBid = async (req: Request, res: Response): Promise<voi
auction.soldPrice = auction.currentHighestBid;
await auction.save();

// Notify seller about bid acceptance (async, don't wait for it)
notifyBidAccepted(auction._id).catch((error) => {
console.error("Failed to send bid accepted notification:", error);
});

res.status(200).json({
message: "Bid accepted successfully",
auction,
Expand Down
164 changes: 164 additions & 0 deletions backend/src/controllers/notification.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Request, Response } from "express";
import Notification from "../models/notification.model";
import mongoose from "mongoose";

/**
* Get all notifications for the authenticated user
*/
export const getNotifications = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: "Unauthorized" });
return;
}

const { read, limit = 50, page = 1 } = req.query;
const skip = (Number(page) - 1) * Number(limit);

const query: any = { userId: new mongoose.Types.ObjectId(userId) };
if (read !== undefined) {
query.read = read === "true";
}

const notifications = await Notification.find(query)
.populate("auctionId", "itemName")
.sort({ createdAt: -1 })
.limit(Number(limit))
.skip(skip);

const total = await Notification.countDocuments(query);
const unreadCount = await Notification.countDocuments({
userId: new mongoose.Types.ObjectId(userId),
read: false,
});

res.status(200).json({
notifications,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / Number(limit)),
},
unreadCount,
});
} catch (error: any) {
console.error("Get notifications error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

/**
* Mark notification as read
*/
export const markAsRead = 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 notification = await Notification.findOne({
_id: id,
userId: new mongoose.Types.ObjectId(userId),
});

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

notification.read = true;
await notification.save();

res.status(200).json({ message: "Notification marked as read", notification });
} catch (error: any) {
console.error("Mark as read error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

/**
* Mark all notifications as read
*/
export const markAllAsRead = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.id;
if (!userId) {
res.status(401).json({ message: "Unauthorized" });
return;
}

const result = await Notification.updateMany(
{
userId: new mongoose.Types.ObjectId(userId),
read: false,
},
{ read: true }
);

res.status(200).json({
message: "All notifications marked as read",
updatedCount: result.modifiedCount,
});
} catch (error: any) {
console.error("Mark all as read error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

/**
* Delete a notification
*/
export const deleteNotification = 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 notification = await Notification.findOneAndDelete({
_id: id,
userId: new mongoose.Types.ObjectId(userId),
});

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

res.status(200).json({ message: "Notification deleted successfully" });
} catch (error: any) {
console.error("Delete notification error:", error);
res.status(500).json({ message: error.message || "Server error" });
}
};

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

const count = await Notification.countDocuments({
userId: new mongoose.Types.ObjectId(userId),
read: false,
});

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

67 changes: 67 additions & 0 deletions backend/src/models/notification.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import mongoose, { Document, Schema } from "mongoose";

export interface INotification extends Document {
userId: mongoose.Types.ObjectId;
type: "new_bid" | "bid_accepted" | "auction_expiring" | "auction_expired";
title: string;
message: string;
auctionId?: mongoose.Types.ObjectId;
bidId?: mongoose.Types.ObjectId;
read: boolean;
emailSent: boolean;
pushSent: boolean;
createdAt: Date;
updatedAt: Date;
}

const notificationSchema = new Schema<INotification>(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
index: true,
},
type: {
type: String,
enum: ["new_bid", "bid_accepted", "auction_expiring", "auction_expired"],
required: true,
},
title: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
auctionId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Auction",
index: true,
},
bidId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Bid",
},
read: {
type: Boolean,
default: false,
},
emailSent: {
type: Boolean,
default: false,
},
pushSent: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);

// Index for efficient queries
notificationSchema.index({ userId: 1, read: 1, createdAt: -1 });

export default mongoose.model<INotification>("Notification", notificationSchema);

32 changes: 32 additions & 0 deletions backend/src/routes/notificationRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import express from "express";
import {
getNotifications,
markAsRead,
markAllAsRead,
deleteNotification,
getUnreadCount,
} from "../controllers/notification.controller";
import { authenticate } from "../middleware/authMiddleware";

const router = express.Router();

// All routes require authentication
router.use(authenticate);

// Get all notifications (with optional filters)
router.get("/", getNotifications);

// Get unread count
router.get("/unread-count", getUnreadCount);

// Mark notification as read
router.patch("/:id/read", markAsRead);

// Mark all notifications as read
router.patch("/read-all", markAllAsRead);

// Delete notification
router.delete("/:id", deleteNotification);

export default router;

4 changes: 4 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import dotenv from "dotenv";
dotenv.config();

import app from './app';
import { initializeAuctionScheduler } from './services/auctionScheduler.service';

const PORT = process.env.PORT ? Number(process.env.PORT) : 5000;

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);

// Initialize auction scheduler for expiry notifications
initializeAuctionScheduler();
});
Loading
Loading