Skip to content

Commit 79f6077

Browse files
authored
Merge pull request #78 from adityacosmos24/main
feat: added notification system
2 parents 89e3c4a + 8ffa33e commit 79f6077

File tree

8 files changed

+680
-2
lines changed

8 files changed

+680
-2
lines changed

backend/src/app.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import cartRoutes from "./routes/cartRoutes.js";
2020
import authRoutes from "./routes/auth.js";
2121
import auctionRoutes from "./routes/auctionRoutes";
2222
import paymentRoutes from "./routes/paymentRoutes";
23+
import notificationRoutes from "./routes/notificationRoutes";
2324

2425
import {
2526
signAccessToken,
@@ -30,8 +31,7 @@ import {
3031
import {
3132
authenticate,
3233
authorizeRole,
33-
authorizeRoles,
34-
AuthRequest
34+
authorizeRoles
3535
} from "./middleware/authMiddleware.js";
3636

3737
dotenv.config();
@@ -238,6 +238,9 @@ app.use("/api/auctions", apiRateLimiter, authenticate, authorizeRoles("senior",
238238
// Payments — senior + admin
239239
app.use("/api/payments", apiRateLimiter, authenticate, authorizeRoles("senior", "admin"), paymentRoutes);
240240

241+
// Notifications — all authenticated users
242+
app.use("/api/notifications", authenticate, notificationRoutes);
243+
241244
// Auth
242245
app.use("/api", authRoutes);
243246

backend/src/controllers/auction.controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Auction from "../models/auction.model";
33
import Bid from "../models/bid.model";
44
import Product from "../models/product.model";
55
import mongoose from "mongoose";
6+
import { notifyNewBid, notifyBidAccepted } from "../services/notification.service";
67

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

111+
// Notify seller about new bid (async, don't wait for it)
112+
notifyNewBid(auction._id, bid._id, amount).catch((error) => {
113+
console.error("Failed to send new bid notification:", error);
114+
});
115+
110116
res.status(201).json({
111117
message: "Bid placed successfully",
112118
bid,
@@ -154,6 +160,11 @@ export const acceptHighestBid = async (req: Request, res: Response): Promise<voi
154160
auction.soldPrice = auction.currentHighestBid;
155161
await auction.save();
156162

163+
// Notify seller about bid acceptance (async, don't wait for it)
164+
notifyBidAccepted(auction._id).catch((error) => {
165+
console.error("Failed to send bid accepted notification:", error);
166+
});
167+
157168
res.status(200).json({
158169
message: "Bid accepted successfully",
159170
auction,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Request, Response } from "express";
2+
import Notification from "../models/notification.model";
3+
import mongoose from "mongoose";
4+
5+
/**
6+
* Get all notifications for the authenticated user
7+
*/
8+
export const getNotifications = async (req: Request, res: Response): Promise<void> => {
9+
try {
10+
const userId = req.user?.id;
11+
if (!userId) {
12+
res.status(401).json({ message: "Unauthorized" });
13+
return;
14+
}
15+
16+
const { read, limit = 50, page = 1 } = req.query;
17+
const skip = (Number(page) - 1) * Number(limit);
18+
19+
const query: any = { userId: new mongoose.Types.ObjectId(userId) };
20+
if (read !== undefined) {
21+
query.read = read === "true";
22+
}
23+
24+
const notifications = await Notification.find(query)
25+
.populate("auctionId", "itemName")
26+
.sort({ createdAt: -1 })
27+
.limit(Number(limit))
28+
.skip(skip);
29+
30+
const total = await Notification.countDocuments(query);
31+
const unreadCount = await Notification.countDocuments({
32+
userId: new mongoose.Types.ObjectId(userId),
33+
read: false,
34+
});
35+
36+
res.status(200).json({
37+
notifications,
38+
pagination: {
39+
page: Number(page),
40+
limit: Number(limit),
41+
total,
42+
pages: Math.ceil(total / Number(limit)),
43+
},
44+
unreadCount,
45+
});
46+
} catch (error: any) {
47+
console.error("Get notifications error:", error);
48+
res.status(500).json({ message: error.message || "Server error" });
49+
}
50+
};
51+
52+
/**
53+
* Mark notification as read
54+
*/
55+
export const markAsRead = async (req: Request, res: Response): Promise<void> => {
56+
try {
57+
const userId = req.user?.id;
58+
if (!userId) {
59+
res.status(401).json({ message: "Unauthorized" });
60+
return;
61+
}
62+
63+
const { id } = req.params;
64+
const notification = await Notification.findOne({
65+
_id: id,
66+
userId: new mongoose.Types.ObjectId(userId),
67+
});
68+
69+
if (!notification) {
70+
res.status(404).json({ message: "Notification not found" });
71+
return;
72+
}
73+
74+
notification.read = true;
75+
await notification.save();
76+
77+
res.status(200).json({ message: "Notification marked as read", notification });
78+
} catch (error: any) {
79+
console.error("Mark as read error:", error);
80+
res.status(500).json({ message: error.message || "Server error" });
81+
}
82+
};
83+
84+
/**
85+
* Mark all notifications as read
86+
*/
87+
export const markAllAsRead = async (req: Request, res: Response): Promise<void> => {
88+
try {
89+
const userId = req.user?.id;
90+
if (!userId) {
91+
res.status(401).json({ message: "Unauthorized" });
92+
return;
93+
}
94+
95+
const result = await Notification.updateMany(
96+
{
97+
userId: new mongoose.Types.ObjectId(userId),
98+
read: false,
99+
},
100+
{ read: true }
101+
);
102+
103+
res.status(200).json({
104+
message: "All notifications marked as read",
105+
updatedCount: result.modifiedCount,
106+
});
107+
} catch (error: any) {
108+
console.error("Mark all as read error:", error);
109+
res.status(500).json({ message: error.message || "Server error" });
110+
}
111+
};
112+
113+
/**
114+
* Delete a notification
115+
*/
116+
export const deleteNotification = async (req: Request, res: Response): Promise<void> => {
117+
try {
118+
const userId = req.user?.id;
119+
if (!userId) {
120+
res.status(401).json({ message: "Unauthorized" });
121+
return;
122+
}
123+
124+
const { id } = req.params;
125+
const notification = await Notification.findOneAndDelete({
126+
_id: id,
127+
userId: new mongoose.Types.ObjectId(userId),
128+
});
129+
130+
if (!notification) {
131+
res.status(404).json({ message: "Notification not found" });
132+
return;
133+
}
134+
135+
res.status(200).json({ message: "Notification deleted successfully" });
136+
} catch (error: any) {
137+
console.error("Delete notification error:", error);
138+
res.status(500).json({ message: error.message || "Server error" });
139+
}
140+
};
141+
142+
/**
143+
* Get unread notification count
144+
*/
145+
export const getUnreadCount = async (req: Request, res: Response): Promise<void> => {
146+
try {
147+
const userId = req.user?.id;
148+
if (!userId) {
149+
res.status(401).json({ message: "Unauthorized" });
150+
return;
151+
}
152+
153+
const count = await Notification.countDocuments({
154+
userId: new mongoose.Types.ObjectId(userId),
155+
read: false,
156+
});
157+
158+
res.status(200).json({ unreadCount: count });
159+
} catch (error: any) {
160+
console.error("Get unread count error:", error);
161+
res.status(500).json({ message: error.message || "Server error" });
162+
}
163+
};
164+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import mongoose, { Document, Schema } from "mongoose";
2+
3+
export interface INotification extends Document {
4+
userId: mongoose.Types.ObjectId;
5+
type: "new_bid" | "bid_accepted" | "auction_expiring" | "auction_expired";
6+
title: string;
7+
message: string;
8+
auctionId?: mongoose.Types.ObjectId;
9+
bidId?: mongoose.Types.ObjectId;
10+
read: boolean;
11+
emailSent: boolean;
12+
pushSent: boolean;
13+
createdAt: Date;
14+
updatedAt: Date;
15+
}
16+
17+
const notificationSchema = new Schema<INotification>(
18+
{
19+
userId: {
20+
type: mongoose.Schema.Types.ObjectId,
21+
ref: "User",
22+
required: true,
23+
index: true,
24+
},
25+
type: {
26+
type: String,
27+
enum: ["new_bid", "bid_accepted", "auction_expiring", "auction_expired"],
28+
required: true,
29+
},
30+
title: {
31+
type: String,
32+
required: true,
33+
},
34+
message: {
35+
type: String,
36+
required: true,
37+
},
38+
auctionId: {
39+
type: mongoose.Schema.Types.ObjectId,
40+
ref: "Auction",
41+
index: true,
42+
},
43+
bidId: {
44+
type: mongoose.Schema.Types.ObjectId,
45+
ref: "Bid",
46+
},
47+
read: {
48+
type: Boolean,
49+
default: false,
50+
},
51+
emailSent: {
52+
type: Boolean,
53+
default: false,
54+
},
55+
pushSent: {
56+
type: Boolean,
57+
default: false,
58+
},
59+
},
60+
{ timestamps: true }
61+
);
62+
63+
// Index for efficient queries
64+
notificationSchema.index({ userId: 1, read: 1, createdAt: -1 });
65+
66+
export default mongoose.model<INotification>("Notification", notificationSchema);
67+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import express from "express";
2+
import {
3+
getNotifications,
4+
markAsRead,
5+
markAllAsRead,
6+
deleteNotification,
7+
getUnreadCount,
8+
} from "../controllers/notification.controller";
9+
import { authenticate } from "../middleware/authMiddleware";
10+
11+
const router = express.Router();
12+
13+
// All routes require authentication
14+
router.use(authenticate);
15+
16+
// Get all notifications (with optional filters)
17+
router.get("/", getNotifications);
18+
19+
// Get unread count
20+
router.get("/unread-count", getUnreadCount);
21+
22+
// Mark notification as read
23+
router.patch("/:id/read", markAsRead);
24+
25+
// Mark all notifications as read
26+
router.patch("/read-all", markAllAsRead);
27+
28+
// Delete notification
29+
router.delete("/:id", deleteNotification);
30+
31+
export default router;
32+

backend/src/server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import dotenv from "dotenv";
33
dotenv.config();
44

55
import app from './app';
6+
import { initializeAuctionScheduler } from './services/auctionScheduler.service';
67

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

910
app.listen(PORT, () => {
1011
console.log(`Server running on port ${PORT}`);
12+
13+
// Initialize auction scheduler for expiry notifications
14+
initializeAuctionScheduler();
1115
});

0 commit comments

Comments
 (0)