500 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			500 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
const express = require("express");
 | 
						||
const mysql = require("mysql2/promise");
 | 
						||
const bodyParser = require("body-parser");
 | 
						||
const XLSX = require("xlsx-js-style");
 | 
						||
const dotenv = require("dotenv");
 | 
						||
dotenv.config();
 | 
						||
 | 
						||
const stringSimilarity = require("string-similarity");
 | 
						||
const inititalWebRoute = require("./route/web");
 | 
						||
 | 
						||
// Nhận mảng message đầu vào, trả về mảng message đã loại bỏ trùng lặp
 | 
						||
function deduplicateErrors(errors, threshold = 0.3) {
 | 
						||
	const unique = [];
 | 
						||
 | 
						||
	for (const err of errors) {
 | 
						||
		const isDuplicate = unique.some((u) => {
 | 
						||
			const similarity = stringSimilarity.compareTwoStrings(
 | 
						||
				err.error_message,
 | 
						||
				u.error_message,
 | 
						||
			);
 | 
						||
			return similarity >= threshold;
 | 
						||
		});
 | 
						||
 | 
						||
		if (!isDuplicate) {
 | 
						||
			unique.push(err);
 | 
						||
		}
 | 
						||
	}
 | 
						||
 | 
						||
	return unique;
 | 
						||
}
 | 
						||
 | 
						||
const app = express();
 | 
						||
const PORT = 4000;
 | 
						||
 | 
						||
app.use(bodyParser.json());
 | 
						||
app.use(express.static("public"));
 | 
						||
app.use(express.urlencoded({ extended: true }));
 | 
						||
 | 
						||
const db = mysql.createPool({
 | 
						||
	host: "localhost",
 | 
						||
	user: "admin",
 | 
						||
	password: "Work1234",
 | 
						||
	database: "log_analysis",
 | 
						||
});
 | 
						||
 | 
						||
async function columnExists(table, column) {
 | 
						||
	const [rows] = await db.query(
 | 
						||
		`SELECT COUNT(*) AS count
 | 
						||
		 FROM INFORMATION_SCHEMA.COLUMNS
 | 
						||
		 WHERE TABLE_NAME = ? AND COLUMN_NAME = ? AND TABLE_SCHEMA = DATABASE()`,
 | 
						||
		[table, column],
 | 
						||
	);
 | 
						||
	return rows[0].count > 0;
 | 
						||
}
 | 
						||
 | 
						||
// === Tạo bảng
 | 
						||
(async () => {
 | 
						||
	await db.query(`
 | 
						||
    CREATE TABLE IF NOT EXISTS devices (
 | 
						||
      id INT AUTO_INCREMENT PRIMARY KEY,
 | 
						||
      pid VARCHAR(100) NOT NULL,
 | 
						||
      version VARCHAR(100) NOT NULL,
 | 
						||
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
						||
      UNIQUE KEY uniq_pid_version (pid, version),
 | 
						||
      INDEX idx_pid (pid),
 | 
						||
      INDEX idx_version (version),
 | 
						||
      INDEX idx_created (created_at)
 | 
						||
    )
 | 
						||
  `);
 | 
						||
 | 
						||
	if (!(await columnExists("devices", "updated_at"))) {
 | 
						||
		await db.query(
 | 
						||
			`ALTER TABLE devices ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`,
 | 
						||
		);
 | 
						||
	}
 | 
						||
 | 
						||
	if (await columnExists("devices", "updated_by")) {
 | 
						||
		await db.query(`ALTER TABLE devices DROP COLUMN updated_by`);
 | 
						||
	}
 | 
						||
 | 
						||
	if (await columnExists("devices", "updated_user")) {
 | 
						||
		await db.query(
 | 
						||
			`ALTER TABLE devices DROP FOREIGN KEY fk_updated_by_user`,
 | 
						||
		);
 | 
						||
		await db.query(`ALTER TABLE devices DROP COLUMN updated_user`);
 | 
						||
	}
 | 
						||
 | 
						||
	if (!(await columnExists("devices", "update_name"))) {
 | 
						||
		await db.query(`
 | 
						||
		ALTER TABLE devices 
 | 
						||
		ADD COLUMN update_name VARCHAR(255)
 | 
						||
	`);
 | 
						||
	}
 | 
						||
 | 
						||
	if (!(await columnExists("devices", "update_email"))) {
 | 
						||
		await db.query(`
 | 
						||
		ALTER TABLE devices 
 | 
						||
		ADD COLUMN update_email VARCHAR(255)
 | 
						||
	`);
 | 
						||
	}
 | 
						||
 | 
						||
	if (!(await columnExists("devices", "is_confirmed"))) {
 | 
						||
		await db.query(
 | 
						||
			`ALTER TABLE devices ADD COLUMN is_confirmed BOOLEAN DEFAULT FALSE`,
 | 
						||
		);
 | 
						||
	}
 | 
						||
 | 
						||
	const createOutputTable = async (command) => {
 | 
						||
		await db.query(`
 | 
						||
      CREATE TABLE IF NOT EXISTS ${command}_outputs (
 | 
						||
        id INT AUTO_INCREMENT PRIMARY KEY,
 | 
						||
        device_id INT NOT NULL,
 | 
						||
        filename VARCHAR(255),
 | 
						||
        output TEXT,
 | 
						||
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
						||
        INDEX idx_device_created (device_id, created_at),
 | 
						||
        FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
 | 
						||
      )
 | 
						||
    `);
 | 
						||
 | 
						||
		if (await columnExists(`${command}_outputs`, "is_deleted")) {
 | 
						||
			await db.query(
 | 
						||
				`ALTER TABLE ${command}_outputs DROP COLUMN is_deleted`,
 | 
						||
			);
 | 
						||
		}
 | 
						||
	};
 | 
						||
 | 
						||
	await createOutputTable("inventory");
 | 
						||
	await createOutputTable("version");
 | 
						||
	await createOutputTable("license");
 | 
						||
	await createOutputTable("logging");
 | 
						||
 | 
						||
	await db.query(`
 | 
						||
		DROP TABLE IF EXISTS users
 | 
						||
	`);
 | 
						||
})();
 | 
						||
 | 
						||
