2025-07-25 22:16:08 +01:00
|
|
|
var express = require("express");
|
2022-09-13 05:04:17 -04:00
|
|
|
var router = express.Router();
|
2025-07-25 22:16:08 +01:00
|
|
|
var path = require("path");
|
|
|
|
|
const fetch = require("node-fetch");
|
|
|
|
|
const fs = require("fs");
|
|
|
|
|
const airportData = JSON.parse(
|
|
|
|
|
fs.readFileSync(path.join(__dirname, "../airportdata.json"), "utf8")
|
|
|
|
|
);
|
|
|
|
|
const db = require("../models");
|
2025-08-12 17:41:06 +01:00
|
|
|
const { create, parse } = require("xmlbuilder2");
|
|
|
|
|
const speakeasy = require("speakeasy");
|
|
|
|
|
const QRCode = require("qrcode");
|
2025-07-28 06:42:00 +01:00
|
|
|
const rateLimitMap = new Map();
|
|
|
|
|
const multer = require("multer");
|
|
|
|
|
const uploadDir = path.join(__dirname, "../public/uploads");
|
|
|
|
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
|
|
|
|
const storage = multer.diskStorage({
|
|
|
|
|
destination: function (req, file, cb) {
|
|
|
|
|
cb(null, uploadDir);
|
|
|
|
|
},
|
|
|
|
|
filename: function (req, file, cb) {
|
|
|
|
|
const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
|
|
|
cb(null, unique + "-" + file.originalname);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const uploadMulter = multer({ storage });
|
|
|
|
|
|
|
|
|
|
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
|
|
|
|
|
const redis = require("redis");
|
|
|
|
|
const client = redis.createClient();
|
|
|
|
|
client.connect().catch(console.error);
|
2022-09-13 05:04:17 -04:00
|
|
|
|
2025-08-12 17:41:06 +01:00
|
|
|
// 2FA middleware
|
|
|
|
|
function require2FA(req, res, next) {
|
|
|
|
|
if (req.session && req.session.twoFactorVerified) {
|
|
|
|
|
next();
|
|
|
|
|
} else {
|
|
|
|
|
res.status(403).json({ error: "2FA verification required" });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-13 05:04:17 -04:00
|
|
|
/* GET home page. */
|
2025-07-25 22:16:08 +01:00
|
|
|
router.get("/", function (req, res, next) {
|
|
|
|
|
res.sendFile(path.join(__dirname, "../views/index.html"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Weather API route
|
|
|
|
|
router.get("/api/weather", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
// Replace with your actual API key and city
|
|
|
|
|
const apiKey = process.env.WEATHER_API_KEY;
|
|
|
|
|
const city = "Toronto";
|
|
|
|
|
const url = `http://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}&aqi=no`;
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
if (!response.ok) throw new Error("Weather API error");
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
const temp_c = data.current.temp_c;
|
|
|
|
|
const text = data.current.condition.text.toLowerCase();
|
|
|
|
|
let condition = "sunny";
|
|
|
|
|
if (text.includes("rain")) condition = "rain";
|
|
|
|
|
else if (text.includes("snow")) condition = "snow";
|
|
|
|
|
res.json({ temp_c, condition });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to fetch weather" });
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// UTC time API route
|
|
|
|
|
router.get("/api/utc", function (req, res) {
|
|
|
|
|
res.json({ utc: new Date().toISOString() });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Autocomplete airports
|
|
|
|
|
router.get("/airports", function (req, res) {
|
|
|
|
|
const search = (req.query.search || "").trim();
|
|
|
|
|
if (search.length < 3) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ error: "Search term must be at least 3 characters" });
|
|
|
|
|
}
|
|
|
|
|
const term = search.toLowerCase();
|
|
|
|
|
const matches = airportData
|
|
|
|
|
.filter(
|
|
|
|
|
(a) =>
|
|
|
|
|
(a.name && a.name.toLowerCase().includes(term)) ||
|
|
|
|
|
(a.code && a.code.toLowerCase().includes(term))
|
|
|
|
|
)
|
|
|
|
|
.slice(0, 10);
|
|
|
|
|
res.json(matches);
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-28 06:42:00 +01:00
|
|
|
// Log analytic event with rate limiting
|
2025-07-25 22:16:08 +01:00
|
|
|
router.post("/analytic", async function (req, res) {
|
|
|
|
|
try {
|
2025-07-28 06:42:00 +01:00
|
|
|
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const windowMs = 60 * 1000;
|
|
|
|
|
const maxReq = 10;
|
|
|
|
|
if (!rateLimitMap.has(ip)) rateLimitMap.set(ip, []);
|
|
|
|
|
let timestamps = rateLimitMap.get(ip).filter((ts) => now - ts < windowMs);
|
|
|
|
|
if (timestamps.length >= maxReq) {
|
|
|
|
|
return res.status(429).json({ redirect: "/pay" });
|
|
|
|
|
}
|
|
|
|
|
timestamps.push(now);
|
|
|
|
|
rateLimitMap.set(ip, timestamps);
|
2025-07-25 22:16:08 +01:00
|
|
|
const { widget_name, browser_type } = req.body;
|
|
|
|
|
if (!widget_name || !browser_type) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ error: "widget_name and browser_type required" });
|
|
|
|
|
}
|
|
|
|
|
await db.analytic.create({ widget_name, browser_type });
|
|
|
|
|
res.json({ success: true });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to log analytic" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Count analytic rows
|
|
|
|
|
router.get("/analytic/count", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const count = await db.analytic.count();
|
|
|
|
|
res.json({ count });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to count analytics" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Export analytic as XML
|
|
|
|
|
router.get("/analytic/export", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const analytics = await db.analytic.findAll({ raw: true });
|
|
|
|
|
const root = create({ version: "1.0" }).ele("analytics");
|
|
|
|
|
analytics.forEach((a) => {
|
|
|
|
|
root
|
|
|
|
|
.ele("analytic")
|
|
|
|
|
.ele("id")
|
|
|
|
|
.txt(a.id)
|
|
|
|
|
.up()
|
|
|
|
|
.ele("create_at")
|
|
|
|
|
.txt(a.create_at)
|
|
|
|
|
.up()
|
|
|
|
|
.ele("widget_name")
|
|
|
|
|
.txt(a.widget_name)
|
|
|
|
|
.up()
|
|
|
|
|
.ele("browser_type")
|
|
|
|
|
.txt(a.browser_type)
|
|
|
|
|
.up()
|
|
|
|
|
.up();
|
|
|
|
|
});
|
|
|
|
|
const xml = root.end({ prettyPrint: true });
|
|
|
|
|
res.set("Content-Type", "application/xml");
|
|
|
|
|
res.set("Content-Disposition", 'attachment; filename="analytics.xml"');
|
|
|
|
|
res.send(xml);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to export analytics" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-12 17:41:06 +01:00
|
|
|
// 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" });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-25 22:16:08 +01:00
|
|
|
// Reddit widget route
|
|
|
|
|
router.get("/reddit", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("https://www.reddit.com/r/programming.json");
|
|
|
|
|
if (!response.ok) throw new Error("Reddit API error");
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
const posts = (data.data.children || [])
|
|
|
|
|
.map((c) => c.data)
|
|
|
|
|
.filter((_, i) => i % 2 === 0)
|
|
|
|
|
.slice(0, 4)
|
|
|
|
|
.map((post) => ({
|
|
|
|
|
title: post.title,
|
|
|
|
|
url: "https://reddit.com" + post.permalink,
|
|
|
|
|
author: post.author,
|
|
|
|
|
}));
|
|
|
|
|
res.json(posts);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to fetch reddit posts" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Coin calculator route
|
|
|
|
|
router.post("/coin-calc", function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
let { amount } = req.body;
|
|
|
|
|
amount = parseFloat(amount);
|
|
|
|
|
if (isNaN(amount) || amount < 0)
|
|
|
|
|
return res.status(400).json({ error: "Invalid amount" });
|
|
|
|
|
const denoms = [20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01];
|
|
|
|
|
const result = [];
|
|
|
|
|
let remaining = Math.round(amount * 100); // work in cents
|
|
|
|
|
for (let d of denoms) {
|
|
|
|
|
let denomCents = Math.round(d * 100);
|
|
|
|
|
let count = Math.floor(remaining / denomCents);
|
|
|
|
|
if (count > 0) {
|
|
|
|
|
result.push({ denomination: d, count });
|
|
|
|
|
remaining -= count * denomCents;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.json(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to calculate coins" });
|
|
|
|
|
}
|
2022-09-13 05:04:17 -04:00
|
|
|
});
|
|
|
|
|
|
2025-07-28 06:42:00 +01:00
|
|
|
// Stripe payment page
|
|
|
|
|
router.get("/pay", async function (req, res) {
|
|
|
|
|
const session = await stripe.checkout.sessions.create({
|
|
|
|
|
payment_method_types: ["card"],
|
|
|
|
|
line_items: [
|
|
|
|
|
{
|
|
|
|
|
price_data: {
|
|
|
|
|
currency: "usd",
|
|
|
|
|
product_data: { name: "Widget Analytics Access" },
|
|
|
|
|
unit_amount: 500,
|
|
|
|
|
},
|
|
|
|
|
quantity: 1,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
mode: "payment",
|
|
|
|
|
success_url: req.protocol + "://" + req.get("host") + "/?paid=1",
|
|
|
|
|
cancel_url: req.protocol + "://" + req.get("host") + "/pay?cancel=1",
|
|
|
|
|
});
|
|
|
|
|
res.send(`
|
|
|
|
|
<html>
|
|
|
|
|
<head><title>Pay for Analytics</title></head>
|
|
|
|
|
<body style="display:flex;align-items:center;justify-content:center;height:100vh;flex-direction:column;">
|
|
|
|
|
<h2>Rate limit exceeded</h2>
|
|
|
|
|
<p>You need to pay $5 to continue using analytics.</p>
|
|
|
|
|
<button id="checkout">Pay with Stripe</button>
|
|
|
|
|
<script src="https://js.stripe.com/v3/"></script>
|
|
|
|
|
<script>
|
|
|
|
|
document.getElementById('checkout').onclick = function() {
|
|
|
|
|
var stripe = Stripe('${STRIPE_PUBLIC_KEY}');
|
|
|
|
|
stripe.redirectToCheckout({ sessionId: '${session.id}' });
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Upload image
|
|
|
|
|
router.post("/upload", uploadMulter.single("image"), async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
|
|
|
|
|
const file = req.file;
|
|
|
|
|
const dbUpload = await db.upload.create({
|
|
|
|
|
filename: file.filename,
|
|
|
|
|
mimetype: file.mimetype,
|
|
|
|
|
path: "/uploads/" + file.filename,
|
|
|
|
|
});
|
|
|
|
|
res.json({ url: "/uploads/" + file.filename });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to upload image" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get latest uploaded image
|
|
|
|
|
router.get("/upload/latest", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const latest = await db.upload.findOne({ order: [["created_at", "DESC"]] });
|
|
|
|
|
if (!latest) return res.json({ url: null });
|
|
|
|
|
res.json({ url: latest.path });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to fetch latest upload" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Chat routes
|
|
|
|
|
router.post("/send", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const { message } = req.body;
|
|
|
|
|
if (!message) return res.status(400).json({ error: "Message required" });
|
|
|
|
|
|
|
|
|
|
const chatMessage = {
|
|
|
|
|
message,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await client.lPush("chatroom", JSON.stringify(chatMessage));
|
|
|
|
|
res.json({ success: true });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to send message" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get("/chat/all", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const messages = await client.lRange("chatroom", 0, -1);
|
|
|
|
|
const parsedMessages = messages.map((msg) => JSON.parse(msg));
|
|
|
|
|
res.json(parsedMessages);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to fetch messages" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get("/poll", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const messageCount = await client.lLen("chatroom");
|
|
|
|
|
const lastCheck = req.query.lastCheck || 0;
|
|
|
|
|
|
|
|
|
|
if (messageCount > lastCheck) {
|
|
|
|
|
res.json({ updated: true, count: messageCount });
|
|
|
|
|
} else {
|
|
|
|
|
res.json({ updated: false, count: messageCount });
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Poll failed" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post("/chat/save", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const messages = await client.lRange("chatroom", 0, -1);
|
|
|
|
|
const chatMessages = JSON.stringify(messages.map((msg) => JSON.parse(msg)));
|
|
|
|
|
|
|
|
|
|
await db.chat.create({
|
|
|
|
|
chat_messages: chatMessages,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ success: true });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to save chat" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Chat page
|
|
|
|
|
router.get("/chat", function (req, res) {
|
|
|
|
|
res.sendFile(path.join(__dirname, "../views/chat.html"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Flow Builder routes
|
|
|
|
|
router.get("/flow", function (req, res) {
|
|
|
|
|
res.sendFile(path.join(__dirname, "../views/flow.html"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get all flows
|
|
|
|
|
router.get("/flows", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const flows = await db.flow.findAll({
|
|
|
|
|
order: [["created_at", "DESC"]],
|
|
|
|
|
});
|
|
|
|
|
res.json(flows);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to fetch flows" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create new flow
|
|
|
|
|
router.post("/flow", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const { name, description } = req.body;
|
|
|
|
|
if (!name) return res.status(400).json({ error: "Flow name required" });
|
|
|
|
|
|
|
|
|
|
const flow = await db.flow.create({ name, description });
|
|
|
|
|
res.json({ id: flow.id, name: flow.name });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to create flow" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add task to flow
|
|
|
|
|
router.post("/flow/:id/task", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const flowId = req.params.id;
|
|
|
|
|
const { action_type, input_data, order_index } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!action_type || !input_data || order_index === undefined) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ error: "action_type, input_data, and order_index required" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const task = await db.task.create({
|
|
|
|
|
flow_id: flowId,
|
|
|
|
|
action_type,
|
|
|
|
|
input_data,
|
|
|
|
|
order_index,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ id: task.id, action_type: task.action_type });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to add task" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get flow details
|
|
|
|
|
router.get("/flow/:id", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const flowId = req.params.id;
|
|
|
|
|
const flow = await db.flow.findByPk(flowId);
|
|
|
|
|
const tasks = await db.task.findAll({
|
|
|
|
|
where: { flow_id: flowId },
|
|
|
|
|
order: [["order_index", "ASC"]],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ flow, tasks });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to fetch flow" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Execute flow
|
|
|
|
|
router.post("/flow/:id/execute", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const flowId = req.params.id;
|
|
|
|
|
const { payload } = req.body;
|
|
|
|
|
|
|
|
|
|
const flow = await db.flow.findByPk(flowId);
|
|
|
|
|
const tasks = await db.task.findAll({
|
|
|
|
|
where: { flow_id: flowId },
|
|
|
|
|
order: [["order_index", "ASC"]],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
for (const task of tasks) {
|
|
|
|
|
let result = "";
|
|
|
|
|
let status = "success";
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
switch (task.action_type) {
|
|
|
|
|
case "send_test_mail":
|
|
|
|
|
// Simulate sending email
|
|
|
|
|
result = `Email sent to: ${task.input_data}`;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "http_get_request":
|
|
|
|
|
const response = await fetch(task.input_data);
|
|
|
|
|
result = `HTTP GET ${task.input_data}: ${response.status}`;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "mysql_select":
|
|
|
|
|
const [table, id] = task.input_data.split("|");
|
|
|
|
|
const query = `SELECT * FROM ${table} WHERE id = ${id}`;
|
|
|
|
|
result = `Query executed: ${query}`;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "drive_upload":
|
|
|
|
|
result = `File uploaded to Google Drive with content: ${task.input_data}`;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
result = `Unknown action type: ${task.action_type}`;
|
|
|
|
|
status = "error";
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
result = `Error: ${err.message}`;
|
|
|
|
|
status = "error";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log the execution
|
|
|
|
|
await db.flow_log.create({
|
|
|
|
|
flow_id: flowId,
|
|
|
|
|
task_id: task.id,
|
|
|
|
|
result,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
results.push({ task_id: task.id, result, status });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ flow_id: flowId, results });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to execute flow" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Webhook trigger
|
|
|
|
|
router.get("/flow/:id/trigger", async function (req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const flowId = req.params.id;
|
|
|
|
|
const payload = req.query.payload;
|
|
|
|
|
|
|
|
|
|
if (!payload) {
|
|
|
|
|
return res.status(400).json({ error: "Payload required" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Execute the flow with the payload
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${req.protocol}://${req.get("host")}/flow/${flowId}/execute`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ payload }),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
res.json(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(500).json({ error: "Failed to trigger flow" });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-12 17:41:06 +01:00
|
|
|
// 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 });
|
|
|
|
|
});
|
|
|
|
|
|
2022-09-13 05:04:17 -04:00
|
|
|
module.exports = router;
|