Compare commits
2 Commits
fe95626d9f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 08615ff590 | |||
| bd70df60b9 |
@@ -1,3 +1,5 @@
|
|||||||
PORT=8000
|
PORT=8000
|
||||||
WEATHER_API_KEY=de18eeaec53e4caca18170027252507
|
WEATHER_API_KEY=de18eeaec53e4caca18170027252507
|
||||||
DB_PASSWORD=ayobamidavid
|
DB_PASSWORD=ayobamidavid
|
||||||
|
STRIPE_SECRET_KEY=sk_test_51IWQUwH8oljXErmds28KftkL6o6jYIcPgYbBdfEmCPSuAlIh0fgoS4NADcCmsIZbdQ3p5nbAeCOcGkSmo38U9BIe00BdOenrqo
|
||||||
|
STRIPE_PUBLIC_KEY=pk_test_51IWQUwH8oljXErmdg6L4MhsuB6tDdmumlHFfyNaopty2U27pmRcqMX1c868zn838lGQtU1eYV6bKRSQtMFWf36VT00aNsvnTOE
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const chat = sequelize.define(
|
||||||
|
"chat",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
create_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
chat_messages: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "chat",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return chat;
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const flow = sequelize.define(
|
||||||
|
"flow",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "flow",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return flow;
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const flow_log = sequelize.define(
|
||||||
|
"flow_log",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
flow_id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
task_id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "pending",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "flow_logs",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return flow_log;
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const task = sequelize.define(
|
||||||
|
"task",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
flow_id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: "flow",
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action_type: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
input_data: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
order_index: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "task",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return task;
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const upload = sequelize.define(
|
||||||
|
"upload",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
filename: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
mimetype: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "upload",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return upload;
|
||||||
|
};
|
||||||
Generated
+1491
-24
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,20 @@
|
|||||||
"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",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"ol": "^7.1.0",
|
"ol": "^7.1.0",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
|
"redis": "^5.6.1",
|
||||||
"sequelize": "^6.21.6",
|
"sequelize": "^6.21.6",
|
||||||
|
"stripe": "^18.3.0",
|
||||||
|
"speakeasy": "^2.0.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
const messageInput = document.getElementById("message-input");
|
||||||
|
const sendBtn = document.getElementById("send-btn");
|
||||||
|
const messageList = document.getElementById("message-list");
|
||||||
|
const saveBtn = document.getElementById("save-btn");
|
||||||
|
const chatMessages = document.getElementById("chat-messages");
|
||||||
|
|
||||||
|
let lastMessageCount = 0;
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
async function sendMessage() {
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/send", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
messageInput.value = "";
|
||||||
|
await fetchMessages();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to send message:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all messages
|
||||||
|
async function fetchMessages() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/chat/all");
|
||||||
|
const messages = await response.json();
|
||||||
|
|
||||||
|
messageList.innerHTML = "";
|
||||||
|
messages.forEach((msg) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "p-2 bg-gray-100 rounded";
|
||||||
|
li.innerHTML = `
|
||||||
|
<div class="text-sm text-gray-600">${new Date(
|
||||||
|
msg.timestamp
|
||||||
|
).toLocaleString()}</div>
|
||||||
|
<div class="font-medium">${msg.message}</div>
|
||||||
|
`;
|
||||||
|
messageList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
lastMessageCount = messages.length;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch messages:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll for updates
|
||||||
|
async function pollForUpdates() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/poll?lastCheck=${lastMessageCount}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.updated) {
|
||||||
|
await fetchMessages();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Poll failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save chat
|
||||||
|
async function saveChat() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/chat/save", { method: "POST" });
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Chat saved successfully!");
|
||||||
|
} else {
|
||||||
|
alert("Failed to save chat");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save chat:", err);
|
||||||
|
alert("Failed to save chat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
sendBtn.addEventListener("click", sendMessage);
|
||||||
|
messageInput.addEventListener("keypress", (e) => {
|
||||||
|
if (e.key === "Enter") sendMessage();
|
||||||
|
});
|
||||||
|
saveBtn.addEventListener("click", saveChat);
|
||||||
|
|
||||||
|
// Initial load and start polling
|
||||||
|
fetchMessages();
|
||||||
|
setInterval(pollForUpdates, 2000);
|
||||||
+227
@@ -0,0 +1,227 @@
|
|||||||
|
// DOM elements
|
||||||
|
const createFlowBtn = document.getElementById("create-flow-btn");
|
||||||
|
const flowNameInput = document.getElementById("flow-name");
|
||||||
|
const flowDescriptionInput = document.getElementById("flow-description");
|
||||||
|
const flowSelect = document.getElementById("flow-select");
|
||||||
|
const actionTypeSelect = document.getElementById("action-type");
|
||||||
|
const taskInput = document.getElementById("task-input");
|
||||||
|
const orderIndexInput = document.getElementById("order-index");
|
||||||
|
const addTaskBtn = document.getElementById("add-task-btn");
|
||||||
|
const flowDetails = document.getElementById("flow-details");
|
||||||
|
const executeFlowSelect = document.getElementById("execute-flow-select");
|
||||||
|
const executePayloadInput = document.getElementById("execute-payload");
|
||||||
|
const executeBtn = document.getElementById("execute-btn");
|
||||||
|
const webhookBtn = document.getElementById("webhook-btn");
|
||||||
|
const executionResults = document.getElementById("execution-results");
|
||||||
|
|
||||||
|
let currentFlows = [];
|
||||||
|
|
||||||
|
// Create new flow
|
||||||
|
async function createFlow() {
|
||||||
|
const name = flowNameInput.value.trim();
|
||||||
|
const description = flowDescriptionInput.value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
alert("Flow name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/flow", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name, description }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const flow = await response.json();
|
||||||
|
alert(`Flow "${flow.name}" created successfully!`);
|
||||||
|
flowNameInput.value = "";
|
||||||
|
flowDescriptionInput.value = "";
|
||||||
|
loadFlows();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`Error: ${error.error}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Failed to create flow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all flows
|
||||||
|
async function loadFlows() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/flows");
|
||||||
|
const flows = await response.json();
|
||||||
|
currentFlows = flows;
|
||||||
|
|
||||||
|
// Update flow selects
|
||||||
|
flowSelect.innerHTML = '<option value="">Select Flow</option>';
|
||||||
|
executeFlowSelect.innerHTML =
|
||||||
|
'<option value="">Select Flow to Execute</option>';
|
||||||
|
|
||||||
|
flows.forEach((flow) => {
|
||||||
|
flowSelect.innerHTML += `<option value="${flow.id}">${flow.name}</option>`;
|
||||||
|
executeFlowSelect.innerHTML += `<option value="${flow.id}">${flow.name}</option>`;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load flows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add task to flow
|
||||||
|
async function addTask() {
|
||||||
|
const flowId = flowSelect.value;
|
||||||
|
const actionType = actionTypeSelect.value;
|
||||||
|
const inputData = taskInput.value.trim();
|
||||||
|
const orderIndex = parseInt(orderIndexInput.value);
|
||||||
|
|
||||||
|
if (!flowId || !actionType || !inputData || isNaN(orderIndex)) {
|
||||||
|
alert("All fields are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/flow/${flowId}/task`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action_type: actionType,
|
||||||
|
input_data: inputData,
|
||||||
|
order_index: orderIndex,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const task = await response.json();
|
||||||
|
alert(`Task "${task.action_type}" added successfully!`);
|
||||||
|
taskInput.value = "";
|
||||||
|
orderIndexInput.value = "";
|
||||||
|
loadFlowDetails(flowId);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`Error: ${error.error}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Failed to add task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load flow details
|
||||||
|
async function loadFlowDetails(flowId) {
|
||||||
|
if (!flowId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/flow/${flowId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
flowDetails.innerHTML = `
|
||||||
|
<div class="border rounded p-4">
|
||||||
|
<h3 class="font-semibold text-lg">${data.flow.name}</h3>
|
||||||
|
<p class="text-gray-600">${
|
||||||
|
data.flow.description || "No description"
|
||||||
|
}</p>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4 class="font-medium">Tasks:</h4>
|
||||||
|
<div class="space-y-2 mt-2">
|
||||||
|
${data.tasks
|
||||||
|
.map(
|
||||||
|
(task) => `
|
||||||
|
<div class="bg-gray-100 p-2 rounded">
|
||||||
|
<div class="font-medium">${task.action_type}</div>
|
||||||
|
<div class="text-sm text-gray-600">Input: ${task.input_data}</div>
|
||||||
|
<div class="text-xs text-gray-500">Order: ${task.order_index}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load flow details");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute flow
|
||||||
|
async function executeFlow() {
|
||||||
|
const flowId = executeFlowSelect.value;
|
||||||
|
const payload = executePayloadInput.value.trim();
|
||||||
|
|
||||||
|
if (!flowId) {
|
||||||
|
alert("Please select a flow to execute");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/flow/${flowId}/execute`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ payload }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
showExecutionResults(result);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`Error: ${error.error}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Failed to execute flow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show execution results
|
||||||
|
function showExecutionResults(result) {
|
||||||
|
executionResults.innerHTML = `
|
||||||
|
<h4 class="font-semibold mb-2">Execution Results:</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
${result.results
|
||||||
|
.map(
|
||||||
|
(r) => `
|
||||||
|
<div class="p-2 ${
|
||||||
|
r.status === "success" ? "bg-green-100" : "bg-red-100"
|
||||||
|
} rounded">
|
||||||
|
<div class="font-medium">Task ${r.task_id}</div>
|
||||||
|
<div class="text-sm">${r.result}</div>
|
||||||
|
<div class="text-xs text-gray-600">Status: ${r.status}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
executionResults.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get webhook URL
|
||||||
|
function getWebhookUrl() {
|
||||||
|
const flowId = executeFlowSelect.value;
|
||||||
|
|
||||||
|
if (!flowId) {
|
||||||
|
alert("Please select a flow");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhookUrl = `${window.location.origin}/flow/${flowId}/trigger?payload=test@example.com`;
|
||||||
|
alert(
|
||||||
|
`Webhook URL:\n${webhookUrl}\n\nCopy this URL to trigger the flow via webhook.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
createFlowBtn.addEventListener("click", createFlow);
|
||||||
|
addTaskBtn.addEventListener("click", addTask);
|
||||||
|
executeBtn.addEventListener("click", executeFlow);
|
||||||
|
webhookBtn.addEventListener("click", getWebhookUrl);
|
||||||
|
|
||||||
|
flowSelect.addEventListener("change", (e) => {
|
||||||
|
if (e.target.value) {
|
||||||
|
loadFlowDetails(e.target.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
loadFlows();
|
||||||
+308
-66
@@ -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 {
|
||||||
@@ -79,68 +278,6 @@ async function updateClocks() {
|
|||||||
updateClocks();
|
updateClocks();
|
||||||
setInterval(updateClocks, 1000);
|
setInterval(updateClocks, 1000);
|
||||||
|
|
||||||
// --- Airport Autocomplete ---
|
|
||||||
// const airportInput = document.querySelector('input[placeholder*="airport"]');
|
|
||||||
// let dropdownDiv;
|
|
||||||
// if (airportInput) {
|
|
||||||
// console.log("hi");
|
|
||||||
// dropdownDiv = document.createElement("div");
|
|
||||||
// dropdownDiv.className =
|
|
||||||
// "absolute bg-white border border-gray-300 rounded shadow z-10 w-full max-h-48 overflow-y-auto top-[calc(100%+10px)]";
|
|
||||||
// dropdownDiv.style.display = "none";
|
|
||||||
// airportInput.parentNode.appendChild(dropdownDiv);
|
|
||||||
// airportInput.addEventListener("input", async function () {
|
|
||||||
// const val = airportInput.value.trim();
|
|
||||||
// if (val.length < 3) {
|
|
||||||
// dropdownDiv.style.display = "none";
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// const res = await fetch(`/airports?search=${encodeURIComponent(val)}`);
|
|
||||||
// if (!res.ok) throw new Error("Failed to fetch airports");
|
|
||||||
// const airports = await res.json();
|
|
||||||
// if (!Array.isArray(airports) || airports.length === 0) {
|
|
||||||
// dropdownDiv.innerHTML =
|
|
||||||
// '<div class="p-2 text-gray-500">No results</div>';
|
|
||||||
// dropdownDiv.style.display = "block";
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// dropdownDiv.innerHTML = airports
|
|
||||||
// .map(
|
|
||||||
// (a, i) =>
|
|
||||||
// `<div class="p-2 hover:bg-sky-100 cursor-pointer" data-index="${i}">${a.name}</div>`
|
|
||||||
// )
|
|
||||||
// .join("");
|
|
||||||
// dropdownDiv.style.display = "block";
|
|
||||||
// Array.from(dropdownDiv.children).forEach((child, i) => {
|
|
||||||
// child.addEventListener("click", () => {
|
|
||||||
// airportInput.value = child.textContent;
|
|
||||||
// dropdownDiv.style.display = "none";
|
|
||||||
// selectedAirport = airports[i];
|
|
||||||
// console.log("hey");
|
|
||||||
// airportInput.textContent = `hold`;
|
|
||||||
// if (selectedAirport) {
|
|
||||||
// console.log(selectedAirport);
|
|
||||||
// airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`;
|
|
||||||
// showMap(
|
|
||||||
// Number(selectedAirport.latitude_deg),
|
|
||||||
// Number(selectedAirport.longitude_deg)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// } catch (err) {
|
|
||||||
// dropdownDiv.innerHTML =
|
|
||||||
// '<div class="p-2 text-red-500">Error loading airports</div>';
|
|
||||||
// dropdownDiv.style.display = "block";
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// // Hide dropdown on blur
|
|
||||||
// airportInput.addEventListener("blur", () =>
|
|
||||||
// setTimeout(() => (dropdownDiv.style.display = "none"), 200)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// --- Airport Autocomplete ---
|
// --- Airport Autocomplete ---
|
||||||
const airportInput = document.querySelector('input[placeholder*="airport"]');
|
const airportInput = document.querySelector('input[placeholder*="airport"]');
|
||||||
let dropdownDiv;
|
let dropdownDiv;
|
||||||
@@ -163,10 +300,6 @@ if (airportInput) {
|
|||||||
selectedAirport = currentAirports[index]; // Use latest fetched data
|
selectedAirport = currentAirports[index]; // Use latest fetched data
|
||||||
airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`; // Set once
|
airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`; // Set once
|
||||||
|
|
||||||
// showMap(
|
|
||||||
// Number(selectedAirport.latitude_deg),
|
|
||||||
// Number(selectedAirport.longitude_deg)
|
|
||||||
// );
|
|
||||||
onAirportSelected(selectedAirport);
|
onAirportSelected(selectedAirport);
|
||||||
dropdownDiv.style.display = "none";
|
dropdownDiv.style.display = "none";
|
||||||
});
|
});
|
||||||
@@ -307,6 +440,11 @@ function logWidgetClick(widgetName) {
|
|||||||
widget_name: widgetName,
|
widget_name: widgetName,
|
||||||
browser_type: navigator.userAgent,
|
browser_type: navigator.userAgent,
|
||||||
}),
|
}),
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res.status === 429) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.redirect) window.location.href = data.redirect;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,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 {
|
||||||
@@ -394,6 +587,55 @@ if (coinInput && coinBtn && coinResult) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Upload Widget ---
|
||||||
|
const uploadInput = document.querySelector(
|
||||||
|
'input[type="file"][id="upload-input"]'
|
||||||
|
);
|
||||||
|
const uploadBtn = document.getElementById("upload-btn");
|
||||||
|
const uploadPreview = document.getElementById("upload-preview");
|
||||||
|
const uploadError = document.getElementById("upload-error");
|
||||||
|
|
||||||
|
async function fetchLatestUpload() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/upload/latest");
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.url && uploadPreview) {
|
||||||
|
uploadPreview.src = data.url;
|
||||||
|
uploadPreview.style.display = "";
|
||||||
|
} else if (uploadPreview) {
|
||||||
|
uploadPreview.style.display = "none";
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadBtn && uploadInput) {
|
||||||
|
uploadBtn.addEventListener("click", async function () {
|
||||||
|
if (!uploadInput.files || !uploadInput.files[0]) {
|
||||||
|
if (uploadError) uploadError.textContent = "Please select an image.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", uploadInput.files[0]);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/upload", { method: "POST", body: formData });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.url) {
|
||||||
|
if (uploadPreview) {
|
||||||
|
uploadPreview.src = data.url;
|
||||||
|
uploadPreview.style.display = "";
|
||||||
|
}
|
||||||
|
if (uploadError) uploadError.textContent = "";
|
||||||
|
} else {
|
||||||
|
if (uploadError)
|
||||||
|
uploadError.textContent = data.error || "Upload failed.";
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (uploadError) uploadError.textContent = "Upload failed.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fetchLatestUpload();
|
||||||
|
|
||||||
// Attach click listeners to widgets
|
// Attach click listeners to widgets
|
||||||
function attachAnalyticListeners() {
|
function attachAnalyticListeners() {
|
||||||
const widgets = [
|
const widgets = [
|
||||||
|
|||||||
+246
-73
@@ -7,21 +7,41 @@
|
|||||||
"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-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);
|
||||||
--color-sky-400: oklch(74.6% 0.16 232.661);
|
--color-sky-400: oklch(74.6% 0.16 232.661);
|
||||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
--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-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-black: #000;
|
||||||
--color-white: #fff;
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
|
--container-md: 28rem;
|
||||||
|
--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;
|
||||||
@@ -183,24 +203,60 @@
|
|||||||
.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;
|
||||||
}
|
}
|
||||||
.row-span-2 {
|
.row-span-2 {
|
||||||
grid-row: span 2 / span 2;
|
grid-row: span 2 / span 2;
|
||||||
}
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
max-width: 48rem;
|
||||||
|
}
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
max-width: 64rem;
|
||||||
|
}
|
||||||
|
@media (width >= 80rem) {
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
@media (width >= 96rem) {
|
||||||
|
max-width: 96rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx-4 {
|
||||||
|
margin-inline: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.mx-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);
|
||||||
}
|
}
|
||||||
@@ -210,6 +266,12 @@
|
|||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.ml-2 {
|
.ml-2 {
|
||||||
margin-left: calc(var(--spacing) * 2);
|
margin-left: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -222,12 +284,18 @@
|
|||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
.h-24 {
|
.h-24 {
|
||||||
height: calc(var(--spacing) * 24);
|
height: calc(var(--spacing) * 24);
|
||||||
}
|
}
|
||||||
|
.h-96 {
|
||||||
|
height: calc(var(--spacing) * 96);
|
||||||
|
}
|
||||||
.h-244 {
|
.h-244 {
|
||||||
height: calc(var(--spacing) * 244);
|
height: calc(var(--spacing) * 244);
|
||||||
}
|
}
|
||||||
@@ -252,6 +320,18 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -261,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));
|
||||||
}
|
}
|
||||||
@@ -276,6 +359,12 @@
|
|||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.gap-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);
|
||||||
}
|
}
|
||||||
@@ -286,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;
|
||||||
}
|
}
|
||||||
@@ -302,21 +405,48 @@
|
|||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
border-color: var(--color-gray-300);
|
border-color: var(--color-gray-300);
|
||||||
}
|
}
|
||||||
.bg-blue-100 {
|
.bg-black {
|
||||||
background-color: var(--color-blue-100);
|
background-color: var(--color-black);
|
||||||
|
}
|
||||||
|
.bg-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 {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
.object-cover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
.p-2 {
|
.p-2 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -329,9 +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 {
|
||||||
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.px-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);
|
||||||
}
|
}
|
||||||
@@ -341,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));
|
||||||
@@ -357,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));
|
||||||
@@ -373,9 +526,24 @@
|
|||||||
--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 {
|
||||||
|
color: var(--color-blue-700);
|
||||||
|
}
|
||||||
.text-gray-500 {
|
.text-gray-500 {
|
||||||
color: var(--color-gray-500);
|
color: var(--color-gray-500);
|
||||||
}
|
}
|
||||||
|
.text-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);
|
||||||
}
|
}
|
||||||
@@ -390,14 +558,45 @@
|
|||||||
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
.blur {
|
|
||||||
--tw-blur: blur(8px);
|
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
|
||||||
}
|
|
||||||
.outline-none {
|
.outline-none {
|
||||||
--tw-outline-style: none;
|
--tw-outline-style: none;
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
}
|
}
|
||||||
|
.hover\:bg-blue-600 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-blue-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:bg-gray-600 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:bg-green-600 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-green-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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) {
|
||||||
@@ -405,12 +604,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:underline {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-2 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-blue-500 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: var(--color-blue-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:outline-none {
|
||||||
|
&:focus {
|
||||||
|
--tw-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;
|
||||||
@@ -485,63 +723,11 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0 0 #0000;
|
initial-value: 0 0 #0000;
|
||||||
}
|
}
|
||||||
@property --tw-blur {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-brightness {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-contrast {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-grayscale {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-hue-rotate {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-invert {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-opacity {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-saturate {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-sepia {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow-alpha {
|
|
||||||
syntax: "<percentage>";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 100%;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow-size {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@layer properties {
|
@layer properties {
|
||||||
@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;
|
||||||
@@ -558,19 +744,6 @@
|
|||||||
--tw-ring-offset-width: 0px;
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
--tw-blur: initial;
|
|
||||||
--tw-brightness: initial;
|
|
||||||
--tw-contrast: initial;
|
|
||||||
--tw-grayscale: initial;
|
|
||||||
--tw-hue-rotate: initial;
|
|
||||||
--tw-invert: initial;
|
|
||||||
--tw-opacity: initial;
|
|
||||||
--tw-saturate: initial;
|
|
||||||
--tw-sepia: initial;
|
|
||||||
--tw-drop-shadow: initial;
|
|
||||||
--tw-drop-shadow-color: initial;
|
|
||||||
--tw-drop-shadow-alpha: 100%;
|
|
||||||
--tw-drop-shadow-size: initial;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+491
-2
@@ -7,7 +7,37 @@ 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 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);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
@@ -60,9 +90,20 @@ router.get("/airports", function (req, res) {
|
|||||||
res.json(matches);
|
res.json(matches);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log analytic event
|
// Log analytic event with rate limiting
|
||||||
router.post("/analytic", async function (req, res) {
|
router.post("/analytic", async function (req, res) {
|
||||||
try {
|
try {
|
||||||
|
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);
|
||||||
const { widget_name, browser_type } = req.body;
|
const { widget_name, browser_type } = req.body;
|
||||||
if (!widget_name || !browser_type) {
|
if (!widget_name || !browser_type) {
|
||||||
return res
|
return res
|
||||||
@@ -117,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 {
|
||||||
@@ -162,4 +281,374 @@ router.post("/coin-calc", function (req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Chat Room</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 min-h-screen">
|
||||||
|
<div class="container mx-auto p-4 max-w-2xl">
|
||||||
|
<h1 class="text-3xl font-bold text-center mb-6">Chat Room</h1>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div
|
||||||
|
id="chat-messages"
|
||||||
|
class="h-96 overflow-y-auto mb-4 border rounded p-4"
|
||||||
|
>
|
||||||
|
<ul id="message-list" class="space-y-2">
|
||||||
|
<!-- Messages will be populated here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="message-input"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
class="flex-1 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="send-btn"
|
||||||
|
class="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="save-btn"
|
||||||
|
class="bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600"
|
||||||
|
>
|
||||||
|
Save Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/chat.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Flow Builder</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 min-h-screen">
|
||||||
|
<div class="container mx-auto p-4 max-w-4xl">
|
||||||
|
<h1 class="text-3xl font-bold text-center mb-6">Flow Builder</h1>
|
||||||
|
|
||||||
|
<!-- Create Flow Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Create New Flow</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="flow-name"
|
||||||
|
placeholder="Flow Name"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="flow-description"
|
||||||
|
placeholder="Description"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="create-flow-btn"
|
||||||
|
class="mt-4 bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Create Flow
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Task Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Add Task to Flow</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<select
|
||||||
|
id="flow-select"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Select Flow</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
id="action-type"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Select Action</option>
|
||||||
|
<option value="send_test_mail">Send Test Mail</option>
|
||||||
|
<option value="http_get_request">HTTP GET Request</option>
|
||||||
|
<option value="mysql_select">MySQL Select</option>
|
||||||
|
<option value="drive_upload">Drive Upload</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="task-input"
|
||||||
|
placeholder="Input Data"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="order-index"
|
||||||
|
placeholder="Order"
|
||||||
|
min="0"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="add-task-btn"
|
||||||
|
class="mt-4 bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600"
|
||||||
|
>
|
||||||
|
Add Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flow Details Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Flow Details</h2>
|
||||||
|
<div id="flow-details" class="space-y-4">
|
||||||
|
<!-- Flow details will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Execute Flow Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Execute Flow</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<select
|
||||||
|
id="execute-flow-select"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Select Flow to Execute</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="execute-payload"
|
||||||
|
placeholder="Payload (optional)"
|
||||||
|
class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 space-x-4">
|
||||||
|
<button
|
||||||
|
id="execute-btn"
|
||||||
|
class="bg-purple-500 text-white px-6 py-2 rounded hover:bg-purple-600"
|
||||||
|
>
|
||||||
|
Execute Flow
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="webhook-btn"
|
||||||
|
class="bg-orange-500 text-white px-6 py-2 rounded hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
Get Webhook URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="execution-results" class="mt-4 p-4 bg-gray-100 rounded hidden">
|
||||||
|
<!-- Execution results will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/flow.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+113
-5
@@ -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"
|
||||||
@@ -137,17 +156,106 @@ https://cdn.jsdelivr.net/npm/tailwindcss@4.1.11/dist/lib.min.js
|
|||||||
<div
|
<div
|
||||||
class="bg-white rounded-lg shadow-md p-6 flex flex-col items-center justify-center mt-8"
|
class="bg-white rounded-lg shadow-md p-6 flex flex-col items-center justify-center mt-8"
|
||||||
>
|
>
|
||||||
<div
|
<img
|
||||||
class="w-24 h-24 flex items-center justify-center bg-blue-100 rounded mb-2"
|
id="upload-preview"
|
||||||
|
src=""
|
||||||
|
alt="Latest upload"
|
||||||
|
class="w-24 h-24 object-cover rounded mb-2"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
<input type="file" id="upload-input" accept="image/*" class="mb-2" />
|
||||||
|
<button
|
||||||
|
id="upload-btn"
|
||||||
|
class="bg-sky-400 text-white font-semibold px-8 py-2 rounded"
|
||||||
>
|
>
|
||||||
<span class="text-4xl">📷</span>
|
|
||||||
</div>
|
|
||||||
<button class="bg-sky-400 text-white font-semibold px-8 py-2 rounded">
|
|
||||||
Upload
|
Upload
|
||||||
</button>
|
</button>
|
||||||
|
<span id="upload-error" class="text-xs text-red-500 mt-2"></span>
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user