fix: code review fixes
This commit is contained in:
@@ -1,26 +1,41 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
var session = require("express-session");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'pug');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "pug");
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
// Session middleware for 2FA
|
||||
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
|
||||
app.use(function (req, res, next) {
|
||||
@@ -31,11 +46,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
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
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
Generated
+691
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"express-session": "^1.17.3",
|
||||
"http-errors": "~1.6.3",
|
||||
"mariadb": "^3.0.1",
|
||||
"morgan": "~1.9.1",
|
||||
@@ -25,6 +26,8 @@
|
||||
"redis": "^5.6.1",
|
||||
"sequelize": "^6.21.6",
|
||||
"stripe": "^18.3.0",
|
||||
"speakeasy": "^2.0.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+254
@@ -1,5 +1,204 @@
|
||||
// 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
|
||||
async function updateWeather() {
|
||||
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 ---
|
||||
async function updateRedditNews() {
|
||||
try {
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
||||
--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-600: oklch(62.7% 0.194 149.214);
|
||||
--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-600: oklch(54.6% 0.245 262.881);
|
||||
--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-300: oklch(87.2% 0.01 258.338);
|
||||
--color-gray-500: oklch(55.1% 0.027 264.364);
|
||||
--color-gray-600: oklch(44.6% 0.03 256.802);
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--container-md: 28rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-4xl: 56rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
--text-sm--line-height: calc(1.25 / 0.875);
|
||||
--text-lg: 1.125rem;
|
||||
--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--line-height: calc(2.25 / 1.875);
|
||||
--text-4xl: 2.25rem;
|
||||
@@ -189,15 +203,24 @@
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.inset-0 {
|
||||
inset: calc(var(--spacing) * 0);
|
||||
}
|
||||
.top-\[calc\(100\%\+10px\)\] {
|
||||
top: calc(100% + 10px);
|
||||
}
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
.z-50 {
|
||||
z-index: 50;
|
||||
}
|
||||
.col-span-1 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
@@ -222,12 +245,18 @@
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.mx-4 {
|
||||
margin-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mt-8 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
}
|
||||
@@ -255,6 +284,9 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
@@ -291,6 +323,12 @@
|
||||
.max-w-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;
|
||||
}
|
||||
@@ -303,6 +341,9 @@
|
||||
.list-disc {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
.grid-cols-5 {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
@@ -321,6 +362,9 @@
|
||||
.gap-2 {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.gap-4 {
|
||||
gap: calc(var(--spacing) * 4);
|
||||
}
|
||||
.gap-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)));
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
@@ -347,18 +405,39 @@
|
||||
.border-gray-300 {
|
||||
border-color: var(--color-gray-300);
|
||||
}
|
||||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
.bg-blue-500 {
|
||||
background-color: var(--color-blue-500);
|
||||
}
|
||||
.bg-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 {
|
||||
background-color: var(--color-green-500);
|
||||
}
|
||||
.bg-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 {
|
||||
background-color: var(--color-sky-400);
|
||||
}
|
||||
@@ -380,15 +459,24 @@
|
||||
.p-8 {
|
||||
padding: calc(var(--spacing) * 8);
|
||||
}
|
||||
.px-2 {
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
.px-3 {
|
||||
padding-inline: calc(var(--spacing) * 3);
|
||||
}
|
||||
.px-4 {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.px-8 {
|
||||
padding-inline: calc(var(--spacing) * 8);
|
||||
}
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -398,6 +486,10 @@
|
||||
.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 {
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||
@@ -414,6 +506,10 @@
|
||||
font-size: var(--text-sm);
|
||||
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 {
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||
@@ -430,6 +526,12 @@
|
||||
--tw-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 {
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
@@ -439,6 +541,9 @@
|
||||
.text-gray-600 {
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
.text-green-500 {
|
||||
color: var(--color-green-500);
|
||||
}
|
||||
.text-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 {
|
||||
@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 {
|
||||
@media (hover: hover) {
|
||||
@@ -502,12 +628,27 @@
|
||||
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 {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0;
|
||||
}
|
||||
@property --tw-space-x-reverse {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
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)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-space-y-reverse: 0;
|
||||
--tw-space-x-reverse: 0;
|
||||
--tw-border-style: solid;
|
||||
--tw-font-weight: initial;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
|
||||
+168
-1
@@ -7,7 +7,9 @@ const airportData = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "../airportdata.json"), "utf8")
|
||||
);
|
||||
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 multer = require("multer");
|
||||
const uploadDir = path.join(__dirname, "../public/uploads");
|
||||
@@ -28,6 +30,15 @@ const redis = require("redis");
|
||||
const client = redis.createClient();
|
||||
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. */
|
||||
router.get("/", function (req, res, next) {
|
||||
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
|
||||
router.get("/reddit", async function (req, res) {
|
||||
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;
|
||||
|
||||
@@ -80,6 +80,25 @@ https://cdn.jsdelivr.net/npm/tailwindcss@4.1.11/dist/lib.min.js
|
||||
Export
|
||||
</button>
|
||||
</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 -->
|
||||
<div
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user