Merge pull request 'update delete log, authentication, add export device' (#4) from vi into main
Reviewed-on: #4
This commit is contained in:
commit
fd51855b72
|
|
@ -1 +1,2 @@
|
||||||
|
BASE_URL=http://localhost:4000
|
||||||
JWT_SECRET=secret
|
JWT_SECRET=secret
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"string-similarity": "^4.0.4"
|
"string-similarity": "^4.0.4",
|
||||||
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10"
|
||||||
|
|
@ -96,6 +97,22 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
|
@ -519,6 +536,28 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/cheerio": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.1.tgz",
|
||||||
|
|
@ -611,6 +650,28 @@
|
||||||
"node": ">=4"
|
"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": {
|
"node_modules/color-support": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
|
|
@ -631,6 +692,12 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -706,6 +773,18 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/css-select": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
|
@ -1043,6 +1122,15 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
|
@ -1096,6 +1184,12 @@
|
||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"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": ">= 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": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
|
@ -2446,6 +2549,18 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
|
@ -2936,6 +3051,18 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/ssri": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||||
|
|
@ -3278,11 +3405,53 @@
|
||||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
"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": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"string-similarity": "^4.0.4"
|
"string-similarity": "^4.0.4",
|
||||||
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10"
|
||||||
|
|
|
||||||
|
|
@ -94,20 +94,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="logSections"></div>
|
<div id="logSections"></div>
|
||||||
|
|
||||||
<div id="logDeleteModal" class="modal hidden">
|
|
||||||
<div style="max-width: 30%" class="modal-content">
|
|
||||||
<div id="deleteLogText"></div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button id="logCancelDeleteBtn" class="secondary-btn">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button id="logDeleteBtn" class="danger-btn">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="deviceConfirmModal" class="modal hidden">
|
<div id="deviceConfirmModal" class="modal hidden">
|
||||||
<div style="max-width: 30%" class="modal-content">
|
<div style="max-width: 30%" class="modal-content">
|
||||||
<div id="deviceConfirmText"></div>
|
<div id="deviceConfirmText"></div>
|
||||||
|
|
@ -137,23 +123,29 @@
|
||||||
|
|
||||||
async function getProfile() {
|
async function getProfile() {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
const user = JSON.parse(localStorage.getItem("user"));
|
||||||
|
|
||||||
await fetch("/api/profile", {
|
await fetch("/api/confirm-count", {
|
||||||
method: "GET",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: user.email,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
"welcome-user",
|
"welcome-user",
|
||||||
).innerHTML = `Welcome, <strong>${data?.user?.name}</strong> ▼`;
|
).innerHTML = `Welcome, <strong>${
|
||||||
|
JSON.parse(localStorage.getItem("user")).name
|
||||||
|
}</strong> ▼`;
|
||||||
|
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
"confirm-count-user",
|
"confirm-count-user",
|
||||||
).innerHTML = `Confirmed: <strong>${data?.user?.confirm_count}</strong>`;
|
).innerHTML = `Confirmed: <strong>${data?.count}</strong>`;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
@ -298,6 +290,7 @@
|
||||||
versionList.addEventListener("change", () => {
|
versionList.addEventListener("change", () => {
|
||||||
const pid = pidList.value;
|
const pid = pidList.value;
|
||||||
const version = versionList.value;
|
const version = versionList.value;
|
||||||
|
deletedLogs = [];
|
||||||
if (pid && version) {
|
if (pid && version) {
|
||||||
// Set param pid, version to URL
|
// Set param pid, version to URL
|
||||||
const currentParams = new URLSearchParams(
|
const currentParams = new URLSearchParams(
|
||||||
|
|
@ -317,8 +310,7 @@
|
||||||
);
|
);
|
||||||
if (currentVerUser) {
|
if (currentVerUser) {
|
||||||
deviceConfirmBtn.style.display = "none";
|
deviceConfirmBtn.style.display = "none";
|
||||||
confirmedUserName.textContent =
|
confirmedUserName.textContent = currentVerUser.user;
|
||||||
currentVerUser.user.name;
|
|
||||||
confirmUserText.style.display = "inline";
|
confirmUserText.style.display = "inline";
|
||||||
} else {
|
} else {
|
||||||
deviceConfirmBtn.style.display = "inline-block";
|
deviceConfirmBtn.style.display = "inline-block";
|
||||||
|
|
@ -354,27 +346,31 @@
|
||||||
section.className = "command-section";
|
section.className = "command-section";
|
||||||
section.id = `section-${cmd}`;
|
section.id = `section-${cmd}`;
|
||||||
section.innerHTML = `
|
section.innerHTML = `
|
||||||
<h3>
|
<h3>
|
||||||
${"show " + cmd} (${data.length})
|
${"show " + cmd} (${data.length})
|
||||||
<div>
|
<div>
|
||||||
<button class="next-btn" id="btn-prev-${cmd}">Previous</button>
|
<button class="next-btn" id="btn-prev-${cmd}">Previous</button>
|
||||||
<span id="page-info-${cmd}">Output 1/${data.length}</span>
|
<span id="page-info-${cmd}">Output 1/${data.length}</span>
|
||||||
<button class="next-btn" id="btn-next-${cmd}">Next</button>
|
<button class="next-btn" id="btn-next-${cmd}">Next</button>
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="log-block ${
|
<div class="log-block ${
|
||||||
data[0].is_deleted ? "soft-deleted" : ""
|
deletedLogs.some((item) => item.id === data[0].id)
|
||||||
}" id="log-${cmd}">
|
? "soft-deleted"
|
||||||
${
|
: ""
|
||||||
currentVerUser
|
}" id="log-${cmd}">
|
||||||
? ""
|
${
|
||||||
: data[0].is_deleted
|
currentVerUser
|
||||||
? `<button type="button" class="secondary-btn cancel-trash-btn" title="Cancel soft delete">✖️</button>`
|
? ""
|
||||||
: `<button type="button" class="danger-btn trash-btn" title="Soft delete">🗑</button>`
|
: deletedLogs.some(
|
||||||
}
|
(item) => item.id === data[0].id,
|
||||||
<b>${data[0].filename}</b>\n${data[0].output}
|
)
|
||||||
</div>
|
? `<button type="button" class="secondary-btn cancel-trash-btn" title="Remove from delete list">✖️</button>`
|
||||||
`;
|
: `<button type="button" class="danger-btn trash-btn" title="Add to delete list">🗑</button>`
|
||||||
|
}
|
||||||
|
<b>${data[0].filename}</b><br>${data[0].output}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
logSections.appendChild(section);
|
logSections.appendChild(section);
|
||||||
|
|
||||||
|
|
@ -382,29 +378,7 @@
|
||||||
const updateDisplay = () => {
|
const updateDisplay = () => {
|
||||||
const info = logsCache[`${pid}_${version}`][cmd];
|
const info = logsCache[`${pid}_${version}`][cmd];
|
||||||
const curr = info.logs[info.index];
|
const curr = info.logs[info.index];
|
||||||
|
updateDisplayActionDelete(curr, cmd, version);
|
||||||
const logBlock = document.getElementById(`log-${cmd}`);
|
|
||||||
logBlock.className = `log-block ${
|
|
||||||
curr.is_deleted ? "soft-deleted" : ""
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const currentVerUser = versionsUser.find(
|
|
||||||
(verItem) => verItem.version === version,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentVerUser) {
|
|
||||||
if (curr.is_deleted) {
|
|
||||||
logBlock.innerHTML = `
|
|
||||||
<button type="button" class="secondary-btn cancel-trash-btn" title="Cancel soft delete">✖️</button>
|
|
||||||
<b>${curr.filename}</b>\n${curr.output}
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
logBlock.innerHTML = `
|
|
||||||
<button type="button" class="danger-btn trash-btn" title="Soft delete">🗑</button>
|
|
||||||
<b>${curr.filename}</b>\n${curr.output}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
`page-info-${cmd}`,
|
`page-info-${cmd}`,
|
||||||
|
|
@ -437,6 +411,73 @@
|
||||||
|
|
||||||
loadPIDs(pidParam, versionParam);
|
loadPIDs(pidParam, versionParam);
|
||||||
|
|
||||||
|
// Handle delete and cancel
|
||||||
|
document.addEventListener("click", async (e) => {
|
||||||
|
const pid = pidList.value;
|
||||||
|
const version = versionList.value;
|
||||||
|
|
||||||
|
if (!pid || !version) return;
|
||||||
|
|
||||||
|
// Find command section
|
||||||
|
const section = e.target.closest(".command-section");
|
||||||
|
if (!section) return;
|
||||||
|
|
||||||
|
const command = section.id.replace("section-", "");
|
||||||
|
const info = logsCache[`${pid}_${version}`][command];
|
||||||
|
const index = info.index;
|
||||||
|
const log = info.logs[index];
|
||||||
|
|
||||||
|
const isDeleted = deletedLogs.some(
|
||||||
|
(item) => item.id === log.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add to delete
|
||||||
|
if (e.target.classList.contains("trash-btn")) {
|
||||||
|
if (!isDeleted) {
|
||||||
|
deletedLogs.push({
|
||||||
|
id: log.id,
|
||||||
|
command: command,
|
||||||
|
});
|
||||||
|
updateDisplayActionDelete(log, command, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel add to delete
|
||||||
|
if (e.target.classList.contains("cancel-trash-btn")) {
|
||||||
|
deletedLogs = deletedLogs.filter(
|
||||||
|
(item) => item.id !== log.id,
|
||||||
|
);
|
||||||
|
updateDisplayActionDelete(log, command, version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update UI delete and cancel button
|
||||||
|
const updateDisplayActionDelete = (log, command, version) => {
|
||||||
|
const logBlock = document.getElementById(`log-${command}`);
|
||||||
|
const isDeleted = deletedLogs.some(
|
||||||
|
(item) => item.id === log.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
logBlock.className = `log-block ${
|
||||||
|
isDeleted ? "soft-deleted" : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const currentVerUser = versionsUser.find(
|
||||||
|
(verItem) => verItem.version === version,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentVerUser) {
|
||||||
|
logBlock.innerHTML = `
|
||||||
|
${
|
||||||
|
isDeleted
|
||||||
|
? `<button type="button" class="secondary-btn cancel-trash-btn" title="Remove from delete list">✖️</button>`
|
||||||
|
: `<button type="button" class="danger-btn trash-btn" title="Add to delete list">🗑</button>`
|
||||||
|
}
|
||||||
|
<b>${log.filename}</b>\n${log.output}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyBtn = document.getElementById("copyBtn");
|
const copyBtn = document.getElementById("copyBtn");
|
||||||
const downloadBtn = document.getElementById("downloadBtn");
|
const downloadBtn = document.getElementById("downloadBtn");
|
||||||
|
|
||||||
|
|
@ -522,116 +563,6 @@ ${license}`.trim();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Handle soft delete log -->
|
|
||||||
<script>
|
|
||||||
let pendingDeleteLog = null;
|
|
||||||
|
|
||||||
const logDeleteModal = document.getElementById("logDeleteModal");
|
|
||||||
const logCancelDeleteBtn =
|
|
||||||
document.getElementById("logCancelDeleteBtn");
|
|
||||||
const logDeleteBtn = document.getElementById("logDeleteBtn");
|
|
||||||
|
|
||||||
document.addEventListener("click", async (e) => {
|
|
||||||
if (e.target.classList.contains("trash-btn")) {
|
|
||||||
const section = e.target.closest(".command-section");
|
|
||||||
const pid = pidList.value;
|
|
||||||
const version = versionList.value;
|
|
||||||
const command = section.id.replace("section-", "");
|
|
||||||
const index = logsCache[`${pid}_${version}`][command].index;
|
|
||||||
const log =
|
|
||||||
logsCache[`${pid}_${version}`][command].logs[index];
|
|
||||||
|
|
||||||
pendingDeleteLog = {
|
|
||||||
id: log.id,
|
|
||||||
command,
|
|
||||||
pid,
|
|
||||||
version,
|
|
||||||
is_deleted: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const text = `Are you sure you want to <strong>soft delete</strong> this log? <br /><code>${pid} - ${version} - ${command}</code> <br /> <div style="font-size: 12px; font-weight: 700" >${log.filename}</div>`;
|
|
||||||
document.getElementById("deleteLogText").innerHTML = text;
|
|
||||||
logDeleteBtn.innerHTML = "Delete";
|
|
||||||
showDeleteModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.classList.contains("cancel-trash-btn")) {
|
|
||||||
const section = e.target.closest(".command-section");
|
|
||||||
const pid = pidList.value;
|
|
||||||
const version = versionList.value;
|
|
||||||
const command = section.id.replace("section-", "");
|
|
||||||
const index = logsCache[`${pid}_${version}`][command].index;
|
|
||||||
const log =
|
|
||||||
logsCache[`${pid}_${version}`][command].logs[index];
|
|
||||||
|
|
||||||
pendingDeleteLog = {
|
|
||||||
id: log.id,
|
|
||||||
command,
|
|
||||||
pid,
|
|
||||||
version,
|
|
||||||
is_deleted: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const text = `Are you sure you want to <strong>cancel soft delete</strong> this log? <br /><code>${pid} - ${version} - ${command}</code> <br /> <div style="font-size: 12px; font-weight: 700" >${log.filename}</div>`;
|
|
||||||
document.getElementById("deleteLogText").innerHTML = text;
|
|
||||||
|
|
||||||
logDeleteBtn.innerHTML = "Cancel Delete";
|
|
||||||
showDeleteModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function showDeleteModal() {
|
|
||||||
logDeleteModal.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
function hideDeleteModal() {
|
|
||||||
logDeleteModal.classList.add("hidden");
|
|
||||||
pendingDeleteLog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logCancelDeleteBtn.addEventListener("click", hideDeleteModal);
|
|
||||||
|
|
||||||
logDeleteBtn.addEventListener("click", async () => {
|
|
||||||
if (!pendingDeleteLog) return;
|
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/soft-delete-log", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(pendingDeleteLog),
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert(data.message || "Soft deleted successfully!");
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert(data.message || "Delete failed.");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
alert("Delete failed.");
|
|
||||||
} finally {
|
|
||||||
hideDeleteModal();
|
|
||||||
pendingDeleteLog = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function showToast(message = "Announce something") {
|
|
||||||
const toast = document.getElementById("logToast");
|
|
||||||
toast.textContent = message;
|
|
||||||
toast.classList.add("show");
|
|
||||||
toast.classList.remove("hidden");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove("show");
|
|
||||||
toast.classList.add("hidden");
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Handle confirm device -->
|
<!-- Handle confirm device -->
|
||||||
<script>
|
<script>
|
||||||
const deviceConfirmBtn =
|
const deviceConfirmBtn =
|
||||||
|
|
@ -646,6 +577,7 @@ ${license}`.trim();
|
||||||
document.getElementById("deviceConfirmText");
|
document.getElementById("deviceConfirmText");
|
||||||
|
|
||||||
let pendingDevice = null;
|
let pendingDevice = null;
|
||||||
|
let deletedLogs = [];
|
||||||
|
|
||||||
deviceConfirmBtn.addEventListener("click", () => {
|
deviceConfirmBtn.addEventListener("click", () => {
|
||||||
const pid = document.getElementById("pidList").value;
|
const pid = document.getElementById("pidList").value;
|
||||||
|
|
@ -656,12 +588,12 @@ ${license}`.trim();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingDevice = { pid, version };
|
pendingDevice = { pid, version, deletedLogs };
|
||||||
|
|
||||||
deviceConfirmText.innerHTML = `
|
deviceConfirmText.innerHTML = `
|
||||||
Are you sure you want to <strong>save</strong> these changes?<br />
|
Are you sure you want to <strong>save</strong> these changes?<br />
|
||||||
<strong>${pid} - ${version}</strong>
|
<strong>${pid} - ${version}</strong>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
deviceConfirmModal.classList.remove("hidden");
|
deviceConfirmModal.classList.remove("hidden");
|
||||||
});
|
});
|
||||||
|
|
@ -675,6 +607,7 @@ ${license}`.trim();
|
||||||
if (!pendingDevice) return;
|
if (!pendingDevice) return;
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
const user = JSON.parse(localStorage.getItem("user"));
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/confirm-device", {
|
const res = await fetch("/api/confirm-device", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -682,7 +615,11 @@ ${license}`.trim();
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(pendingDevice),
|
body: JSON.stringify({
|
||||||
|
...pendingDevice,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,17 @@
|
||||||
const errorMsg = document.querySelector(".error-msg");
|
const errorMsg = document.querySelector(".error-msg");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/login", {
|
const res = await fetch(
|
||||||
method: "POST",
|
"https://stage.nswteam.net/api/login",
|
||||||
headers: { "Content-Type": "application/json" },
|
{
|
||||||
body: JSON.stringify({ email, password }),
|
method: "POST",
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
userEmail: email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
|
@ -83,7 +89,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("token", data.token);
|
localStorage.setItem("token", data.token);
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
localStorage.setItem(
|
||||||
|
"user",
|
||||||
|
JSON.stringify({
|
||||||
|
name:
|
||||||
|
data.data.firstName +
|
||||||
|
" " +
|
||||||
|
data.data.lastName,
|
||||||
|
email: data.data.userEmail,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
58
route/web.js
58
route/web.js
|
|
@ -1,8 +1,5 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const bcrypt = require("bcrypt");
|
|
||||||
const { createToken } = require("../utils/jwt");
|
|
||||||
const { authenticateToken } = require("../middleware/auth");
|
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
|
|
@ -15,60 +12,5 @@ module.exports = (app, db) => {
|
||||||
res.sendFile(path.join(__dirname, "../public/login.html"));
|
res.sendFile(path.join(__dirname, "../public/login.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/api/login", async (req, res) => {
|
|
||||||
const { email, password } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [rows] = await db.query(
|
|
||||||
"SELECT * FROM users WHERE email = ?",
|
|
||||||
[email],
|
|
||||||
);
|
|
||||||
const user = rows[0];
|
|
||||||
const isMatch = await bcrypt.compare(
|
|
||||||
password,
|
|
||||||
user?.password || "!@#",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user || !isMatch) {
|
|
||||||
return res
|
|
||||||
.status(401)
|
|
||||||
.json({ message: "Invalid email or password" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = createToken({
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
});
|
|
||||||
return res
|
|
||||||
.status(200)
|
|
||||||
.json({ token, user: { name: user.name, email: user.email } });
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Login error:", err);
|
|
||||||
return res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/api/profile", authenticateToken, async (req, res) => {
|
|
||||||
const userId = req.user?.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [rows] = await db.query("SELECT * FROM users WHERE id = ?", [
|
|
||||||
userId,
|
|
||||||
]);
|
|
||||||
const user = rows[0];
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
user: {
|
|
||||||
name: user.name,
|
|
||||||
confirm_count: user.confirm_count,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use("/", router);
|
app.use("/", router);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
284
server.js
284
server.js
|
|
@ -1,13 +1,12 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const bodyParser = require("body-parser");
|
const bodyParser = require("body-parser");
|
||||||
|
const XLSX = require("xlsx-js-style");
|
||||||
const dotenv = require("dotenv");
|
const dotenv = require("dotenv");
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const stringSimilarity = require("string-similarity");
|
const stringSimilarity = require("string-similarity");
|
||||||
const bcrypt = require("bcrypt");
|
|
||||||
const inititalWebRoute = require("./route/web");
|
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
|
// 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) {
|
function deduplicateErrors(errors, threshold = 0.3) {
|
||||||
|
|
@ -54,21 +53,6 @@ async function columnExists(table, column) {
|
||||||
return rows[0].count > 0;
|
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
|
// === Tạo bảng
|
||||||
(async () => {
|
(async () => {
|
||||||
await db.query(`
|
await db.query(`
|
||||||
|
|
@ -94,12 +78,25 @@ const checkAndInsertAdminUser = async () => {
|
||||||
await db.query(`ALTER TABLE devices DROP COLUMN updated_by`);
|
await db.query(`ALTER TABLE devices DROP COLUMN updated_by`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await columnExists("devices", "updated_user"))) {
|
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(`
|
await db.query(`
|
||||||
ALTER TABLE devices
|
ALTER TABLE devices
|
||||||
ADD COLUMN updated_user INT DEFAULT NULL,
|
ADD COLUMN update_name VARCHAR(255)
|
||||||
ADD CONSTRAINT fk_updated_by_user FOREIGN KEY (updated_user) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
`);
|
||||||
`);
|
}
|
||||||
|
|
||||||
|
if (!(await columnExists("devices", "update_email"))) {
|
||||||
|
await db.query(`
|
||||||
|
ALTER TABLE devices
|
||||||
|
ADD COLUMN update_email VARCHAR(255)
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await columnExists("devices", "is_confirmed"))) {
|
if (!(await columnExists("devices", "is_confirmed"))) {
|
||||||
|
|
@ -121,9 +118,9 @@ const checkAndInsertAdminUser = async () => {
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (!(await columnExists(`${command}_outputs`, "is_deleted"))) {
|
if (await columnExists(`${command}_outputs`, "is_deleted")) {
|
||||||
await db.query(
|
await db.query(
|
||||||
`ALTER TABLE ${command}_outputs ADD COLUMN is_deleted BOOLEAN DEFAULT FALSE`,
|
`ALTER TABLE ${command}_outputs DROP COLUMN is_deleted`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -134,18 +131,8 @@ const checkAndInsertAdminUser = async () => {
|
||||||
await createOutputTable("logging");
|
await createOutputTable("logging");
|
||||||
|
|
||||||
await db.query(`
|
await db.query(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
DROP TABLE IF 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 ===
|
// === APIs ===
|
||||||
|
|
@ -162,7 +149,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
|
||||||
|
|
||||||
// Lấy danh sách các version tương ứng với PID
|
// Lấy danh sách các version tương ứng với PID
|
||||||
const [devices] = await db.query(
|
const [devices] = await db.query(
|
||||||
"SELECT id, version, updated_user FROM devices WHERE pid = ?",
|
"SELECT id, version, update_name FROM devices WHERE pid = ?",
|
||||||
[pid],
|
[pid],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -171,12 +158,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
const deviceId = device.id;
|
const deviceId = device.id;
|
||||||
const version = device.version;
|
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(
|
const [[inv]] = await db.query(
|
||||||
"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
|
"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
|
||||||
[deviceId],
|
[deviceId],
|
||||||
|
|
@ -195,7 +177,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
user: user[0],
|
user: device.update_name,
|
||||||
version,
|
version,
|
||||||
commands: {
|
commands: {
|
||||||
inventory: inv.c,
|
inventory: inv.c,
|
||||||
|
|
@ -219,62 +201,15 @@ app.get("/api/device/:pid/:version/:command", async (req, res) => {
|
||||||
if (!device) return res.status(404).json({ message: "Not found" });
|
if (!device) return res.status(404).json({ message: "Not found" });
|
||||||
|
|
||||||
const [logs] = await db.query(
|
const [logs] = await db.query(
|
||||||
`SELECT id, filename, output, created_at, is_deleted FROM ${command}_outputs WHERE device_id = ? ORDER BY created_at DESC`,
|
`SELECT id, filename, output, created_at FROM ${command}_outputs WHERE device_id = ? ORDER BY created_at DESC`,
|
||||||
[device.id],
|
[device.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(logs);
|
res.json(logs);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/soft-delete-log", authenticateToken, async (req, res) => {
|
app.post("/api/confirm-device", async (req, res) => {
|
||||||
const logId = req.body.id;
|
const { pid, version, deletedLogs, name, email } = req.body;
|
||||||
const logCommand = req.body.command;
|
|
||||||
const isDeleted = req.body.is_deleted;
|
|
||||||
|
|
||||||
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(
|
|
||||||
`UPDATE \`${logCommand}_outputs\` SET is_deleted = ? WHERE id = ?`,
|
|
||||||
[isDeleted, logId],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDeleted) {
|
|
||||||
res.json({ success: true, message: "Soft deleted successfully!" });
|
|
||||||
} else {
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: "Cancel soft deleted successfully!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Delete error:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "Server error, please try again!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/confirm-device", authenticateToken, async (req, res) => {
|
|
||||||
const { pid, version } = req.body;
|
|
||||||
const updatedBy = req.user?.id;
|
|
||||||
|
|
||||||
if (!pid || !version) {
|
if (!pid || !version) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
|
|
@ -297,7 +232,6 @@ app.post("/api/confirm-device", authenticateToken, async (req, res) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceId = rows[0].id;
|
|
||||||
if (rows[0].is_confirmed) {
|
if (rows[0].is_confirmed) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -310,29 +244,40 @@ app.post("/api/confirm-device", authenticateToken, async (req, res) => {
|
||||||
`UPDATE devices
|
`UPDATE devices
|
||||||
SET is_confirmed = true,
|
SET is_confirmed = true,
|
||||||
updated_at = CURRENT_TIMESTAMP,
|
updated_at = CURRENT_TIMESTAMP,
|
||||||
updated_user = ?
|
update_name = ?,
|
||||||
|
update_email = ?
|
||||||
WHERE pid = ? AND version = ?`,
|
WHERE pid = ? AND version = ?`,
|
||||||
[updatedBy, pid, version],
|
[name, email, pid, version],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete Log
|
// Delete Log
|
||||||
const tables = [
|
for (const item of deletedLogs) {
|
||||||
"inventory_outputs",
|
const { command, id } = item;
|
||||||
"version_outputs",
|
|
||||||
"license_outputs",
|
|
||||||
"logging_outputs",
|
|
||||||
];
|
|
||||||
for (const table of tables) {
|
|
||||||
await db.query(
|
|
||||||
`DELETE FROM ${table} WHERE device_id = ? AND is_deleted = TRUE`,
|
|
||||||
[deviceId],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.query(
|
if (!command || typeof id !== "number") continue;
|
||||||
"UPDATE users SET confirm_count = confirm_count + 1 WHERE id = ?",
|
|
||||||
[updatedBy],
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -347,6 +292,123 @@ app.post("/api/confirm-device", authenticateToken, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
// Danh sách regex lọc lỗi
|
||||||
const errorPatterns = [
|
const errorPatterns = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue