forked from UKSOURCE/cms.lams
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
195 lines
5.4 KiB
JavaScript
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" });
|
|
}
|
|
};
|