diff --git a/controllers/aboutUsController.js b/controllers/aboutUsController.js deleted file mode 100644 index a41af32..0000000 --- a/controllers/aboutUsController.js +++ /dev/null @@ -1,251 +0,0 @@ -const { addBaseUrlToImages } = require("../utils/imageHelper"); -const AboutUs = require("../models/aboutUs"); -const Blog = require("../models/blog"); -const jsonHelper = require("../utils/jsonHelper"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); - -/** - * GET /api/about - * Lấy dữ liệu About Us (Public API cho website và CMS load dữ liệu) - */ -exports.getAbout = async (req, res) => { - try { - // Force no-cache headers - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader("Pragma", "no-cache"); - res.setHeader("Expires", "0"); - - const data = await AboutUs.getSingle(); - const rawData = data.toObject(); - - // === Dynamic Blog News Section === - const news = rawData.news || {}; - let blogs = []; - - // Nếu có chọn blog cụ thể - if (news.selectedBlogIds && news.selectedBlogIds.length > 0) { - blogs = await Blog.find({ - _id: { $in: news.selectedBlogIds }, - status: "published", - }).lean(); - - // Sắp xếp theo thứ tự đã chọn trong selectedBlogIds - blogs.sort((a, b) => { - return ( - news.selectedBlogIds.indexOf(a._id.toString()) - - news.selectedBlogIds.indexOf(b._id.toString()) - ); - }); - } - - // Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất - if (blogs.length === 0) { - blogs = await Blog.find({ status: "published" }) - .sort({ createdAt: -1 }) - .limit(3) - .lean(); - } - - // Map dữ liệu blog sang format mà frontend mong đợi - news.items = blogs.map((blog) => ({ - title: blog.title, - category: blog.category && blog.category[0] ? blog.category[0] : "Visa", - date: - blog.publishedAt || - new Date(blog.createdAt).toLocaleDateString("en-GB", { - day: "numeric", - month: "long", - year: "numeric", - }), - comments: blog.commentsCount || 0, - author: { - name: blog.author || "Admin", - avatar: "/assets/img/home-1/news/client.png", // Default avatar - }, - link: `/blog/${blog.slug}`, - thumbnail: blog.featuredImage, - })); - - rawData.news = news; - // =============================== - - const baseUrl = - process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`; - const processedData = addBaseUrlToImages(rawData, baseUrl); - - res.json(processedData); - } catch (error) { - console.error("Error getting about data:", error); - res.status(500).json({ - success: false, - error: "Failed to get about data", - }); - } -}; - -/** - * PUT /api/about - * Cập nhật dữ liệu About Us (Dùng cho AJAX từ CMS) - */ -exports.updateAbout = async (req, res) => { - try { - let updateData = req.body; - - // Nếu dữ liệu gửi qua trường aboutJson (dạng string JSON) - if (updateData.aboutJson && typeof updateData.aboutJson === "string") { - try { - updateData = JSON.parse(updateData.aboutJson); - } catch (e) { - return res.status(400).json({ - success: false, - message: "Invalid JSON in aboutJson", - }); - } - } - - const doc = await AboutUs.getSingle(); - - // ✅ Capture BEFORE state - const beforeData = JSON.parse(JSON.stringify(doc.toObject())); - - // Use .set() for better handling of nested objects/arrays in Mongoose - doc.set(updateData); - await doc.save(); - - // ✅ Capture AFTER state - const afterData = JSON.parse(JSON.stringify(doc.toObject())); - - // ✅ AUDIT LOGGING - About Us Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "AboutUs", - documentId: doc._id, - action: AUDIT_ACTIONS.UPDATE_ABOUT_US, - before: beforeData, - after: afterData, - changes, - req, - }); - console.log( - `✅ Audit log created for About Us update: ${changes.length} changes`, - ); - } else { - console.log("ℹ️ No changes detected for About Us update"); - } - - // Fetch fresh data for syncing and returning - const finalData = await AboutUs.findOne() - .select("-_id -__v -createdAt -updatedAt") - .lean(); - - // Update about.json file to keep it in sync - jsonHelper.writeJsonFile("about", finalData); - - res.json({ - success: true, - message: "About Us updated successfully", - data: finalData, - }); - } catch (error) { - console.error("Error updating about data:", error); - res.status(500).json({ - success: false, - error: "Failed to update about data: " + error.message, - }); - } -}; - -/** - * Render admin page (Dùng cho Admin UI) - */ -exports.index = async (req, res) => { - try { - const data = await AboutUs.getSingle(); - const rawData = data.toObject(); - - // Lấy tất cả blog để chọn trong CMS - const allBlogs = await Blog.find({ status: "published" }) - .sort({ createdAt: -1 }) - .lean(); - - const activeTab = req.query.activeTab || "hero"; - res.render("admin/aboutUs/index", { - layout: "layouts/main", - title: "About Us Management", - data: rawData, - allBlogs, - activeTab, - user: req.session.user, - currentPath: req.path, - frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000", - backendUrl: process.env.BACKEND_URL || "http://localhost:3001", - }); - } catch (err) { - console.error("Error in about index:", err); - req.flash("error_msg", "Error loading About Us page"); - res.redirect("/admin/dashboard"); - } -}; - -/** - * Update method cho form-based submission (Admin UI - Post fallback) - */ -exports.update = async (req, res) => { - try { - let updateData = req.body; - if (updateData.aboutJson && typeof updateData.aboutJson === "string") { - try { - updateData = JSON.parse(updateData.aboutJson); - } catch (e) { - req.flash("error_msg", "Invalid JSON data"); - return res.redirect("/admin/about-us"); - } - } - - const doc = await AboutUs.getSingle(); - - // ✅ Capture BEFORE state - const beforeData = JSON.parse(JSON.stringify(doc.toObject())); - - doc.set(updateData); - await doc.save(); - - // ✅ Capture AFTER state - const afterData = JSON.parse(JSON.stringify(doc.toObject())); - - // ✅ AUDIT LOGGING - About Us Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "AboutUs", - documentId: doc._id, - action: AUDIT_ACTIONS.UPDATE_ABOUT_US, - before: beforeData, - after: afterData, - changes, - req, - }); - } - - const finalData = await AboutUs.findOne() - .select("-_id -__v -createdAt -updatedAt") - .lean(); - jsonHelper.writeJsonFile("about", finalData); - - req.flash("success_msg", "About Us updated successfully"); - const activeTab = req.query.activeTab || "hero"; - res.redirect(`/admin/about-us?activeTab=${activeTab}`); - } catch (err) { - console.error("Update error:", err); - req.flash("error_msg", "Error updating About Us: " + err.message); - res.redirect("/admin/about-us"); - } -}; - -// Aliases for compatibility -exports.api = exports.getAbout; -exports.page = exports.getAbout; -exports.updateAboutUs = exports.updateAbout; diff --git a/data/history.json b/data/history.json index cef9de3..0019672 100644 --- a/data/history.json +++ b/data/history.json @@ -37,7 +37,7 @@ "categoryLabel": "Student Experience", "title": "Innovation in Student Experience", "description": "Enhanced student support through integrated digital services, academic advising, and career development platforms.", - "image": "/uploads/history/Colorful_Square_Background.png", + "image": "/uploads/history/2026.png", "imageAlt": "", "stats": [], "featured": true @@ -50,7 +50,7 @@ "categoryLabel": "Global", "title": "Expansion of International Partnerships", "description": "Established collaborations with academic institutions and industry partners across regions, enabling dual qualifications and cross-border learning opportunities.", - "image": "/uploads/history/7281.jpg", + "image": "/uploads/history/2025.png", "imageAlt": "", "stats": [], "featured": false @@ -95,7 +95,7 @@ "featured": false }, { - "id": "strategic-academic-framework", + "id": "strategic-academic-framework-2", "year": "2027", "yearRange": "2020 - Present", "category": "All Categories", @@ -109,4 +109,4 @@ } ] } -} +} \ No newline at end of file diff --git a/data/partnerships.json b/data/partnerships.json index e3790bd..c1b9b73 100644 --- a/data/partnerships.json +++ b/data/partnerships.json @@ -1,8 +1,8 @@ { "hero": { - "badge": "Global NetworkGlobal NetworkGlobal Netwo", + "badge": "Global Network", "title": "Industry & Academic Partnerships.Industry & Academic Partnerships.Industry & Academic Part", - "description": "Global NetworkGlobal NetworkGlobal NetwoGlobal NetworkGlobal NetworkGlobal NetwoGlobal NetworkGlobal NetworkGlobal NetwoGlobal NetworkGlobal NetworkGlobal NetwoGlobal NetworkGlobal NetworkGlobal NetwoGlobal NetworkGlobal", + "description": "Global NetworkGlobal ", "linkLabel": "Explore DirectoryExplore DirectoryExplor", "image": "/uploads/partnerships/kVI17_2B.webp", "imageAlt": "Modern university campus and corporate office buildingModern university campus and corporate office buildingModern unive" @@ -42,7 +42,7 @@ "benefits": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryInIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndust" }, { - "id": "IndustryIndustryIndustryIndustryIndustryIndustryIn", + "id": "industryindustryindustryindustryindustryindustryindustryindustryindustryindustryindustryin-2", "name": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", "category": "IndustryIndustryIndustryIndust", "summary": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", @@ -53,7 +53,7 @@ "benefits": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryInIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndust" }, { - "id": "IndustryIndustryIndustryIndustryIndustryIndustryIn", + "id": "industryindustryindustryindustryindustryindustryindustryindustryindustryindustryindustryin-3", "name": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", "category": "IndustryIndustryIndustryIndust", "summary": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", @@ -64,7 +64,7 @@ "benefits": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryInIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndust" }, { - "id": "IndustryIndustryIndustryIndustryIndustryIndustryIn", + "id": "industryindustryindustryindustryindustryindustryindustryindustryindustryindustryindustryin-4", "name": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", "category": "IndustryIndustryIndustryIndust", "summary": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", @@ -75,7 +75,7 @@ "benefits": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryInIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndust" }, { - "id": "IndustryIndustryIndustryIndustryIndustryIndustryIn", + "id": "industryindustryindustryindustryindustryindustryindustryindustryindustryindustryindustryin-5", "name": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", "category": "IndustryIndustryIndustryIndust", "summary": "IndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIndustryIn", @@ -147,4 +147,4 @@ } ] } -} +} \ No newline at end of file diff --git a/models/about.js b/models/about.js index 99ee943..dfa3a05 100644 --- a/models/about.js +++ b/models/about.js @@ -1,7 +1,5 @@ const mongoose = require("mongoose"); - const { Schema } = mongoose; - // Hero const HeroSchema = new Schema( { @@ -15,7 +13,6 @@ const HeroSchema = new Schema( }, { _id: false }, ); - // Leadership const SocialSchema = new Schema( { @@ -24,7 +21,6 @@ const SocialSchema = new Schema( }, { _id: false }, ); - const LeadershipMemberSchema = new Schema( { name: { type: String, default: "" }, @@ -35,7 +31,6 @@ const LeadershipMemberSchema = new Schema( }, { _id: false }, ); - const LeadershipSchema = new Schema( { heading: { type: String, default: "" }, @@ -44,7 +39,6 @@ const LeadershipSchema = new Schema( }, { _id: false }, ); - // Learning Model const LearningFeatureSchema = new Schema( { @@ -54,7 +48,6 @@ const LearningFeatureSchema = new Schema( }, { _id: false }, ); - const LearningModeSchema = new Schema( { title: { type: String, default: "" }, @@ -63,7 +56,6 @@ const LearningModeSchema = new Schema( }, { _id: false }, ); - const LearningModelSchema = new Schema( { heading: { type: String, default: "" }, @@ -73,7 +65,6 @@ const LearningModelSchema = new Schema( }, { _id: false }, ); - // Accreditation const AccreditationBadgeSchema = new Schema( { @@ -83,7 +74,6 @@ const AccreditationBadgeSchema = new Schema( }, { _id: false }, ); - const AccreditationStatSchema = new Schema( { value: { type: String, default: "" }, @@ -92,7 +82,6 @@ const AccreditationStatSchema = new Schema( }, { _id: false }, ); - const AccreditationSchema = new Schema( { heading: { type: String, default: "" }, @@ -102,7 +91,6 @@ const AccreditationSchema = new Schema( }, { _id: false }, ); - // Success Stories const StorySchema = new Schema( { @@ -113,7 +101,6 @@ const StorySchema = new Schema( }, { _id: false }, ); - const SuccessStoriesSchema = new Schema( { heading: { type: String, default: "" }, @@ -122,7 +109,6 @@ const SuccessStoriesSchema = new Schema( }, { _id: false }, ); - // CTA const CtaButtonSchema = new Schema( { @@ -131,7 +117,6 @@ const CtaButtonSchema = new Schema( }, { _id: false }, ); - const CtaSchema = new Schema( { heading: { type: String, default: "" }, @@ -141,7 +126,6 @@ const CtaSchema = new Schema( }, { _id: false }, ); - // Root schema const AboutSchema = new Schema( { @@ -157,5 +141,4 @@ const AboutSchema = new Schema( strict: false, }, ); - -module.exports = mongoose.model("About", AboutSchema); +module.exports = mongoose.model("About", AboutSchema); \ No newline at end of file diff --git a/models/aboutUs.js b/models/aboutUs.js deleted file mode 100644 index d72dc07..0000000 --- a/models/aboutUs.js +++ /dev/null @@ -1,108 +0,0 @@ -const mongoose = require("mongoose"); - -const aboutUsSchema = new mongoose.Schema( - { - hero: { - title: String, - breadcrumb: [String], - backgroundImage: String, - }, - intro: { - subheading: String, - heading: String, - description: String, - image: String, - }, - mission: { - subheading: String, - heading: String, - description: String, - images: { - main: String, - secondary: String, - bgShape: String, - planeShape: String, - topShape: String, - globeShape: String, - }, - items: [ - new mongoose.Schema( - { - icon: String, - label: String, - description: String, - }, - { _id: false }, - ), - ], - features: [String], - ctaButton: { - label: String, - href: String, - }, - }, - features: { - backgroundImage: String, - subheading: String, - heading: String, - description: String, - image: String, - items: [ - new mongoose.Schema( - { - icon: String, - title: String, - description: String, - }, - { _id: false }, - ), - ], - ctaButton: { - label: String, - href: String, - }, - }, - news: { - subheading: String, - heading: String, - ctaButton: { - label: String, - href: String, - }, - selectedBlogIds: [{ type: mongoose.Schema.Types.ObjectId, ref: "Blog" }], - // Deprecated: items field kept for backward compatibility during migration - items: [ - new mongoose.Schema( - { - title: String, - category: String, - date: String, - comments: Number, - author: { - name: String, - avatar: String, - }, - link: String, - thumbnail: String, - }, - { _id: false }, - ), - ], - }, - }, - { - timestamps: true, - collection: "aboutus", - }, -); - -// Static method để đảm bảo luôn chỉ có 1 bản ghi duy nhất (Singleton) -aboutUsSchema.statics.getSingle = async function () { - let doc = await this.findOne(); - if (!doc) { - doc = await this.create({}); - } - return doc; -}; - -module.exports = mongoose.model("AboutUs", aboutUsSchema); diff --git a/routes/admin.js b/routes/admin.js index 3270216..2662b2d 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -7,7 +7,7 @@ const uploadController = require("../controllers/uploadController"); const homeController = require("../controllers/homeController"); const headerController = require("../controllers/headerController"); const footerController = require("../controllers/footerController"); -const aboutUsController = require("../controllers/aboutUsController"); +const aboutController = require("../controllers/aboutController"); const partnershipsController = require("../controllers/partnershipsController"); const historyPageController = require("../controllers/historyPageController"); const accreditationController = require("../controllers/accreditationController"); @@ -54,9 +54,10 @@ router.param("code", (req, res, next, code) => { next(); }); -// About Us -router.get("/about-us", ensureAuthenticated, aboutUsController.index); -router.post("/about-us/update", ensureAuthenticated, aboutUsController.update); +// About +router.get("/about", ensureAuthenticated, aboutController.index); +router.post("/about/update", ensureAuthenticated, aboutController.update); + router.get("/partnerships", ensureAuthenticated, partnershipsController.index); router.post( "/partnerships/update", diff --git a/routes/index.js b/routes/index.js index 611afee..657841e 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,7 +2,7 @@ const express = require("express"); const path = require("path"); const router = express.Router(); const homeController = require("../controllers/homeController"); -const aboutUsController = require("../controllers/aboutUsController"); +const aboutController = require("../controllers/aboutController"); const partnershipsController = require("../controllers/partnershipsController"); const historyPageController = require("../controllers/historyPageController"); const accreditationController = require("../controllers/accreditationController"); @@ -43,18 +43,14 @@ router.get("/", (req, res) => { router.get("/api/home", homeController.api); // API để lấy dữ liệu about -router.get("/api/about", aboutUsController.getAbout); -router.put("/api/about", aboutUsController.updateAbout); +router.get("/api/about", aboutController.api); + router.get("/api/partnerships", partnershipsController.api); router.get("/api/history", historyPageController.api); router.get("/api/accreditation", accreditationController.api); router.get("/api/admissions", admissionsController.api); router.get("/api/policies", policiesController.api); -// Public about-us page and API (legacy support) -router.get("/about-us", aboutUsController.getAbout); -router.get("/api/about-us", aboutUsController.getAbout); - // Header API route router.get("/api/header", headerController.api); diff --git a/scripts/make-migration.js b/scripts/make-migration.js new file mode 100644 index 0000000..8c5a25a --- /dev/null +++ b/scripts/make-migration.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); +/** + * Tạo migration file mới với format giống Laravel + * Format: YYYY_MM_DD_HHMMSS_migration_name.js + */ +function makeMigration(migrationName) { + if (!migrationName) { + console.error('Error: Migration name is required'); + console.log('\nUsage: node scripts/make-migration.js '); + console.log('Example: node scripts/make-migration.js create_users_table'); + process.exit(1); + } + // Tạo timestamp theo format Laravel: YYYY_MM_DD_HHMMSS + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + + const timestamp = `${year}_${month}_${day}_${hours}${minutes}${seconds}`; + const fileName = `${timestamp}_${migrationName}.js`; + const filePath = path.join(__dirname, fileName); + // Template migration mẫu + const template = `require('dotenv').config(); +const connectDB = require('../config/database'); +/** + * Migration: ${migrationName} + * Created: ${now.toLocaleString('vi-VN')} + */ +async function migrate() { + try { + // Kết nối database + await connectDB(); + console.log('Starting migration: ${migrationName}...'); + + // TODO: Thêm code migration của bạn ở đây + + console.log('Migration ${migrationName} completed successfully!'); + + const mongoose = require('mongoose'); + await mongoose.disconnect(); + process.exit(0); + } catch (error) { + console.error('Migration error:', error); + process.exit(1); + } +} +// Chạy migration nếu được gọi trực tiếp +if (require.main === module) { + migrate(); +} +module.exports = { migrate }; +`; + // Kiểm tra file đã tồn tại chưa + if (fs.existsSync(filePath)) { + console.error(`Error: Migration file already exists: ${fileName}`); + process.exit(1); + } + // Tạo file migration + try { + fs.writeFileSync(filePath, template, 'utf8'); + console.log(`Migration created successfully: ${fileName}`); + console.log(`Path: ${filePath}`); + } catch (error) { + console.error('Error creating migration file:', error.message); + process.exit(1); + } +} +// Lấy migration name từ command line arguments +const migrationName = process.argv[2]; +// Chạy hàm tạo migration +makeMigration(migrationName); \ No newline at end of file diff --git a/scripts/migrate-about.js b/scripts/migrate-about.js index 35add47..9b7d23c 100644 --- a/scripts/migrate-about.js +++ b/scripts/migrate-about.js @@ -10,12 +10,8 @@ async function validateAboutData(data) { } // Tuỳ schema bạn chỉnh lại cho chuẩn hơn - if (!data.title || !data.sections) { - throw new Error('Missing required fields: title or sections'); - } - - if (!Array.isArray(data.sections)) { - throw new Error('sections must be an array'); + if (!data.hero || !data.leadership) { + throw new Error('Missing required fields: hero or leadership'); } } @@ -24,11 +20,9 @@ async function migrateAboutData() { await connectDB(); console.log('Đã kết nối MongoDB...'); - // Xóa dữ liệu cũ await About.deleteMany({}); console.log('Đã xóa dữ liệu About cũ'); - // Đọc file JSON const aboutData = JSON.parse( await fs.readFile( path.join(__dirname, '../data/about.json'), @@ -38,13 +32,11 @@ async function migrateAboutData() { // Validate await validateAboutData(aboutData); - // Transform (optional) const finalData = { ...aboutData, updatedAt: new Date() }; - // Insert await About.create(finalData); diff --git a/scripts/migrate-all.js b/scripts/migrate-all.js index 76681a5..ac5a49b 100644 --- a/scripts/migrate-all.js +++ b/scripts/migrate-all.js @@ -1,7 +1,7 @@ require("dotenv").config(); const path = require("path"); const fs = require("fs"); -const {execSync} = require("child_process"); +const { execSync } = require("child_process"); const connectDB = require("../config/database"); const migrationHelper = require("../utils/migrationHelper"); @@ -16,9 +16,10 @@ function discoverMigrations() { // Danh sách các file quản lý migration cần loại trừ const excludeFiles = [ "migrate-all.js", - "migrate-home.js", - "migrate-about.js", - + "migrate-status.js", + "migrate-rollback.js", + "migrate-fresh.js", + "make-migration.js", ]; const migrations = files @@ -77,7 +78,7 @@ async function runMigrationScript(migration) { async function runAllMigrations() { const mongoose = require("mongoose"); let ownConn = false; - + try { const wasConnected = mongoose.connection.readyState === 1; await connectDB(); @@ -95,7 +96,7 @@ async function runAllMigrations() { const hasRun = await migrationHelper.hasRun(migration.name); if (hasRun) { - results.push({name: migration.name, status: "SKIPPED"}); + results.push({ name: migration.name, status: "SKIPPED" }); continue; } @@ -107,7 +108,7 @@ async function runAllMigrations() { } await migrationHelper.markAsRun(migration.name, batch); - results.push({name: migration.name, status: "DONE"}); + results.push({ name: migration.name, status: "DONE" }); } catch (error) { results.push({ name: migration.name, diff --git a/scripts/migrate-fresh.js b/scripts/migrate-fresh.js new file mode 100644 index 0000000..e18f819 --- /dev/null +++ b/scripts/migrate-fresh.js @@ -0,0 +1,177 @@ +require('dotenv').config(); +const path = require("path"); +const fs = require("fs"); +const { execSync } = require("child_process"); +const connectDB = require("../config/database"); +const migrationHelper = require("../utils/migrationHelper"); +/** + * Tự động phát hiện tất cả các file script trong thư mục scripts + * Loại trừ các file quản lý migration và file không phải .js + */ +function discoverMigrations() { + const scriptsDir = __dirname; + const files = fs.readdirSync(scriptsDir); + + // Danh sách các file quản lý migration cần loại trừ + const excludeFiles = [ + 'migrate-all.js', + 'migrate-status.js', + 'migrate-rollback.js', + 'migrate-fresh.js', + 'make-migration.js', + 'MIGRATION_README.md' + ]; + + const migrations = files + .filter(file => { + // Lấy tất cả file .js, trừ các file quản lý + return file.endsWith('.js') && !excludeFiles.includes(file); + }) + .map(file => { + // Tạo tên migration từ tên file (bỏ .js) + const name = file.replace('.js', ''); + return { + name: name, + script: file + }; + }) + .sort((a, b) => { + // Sắp xếp theo tên để đảm bảo thứ tự nhất quán + return a.name.localeCompare(b.name); + }); + + return migrations; +} +/** + * Chạy migration script (suppress output) + * Sử dụng child_process để chạy script độc lập vì các script tự quản lý DB connection + * Output từ script sẽ bị suppress để chỉ hiển thị status + */ +async function runMigrationScript(migration) { + return new Promise((resolve, reject) => { + try { + // Chạy script bằng child_process với stdio: 'pipe' để suppress output + // Nhưng vẫn capture stderr để có thể hiển thị lỗi nếu cần + const result = execSync(`node scripts/${migration.script}`, { + stdio: ['ignore', 'pipe', 'pipe'], // stdin: ignore, stdout: pipe, stderr: pipe + cwd: path.join(__dirname, ".."), + encoding: 'utf8' + }); + resolve(); + } catch (error) { + // Attach stderr vào error để có thể hiển thị sau + if (error.stderr) { + error.stderr = error.stderr; + } + reject(error); + } + }); +} +/** + * Hiển thị bảng kết quả migration đơn giản + */ +function displayResults(results) { + console.log("\nRunning migrations...\n"); + + // Tìm độ dài tên migration dài nhất để format bảng + const maxNameLength = Math.max(...results.map(r => r.name.length), 20); + const statusWidth = 10; + const totalWidth = maxNameLength + statusWidth + 7; // 7 = spaces and separators + + // Header + console.log("=".repeat(totalWidth)); + console.log(`${'Migration'.padEnd(maxNameLength)} | ${'Status'.padEnd(statusWidth)}`); + console.log("=".repeat(totalWidth)); + + // Rows + results.forEach(result => { + let statusText = ""; + if (result.status === 'DONE') { + statusText = "DONE".padEnd(statusWidth); + } else if (result.status === 'FAIL') { + statusText = "FAIL".padEnd(statusWidth); + } + + console.log(`${result.name.padEnd(maxNameLength)} | ${statusText}`); + }); + + // Footer + console.log("=".repeat(totalWidth)); + + // Summary + const doneCount = results.filter(r => r.status === 'DONE').length; + const failCount = results.filter(r => r.status === 'FAIL').length; + + console.log(""); + if (doneCount > 0) { + console.log(`${doneCount} migration(s) completed`); + } + if (failCount > 0) { + console.log(`${failCount} migration(s) failed`); + } + console.log(""); +} +/** + * Hàm chính để chạy lại tất cả migrations từ đầu (fresh) + * Xóa tất cả tracking và chạy lại từ đầu + */ +async function runFreshMigrations() { + const mongoose = require('mongoose'); + let ownConn = false; + + try { + const wasConnected = mongoose.connection.readyState === 1; + await connectDB(); + if (!wasConnected) ownConn = true; + // Tự động phát hiện migrations + const migrations = discoverMigrations(); + // Xóa tất cả tracking migrations + const Migration = require('../models/migration'); + await Migration.deleteMany({}); + const batch = 1; // Batch mới bắt đầu từ 1 + const results = []; + // Chạy từng migration + for (let i = 0; i < migrations.length; i++) { + const migration = migrations[i]; + + try { + // Chạy migration script (output bị suppress) + await runMigrationScript(migration); + + if (mongoose.connection.readyState !== 1) { + await connectDB(); + } + await migrationHelper.markAsRun(migration.name, batch); + + results.push({ name: migration.name, status: 'DONE' }); + } catch (error) { + results.push({ name: migration.name, status: 'FAIL', error: error.message }); + // Hiển thị bảng kết quả trước khi exit + displayResults(results); + console.error(`\n❌ Migration "${migration.name}" failed: ${error.message}`); + if (error.stderr) { + console.error(error.stderr.toString()); + } + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(1); + } + } + // Hiển thị bảng kết quả + displayResults(results); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + + process.exit(0); + } catch (error) { + console.error("\n❌ Error:", error.message); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(1); + } +} +// Chạy hàm chính +runFreshMigrations(); \ No newline at end of file diff --git a/scripts/migrate-rollback.js b/scripts/migrate-rollback.js new file mode 100644 index 0000000..68b8cf5 --- /dev/null +++ b/scripts/migrate-rollback.js @@ -0,0 +1,105 @@ +require('dotenv').config(); +const connectDB = require('../config/database'); +const migrationHelper = require('../utils/migrationHelper'); +/** + * Rollback một migration cụ thể + */ +async function rollbackMigration(migrationName) { + const mongoose = require('mongoose'); + let ownConn = false; + + try { + const wasConnected = mongoose.connection.readyState === 1; + await connectDB(); + if (!wasConnected) ownConn = true; + + const hasRun = await migrationHelper.hasRun(migrationName); + if (!hasRun) { + console.log(`⚠️ Migration "${migrationName}" chưa được chạy, không thể rollback.`); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(0); + } + + const result = await migrationHelper.rollback(migrationName); + if (result) { + console.log(`✅ Đã rollback migration: ${migrationName}`); + } else { + console.log(`❌ Không thể rollback migration: ${migrationName}`); + } + + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + } catch (error) { + console.error('❌ Lỗi:', error.message); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(1); + } +} +/** + * Rollback batch cuối cùng + */ +async function rollbackLastBatch() { + const mongoose = require('mongoose'); + let ownConn = false; + + try { + const wasConnected = mongoose.connection.readyState === 1; + await connectDB(); + if (!wasConnected) ownConn = true; + + const lastBatch = await migrationHelper.getLastBatch(); + if (lastBatch === 0) { + console.log('⚠️ Không có batch nào để rollback.'); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(0); + } + + const migrations = await migrationHelper.getMigrationsByBatch(lastBatch); + if (migrations.length === 0) { + console.log(`⚠️ Batch ${lastBatch} không có migration nào.`); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(0); + } + + console.log(`\n🔄 Đang rollback batch ${lastBatch}...`); + console.log(`📋 Các migration sẽ được rollback:`); + migrations.forEach(m => { + console.log(` - ${m.name}`); + }); + + const deletedCount = await migrationHelper.rollbackBatch(lastBatch); + console.log(`\n✅ Đã rollback ${deletedCount} migration(s) trong batch ${lastBatch}`); + + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + } catch (error) { + console.error('❌ Lỗi:', error.message); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(1); + } +} +// Xử lý command line arguments +const args = process.argv.slice(2); +if (args.length === 0) { + console.log('Usage:'); + console.log(' node scripts/migrate-rollback.js - Rollback một migration cụ thể'); + console.log(' node scripts/migrate-rollback.js --batch - Rollback batch cuối cùng'); + process.exit(0); +} +if (args[0] === '--batch') { + rollbackLastBatch(); +} else { + rollbackMigration(args[0]); +} \ No newline at end of file diff --git a/scripts/migrate-status.js b/scripts/migrate-status.js new file mode 100644 index 0000000..103ab1e --- /dev/null +++ b/scripts/migrate-status.js @@ -0,0 +1,114 @@ +require('dotenv').config(); +const connectDB = require('../config/database'); +const migrationHelper = require('../utils/migrationHelper'); +const path = require('path'); +const fs = require('fs'); +/** + * Tự động phát hiện tất cả các file script trong thư mục scripts + * Loại trừ các file quản lý migration và file không phải .js + */ +function discoverMigrations() { + const scriptsDir = __dirname; + const files = fs.readdirSync(scriptsDir); + + // Danh sách các file quản lý migration cần loại trừ + const excludeFiles = [ + 'migrate-all.js', + 'migrate-status.js', + 'migrate-rollback.js', + 'migrate-fresh.js', + 'make-migration.js', + 'MIGRATION_README.md' + ]; + + const migrations = files + .filter(file => { + // Lấy tất cả file .js, trừ các file quản lý + return file.endsWith('.js') && !excludeFiles.includes(file); + }) + .map(file => file.replace('.js', '')) + .sort(); + + return migrations; +} +// Tự động phát hiện migrations +const availableMigrations = discoverMigrations(); +/** + * Hiển thị trạng thái của tất cả migrations + */ +async function showStatus() { + const mongoose = require('mongoose'); + let ownConn = false; + + try { + const wasConnected = mongoose.connection.readyState === 1; + await connectDB(); + if (!wasConnected) ownConn = true; + + console.log('\nMigration Status:\n'); + + const ranMigrations = await migrationHelper.getRanMigrations(); + const ranMap = new Map(); + ranMigrations.forEach(m => ranMap.set(m.name, m)); + + // Tính toán độ rộng cột + const maxNameLength = Math.max(...availableMigrations.map(name => name.length), 20); + const statusWidth = 10; + const batchWidth = 6; + const ranAtWidth = 20; + const totalWidth = maxNameLength + statusWidth + batchWidth + ranAtWidth + 11; // 11 = spaces and separators + + // Header + console.log('='.repeat(totalWidth)); + console.log( + `${'Migration Name'.padEnd(maxNameLength)} | ${'Status'.padEnd(statusWidth)} | ${'Batch'.padEnd(batchWidth)} | Ran At` + ); + console.log('='.repeat(totalWidth)); + + let pendingCount = 0; + let ranCount = 0; + + for (const migrationName of availableMigrations) { + const migration = ranMap.get(migrationName); + if (migration) { + const ranAt = new Date(migration.ranAt).toLocaleString('vi-VN'); + console.log( + `${migrationName.padEnd(maxNameLength)} | ${'Ran'.padEnd(statusWidth)} | ${String(migration.batch).padEnd(batchWidth)} | ${ranAt}` + ); + ranCount++; + } else { + console.log( + `${migrationName.padEnd(maxNameLength)} | ${'Pending'.padEnd(statusWidth)} | ${'-'.padEnd(batchWidth)} | -` + ); + pendingCount++; + } + } + + console.log('='.repeat(totalWidth)); + console.log(`\nSummary:`); + console.log(` Ran: ${ranCount} migration(s)`); + console.log(` Pending: ${pendingCount} migration(s)`); + + const lastBatch = await migrationHelper.getLastBatch(); + if (lastBatch > 0) { + console.log(` Last batch: ${lastBatch}`); + } + + console.log(''); + + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + } catch (error) { + console.error('Error:', error.message); + if (ownConn && mongoose.connection.readyState === 1) { + await mongoose.disconnect(); + } + process.exit(1); + } +} +// Chạy nếu được gọi trực tiếp +if (require.main === module) { + showStatus(); +} +module.exports = { showStatus }; \ No newline at end of file diff --git a/views/layouts/main.ejs b/views/layouts/main.ejs index a05e893..9d96ef0 100644 --- a/views/layouts/main.ejs +++ b/views/layouts/main.ejs @@ -1036,12 +1036,12 @@