// === APIs ===
 | 
						||
 | 
						||
app.get("/api/pids", async (req, res) => {
 | 
						||
	const [rows] = await db.query(
 | 
						||
		"SELECT DISTINCT pid FROM devices ORDER BY pid",
 | 
						||
	);
 | 
						||
	res.json(rows.map((r) => r.pid));
 | 
						||
});
 | 
						||
 | 
						||
app.get("/api/pid/:pid/versions", async (req, res) => {
 | 
						||
	const { pid } = req.params;
 | 
						||
 | 
						||
	// Lấy danh sách các version tương ứng với PID
 | 
						||
	const [devices] = await db.query(
 | 
						||
		"SELECT id, version, update_name FROM devices WHERE pid = ?",
 | 
						||
		[pid],
 | 
						||
	);
 | 
						||
 | 
						||
	const results = [];
 | 
						||
 | 
						||
	for (const device of devices) {
 | 
						||
		const deviceId = device.id;
 | 
						||
		const version = device.version;
 | 
						||
 | 
						||
		const [[inv]] = await db.query(
 | 
						||
			"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
 | 
						||
			[deviceId],
 | 
						||
		);
 | 
						||
		const [[ver]] = await db.query(
 | 
						||
			"SELECT COUNT(*) AS c FROM version_outputs WHERE device_id = ?",
 | 
						||
			[deviceId],
 | 
						||
		);
 | 
						||
		const [[lic]] = await db.query(
 | 
						||
			"SELECT COUNT(*) AS c FROM license_outputs WHERE device_id = ?",
 | 
						||
			[deviceId],
 | 
						||
		);
 | 
						||
		const [[log]] = await db.query(
 | 
						||
			"SELECT COUNT(*) AS c FROM logging_outputs WHERE device_id = ?",
 | 
						||
			[deviceId],
 | 
						||
		);
 | 
						||
 | 
						||
		results.push({
 | 
						||
			user: device.update_name,
 | 
						||
			version,
 | 
						||
			commands: {
 | 
						||
				inventory: inv.c,
 | 
						||
				version: ver.c,
 | 
						||
				license: lic.c,
 | 
						||
				logging: log.c,
 | 
						||
			},
 | 
						||
		});
 | 
						||
	}
 | 
						||
 | 
						||
	res.json(results);
 | 
						||
});
 | 
						||
 | 
						||
