fix: code review fixes

This commit is contained in:
Ayobami
2025-08-12 17:41:06 +01:00
parent bd70df60b9
commit 08615ff590
7 changed files with 1389 additions and 16 deletions
+30 -15
View File
@@ -1,26 +1,41 @@
var createError = require('http-errors'); var createError = require("http-errors");
var express = require('express'); var express = require("express");
var path = require('path'); var path = require("path");
var cookieParser = require('cookie-parser'); var cookieParser = require("cookie-parser");
var logger = require('morgan'); var logger = require("morgan");
var session = require("express-session");
var indexRouter = require('./routes/index'); var indexRouter = require("./routes/index");
var usersRouter = require('./routes/users'); var usersRouter = require("./routes/users");
var app = express(); var app = express();
// view engine setup // view engine setup
app.set('views', path.join(__dirname, 'views')); app.set("views", path.join(__dirname, "views"));
app.set('view engine', 'pug'); app.set("view engine", "pug");
app.use(logger('dev')); app.use(logger("dev"));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter); // Session middleware for 2FA
app.use('/users', usersRouter); app.use(
session({
secret:
process.env.SESSION_SECRET || "your-secret-key-change-in-production",
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
})
);
app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter);
app.use("/users", usersRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function (req, res, next) { app.use(function (req, res, next) {
@@ -31,11 +46,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
// set locals, only providing error in development // set locals, only providing error in development
res.locals.message = err.message; res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {}; res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page // render the error page
res.status(err.status || 500); res.status(err.status || 500);
res.render('error'); res.render("error");
}); });
module.exports = app; module.exports = app;
+691
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -14,6 +14,7 @@
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"debug": "~2.6.9", "debug": "~2.6.9",
"express": "~4.16.1", "express": "~4.16.1",
"express-session": "^1.17.3",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"mariadb": "^3.0.1", "mariadb": "^3.0.1",
"morgan": "~1.9.1", "morgan": "~1.9.1",
@@ -25,6 +26,8 @@
"redis": "^5.6.1", "redis": "^5.6.1",
"sequelize": "^6.21.6", "sequelize": "^6.21.6",
"stripe": "^18.3.0", "stripe": "^18.3.0",
"speakeasy": "^2.0.0",
"qrcode": "^1.5.3",
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
+254
View File
@@ -1,5 +1,204 @@
// main.js // main.js
// --- 2FA Implementation ---
let twoFactorVerified = false;
// Check 2FA status on page load
async function check2FAStatus() {
try {
const res = await fetch("/2fa/status");
if (res.ok) {
const data = await res.json();
twoFactorVerified = data.verified;
if (!twoFactorVerified) {
show2FAModal();
}
} else {
show2FAModal();
}
} catch (err) {
show2FAModal();
}
}
// Show 2FA modal
function show2FAModal() {
const modal = document.getElementById("2fa-modal");
const setup = document.getElementById("2fa-setup");
const verify = document.getElementById("2fa-verify");
const loading = document.getElementById("2fa-loading");
modal.classList.remove("hidden");
loading.classList.remove("hidden");
setup.classList.add("hidden");
verify.classList.add("hidden");
// Generate 2FA secret
generate2FASecret();
}
// Generate 2FA secret
async function generate2FASecret() {
try {
const res = await fetch("/2fa/generate");
if (res.ok) {
const data = await res.json();
const setup = document.getElementById("2fa-setup");
const verify = document.getElementById("2fa-verify");
const loading = document.getElementById("2fa-loading");
loading.classList.add("hidden");
setup.classList.remove("hidden");
// Display secret key
document.getElementById("secret-key").textContent = data.secret;
// Generate QR code image
const qrCode = document.getElementById("qr-code");
qrCode.innerHTML = `
<div class="bg-white p-4 rounded text-center">
<img src="${data.qrCode}" alt="QR Code for 2FA" class="mx-auto" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';" />
<div class="text-xs text-gray-500 mt-2" style="display: none;">
<p>QR Code failed to load. Use this URL instead:</p>
<p class="font-mono bg-gray-100 px-2 py-1 rounded break-all">${data.qrCodeUrl}</p>
</div>
<p class="text-xs text-gray-500 mt-2">Scan this QR code with your authenticator app</p>
</div>
`;
} else {
throw new Error("Failed to generate 2FA secret");
}
} catch (err) {
console.error("2FA generation error:", err);
document.getElementById("2fa-loading").innerHTML =
'<p class="text-red-500">Error generating 2FA. Please refresh the page.</p>';
}
}
// Setup 2FA event listeners
document.addEventListener("DOMContentLoaded", function () {
const generateBtn = document.getElementById("generate-2fa");
const verifyBtn = document.getElementById("verify-2fa");
const backBtn = document.getElementById("back-to-setup");
const tokenInput = document.getElementById("2fa-token");
if (generateBtn) {
generateBtn.addEventListener("click", generate2FASecret);
}
if (verifyBtn) {
verifyBtn.addEventListener("click", verify2FA);
}
if (backBtn) {
backBtn.addEventListener("click", function () {
document.getElementById("2fa-setup").classList.remove("hidden");
document.getElementById("2fa-verify").classList.add("hidden");
});
}
if (tokenInput) {
tokenInput.addEventListener("input", function () {
if (this.value.length === 6) {
verify2FA();
}
});
}
const proceedBtn = document.getElementById("proceed-to-verify");
if (proceedBtn) {
proceedBtn.addEventListener("click", function () {
document.getElementById("2fa-setup").classList.add("hidden");
document.getElementById("2fa-verify").classList.remove("hidden");
document.getElementById("2fa-token").focus();
});
}
const downloadQrBtn = document.getElementById("download-qr");
if (downloadQrBtn) {
downloadQrBtn.addEventListener("click", function () {
const qrImg = document.querySelector("#qr-code img");
if (qrImg && qrImg.src) {
const link = document.createElement("a");
link.download = "2fa-qr-code.png";
link.href = qrImg.src;
link.click();
}
});
}
const copySecretBtn = document.getElementById("copy-secret");
if (copySecretBtn) {
copySecretBtn.addEventListener("click", function () {
const secretKey = document.getElementById("secret-key").textContent;
if (secretKey) {
navigator.clipboard
.writeText(secretKey)
.then(() => {
// Show temporary success message
const originalText = copySecretBtn.textContent;
copySecretBtn.textContent = "Copied!";
copySecretBtn.classList.remove("bg-gray-500", "hover:bg-gray-600");
copySecretBtn.classList.add("bg-green-500");
setTimeout(() => {
copySecretBtn.textContent = originalText;
copySecretBtn.classList.remove("bg-green-500");
copySecretBtn.classList.add("bg-gray-500", "hover:bg-gray-600");
}, 2000);
})
.catch(() => {
alert("Failed to copy to clipboard. Secret: " + secretKey);
});
}
});
}
});
// Verify 2FA token
async function verify2FA() {
const token = document.getElementById("2fa-token").value.trim();
if (token.length !== 6) {
alert("Please enter a 6-digit code");
return;
}
try {
const res = await fetch("/2fa/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
if (res.ok) {
const data = await res.json();
if (data.success) {
twoFactorVerified = true;
document.getElementById("2fa-modal").classList.add("hidden");
// Enable all widgets
enableDashboard();
} else {
alert("Invalid token. Please try again.");
}
} else {
const errorData = await res.json();
alert(errorData.error || "Verification failed");
}
} catch (err) {
alert("Verification failed. Please try again.");
}
}
// Enable dashboard after 2FA verification
function enableDashboard() {
// Dashboard is already visible, just ensure all functionality is enabled
console.log("Dashboard enabled after 2FA verification");
}
// Check 2FA status on page load
check2FAStatus();
// Function to update weather widget // Function to update weather widget
async function updateWeather() { async function updateWeather() {
try { try {
@@ -275,6 +474,61 @@ if (exportBtn && exportBtn.textContent.trim().toLowerCase() === "export") {
}); });
} }
// --- Import XML Widget ---
const importXmlInput = document.getElementById("import-xml-input");
const importXmlBtn = document.getElementById("import-xml-btn");
const importStatus = document.getElementById("import-status");
if (importXmlBtn && importXmlInput && importStatus) {
importXmlBtn.addEventListener("click", async function () {
if (!importXmlInput.files || !importXmlInput.files[0]) {
importStatus.textContent = "Please select an XML file";
importStatus.className = "text-xs mt-2 text-red-500";
return;
}
const file = importXmlInput.files[0];
// Validate file type
if (!file.type.includes("xml")) {
importStatus.textContent = "Please select a valid XML file";
importStatus.className = "text-xs mt-2 text-red-500";
return;
}
const formData = new FormData();
formData.append("xmlfile", file);
try {
importStatus.textContent = "Importing...";
importStatus.className = "text-xs mt-2 text-blue-500";
const res = await fetch("/analytic/import", {
method: "POST",
body: formData,
});
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || "Import failed");
}
const data = await res.json();
importStatus.textContent = data.message;
importStatus.className = "text-xs mt-2 text-green-500";
// Clear the file input
importXmlInput.value = "";
// Update the click counter to reflect new data
setTimeout(updateClickCounter, 1000);
} catch (err) {
importStatus.textContent = `Error: ${err.message}`;
importStatus.className = "text-xs mt-2 text-red-500";
}
});
}
// --- Reddit News Widget --- // --- Reddit News Widget ---
async function updateRedditNews() { async function updateRedditNews() {
try { try {
+142
View File
@@ -7,7 +7,12 @@
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace; "Courier New", monospace;
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-500: oklch(63.7% 0.237 25.331); --color-red-500: oklch(63.7% 0.237 25.331);
--color-orange-500: oklch(70.5% 0.213 47.604);
--color-orange-600: oklch(64.6% 0.222 41.116);
--color-green-100: oklch(96.2% 0.044 156.743);
--color-green-400: oklch(79.2% 0.209 151.711);
--color-green-500: oklch(72.3% 0.219 149.579); --color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214); --color-green-600: oklch(62.7% 0.194 149.214);
--color-sky-100: oklch(95.1% 0.026 236.824); --color-sky-100: oklch(95.1% 0.026 236.824);
@@ -15,19 +20,28 @@
--color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376); --color-blue-700: oklch(48.8% 0.243 264.376);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-purple-600: oklch(55.8% 0.288 302.321);
--color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-100: oklch(96.7% 0.003 264.542);
--color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-300: oklch(87.2% 0.01 258.338);
--color-gray-500: oklch(55.1% 0.027 264.364); --color-gray-500: oklch(55.1% 0.027 264.364);
--color-gray-600: oklch(44.6% 0.03 256.802); --color-gray-600: oklch(44.6% 0.03 256.802);
--color-black: #000;
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--container-md: 28rem;
--container-2xl: 42rem; --container-2xl: 42rem;
--container-4xl: 56rem;
--text-xs: 0.75rem; --text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75); --text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem; --text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875); --text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem; --text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125); --text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--text-3xl: 1.875rem; --text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875); --text-3xl--line-height: calc(2.25 / 1.875);
--text-4xl: 2.25rem; --text-4xl: 2.25rem;
@@ -189,15 +203,24 @@
.absolute { .absolute {
position: absolute; position: absolute;
} }
.fixed {
position: fixed;
}
.relative { .relative {
position: relative; position: relative;
} }
.inset-0 {
inset: calc(var(--spacing) * 0);
}
.top-\[calc\(100\%\+10px\)\] { .top-\[calc\(100\%\+10px\)\] {
top: calc(100% + 10px); top: calc(100% + 10px);
} }
.z-10 { .z-10 {
z-index: 10; z-index: 10;
} }
.z-50 {
z-index: 50;
}
.col-span-1 { .col-span-1 {
grid-column: span 1 / span 1; grid-column: span 1 / span 1;
} }
@@ -222,12 +245,18 @@
max-width: 96rem; max-width: 96rem;
} }
} }
.mx-4 {
margin-inline: calc(var(--spacing) * 4);
}
.mx-auto { .mx-auto {
margin-inline: auto; margin-inline: auto;
} }
.mt-2 { .mt-2 {
margin-top: calc(var(--spacing) * 2); margin-top: calc(var(--spacing) * 2);
} }
.mt-4 {
margin-top: calc(var(--spacing) * 4);
}
.mt-8 { .mt-8 {
margin-top: calc(var(--spacing) * 8); margin-top: calc(var(--spacing) * 8);
} }
@@ -255,6 +284,9 @@
.grid { .grid {
display: grid; display: grid;
} }
.hidden {
display: none;
}
.table { .table {
display: table; display: table;
} }
@@ -291,6 +323,12 @@
.max-w-2xl { .max-w-2xl {
max-width: var(--container-2xl); max-width: var(--container-2xl);
} }
.max-w-4xl {
max-width: var(--container-4xl);
}
.max-w-md {
max-width: var(--container-md);
}
.flex-1 { .flex-1 {
flex: 1; flex: 1;
} }
@@ -303,6 +341,9 @@
.list-disc { .list-disc {
list-style-type: disc; list-style-type: disc;
} }
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-5 { .grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
@@ -321,6 +362,9 @@
.gap-2 { .gap-2 {
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
} }
.gap-4 {
gap: calc(var(--spacing) * 4);
}
.gap-8 { .gap-8 {
gap: calc(var(--spacing) * 8); gap: calc(var(--spacing) * 8);
} }
@@ -331,6 +375,20 @@
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
} }
} }
.space-y-4 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-x-4 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
}
}
.overflow-y-auto { .overflow-y-auto {
overflow-y: auto; overflow-y: auto;
} }
@@ -347,18 +405,39 @@
.border-gray-300 { .border-gray-300 {
border-color: var(--color-gray-300); border-color: var(--color-gray-300);
} }
.bg-black {
background-color: var(--color-black);
}
.bg-blue-500 { .bg-blue-500 {
background-color: var(--color-blue-500); background-color: var(--color-blue-500);
} }
.bg-gray-100 { .bg-gray-100 {
background-color: var(--color-gray-100); background-color: var(--color-gray-100);
} }
.bg-gray-500 {
background-color: var(--color-gray-500);
}
.bg-green-100 {
background-color: var(--color-green-100);
}
.bg-green-400 {
background-color: var(--color-green-400);
}
.bg-green-500 { .bg-green-500 {
background-color: var(--color-green-500); background-color: var(--color-green-500);
} }
.bg-inherit { .bg-inherit {
background-color: inherit; background-color: inherit;
} }
.bg-orange-500 {
background-color: var(--color-orange-500);
}
.bg-purple-500 {
background-color: var(--color-purple-500);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-sky-400 { .bg-sky-400 {
background-color: var(--color-sky-400); background-color: var(--color-sky-400);
} }
@@ -380,15 +459,24 @@
.p-8 { .p-8 {
padding: calc(var(--spacing) * 8); padding: calc(var(--spacing) * 8);
} }
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-3 { .px-3 {
padding-inline: calc(var(--spacing) * 3); padding-inline: calc(var(--spacing) * 3);
} }
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.px-6 { .px-6 {
padding-inline: calc(var(--spacing) * 6); padding-inline: calc(var(--spacing) * 6);
} }
.px-8 { .px-8 {
padding-inline: calc(var(--spacing) * 8); padding-inline: calc(var(--spacing) * 8);
} }
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
.py-2 { .py-2 {
padding-block: calc(var(--spacing) * 2); padding-block: calc(var(--spacing) * 2);
} }
@@ -398,6 +486,10 @@
.font-mono { .font-mono {
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-3xl { .text-3xl {
font-size: var(--text-3xl); font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height)); line-height: var(--tw-leading, var(--text-3xl--line-height));
@@ -414,6 +506,10 @@
font-size: var(--text-sm); font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height)); line-height: var(--tw-leading, var(--text-sm--line-height));
} }
.text-xl {
font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height));
}
.text-xs { .text-xs {
font-size: var(--text-xs); font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height)); line-height: var(--tw-leading, var(--text-xs--line-height));
@@ -430,6 +526,12 @@
--tw-font-weight: var(--font-weight-semibold); --tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
} }
.break-all {
word-break: break-all;
}
.text-blue-500 {
color: var(--color-blue-500);
}
.text-blue-700 { .text-blue-700 {
color: var(--color-blue-700); color: var(--color-blue-700);
} }
@@ -439,6 +541,9 @@
.text-gray-600 { .text-gray-600 {
color: var(--color-gray-600); color: var(--color-gray-600);
} }
.text-green-500 {
color: var(--color-green-500);
}
.text-red-500 { .text-red-500 {
color: var(--color-red-500); color: var(--color-red-500);
} }
@@ -464,6 +569,13 @@
} }
} }
} }
.hover\:bg-gray-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-gray-600);
}
}
}
.hover\:bg-green-600 { .hover\:bg-green-600 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -471,6 +583,20 @@
} }
} }
} }
.hover\:bg-orange-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-orange-600);
}
}
}
.hover\:bg-purple-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-purple-600);
}
}
}
.hover\:bg-sky-100 { .hover\:bg-sky-100 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -502,12 +628,27 @@
outline-style: none; outline-style: none;
} }
} }
.md\:grid-cols-2 {
@media (width >= 48rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.md\:grid-cols-4 {
@media (width >= 48rem) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
} }
@property --tw-space-y-reverse { @property --tw-space-y-reverse {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
initial-value: 0; initial-value: 0;
} }
@property --tw-space-x-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-border-style { @property --tw-border-style {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -586,6 +727,7 @@
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop { *, ::before, ::after, ::backdrop {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
--tw-space-x-reverse: 0;
--tw-border-style: solid; --tw-border-style: solid;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000;
+168 -1
View File
@@ -7,7 +7,9 @@ const airportData = JSON.parse(
fs.readFileSync(path.join(__dirname, "../airportdata.json"), "utf8") fs.readFileSync(path.join(__dirname, "../airportdata.json"), "utf8")
); );
const db = require("../models"); const db = require("../models");
const { create } = require("xmlbuilder2"); const { create, parse } = require("xmlbuilder2");
const speakeasy = require("speakeasy");
const QRCode = require("qrcode");
const rateLimitMap = new Map(); const rateLimitMap = new Map();
const multer = require("multer"); const multer = require("multer");
const uploadDir = path.join(__dirname, "../public/uploads"); const uploadDir = path.join(__dirname, "../public/uploads");
@@ -28,6 +30,15 @@ const redis = require("redis");
const client = redis.createClient(); const client = redis.createClient();
client.connect().catch(console.error); client.connect().catch(console.error);
// 2FA middleware
function require2FA(req, res, next) {
if (req.session && req.session.twoFactorVerified) {
next();
} else {
res.status(403).json({ error: "2FA verification required" });
}
}
/* GET home page. */ /* GET home page. */
router.get("/", function (req, res, next) { router.get("/", function (req, res, next) {
res.sendFile(path.join(__dirname, "../views/index.html")); res.sendFile(path.join(__dirname, "../views/index.html"));
@@ -147,6 +158,84 @@ router.get("/analytic/export", async function (req, res) {
} }
}); });
// Import analytic from XML
router.post(
"/analytic/import",
uploadMulter.single("xmlfile"),
async function (req, res) {
try {
if (!req.file) {
return res.status(400).json({ error: "No XML file uploaded" });
}
// Check file type
if (
req.file.mimetype !== "text/xml" &&
req.file.mimetype !== "application/xml"
) {
return res.status(400).json({ error: "File must be XML format" });
}
// Read and parse the uploaded XML file
const xmlContent = fs.readFileSync(req.file.path, "utf8");
const doc = parse(xmlContent);
// Extract analytics data from XML
const analytics = [];
const analyticNodes = doc.find("//analytic");
for (const node of analyticNodes) {
const id = node.find("id")[0]?.text || null;
const createAt = node.find("create_at")[0]?.text || null;
const widgetName = node.find("widget_name")[0]?.text || null;
const browserType = node.find("browser_type")[0]?.text || null;
if (widgetName && browserType) {
analytics.push({
id: id ? parseInt(id) : undefined,
create_at: createAt ? new Date(createAt) : new Date(),
widget_name: widgetName,
browser_type: browserType,
});
}
}
if (analytics.length === 0) {
return res
.status(400)
.json({ error: "No valid analytics data found in XML" });
}
// Insert analytics into database
const insertedAnalytics = await db.analytic.bulkCreate(analytics, {
ignoreDuplicates: true,
updateOnDuplicate: ["widget_name", "browser_type"],
});
// Clean up uploaded file
fs.unlinkSync(req.file.path);
res.json({
success: true,
message: `Successfully imported ${insertedAnalytics.length} analytics records`,
count: insertedAnalytics.length,
});
} catch (err) {
// Clean up uploaded file on error
if (req.file && req.file.path) {
try {
fs.unlinkSync(req.file.path);
} catch (cleanupErr) {
console.error("Failed to cleanup uploaded file:", cleanupErr);
}
}
console.error("Import error:", err);
res.status(500).json({ error: "Failed to import analytics from XML" });
}
}
);
// Reddit widget route // Reddit widget route
router.get("/reddit", async function (req, res) { router.get("/reddit", async function (req, res) {
try { try {
@@ -484,4 +573,82 @@ router.get("/flow/:id/trigger", async function (req, res) {
} }
}); });
// 2FA Routes
// Generate 2FA secret
router.get("/2fa/generate", async function (req, res) {
try {
const secret = speakeasy.generateSecret({
name: "Dashboard 2FA",
length: 20,
});
// Store secret in session for verification
req.session = req.session || {};
req.session.tempSecret = secret.base32;
// Generate QR code as data URL
const qrCodeDataUrl = await QRCode.toDataURL(secret.otpauth_url, {
width: 200,
margin: 2,
color: {
dark: "#000000",
light: "#FFFFFF",
},
});
res.json({
secret: secret.base32,
qrCode: qrCodeDataUrl,
qrCodeUrl: secret.otpauth_url,
});
} catch (err) {
console.error("2FA generation error:", err);
res.status(500).json({ error: "Failed to generate 2FA secret" });
}
});
// Verify 2FA token
router.post("/2fa/verify", function (req, res) {
try {
const { token } = req.body;
const tempSecret = req.session?.tempSecret;
if (!tempSecret) {
return res
.status(400)
.json({ error: "No 2FA secret found. Please generate a new one." });
}
if (!token) {
return res.status(400).json({ error: "Token required" });
}
const verified = speakeasy.totp.verify({
secret: tempSecret,
encoding: "base32",
token: token,
window: 2, // Allow 2 time steps for clock skew
});
if (verified) {
// Mark 2FA as verified in session
req.session = req.session || {};
req.session.twoFactorVerified = true;
delete req.session.tempSecret; // Clean up temp secret
res.json({ success: true, message: "2FA verification successful" });
} else {
res.status(400).json({ error: "Invalid 2FA token" });
}
} catch (err) {
res.status(500).json({ error: "Failed to verify 2FA token" });
}
});
// Check 2FA status
router.get("/2fa/status", function (req, res) {
const verified = req.session?.twoFactorVerified || false;
res.json({ verified });
});
module.exports = router; module.exports = router;
+101
View File
@@ -80,6 +80,25 @@ https://cdn.jsdelivr.net/npm/tailwindcss@4.1.11/dist/lib.min.js
Export Export
</button> </button>
</div> </div>
<!-- Import XML -->
<div
class="bg-white rounded-lg shadow-md p-6 flex flex-col items-center mt-8"
>
<div class="text-xs text-gray-500 mb-2">Import XML</div>
<input
type="file"
id="import-xml-input"
accept=".xml,text/xml,application/xml"
class="mb-2 text-xs"
/>
<button
id="import-xml-btn"
class="bg-green-400 text-white font-semibold px-8 py-2 rounded"
>
Import
</button>
<div id="import-status" class="text-xs mt-2"></div>
</div>
<!-- Map --> <!-- Map -->
<div <div
class="row-span-2 bg-white rounded-lg shadow-md p-2 flex items-center justify-center" class="row-span-2 bg-white rounded-lg shadow-md p-2 flex items-center justify-center"
@@ -155,6 +174,88 @@ https://cdn.jsdelivr.net/npm/tailwindcss@4.1.11/dist/lib.min.js
</div> </div>
</div> </div>
</div> </div>
<!-- 2FA Modal -->
<div
id="2fa-modal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"
>
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
<h2 class="text-2xl font-bold mb-4 text-center">
Two-Factor Authentication
</h2>
<div id="2fa-setup" class="hidden">
<p class="text-gray-600 mb-4 text-center">
Scan this QR code with your authenticator app:
</p>
<div id="qr-code" class="flex justify-center mb-4"></div>
<div class="flex gap-2 mb-4">
<button
id="download-qr"
class="flex-1 bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600 text-sm"
>
Download QR
</button>
<button
id="copy-secret"
class="flex-1 bg-gray-500 text-white py-2 px-4 rounded hover:bg-gray-600 text-sm"
>
Copy Secret
</button>
</div>
<p class="text-xs text-gray-500 mb-4 text-center">
Or manually enter this secret:
<span
id="secret-key"
class="font-mono bg-gray-100 px-2 py-1 rounded"
></span>
</p>
<button
id="generate-2fa"
class="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 mb-4"
>
Generate New Secret
</button>
<button
id="proceed-to-verify"
class="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600"
>
I've Added the Code, Verify Now
</button>
</div>
<div id="2fa-verify" class="hidden">
<p class="text-gray-600 mb-4 text-center">
Enter the 6-digit code from your authenticator app:
</p>
<input
type="text"
id="2fa-token"
placeholder="000000"
maxlength="6"
class="w-full text-center text-2xl font-mono border border-gray-300 rounded px-4 py-2 mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
id="verify-2fa"
class="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 mb-2"
>
Verify
</button>
<button
id="back-to-setup"
class="w-full bg-gray-500 text-white py-2 px-4 rounded hover:bg-gray-600"
>
Back to Setup
</button>
</div>
<div id="2fa-loading" class="text-center">
<p class="text-gray-600 mb-4">Loading 2FA...</p>
</div>
</div>
</div>
<script src="/main.js"></script> <script src="/main.js"></script>
</body> </body>
</html> </html>