feat: standardize admin form limits and guidance

This commit is contained in:
Tống Thành Đạt
2026-04-10 15:55:15 +07:00
parent 7ce5921fe0
commit 51c6303437
34 changed files with 1692 additions and 361 deletions

View File

@@ -2,10 +2,33 @@ const { addBaseUrlToImages } = require("../utils/imageHelper");
const AboutUs = require("../models/aboutUs");
const Blog = require("../models/blog");
const jsonHelper = require("../utils/jsonHelper");
const {
validateLengthRules,
summarizeLengthErrors,
} = require("../utils/lengthValidation");
const {
ABOUT_US_LENGTH_RULES,
} = require("../constants/contentLengthRules");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
const handleLengthValidation = (validation, req, res, options = {}) => {
const message =
summarizeLengthErrors(validation, 3) || "One or more fields exceed the allowed length.";
if (options.json) {
return res.status(400).json({
success: false,
error: message,
errors: validation.errors,
});
}
req.flash("error_msg", message);
return res.redirect(options.redirectTo || "/admin/about-us");
};
/**
* GET /api/about
* Lấy dữ liệu About Us (Public API cho website và CMS load dữ liệu)
@@ -110,6 +133,11 @@ exports.updateAbout = async (req, res) => {
// ✅ Capture BEFORE state
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
const validation = validateLengthRules(updateData, ABOUT_US_LENGTH_RULES);
if (!validation.valid) {
return handleLengthValidation(validation, req, res, { json: true });
}
// Use .set() for better handling of nested objects/arrays in Mongoose
doc.set(updateData);
await doc.save();
@@ -210,6 +238,13 @@ exports.update = async (req, res) => {
// ✅ Capture BEFORE state
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
const validation = validateLengthRules(updateData, ABOUT_US_LENGTH_RULES);
if (!validation.valid) {
return handleLengthValidation(validation, req, res, {
redirectTo: `/admin/about-us?activeTab=${req.query.activeTab || "hero"}`,
});
}
doc.set(updateData);
await doc.save();

View File

@@ -1,6 +1,22 @@
const {addBaseUrlToImages} = require("../utils/imageHelper");
const Activity = require("../models/activity");
const mongoose = require('mongoose');
const {
validateLengthRules,
summarizeLengthErrors,
} = require("../utils/lengthValidation");
const {
ACTIVITY_LENGTH_RULES,
} = require("../constants/contentLengthRules");
const getActivityLengthMessage = (validation) =>
summarizeLengthErrors(validation, 3) ||
"One or more fields exceed the allowed length.";
const redirectWithLengthError = (req, res, path, validation) => {
req.flash("error_msg", getActivityLengthMessage(validation));
return req.session.save(() => res.redirect(path));
};
// -------------------- Public (API) exports --------------------
@@ -302,6 +318,15 @@ exports.updateFilters = async (req, res) => {
try {
// Provide minimal valid fields when inserting a new filters document so
// schema validators (e.g., age validator) do not fail on upsert.
const filterLengthValidation = validateLengthRules(
{ filters: sanitizedFilters },
ACTIVITY_LENGTH_RULES,
);
if (!filterLengthValidation.valid) {
req.flash("error_msg", getActivityLengthMessage(filterLengthValidation));
return res.redirect("/admin/activity");
}
const setOnInsert = {
name: "_filters_doc",
price: 0,
@@ -353,6 +378,14 @@ exports.updateHero = async (req, res) => {
bannerImageBooking: bannerImageBooking || '/uploads/banner/b9.jpg',
};
const heroLengthValidation = validateLengthRules(
{ hero },
ACTIVITY_LENGTH_RULES,
);
if (!heroLengthValidation.valid) {
return redirectWithLengthError(req, res, "/admin/activity", heroLengthValidation);
}
// Update all activity docs to keep hero consistent
await Activity.updateMany({ isFiltersDoc: { $ne: true } }, { $set: { hero } });
@@ -413,6 +446,16 @@ exports.create = async (req, res) => {
try {
const activityData = parseActivityFormData(req.body);
const lengthValidation = validateLengthRules(activityData, ACTIVITY_LENGTH_RULES);
if (!lengthValidation.valid) {
return redirectWithLengthError(
req,
res,
"/admin/activity/create",
lengthValidation,
);
}
const newActivity = new Activity(activityData);
await newActivity.save();
@@ -465,6 +508,16 @@ exports.update = async (req, res) => {
// Force status to active on update (always set isActive true when editing)
activityData.isActive = true;
const lengthValidation = validateLengthRules(activityData, ACTIVITY_LENGTH_RULES);
if (!lengthValidation.valid) {
return redirectWithLengthError(
req,
res,
`/admin/activity/${req.params.id}/edit`,
lengthValidation,
);
}
await Activity.findByIdAndUpdate(req.params.id, activityData, {new: true});
req.flash("success_msg", "Activity updated successfully");

View File

@@ -2,6 +2,17 @@ const fs = require('fs');
const path = require('path');
const { addBaseUrlToImages } = require("../utils/imageHelper");
const Booking = require("../models/booking");
const {
validateLengthRules,
summarizeLengthErrors,
} = require("../utils/lengthValidation");
const {
BOOKING_LENGTH_RULES,
} = require("../constants/contentLengthRules");
const getBookingLengthMessage = (validation) =>
summarizeLengthErrors(validation, 3) ||
"One or more fields exceed the allowed length.";
// -------------------- Public helpers --------------------
const getBookingData = async () => {
@@ -398,6 +409,12 @@ exports.update = async (req, res) => {
return req.session.save(() => res.redirect("/admin/booking"));
}
const lengthValidation = validateLengthRules(updateData, BOOKING_LENGTH_RULES);
if (!lengthValidation.valid) {
req.flash("error_msg", getBookingLengthMessage(lengthValidation));
return req.session.save(() => res.redirect("/admin/booking"));
}
// Validate data structure
const validation = validateBookingData(updateData);
if (!validation.isValid) {
@@ -546,4 +563,4 @@ const getFinalBooking = (staticBooking, dbBooking) => {
}
return merged;
};
};

View File

@@ -7,13 +7,13 @@ const formController = {
try {
res.render('admin/form/index', {
layout: 'layouts/admin',
title: 'Quản lý Form',
title: 'Form Management',
user: req.session.user,
});
} catch (error) {
console.error('Error loading form management page:', error);
res.status(500).render('error', {
message: 'Lỗi khi tải trang quản lý form',
message: 'Failed to load the form management page',
error: error
});
}
@@ -29,16 +29,16 @@ const formController = {
res.json({
success: true,
message: 'Cập nhật form thành công'
message: 'Form settings updated successfully'
});
} catch (error) {
console.error('Error updating form:', error);
res.status(500).json({
success: false,
message: 'Lỗi khi cập nhật form'
message: 'Failed to update form settings'
});
}
}
};
module.exports = formController;
module.exports = formController;

View File

@@ -118,7 +118,7 @@ const normalizeStoredImagePath = (imagePath) => {
const getDefaultFloatingContactData = () => ({
enabled: true,
position: "bottom-right",
panelTitle: "Anh chị cần tư vấn hay hỗ trợ gì thêm không ạ?",
panelTitle: "Do you need any additional advice or support?",
brand: {
imageSrc: "/assets/img/logo/black-logo.svg",
imageAlt: "HAI Learning",
@@ -132,7 +132,7 @@ const getDefaultFloatingContactData = () => ({
id: "facebook",
platform: "facebook",
enabled: true,
label: "Nhắn tin qua Facebook",
label: "Message via Facebook",
subtitle: "facebook.com/hailearning.edu.vn",
href: "https://www.facebook.com/hailearning.edu.vn/",
iconImage: "/uploads/home/floating-contact/Facebook_Logo_Primary.webp",
@@ -145,7 +145,7 @@ const getDefaultFloatingContactData = () => ({
id: "zalo",
platform: "zalo",
enabled: true,
label: "Nhắn tin qua Zalo",
label: "Message via Zalo",
subtitle: "zalo.me/84961834040",
href: "https://zalo.me/84961834040",
iconImage: "/uploads/home/floating-contact/Icon_of_Zalo.svg.webp",

View File

@@ -8,7 +8,7 @@ exports.getAllPages = async (req, res) => {
const pages = content.pages || [];
res.render('admin/pages/index', {
title: 'Quản lý trang',
title: 'Page Management',
pages
});
} catch (err) {
@@ -21,7 +21,7 @@ exports.getAllPages = async (req, res) => {
// Hiển thị form tạo trang mới
exports.getAddPage = (req, res) => {
res.render('admin/pages/add', {
title: 'Thêm trang mới'
title: 'Add New Page'
});
};
@@ -95,7 +95,7 @@ exports.getEditPage = async (req, res) => {
}
res.render('admin/pages/edit', {
title: 'Chỉnh sửa trang',
title: 'Edit Page',
page
});
} catch (err) {
@@ -225,4 +225,4 @@ exports.getPageBySlug = async (req, res) => {
message: 'An error occurred while loading the page. Please try again later.'
});
}
};
};

View File

@@ -1,8 +1,19 @@
const Pricing = require("../models/pricing");
const {
validateLengthRules,
summarizeLengthErrors,
} = require("../utils/lengthValidation");
const {
PRICING_LENGTH_RULES,
} = require("../constants/contentLengthRules");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
const getLengthValidationMessage = (validation) =>
summarizeLengthErrors(validation, 3) ||
"One or more fields exceed the allowed length.";
// ==================== CMS ADMIN FUNCTIONS ====================
// Render admin page for pricing management
@@ -86,6 +97,20 @@ exports.update = async (req, res) => {
? JSON.parse(testimonials)
: testimonials;
const validation = validateLengthRules(
{
hero: heroData,
pricingSection: pricingSectionData,
plans: plansData,
testimonials: testimonialsData,
},
PRICING_LENGTH_RULES,
);
if (!validation.valid) {
req.flash("error", getLengthValidationMessage(validation));
return res.redirect("/admin/pricing");
}
let pricing = await Pricing.findOne({ name: "default" });
// ✅ Capture BEFORE state

View File

@@ -7,11 +7,11 @@ exports.getSettings = async (req, res) => {
const content = readJsonFile('content');
const settings = content.settings || {
siteName: 'CMS-SIMS',
description: 'Hệ thống quản lý nội dung đơn giản'
description: 'Simple content management system'
};
res.render('admin/settings', {
title: 'Cài đặt hệ thống',
title: 'System Settings',
settings
});
} catch (err) {
@@ -53,4 +53,4 @@ exports.updateSettings = async (req, res) => {
req.flash('error_msg', 'Error updating settings');
res.redirect('/admin/settings');
}
};
};