app.get("/api/device/:pid/:version/:command", async (req, res) => {
 | 
						||
	const { pid, version, command } = req.params;
 | 
						||
 | 
						||
	const [[device]] = await db.query(
 | 
						||
		`SELECT id FROM devices WHERE pid = ? AND version = ?`,
 | 
						||
		[pid, version],
 | 
						||
	);
 | 
						||
	if (!device) return res.status(404).json({ message: "Not found" });
 | 
						||
 | 
						||
	const [logs] = await db.query(
 | 
						||
		`SELECT id, filename, output, created_at FROM ${command}_outputs WHERE device_id = ? ORDER BY created_at DESC`,
 | 
						||
		[device.id],
 | 
						||
	);
 | 
						||
 | 
						||
	res.json(logs);
 | 
						||
});
 | 
						||
 | 
						||
app.post("/api/confirm-device", async (req, res) => {
 | 
						||
	const { pid, version, deletedLogs, name, email } = req.body;
 | 
						||
 | 
						||
	if (!pid || !version) {
 | 
						||
		return res.status(400).json({
 | 
						||
			success: false,
 | 
						||
			message: "Missing pid or version",
 | 
						||
		});
 | 
						||
	}
 | 
						||
 | 
						||
	try {
 | 
						||
		// Check if device exists
 | 
						||
		const [rows] = await db.query(
 | 
						||
			`SELECT id FROM devices WHERE pid = ? AND version = ?`,
 | 
						||
			[pid, version],
 | 
						||
		);
 | 
						||
 | 
						||
		if (rows.length === 0) {
 | 
						||
			return res.status(404).json({
 | 
						||
				success: false,
 | 
						||
				message: "Device not found.",
 | 
						||
			});
 | 
						||
		}
 | 
						||
 | 
						||
		if (rows[0].is_confirmed) {
 | 
						||
			return res.status(400).json({
 | 
						||
				success: false,
 | 
						||
				message: "Device is already confirmed.",
 | 
						||
			});
 | 
						||
		}
 | 
						||
 | 
						||
		// Update device confirmation
 | 
						||
		await db.query(
 | 
						||
			`UPDATE devices
 | 
						||
				SET is_confirmed = true,
 | 
						||
					updated_at = CURRENT_TIMESTAMP,
 | 
						||
					update_name = ?,
 | 
						||
					update_email = ?
 | 
						||
				WHERE pid = ? AND version = ?`,
 | 
						||
			[name, email, pid, version],
 | 
						||
		);
 | 
						||
 | 
						||
		// Delete Log
 | 
						||
		for (const item of deletedLogs) {
 | 
						||
			const { command, id } = item;
 | 
						||
 | 
						||
			if (!command || typeof id !== "number") continue;
 | 
						||
 | 
						||
			const allowedCommands = [
 | 
						||
				"inventory",
 | 
						||
				"version",
 | 
						||
				"license",
 | 
						||
				"logging",
 | 
						||
			];
 | 
						||
			if (!allowedCommands.includes(command)) continue;
 | 
						||
 | 
						||
			const [rows] = await db.query(
 | 
						||
				`SELECT id FROM \`${command}_outputs\` WHERE id = ?`,
 | 
						||
				[id],
 | 
						||
			);
 | 
						||
 | 
						||
			if (rows.length === 0) {
 | 
						||
				continue;
 | 
						||
			}
 | 
						||
 | 
						||
			// Delete log
 | 
						||
			await db.query(`DELETE FROM \`${command}_outputs\` WHERE id = ?`, [
 | 
						||
				id,
 | 
						||
			]);
 | 
						||
		}
 | 
						||
 | 
						||
		res.json({
 | 
						||
			success: true,
 | 
						||
			message: "Device confirmed successfully!",
 | 
						||
		});
 | 
						||
	} catch (error) {
 | 
						||
		console.error("Confirm device error:", error);
 | 
						||
		res.status(500).json({
 | 
						||
			success: false,
 | 
						||
			message: "Server error, please try again!",
 | 
						||
		});
 | 
						||
	}
 | 
						||
});
 | 
						||
 | 
						||
app.post("/api/confirm-count", async (req, res) => {
 | 
						||
	try {
 | 
						||
		const { email } = req.body;
 | 
						||
 | 
						||
		if (!email) {
 | 
						||
			return res.status(400).json({ error: "Email is required" });
 | 
						||
		}
 | 
						||
 | 
						||
		const [[result]] = await db.query(
 | 
						||
			"SELECT COUNT(*) AS c FROM devices WHERE update_email = ?",
 | 
						||
			[email],
 | 
						||
		);
 | 
						||
 | 
						||
		res.json({ count: result.c });
 | 
						||
	} catch (err) {
 | 
						||
		console.error("Error fetching confirm count:", err);
 | 
						||
		res.status(500).json({ error: "Internal server error" });
 | 
						||
	}
 | 
						||
});
 | 
						||
 | 
						||
