Files
cms.techvanguard.vn/controllers/newsletterSubscriptionController.js
Tống Thành Đạt f6f2e0e03b feat(admin): implement unified submission and newsletter management
Introduce a centralized system to manage all website form submissions and newsletter subscriptions.
- Add `Submission` and `NewsletterSubscription` models with MongoDB schema validation
- Implement `submissionController` and `newsletterSubscriptionController` for CRUD operations and filtering
- Create a unified admin UI for reviewing submissions across different sources (home, request, contact, partnership, newsletter)
- Add database migration scripts for creating collections and indexes
- Refactor partnership inquiry forms to use a fixed field structure
- Update admin navigation and server CORS settings to support PATCH requests
2026-04-24 17:11:23 +07:00

195 lines
5.4 KiB
JavaScript

const NewsletterSubscription = require("../models/newsletterSubscription");
const emailPattern = /^\S+@\S+\.\S+$/;
function cleanString(value) {
return String(value || "").trim();
}
function normalizeEmail(value) {
return cleanString(value).toLowerCase();
}
function toSubmissionShape(item) {
return {
_id: item._id,
source: "newsletter",
name: item.email,
email: item.email,
phone: "",
pageUrl: item.pageUrl || "",
status: item.status,
internalNote: item.internalNote || "",
ipAddress: item.ipAddress || "",
userAgent: item.userAgent || "",
payload: {
email: item.email,
},
createdAt: item.createdAt,
updatedAt: item.updatedAt,
};
}
function buildListQuery(query) {
const mongoQuery = {};
const search = cleanString(query.search);
const status = cleanString(query.status);
if (status && NewsletterSubscription.STATUSES.includes(status)) {
mongoQuery.status = status;
}
if (query.startDate || query.endDate) {
mongoQuery.createdAt = {};
if (query.startDate) {
mongoQuery.createdAt.$gte = new Date(query.startDate);
}
if (query.endDate) {
const end = new Date(query.endDate);
end.setHours(23, 59, 59, 999);
mongoQuery.createdAt.$lte = end;
}
}
if (search) {
mongoQuery.email = new RegExp(
search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
"i",
);
}
return mongoQuery;
}
exports.subscribe = async (req, res) => {
try {
const email = normalizeEmail(req.body.email);
if (!email || !emailPattern.test(email)) {
return res.status(400).json({ success: false, error: "Please enter a valid email" });
}
const subscription = await NewsletterSubscription.findOneAndUpdate(
{ email },
{
$set: {
email,
status: "subscribed",
pageUrl: cleanString(req.body.pageUrl) || "/",
ipAddress: req.ip || req.connection?.remoteAddress || "",
userAgent: req.get("User-Agent") || "",
},
$setOnInsert: {
internalNote: "",
},
},
{ new: true, upsert: true, runValidators: true, setDefaultsOnInsert: true },
);
return res.status(201).json({
success: true,
message: "Subscription received",
data: {
id: subscription._id,
email: subscription.email,
status: subscription.status,
},
});
} catch (error) {
if (error.name === "ValidationError") {
const errors = Object.values(error.errors).map((item) => item.message);
return res.status(400).json({ success: false, error: errors.join(", ") });
}
if (error.code === 11000) {
return res.status(409).json({ success: false, error: "Email is already subscribed" });
}
console.error("newsletter.subscribe error:", error);
return res.status(500).json({ success: false, error: "Error subscribing email" });
}
};
exports.list = async (req, res) => {
try {
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 20, 1), 100);
const skip = (page - 1) * limit;
const query = buildListQuery(req.query);
const [items, total] = await Promise.all([
NewsletterSubscription.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean(),
NewsletterSubscription.countDocuments(query),
]);
return res.json({
success: true,
data: items.map(toSubmissionShape),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
},
});
} catch (error) {
console.error("newsletter.list error:", error);
return res.status(500).json({ success: false, error: "Error loading subscriptions" });
}
};
exports.detail = async (req, res) => {
try {
const subscription = await NewsletterSubscription.findById(req.params.id).lean();
if (!subscription) {
return res.status(404).json({ success: false, error: "Subscription not found" });
}
return res.json({ success: true, data: toSubmissionShape(subscription) });
} catch (error) {
console.error("newsletter.detail error:", error);
return res.status(500).json({ success: false, error: "Error loading subscription" });
}
};
exports.update = async (req, res) => {
try {
const update = {};
const status = cleanString(req.body.status);
if (status) {
if (!NewsletterSubscription.STATUSES.includes(status)) {
return res.status(400).json({ success: false, error: "Invalid status" });
}
update.status = status;
}
if (req.body.internalNote !== undefined) {
update.internalNote = cleanString(req.body.internalNote);
}
const subscription = await NewsletterSubscription.findByIdAndUpdate(
req.params.id,
update,
{ new: true, runValidators: true },
);
if (!subscription) {
return res.status(404).json({ success: false, error: "Subscription not found" });
}
return res.json({ success: true, data: toSubmissionShape(subscription) });
} catch (error) {
if (error.name === "ValidationError") {
const errors = Object.values(error.errors).map((item) => item.message);
return res.status(400).json({ success: false, error: errors.join(", ") });
}
console.error("newsletter.update error:", error);
return res.status(500).json({ success: false, error: "Error updating subscription" });
}
};