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}`),
|
||
);
|