app.get("/api/device-export-excel", async (req, res) => {
 | 
						||
	const [rows] = await db.query(
 | 
						||
		"SELECT pid, version, update_name, update_email, updated_at FROM devices",
 | 
						||
	);
 | 
						||
 | 
						||
	const worksheetData = [
 | 
						||
		["No", "PID", "Verion", "Url", "Name", "Email", "Updated At"],
 | 
						||
		...rows.map((deviceItem, deviceIndex) => {
 | 
						||
			const url = `${process.env.BASE_URL}?pid=${deviceItem.pid}&version=${deviceItem.version}`;
 | 
						||
			return [
 | 
						||
				deviceIndex + 1,
 | 
						||
				deviceItem.pid,
 | 
						||
				deviceItem.version,
 | 
						||
				{ v: url, l: { Target: url } },
 | 
						||
				deviceItem.update_name,
 | 
						||
				deviceItem.update_email,
 | 
						||
				deviceItem.updated_at,
 | 
						||
			];
 | 
						||
		}),
 | 
						||
	];
 | 
						||
 | 
						||
	// Create worksheet
 | 
						||
	const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
 | 
						||
 | 
						||
	// Add styles to header row
 | 
						||
	const headerStyle = {
 | 
						||
		font: { bold: true, color: { rgb: "FFFFFF" } },
 | 
						||
		fill: { fgColor: { rgb: "4F81BD" } },
 | 
						||
		alignment: { horizontal: "center" },
 | 
						||
		border: {
 | 
						||
			top: { style: "thin", color: { rgb: "000000" } },
 | 
						||
			bottom: { style: "thin", color: { rgb: "000000" } },
 | 
						||
			left: { style: "thin", color: { rgb: "000000" } },
 | 
						||
			right: { style: "thin", color: { rgb: "000000" } },
 | 
						||
		},
 | 
						||
	};
 | 
						||
 | 
						||
	const commonCellStyle = {
 | 
						||
		alignment: { horizontal: "center", vertical: "center" },
 | 
						||
	};
 | 
						||
 | 
						||
	const wrapTextStyle = {
 | 
						||
		alignment: { horizontal: "left", wrapText: true, vertical: "center" },
 | 
						||
	};
 | 
						||
 | 
						||
	const dateCellStyle = {
 | 
						||
		alignment: { horizontal: "center", vertical: "center" },
 | 
						||
	};
 | 
						||
 | 
						||
	// Set column widths
 | 
						||
	worksheet["!cols"] = [
 | 
						||
		{ wch: 6 }, // No
 | 
						||
		{ wch: 20 }, // PID
 | 
						||
		{ wch: 10 }, // Verion
 | 
						||
		{ wch: 60 }, // Url
 | 
						||
		{ wch: 20 }, // Name
 | 
						||
		{ wch: 30 }, // Email
 | 
						||
		{ wch: 10 }, // Updated At
 | 
						||
	];
 | 
						||
 | 
						||
	["A1", "B1", "C1", "D1", "E1", "F1", "G1"].forEach((cell) => {
 | 
						||
		worksheet[cell].s = headerStyle;
 | 
						||
	});
 | 
						||
 | 
						||
	const totalRows = worksheetData.length;
 | 
						||
	for (let row = 2; row <= totalRows; row++) {
 | 
						||
		const noCell = worksheet[`A${row}`];
 | 
						||
		if (noCell) noCell.s = commonCellStyle;
 | 
						||
 | 
						||
		const dateCell = worksheet[`G${row}`];
 | 
						||
		if (dateCell) {
 | 
						||
			dateCell.s = dateCellStyle;
 | 
						||
		}
 | 
						||
 | 
						||
		["B", "C", "D", "E", "F"].forEach((col) => {
 | 
						||
			const cell = worksheet[`${col}${row}`];
 | 
						||
			if (cell) cell.s = wrapTextStyle;
 | 
						||
		});
 | 
						||
	}
 | 
						||
 | 
						||
	const workbook = XLSX.utils.book_new();
 | 
						||
	XLSX.utils.book_append_sheet(workbook, worksheet, "Devices");
 | 
						||
 | 
						||
	const buffer = XLSX.write(workbook, {
 | 
						||
		type: "buffer",
 | 
						||
		bookType: "xlsx",
 | 
						||
	});
 | 
						||
 | 
						||
	res.setHeader("Content-Disposition", "attachment; filename=devices.xlsx");
 | 
						||
	res.setHeader(
 | 
						||
		"Content-Type",
 | 
						||
		"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 | 
						||
	);
 | 
						||
 | 
						||
	res.send(buffer);
 | 
						||
});
 | 
						||
 | 
						||
