From 557e267e2132933b49cd2d47697ff4fa95214157 Mon Sep 17 00:00:00 2001 From: dbdbd9 Date: Mon, 28 Jul 2025 16:23:45 +0700 Subject: [PATCH] add API export device --- .env.example | 1 + package-lock.json | 171 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- server.js | 109 ++++++++++++++++++++++++++--- 4 files changed, 273 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index ab1e1c5..b73d115 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ +BASE_URL=http://localhost:4000 JWT_SECRET=secret \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8480d21..da4770d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "multer": "^1.4.5-lts.1", "mysql2": "^3.14.2", "sqlite3": "^5.1.6", - "string-similarity": "^4.0.4" + "string-similarity": "^4.0.4", + "xlsx-js-style": "^1.2.0" }, "devDependencies": { "nodemon": "^3.1.10" @@ -96,6 +97,22 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==", + "license": "Apache-2.0", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -519,6 +536,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cheerio": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.1.tgz", @@ -611,6 +650,28 @@ "node": ">=4" } }, + "node_modules/codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==", + "license": "Apache-2.0", + "dependencies": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage/node_modules/commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "license": "MIT" + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -631,6 +692,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -706,6 +773,18 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1043,6 +1122,15 @@ "node": ">= 0.6" } }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1096,6 +1184,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fflate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", + "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==", + "license": "MIT" + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1172,6 +1266,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -2446,6 +2549,18 @@ "node": ">=10" } }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "license": "Apache-2.0", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2936,6 +3051,18 @@ "node": ">= 0.6" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -3278,11 +3405,53 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xlsx-js-style": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz", + "integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.3.8", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index d49ecd3..beeb0cd 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "multer": "^1.4.5-lts.1", "mysql2": "^3.14.2", "sqlite3": "^5.1.6", - "string-similarity": "^4.0.4" + "string-similarity": "^4.0.4", + "xlsx-js-style": "^1.2.0" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/server.js b/server.js index d13ebaa..ab466e7 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,7 @@ 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(); @@ -35,17 +36,10 @@ 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", -// }); - const db = mysql.createPool({ host: "localhost", - user: "root", - password: "", + user: "admin", + password: "Work1234", database: "log_analysis", }); @@ -318,6 +312,103 @@ app.post("/api/confirm-count", async (req, res) => { } }); +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 = [ {