405 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			405 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
const express = require("express");
 | 
						||
const mysql = require("mysql2/promise");
 | 
						||
const bodyParser = require("body-parser");
 | 
						||
const dotenv = require("dotenv");
 | 
						||
dotenv.config();
 | 
						||
 | 
						||
const stringSimilarity = require("string-similarity");
 | 
						||
const bcrypt = require("bcrypt");
 | 
						||
const inititalWebRoute = require("./route/web");
 | 
						||
const { authenticateToken } = require("./middleware/auth");
 | 
						||
 | 
						||
// 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;
 | 
						||
}
 | 
						||
 | 
						||
const checkAndInsertAdminUser = async () => {
 | 
						||
	const [rows] = await db.query(`SELECT id FROM users WHERE email = ?`, [
 | 
						||
		"admin@apactech.io",
 | 
						||
	]);
 | 
						||
 | 
						||
	if (rows.length === 0) {
 | 
						||
		const hashedPassword = await bcrypt.hash("admin0312", 10);
 | 
						||
 | 
						||
		await db.query(
 | 
						||
			`INSERT INTO users (name, email, password) VALUES (?, ?, ?)`,
 | 
						||
			["Admin", "admin@apactech.io", hashedPassword],
 | 
						||
		);
 | 
						||
	}
 | 
						||
};
 | 
						||
 | 
						||
// === 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 
 | 
						||
			ADD COLUMN updated_user INT DEFAULT NULL,
 | 
						||
			ADD CONSTRAINT fk_updated_by_user FOREIGN KEY (updated_user) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
 | 
						||
		`);
 | 
						||
	}
 | 
						||
 | 
						||
	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
 | 
						||
      )
 | 
						||
    `);
 | 
						||
	};
 | 
						||
 | 
						||
	await createOutputTable("inventory");
 | 
						||
	await createOutputTable("version");
 | 
						||
	await createOutputTable("license");
 | 
						||
	await createOutputTable("logging");
 | 
						||
 | 
						||
	await db.query(`
 | 
						||
    CREATE TABLE IF NOT EXISTS users (
 | 
						||
		id INT AUTO_INCREMENT PRIMARY KEY,
 | 
						||
		name VARCHAR(100) NOT NULL,
 | 
						||
		email VARCHAR(100) NOT NULL UNIQUE,
 | 
						||
		password VARCHAR(255) NOT NULL,
 | 
						||
		confirm_count INT DEFAULT 0,
 | 
						||
		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
						||
		updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
 | 
						||
		)
 | 
						||
	`);
 | 
						||
 | 
						||
	await checkAndInsertAdminUser();
 | 
						||
})();
 | 
						||
 | 
						||
// === 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, updated_user FROM devices WHERE pid = ?",
 | 
						||
		[pid],
 | 
						||
	);
 | 
						||
 | 
						||
	const results = [];
 | 
						||
 | 
						||
	for (const device of devices) {
 | 
						||
		const deviceId = device.id;
 | 
						||
		const version = device.version;
 | 
						||
		const userId = device.updated_user;
 | 
						||
 | 
						||
		const [user] = await db.query(
 | 
						||
			"SELECT id, name FROM users WHERE id = ?",
 | 
						||
			[userId],
 | 
						||
		);
 | 
						||
		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: user[0],
 | 
						||
			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/delete-log", authenticateToken, async (req, res) => {
 | 
						||
	const logId = req.body.id;
 | 
						||
	const logCommand = req.body.command;
 | 
						||
 | 
						||
	try {
 | 
						||
		if (!logCommand || typeof logId !== "number") {
 | 
						||
			return res
 | 
						||
				.status(400)
 | 
						||
				.json({ success: false, message: "Invalid input" });
 | 
						||
		}
 | 
						||
 | 
						||
		const [rows] = await db.query(
 | 
						||
			`SELECT id FROM \`${logCommand}_outputs\` WHERE id = ?`,
 | 
						||
			[logId],
 | 
						||
		);
 | 
						||
 | 
						||
		if (rows.length === 0) {
 | 
						||
			return res.status(404).json({
 | 
						||
				success: false,
 | 
						||
				message: "Log not found",
 | 
						||
			});
 | 
						||
		}
 | 
						||
 | 
						||
		await db.query(`DELETE FROM \`${logCommand}_outputs\` WHERE id = ?`, [
 | 
						||
			logId,
 | 
						||
		]);
 | 
						||
 | 
						||
		res.json({ success: true });
 | 
						||
	} catch (error) {
 | 
						||
		console.error("Delete error:", error);
 | 
						||
		res.status(500).json({ success: false, message: "Server error" });
 | 
						||
	}
 | 
						||
});
 | 
						||
 | 
						||
app.post("/api/confirm-device", authenticateToken, async (req, res) => {
 | 
						||
	const { pid, version } = req.body;
 | 
						||
	const updatedBy = req.user?.id;
 | 
						||
 | 
						||
	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 * 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,
 | 
						||
					updated_user = ?
 | 
						||
				WHERE pid = ? AND version = ?`,
 | 
						||
			[updatedBy, pid, version],
 | 
						||
		);
 | 
						||
 | 
						||
		await db.query(
 | 
						||
			"UPDATE users SET confirm_count = confirm_count + 1 WHERE id = ?",
 | 
						||
			[updatedBy],
 | 
						||
		);
 | 
						||
 | 
						||
		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!",
 | 
						||
		});
 | 
						||
	}
 | 
						||
});
 | 
						||
 | 
						||
// 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}`),
 | 
						||
);
 |