// Danh sách regex lọc lỗi
 | 
						||
const errorPatterns = [
 | 
						||
	{
 | 
						||
		regex: /%\w+-[123]-\w+:/i,
 | 
						||
		description:
 | 
						||
			"Lỗi mức độ 1–3 (critical, error, alert) trong định dạng syslog Cisco",
 | 
						||
	},
 | 
						||
	{
 | 
						||
		regex: /%\w+-4-\w+:/i,
 | 
						||
		description: "Lỗi mức độ cảnh báo (level 4 - warning)",
 | 
						||
	},
 | 
						||
	{
 | 
						||
		regex: /(fail|error|unexpected|badcfg|invalid|denied|disabled|timeout|panic)/i,
 | 
						||
		description: "Từ khóa phổ biến của lỗi trong nội dung log",
 | 
						||
	},
 | 
						||
	{
 | 
						||
		regex: /^\d{1,2}:\d{2}:\d{2}:.*?%\w+-[1-4]-\w+:/i,
 | 
						||
		description: "Dòng log bắt đầu bằng timestamp có mã lỗi Cisco",
 | 
						||
	},
 | 
						||
];
 | 
						||
 | 
						||
// API: Trả về các dòng lỗi từ bảng logging_outputs theo PID
 | 
						||
app.get("/api/errors/:pid", async (req, res) => {
 | 
						||
	const pid = req.params.pid;
 | 
						||
 | 
						||
	try {
 | 
						||
		const [rows] = await db.execute(
 | 
						||
			`
 | 
						||
        SELECT lo.output, d.pid, lo.filename
 | 
						||
        FROM logging_outputs lo
 | 
						||
        JOIN devices d ON lo.device_id = d.id
 | 
						||
        WHERE d.pid = ?
 | 
						||
      `,
 | 
						||
			[pid],
 | 
						||
		);
 | 
						||
 | 
						||
		const matchedLines = [];
 | 
						||
 | 
						||
		for (const row of rows) {
 | 
						||
			const lines = row.output.split(/\r?\n/);
 | 
						||
 | 
						||
			for (const line of lines.filter(
 | 
						||
				(l) => l.includes("*") || l.includes("%"),
 | 
						||
			)) {
 | 
						||
				for (const pattern of errorPatterns) {
 | 
						||
					const regex = new RegExp(pattern.regex.source, "gi");
 | 
						||
					if (regex.test(line)) {
 | 
						||
						// Reset regex to start (because `.test()` may advance position in some cases)
 | 
						||
						regex.lastIndex = 0;
 | 
						||
						const highlighted = line.replace(
 | 
						||
							regex,
 | 
						||
							(match) => `<mark>${match}</mark>`,
 | 
						||
						);
 | 
						||
 | 
						||
						matchedLines.push({
 | 
						||
							pid: row.pid,
 | 
						||
							filename: row.filename,
 | 
						||
							error_message: line.trim(),
 | 
						||
							highlighted_message: highlighted.trim(),
 | 
						||
							regex_pattern: pattern.regex.source,
 | 
						||
							description: pattern.description,
 | 
						||
						});
 | 
						||
 | 
						||
						break;
 | 
						||
					}
 | 
						||
				}
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		const filtered = deduplicateErrors(matchedLines, 0.6);
 | 
						||
		res.json(filtered);
 | 
						||
	} catch (err) {
 | 
						||
		console.error(err);
 | 
						||
		res.status(500).json({ error: "Internal server error" });
 | 
						||
	}
 | 
						||
});
 | 
						||
 | 
						||
app.get("/api/test", async (req, res) => {
 | 
						||
	console.log(req);
 | 
						||
	res.send("ok");
 | 
						||
});
 | 
						||
 | 
						||
// Web Route
 | 
						||
inititalWebRoute(app, db);
 | 
						||
 | 
						||
app.listen(PORT, () =>
 | 
						||
	console.log(`🚀 Server running at http://localhost:${PORT}`),
 | 
						||
);
 |