Compare commits
	
		
			4 Commits
		
	
	
		
			0c969bd4ab
			...
			fd51855b72
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						fd51855b72 | |
| 
							
							
								 | 
						557e267e21 | |
| 
							
							
								 | 
						2a6fcd6227 | |
| 
							
							
								 | 
						063f96c19d | 
| 
						 | 
				
			
			@ -1 +1,2 @@
 | 
			
		|||
BASE_URL=http://localhost:4000
 | 
			
		||||
JWT_SECRET=secret
 | 
			
		||||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,20 +94,6 @@
 | 
			
		|||
			</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 style="max-width: 30%" class="modal-content">
 | 
			
		||||
					<div id="deviceConfirmText"></div>
 | 
			
		||||
| 
						 | 
				
			
			@ -137,23 +123,29 @@
 | 
			
		|||
 | 
			
		||||
			async function getProfile() {
 | 
			
		||||
				const token = localStorage.getItem("token");
 | 
			
		||||
				const user = JSON.parse(localStorage.getItem("user"));
 | 
			
		||||
 | 
			
		||||
				await fetch("/api/profile", {
 | 
			
		||||
					method: "GET",
 | 
			
		||||
				await fetch("/api/confirm-count", {
 | 
			
		||||
					method: "POST",
 | 
			
		||||
					headers: {
 | 
			
		||||
						"Content-Type": "application/json",
 | 
			
		||||
						Authorization: `Bearer ${token}`,
 | 
			
		||||
					},
 | 
			
		||||
					body: JSON.stringify({
 | 
			
		||||
						email: user.email,
 | 
			
		||||
					}),
 | 
			
		||||
				})
 | 
			
		||||
					.then((res) => res.json())
 | 
			
		||||
					.then((data) => {
 | 
			
		||||
						document.getElementById(
 | 
			
		||||
							"welcome-user",
 | 
			
		||||
						).innerHTML = `Welcome, <strong>${data?.user?.name}</strong> ▼`;
 | 
			
		||||
						).innerHTML = `Welcome, <strong>${
 | 
			
		||||
							JSON.parse(localStorage.getItem("user")).name
 | 
			
		||||
						}</strong> ▼`;
 | 
			
		||||
 | 
			
		||||
						document.getElementById(
 | 
			
		||||
							"confirm-count-user",
 | 
			
		||||
						).innerHTML = `Confirmed: <strong>${data?.user?.confirm_count}</strong>`;
 | 
			
		||||
						).innerHTML = `Confirmed: <strong>${data?.count}</strong>`;
 | 
			
		||||
					})
 | 
			
		||||
					.catch((err) => {
 | 
			
		||||
						console.error(err);
 | 
			
		||||
| 
						 | 
				
			
			@ -298,6 +290,7 @@
 | 
			
		|||
			versionList.addEventListener("change", () => {
 | 
			
		||||
				const pid = pidList.value;
 | 
			
		||||
				const version = versionList.value;
 | 
			
		||||
				deletedLogs = [];
 | 
			
		||||
				if (pid && version) {
 | 
			
		||||
					// Set param pid, version to URL
 | 
			
		||||
					const currentParams = new URLSearchParams(
 | 
			
		||||
| 
						 | 
				
			
			@ -317,8 +310,7 @@
 | 
			
		|||
					);
 | 
			
		||||
					if (currentVerUser) {
 | 
			
		||||
						deviceConfirmBtn.style.display = "none";
 | 
			
		||||
						confirmedUserName.textContent =
 | 
			
		||||
							currentVerUser.user.name;
 | 
			
		||||
						confirmedUserName.textContent = currentVerUser.user;
 | 
			
		||||
						confirmUserText.style.display = "inline";
 | 
			
		||||
					} else {
 | 
			
		||||
						deviceConfirmBtn.style.display = "inline-block";
 | 
			
		||||
| 
						 | 
				
			
			@ -354,27 +346,31 @@
 | 
			
		|||
					section.className = "command-section";
 | 
			
		||||
					section.id = `section-${cmd}`;
 | 
			
		||||
					section.innerHTML = `
 | 
			
		||||
            <h3>
 | 
			
		||||
                ${"show " + cmd} (${data.length})
 | 
			
		||||
                <div>
 | 
			
		||||
                <button class="next-btn" id="btn-prev-${cmd}">Previous</button>
 | 
			
		||||
                <span id="page-info-${cmd}">Output 1/${data.length}</span>
 | 
			
		||||
                <button class="next-btn" id="btn-next-${cmd}">Next</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </h3>
 | 
			
		||||
            <div class="log-block ${
 | 
			
		||||
				data[0].is_deleted ? "soft-deleted" : ""
 | 
			
		||||
			}" id="log-${cmd}">
 | 
			
		||||
		${
 | 
			
		||||
			currentVerUser
 | 
			
		||||
				? ""
 | 
			
		||||
				: data[0].is_deleted
 | 
			
		||||
				? `<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>`
 | 
			
		||||
		}
 | 
			
		||||
                <b>${data[0].filename}</b>\n${data[0].output}
 | 
			
		||||
            </div>
 | 
			
		||||
            `;
 | 
			
		||||
						<h3>
 | 
			
		||||
							${"show " + cmd} (${data.length})
 | 
			
		||||
							<div>
 | 
			
		||||
							<button class="next-btn" id="btn-prev-${cmd}">Previous</button>
 | 
			
		||||
							<span id="page-info-${cmd}">Output 1/${data.length}</span>
 | 
			
		||||
							<button class="next-btn" id="btn-next-${cmd}">Next</button>
 | 
			
		||||
							</div>
 | 
			
		||||
						</h3>
 | 
			
		||||
						<div class="log-block ${
 | 
			
		||||
							deletedLogs.some((item) => item.id === data[0].id)
 | 
			
		||||
								? "soft-deleted"
 | 
			
		||||
								: ""
 | 
			
		||||
						}" id="log-${cmd}">
 | 
			
		||||
							${
 | 
			
		||||
								currentVerUser
 | 
			
		||||
									? ""
 | 
			
		||||
									: deletedLogs.some(
 | 
			
		||||
											(item) => item.id === data[0].id,
 | 
			
		||||
									  )
 | 
			
		||||
									? `<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);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -382,29 +378,7 @@
 | 
			
		|||
					const updateDisplay = () => {
 | 
			
		||||
						const info = logsCache[`${pid}_${version}`][cmd];
 | 
			
		||||
						const curr = info.logs[info.index];
 | 
			
		||||
 | 
			
		||||
						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}
 | 
			
		||||
								`;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						updateDisplayActionDelete(curr, cmd, version);
 | 
			
		||||
 | 
			
		||||
						document.getElementById(
 | 
			
		||||
							`page-info-${cmd}`,
 | 
			
		||||
| 
						 | 
				
			
			@ -437,6 +411,73 @@
 | 
			
		|||
 | 
			
		||||
			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 downloadBtn = document.getElementById("downloadBtn");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -522,116 +563,6 @@ ${license}`.trim();
 | 
			
		|||
			}
 | 
			
		||||
		</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 -->
 | 
			
		||||
		<script>
 | 
			
		||||
			const deviceConfirmBtn =
 | 
			
		||||
| 
						 | 
				
			
			@ -646,6 +577,7 @@ ${license}`.trim();
 | 
			
		|||
				document.getElementById("deviceConfirmText");
 | 
			
		||||
 | 
			
		||||
			let pendingDevice = null;
 | 
			
		||||
			let deletedLogs = [];
 | 
			
		||||
 | 
			
		||||
			deviceConfirmBtn.addEventListener("click", () => {
 | 
			
		||||
				const pid = document.getElementById("pidList").value;
 | 
			
		||||
| 
						 | 
				
			
			@ -656,12 +588,12 @@ ${license}`.trim();
 | 
			
		|||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				pendingDevice = { pid, version };
 | 
			
		||||
				pendingDevice = { pid, version, deletedLogs };
 | 
			
		||||
 | 
			
		||||
				deviceConfirmText.innerHTML = `
 | 
			
		||||
			Are you sure you want to <strong>save</strong> these changes?<br />
 | 
			
		||||
			<strong>${pid} - ${version}</strong>
 | 
			
		||||
		`;
 | 
			
		||||
					Are you sure you want to <strong>save</strong> these changes?<br />
 | 
			
		||||
					<strong>${pid} - ${version}</strong>
 | 
			
		||||
				`;
 | 
			
		||||
 | 
			
		||||
				deviceConfirmModal.classList.remove("hidden");
 | 
			
		||||
			});
 | 
			
		||||
| 
						 | 
				
			
			@ -675,6 +607,7 @@ ${license}`.trim();
 | 
			
		|||
				if (!pendingDevice) return;
 | 
			
		||||
 | 
			
		||||
				const token = localStorage.getItem("token");
 | 
			
		||||
				const user = JSON.parse(localStorage.getItem("user"));
 | 
			
		||||
				try {
 | 
			
		||||
					const res = await fetch("/api/confirm-device", {
 | 
			
		||||
						method: "POST",
 | 
			
		||||
| 
						 | 
				
			
			@ -682,7 +615,11 @@ ${license}`.trim();
 | 
			
		|||
							"Content-Type": "application/json",
 | 
			
		||||
							Authorization: `Bearer ${token}`,
 | 
			
		||||
						},
 | 
			
		||||
						body: JSON.stringify(pendingDevice),
 | 
			
		||||
						body: JSON.stringify({
 | 
			
		||||
							...pendingDevice,
 | 
			
		||||
							name: user.name,
 | 
			
		||||
							email: user.email,
 | 
			
		||||
						}),
 | 
			
		||||
					});
 | 
			
		||||
					const data = await res.json();
 | 
			
		||||
					if (data.success) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,11 +67,17 @@
 | 
			
		|||
					const errorMsg = document.querySelector(".error-msg");
 | 
			
		||||
 | 
			
		||||
					try {
 | 
			
		||||
						const res = await fetch("/api/login", {
 | 
			
		||||
							method: "POST",
 | 
			
		||||
							headers: { "Content-Type": "application/json" },
 | 
			
		||||
							body: JSON.stringify({ email, password }),
 | 
			
		||||
						});
 | 
			
		||||
						const res = await fetch(
 | 
			
		||||
							"https://stage.nswteam.net/api/login",
 | 
			
		||||
							{
 | 
			
		||||
								method: "POST",
 | 
			
		||||
								headers: { "Content-Type": "application/json" },
 | 
			
		||||
								body: JSON.stringify({
 | 
			
		||||
									userEmail: email,
 | 
			
		||||
									password,
 | 
			
		||||
								}),
 | 
			
		||||
							},
 | 
			
		||||
						);
 | 
			
		||||
 | 
			
		||||
						const data = await res.json();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +89,16 @@
 | 
			
		|||
						}
 | 
			
		||||
 | 
			
		||||
						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 = "/";
 | 
			
		||||
					} catch (err) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										58
									
								
								route/web.js
								
								
								
								
							
							
						
						
									
										58
									
								
								route/web.js
								
								
								
								
							| 
						 | 
				
			
			@ -1,8 +1,5 @@
 | 
			
		|||
const express = require("express");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const bcrypt = require("bcrypt");
 | 
			
		||||
const { createToken } = require("../utils/jwt");
 | 
			
		||||
const { authenticateToken } = require("../middleware/auth");
 | 
			
		||||
 | 
			
		||||
let router = express.Router();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,60 +12,5 @@ module.exports = (app, db) => {
 | 
			
		|||
		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);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										284
									
								
								server.js
								
								
								
								
							
							
						
						
									
										284
									
								
								server.js
								
								
								
								
							| 
						 | 
				
			
			@ -1,13 +1,12 @@
 | 
			
		|||
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 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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,21 +53,6 @@ async function columnExists(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(`
 | 
			
		||||
| 
						 | 
				
			
			@ -94,12 +78,25 @@ const checkAndInsertAdminUser = async () => {
 | 
			
		|||
		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(`
 | 
			
		||||
			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
 | 
			
		||||
		`);
 | 
			
		||||
		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"))) {
 | 
			
		||||
| 
						 | 
				
			
			@ -121,9 +118,9 @@ const checkAndInsertAdminUser = async () => {
 | 
			
		|||
      )
 | 
			
		||||
    `);
 | 
			
		||||
 | 
			
		||||
		if (!(await columnExists(`${command}_outputs`, "is_deleted"))) {
 | 
			
		||||
		if (await columnExists(`${command}_outputs`, "is_deleted")) {
 | 
			
		||||
			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 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
 | 
			
		||||
		)
 | 
			
		||||
		DROP TABLE IF EXISTS users
 | 
			
		||||
	`);
 | 
			
		||||
 | 
			
		||||
	await checkAndInsertAdminUser();
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
// === 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
 | 
			
		||||
	const [devices] = await db.query(
 | 
			
		||||
		"SELECT id, version, updated_user FROM devices WHERE pid = ?",
 | 
			
		||||
		"SELECT id, version, update_name FROM devices WHERE pid = ?",
 | 
			
		||||
		[pid],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -171,12 +158,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
 | 
			
		|||
	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],
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +177,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
 | 
			
		|||
		);
 | 
			
		||||
 | 
			
		||||
		results.push({
 | 
			
		||||
			user: user[0],
 | 
			
		||||
			user: device.update_name,
 | 
			
		||||
			version,
 | 
			
		||||
			commands: {
 | 
			
		||||
				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" });
 | 
			
		||||
 | 
			
		||||
	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],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	res.json(logs);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.post("/api/soft-delete-log", authenticateToken, async (req, res) => {
 | 
			
		||||
	const logId = req.body.id;
 | 
			
		||||
	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;
 | 
			
		||||
app.post("/api/confirm-device", async (req, res) => {
 | 
			
		||||
	const { pid, version, deletedLogs, name, email } = req.body;
 | 
			
		||||
 | 
			
		||||
	if (!pid || !version) {
 | 
			
		||||
		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) {
 | 
			
		||||
			return res.status(400).json({
 | 
			
		||||
				success: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -310,29 +244,40 @@ app.post("/api/confirm-device", authenticateToken, async (req, res) => {
 | 
			
		|||
			`UPDATE devices
 | 
			
		||||
				SET is_confirmed = true,
 | 
			
		||||
					updated_at = CURRENT_TIMESTAMP,
 | 
			
		||||
					updated_user = ?
 | 
			
		||||
					update_name = ?,
 | 
			
		||||
					update_email = ?
 | 
			
		||||
				WHERE pid = ? AND version = ?`,
 | 
			
		||||
			[updatedBy, pid, version],
 | 
			
		||||
			[name, email, pid, version],
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		// Delete Log
 | 
			
		||||
		const tables = [
 | 
			
		||||
			"inventory_outputs",
 | 
			
		||||
			"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],
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		for (const item of deletedLogs) {
 | 
			
		||||
			const { command, id } = item;
 | 
			
		||||
 | 
			
		||||
		await db.query(
 | 
			
		||||
			"UPDATE users SET confirm_count = confirm_count + 1 WHERE id = ?",
 | 
			
		||||
			[updatedBy],
 | 
			
		||||
		);
 | 
			
		||||
			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,
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
const errorPatterns = [
 | 
			
		||||
	{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue