From f93781931fac1460793f7edacf9b8c39de09520d Mon Sep 17 00:00:00 2001 From: Mikolaj Wojciech Gorski Date: Sat, 26 Jul 2025 05:43:38 +0200 Subject: [PATCH] Reworked auth, added WS for imediate communication, cleaned up architecture. --- .dockerignore | 9 + Dockerfile.dev | 51 ++++++ bot.js | 284 +++++++++++------------------- docker-compose.dev.yml | 32 ++++ entry.sh | 0 public/socket_logic.js | 94 ++++++++++ src/controllers/authController.js | 127 +++++++++++++ src/events/disconnect.js | 8 + src/events/messageCreate.js | 22 +++ src/events/ready.js | 23 +++ src/routes/authRoutes.js | 16 ++ views/index.ejs | 62 ++++++- 12 files changed, 541 insertions(+), 187 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.dev create mode 100644 docker-compose.dev.yml mode change 100644 => 100755 entry.sh create mode 100644 public/socket_logic.js create mode 100644 src/controllers/authController.js create mode 100644 src/events/disconnect.js create mode 100644 src/events/messageCreate.js create mode 100644 src/events/ready.js create mode 100644 src/routes/authRoutes.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d48b744 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +output/ +Dockerfile +Dockerfile.dev +docker-compose.yaml +docker-compose.dev.yml +.git +.gitignore +README.md diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..d59bf71 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,51 @@ +# --- Stage 1: Install node_modules --- +FROM node:20-slim as dependencies + +WORKDIR /opt/bot +COPY package*.json ./ +RUN npm install --no-audit --no-fund + +# --- Stage 2: Development Build --- +FROM node:20-slim + +# Install system dependencies for Discord and virtual display +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl ca-certificates sudo \ + # ── Discord runtime deps ────────────────── + libatomic1 libnotify4 libnspr4 libnss3 libxss1 libgbm1 \ + libgconf-2-4 libxtst6 libgtk-3-0 \ + # ── GUI / audio / misc ─────────────────── + xvfb pulseaudio openbox \ + tigervnc-standalone-server tigervnc-common fonts-liberation \ + x11vnc \ + && rm -rf /var/lib/apt/lists/* + +# Download and install Discord +WORKDIR /opt/preinstall +RUN curl -L https://discord.com/api/download/stable?platform=linux -o discord.deb && \ + dpkg -i discord.deb || apt-get -f install -y + +# Create a non-root user +RUN useradd --create-home --shell /bin/bash bot && \ + adduser bot sudo && \ + echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +# Set up the bot directory +WORKDIR /opt/bot +# Copy pre-installed node_modules from the 'dependencies' stage +COPY --from=dependencies /opt/bot/node_modules ./node_modules +# Copy entrypoint and set permissions +COPY entry.sh . +RUN chmod +x /opt/bot/entry.sh +# The rest of the source code will be mounted as a volume + +# Set ownership and user +RUN chown -R bot:bot /opt/bot +USER bot + +# Set environment and entrypoint +ENV DISPLAY=:99 +RUN mkdir -p /run/dbus +ENTRYPOINT dbus-daemon --system --fork --nofork --address unix:/run/dbus/system_bus_socket & exec /opt/bot/entry.sh "$@" diff --git a/bot.js b/bot.js index 92eb033..a720c43 100644 --- a/bot.js +++ b/bot.js @@ -4,15 +4,22 @@ import { Client as DiscordClient } from "discord.js-selfbot-v13"; import express from "express"; import session from "express-session"; import path from "path"; -import { fileURLToPath } from "url"; -import fetch from "node-fetch"; // Import fetch +import { fileURLToPath, pathToFileURL } from "url"; +import { WebSocketServer } from "ws"; +import authRouter from "./src/routes/authRoutes.js"; +import fs from "fs"; // --- CONFIGURATION --- -const { USER_TOKEN, BOT_CLIENT_ID, BOT_CLIENT_SECRET, BOT_REDIRECT_URI } = - process.env; +const { + USER_TOKEN, + BOT_CLIENT_ID, + BOT_CLIENT_SECRET, + BOT_REDIRECT_URI, + COOKIE_SECRET, +} = process.env; const WEB_PORT = 3000; -// IMPORTANT: Replace with the Discord User IDs of people allowed to access the dashboard. const ADMIN_USER_IDS = ["339753362297847810"]; +const MAX_LOG_SIZE = 20; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -20,217 +27,122 @@ const __dirname = path.dirname(__filename); // --- IN-MEMORY STATE --- const activityLog = []; const actionsLog = []; -const MAX_LOG_SIZE = 20; - -const logItem = (logArray, item) => { - logArray.unshift(item); - if (logArray.length > MAX_LOG_SIZE) logArray.pop(); -}; // --- WEB SERVER SETUP --- const app = express(); +// Make config and data available to routers/controllers via app.locals +app.locals.config = { + BOT_CLIENT_ID, + BOT_CLIENT_SECRET, + BOT_REDIRECT_URI, + ADMIN_USER_IDS, +}; +app.locals.data = { + activityLog, + actionsLog, +}; + app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); -app.set("trust proxy", 1); +app.set("trust proxy", 1); // Required for secure cookies if behind a proxy app.use( session({ - secret: "a-very-secret-string-for-teto", + secret: COOKIE_SECRET || "a-very-secret-string-for-teto-is-required", resave: false, saveUninitialized: false, cookie: { - secure: true, + secure: process.env.NODE_ENV === "production", httpOnly: true, sameSite: "lax", }, }), ); -// --- OAUTH2 AUTHENTICATION & DASHBOARD ROUTES --- -app.get("/auth/discord", (req, res) => { - const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${BOT_CLIENT_ID}&redirect_uri=${encodeURIComponent(BOT_REDIRECT_URI)}&response_type=code&scope=identify`; - res.redirect(authUrl); -}); - -app.get("/auth/callback", async (req, res) => { - const code = req.query.code; - if (!code) return res.status(400).send("No code provided."); - - try { - const params = { - client_id: BOT_CLIENT_ID, - client_secret: BOT_CLIENT_SECRET, - code, - grant_type: "authorization_code", - redirect_uri: BOT_REDIRECT_URI, - }; - console.log("Attempting token exchange with params:", params); - - const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { - method: "POST", - body: new URLSearchParams(params), - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - }); - const tokenData = await tokenResponse.json(); - - if (tokenData.error) { - console.error("Token Exchange Error:", tokenData); // Log the full error response - return res - .status(500) - .send( - `Error exchanging code for token. Details: ${JSON.stringify(tokenData)}`, - ); - } - - const userResponse = await fetch("https://discord.com/api/users/@me", { - headers: { Authorization: `Bearer ${tokenData.access_token}` }, - }); - const userData = await userResponse.json(); - - if (!ADMIN_USER_IDS.includes(userData.id)) { - return res - .status(403) - .send("You are not authorized to view this dashboard."); - } - - req.session.user = { - id: userData.id, - username: userData.username, - discriminator: userData.discriminator, - avatar: userData.avatar, - }; - req.session.save((err) => { - if (err) { - console.error("Error saving session:", err); - return res - .status(500) - .send("An error occurred while saving the session."); - } - console.log("Session saved successfully, redirecting."); - res.redirect("/"); - }); - } catch (error) { - console.error("OAuth2 Callback Error:", error); - res.status(500).send("An error occurred during authentication."); - } -}); - -app.get("/logout", (req, res, next) => { - req.session.destroy((err) => { - if (err) return next(err); - res.redirect("/"); - }); -}); - -const checkAuth = (req, res, next) => { - if (req.session.user) return next(); - res.redirect("/auth/discord"); -}; - +// --- STATIC ASSETS & ROUTES --- app.use(express.static(path.join(__dirname, "public"))); +app.use("/", authRouter); -app.get("/", checkAuth, (req, res) => { - const systemResources = { - memory: { percentage: 91, used: 7.3, total: 8 }, - vram: { percentage: 69, used: 5.5, total: 8 }, - avgResponse: 32, - shutdown: "3d 0h", - sessionEnd: "Jul 24, 2025, 07:21 PM", - }; - res.render("index", { - user: req.session.user, - activityLog, - actionsLog, - systemResources, - }); -}); - -// --- DISCORD BOT LOGIC (no changes here) --- +// --- DISCORD BOT & SERVER INITIALIZATION --- (async () => { + // Puppeteer setup (currently optional) puppeteer.use(StealthPlugin()); - - // This part is disabled for now but can be re-enabled if needed for screenshots /* - const browser = await puppeteer.launch({ - executablePath: '/usr/bin/discord', - headless: false, - args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--start-maximized'], - defaultViewport: null, - }); - const [page] = await browser.pages(); - console.log("Puppeteer has attached to Discord's browser."); + const browser = await puppeteer.launch({ headless: false }); + // ... more puppeteer logic if needed */ + // Start the web server AND capture the HTTP server instance it creates + const server = app.listen(WEB_PORT, () => { + console.log(`Dashboard server running on http://localhost:${WEB_PORT}`); + }); + + // Create the WebSocket server and attach it to our existing HTTP server + const wss = new WebSocketServer({ server }); + + // Function to broadcast data to all connected WebSocket clients + const broadcast = (data) => { + // The 'wss.clients' property is a Set of all connected clients + wss.clients.forEach((client) => { + // Check if the connection is still open before sending + if (client.readyState === client.OPEN) { + // WebSocket only sends strings, so we stringify our object + client.send(JSON.stringify(data)); + } + }); + }; + + const logItem = (logArray, item) => { + logArray.unshift(item); + if (logArray.length > MAX_LOG_SIZE) logArray.pop(); + + if (logArray === activityLog) { + broadcast({ type: "activityLogUpdate", payload: item }); + } + }; + + wss.on("connection", (ws) => { + console.log("Dashboard client connected via WebSocket."); + ws.on("close", () => { + console.log("Dashboard client disconnected."); + }); + }); + const client = new DiscordClient({ checkUpdate: false }); - client.once("ready", () => { - console.log(`Selfbot ready as ${client.user.tag} (${client.user.id})`); + // --- DYNAMIC EVENT HANDLER LOADING --- + const eventsPath = path.join(__dirname, "src", "events"); + const eventFiles = fs + .readdirSync(eventsPath) + .filter((file) => file.endsWith(".js")); - // Add dummy data - logItem(actionsLog, { - message: "Responded to Alice in", - channel: "#gaming", - icon: "💬", - }); - logItem(actionsLog, { - message: "Joined Voice Chat", - channel: "#general", - icon: "🎤", - }); - logItem(actionsLog, { - message: "Analyzed image from Charlie in", - channel: "#memes", - icon: "👁️", - }); + const context = { + client, + logItem, + activityLog, + actionsLog, + MAX_LOG_SIZE, + }; - const now = new Date(); - logItem(activityLog, { - message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Memory consolidation complete`, - }); - now.setSeconds(now.getSeconds() - 4); - logItem(activityLog, { - message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Learning: Alice enjoys my French bread jokes`, - }); - now.setSeconds(now.getSeconds() - 4); - logItem(activityLog, { - message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Updating conversation context in vector database...`, - }); - now.setSeconds(now.getSeconds() - 4); - logItem(activityLog, { - message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Alice reacted with 😊 - response was well received`, - }); - now.setSeconds(now.getSeconds() - 4); - logItem(activityLog, { - message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Monitoring user reactions and engagement...`, - }); - }); + for (const file of eventFiles) { + const filePath = path.join(eventsPath, file); + const event = (await import(pathToFileURL(filePath))).default; - client.on("messageCreate", (msg) => { - if (msg.author.id === client.user.id) return; - - const message = `[${new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Message from ${msg.author.tag} in #${msg.channel.name || "DM"}: "${msg.content.slice(0, 50)}..."`; - logItem(activityLog, { message }); - - // Example of logging a bot action - if (msg.content.toLowerCase().includes("hello teto")) { - msg.channel.send("Hello there!"); - logItem(actionsLog, { - message: `Responded to ${msg.author.tag}`, - channel: `#${msg.channel.name || "DM"}`, - icon: "💬", - }); + if (event.name && event.execute) { + if (event.once) { + client.once(event.name, (...args) => event.execute(context, ...args)); + } else { + client.on(event.name, (...args) => event.execute(context, ...args)); + } } - }); + } + // End dynamic event handler loading - client.on("disconnect", () => { - const message = `[${new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Bot disconnected.`; - logItem(activityLog, { message }); - }); - - client.login(USER_TOKEN); - - app.listen(WEB_PORT, () => { - console.log(`Dashboard server running on https://teto.getsilly.org`); - }); + try { + await client.login(USER_TOKEN); + } catch (error) { + console.error("Failed to login to Discord. Check your USER_TOKEN.", error); + process.exit(1); + } })(); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..710887b --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,32 @@ +version: "3.8" + +services: + teto_ai: + build: + context: . + dockerfile: Dockerfile.dev + container_name: teto_ai_dev + cap_add: + - SYS_ADMIN + environment: + # supply the user token at runtime: + USER_TOKEN: "${USER_TOKEN}" + BOT_CLIENT_ID: "${BOT_CLIENT_ID}" + BOT_CLIENT_SECRET: "${BOT_CLIENT_SECRET}" + BOT_REDIRECT_URI: "https://teto.getsilly.org/auth/callback" + volumes: + # live-peek folder so you can grab screenshots outside the container + - ./output:/tmp/output + # Mount the current directory into the container for live-reloading + - .:/opt/bot + # Use a named volume to keep the container's node_modules from being overwritten + - node_modules:/opt/bot/node_modules + ports: + - "5901:5901" # VNC + - "3000:3000" # optional HTTP server if you add one + tmpfs: + - /tmp:size=512M + restart: unless-stopped + +volumes: + node_modules: diff --git a/entry.sh b/entry.sh old mode 100644 new mode 100755 diff --git a/public/socket_logic.js b/public/socket_logic.js new file mode 100644 index 0000000..914a6d5 --- /dev/null +++ b/public/socket_logic.js @@ -0,0 +1,94 @@ +let socket; +let heartbeatInterval; + +function connect() { + // Use wss:// for secure connections (in production) or ws:// for local + const protocol = window.location.protocol === "https:" ? "wss" : "ws"; + socket = new WebSocket(`${protocol}://${window.location.host}`); + + socket.onopen = () => { + console.log("WebSocket connection established."); + // Start a heartbeat to keep the connection alive + clearInterval(heartbeatInterval); // Clear previous interval if it exists + heartbeatInterval = setInterval(() => { + if (socket.readyState === WebSocket.OPEN) { + // Sending a simple ping message. The server's 'ws' library will handle this + // automatically to keep the connection from being marked as idle by proxies. + socket.send(JSON.stringify({ type: "ping" })); + } + }, 30000); // every 30 seconds + }; + + socket.onmessage = (event) => { + // The server's 'ws' library might automatically respond to pings with pongs. + // We can safely ignore them, along with our own pings if they get echoed. + if (event.data.includes("ping") || event.data.includes("pong")) { + return; + } + + const data = JSON.parse(event.data); + + // --- This is our client-side message router --- + switch (data.type) { + case "activityLogUpdate": + handleActivityLogUpdate(data.payload); + break; + case "systemResourcesUpdate": + handleSystemResourceUpdate(data.payload); + break; + // Add more cases here for other real-time updates + default: + console.log("Received unknown message type:", data.type); + } + }; + + socket.onclose = () => { + console.log( + "WebSocket connection closed. Attempting to reconnect in 3 seconds...", + ); + clearInterval(heartbeatInterval); // Stop the heartbeat when the connection is lost + // **This is the key part:** Wait 3 seconds, then try to connect again. + setTimeout(connect, 3000); + }; + + socket.onerror = (error) => { + console.error("WebSocket Error:", error); + // The 'onclose' event will fire automatically after an error, + // which will then trigger our reconnection logic. + socket.close(); // Ensure the socket is closed to trigger onclose + }; +} + +function handleActivityLogUpdate(payload) { + const activityLog = document.getElementById("activity-log"); + if (!activityLog || !payload.message) return; + + const newLogItem = document.createElement("p"); + const styledMessage = payload.message.replace( + /(\[[^\]]+\])/, + '$1', + ); + newLogItem.innerHTML = styledMessage; + + activityLog.prepend(newLogItem); + + while (activityLog.children.length > 20) { + activityLog.lastChild.remove(); + } +} + +function handleSystemResourceUpdate(payload) { + // Conceptual: Find the memory percentage element and update its text and style + const memPercentage = document.querySelector(".metric-header strong"); // Simplified selector + const memProgressBar = document.querySelector(".progress-bar.memory"); // Simplified selector + + if (memPercentage) + memPercentage.textContent = `${payload.memory.percentage}%`; + if (memProgressBar) + memProgressBar.style.width = `${payload.memory.percentage}%`; + + console.log("System resources updated!", payload); +} + +// --- Initial Connection --- +connect(); diff --git a/src/controllers/authController.js b/src/controllers/authController.js new file mode 100644 index 0000000..3bce1de --- /dev/null +++ b/src/controllers/authController.js @@ -0,0 +1,127 @@ +import fetch from "node-fetch"; + +const authController = { + // --- Route Handlers --- + + redirectToDiscord: (req, res) => { + const { BOT_CLIENT_ID, BOT_REDIRECT_URI } = req.app.locals.config; + const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${BOT_CLIENT_ID}&redirect_uri=${encodeURIComponent( + BOT_REDIRECT_URI, + )}&response_type=code&scope=identify`; + res.redirect(authUrl); + }, + + handleCallback: async (req, res) => { + const code = req.query.code; + if (!code) { + return res.status(400).send("No code provided."); + } + + const { + BOT_CLIENT_ID, + BOT_CLIENT_SECRET, + BOT_REDIRECT_URI, + ADMIN_USER_IDS, + } = req.app.locals.config; + + try { + const params = new URLSearchParams({ + client_id: BOT_CLIENT_ID, + client_secret: BOT_CLIENT_SECRET, + code, + grant_type: "authorization_code", + redirect_uri: BOT_REDIRECT_URI, + }); + + const tokenResponse = await fetch( + "https://discord.com/api/oauth2/token", + { + method: "POST", + body: params, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }, + ); + const tokenData = await tokenResponse.json(); + + if (tokenData.error) { + console.error("Token Exchange Error:", tokenData); + return res + .status(500) + .send( + `Error exchanging code for token. Details: ${JSON.stringify( + tokenData, + )}`, + ); + } + + const userResponse = await fetch("https://discord.com/api/users/@me", { + headers: { Authorization: `Bearer ${tokenData.access_token}` }, + }); + const userData = await userResponse.json(); + + if (!ADMIN_USER_IDS.includes(userData.id)) { + return res + .status(403) + .send("You are not authorized to view this dashboard."); + } + + req.session.user = { + id: userData.id, + username: userData.username, + discriminator: userData.discriminator, + avatar: userData.avatar, + }; + + req.session.save((err) => { + if (err) { + console.error("Error saving session:", err); + return res + .status(500) + .send("An error occurred while saving the session."); + } + res.redirect("/"); + }); + } catch (error) { + console.error("OAuth2 Callback Error:", error); + res.status(500).send("An error occurred during authentication."); + } + }, + + logout: (req, res, next) => { + req.session.destroy((err) => { + if (err) { + console.error("Error destroying session:", err); + return next(err); + } + res.redirect("/"); + }); + }, + + renderDashboard: (req, res) => { + const { activityLog, actionsLog } = req.app.locals.data; + const systemResources = { + memory: { percentage: 91, used: 7.3, total: 8 }, + vram: { percentage: 69, used: 5.5, total: 8 }, + avgResponse: 32, + shutdown: "3d 0h", + sessionEnd: "Jul 24, 2025, 07:21 PM", + }; + res.render("index", { + user: req.session.user, + activityLog, + actionsLog, + systemResources, + }); + }, + + // --- Middleware --- + + checkAuth: (req, res, next) => { + if (req.session.user) { + return next(); + } + res.redirect("/auth/discord"); + }, +}; + +export default authController; diff --git a/src/events/disconnect.js b/src/events/disconnect.js new file mode 100644 index 0000000..8bf96f9 --- /dev/null +++ b/src/events/disconnect.js @@ -0,0 +1,8 @@ +// src/events/disconnect.js +export default { + name: "disconnect", + execute({ logItem, activityLog }) { + const message = `[${new Date().toLocaleTimeString()}] Bot disconnected.`; + logItem(activityLog, { message }); + }, +}; diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js new file mode 100644 index 0000000..77e66aa --- /dev/null +++ b/src/events/messageCreate.js @@ -0,0 +1,22 @@ +// src/events/messageCreate.js +export default { + name: "messageCreate", + execute({ client, logItem, activityLog, actionsLog, MAX_LOG_SIZE }, msg) { + if (msg.author.id === client.user.id) return; + const message = `[${new Date().toLocaleTimeString()}] Message from ${ + msg.author.tag + } in #${msg.channel.name || "DM"}: "${msg.content.slice(0, 50)}..."`; + logItem(activityLog, { message }); // This will now also broadcast + + if (msg.content.toLowerCase().includes("hello teto")) { + msg.channel.send("Hello there!"); + const action = { + message: `Responded to ${msg.author.tag}`, + channel: `#${msg.channel.name || "DM"}`, + icon: "💬", + }; + actionsLog.unshift(action); + if (actionsLog.length > MAX_LOG_SIZE) actionsLog.pop(); + } + }, +}; diff --git a/src/events/ready.js b/src/events/ready.js new file mode 100644 index 0000000..ae531cd --- /dev/null +++ b/src/events/ready.js @@ -0,0 +1,23 @@ +export default { + name: "ready", + once: true, + execute({ client, logItem, activityLog, actionsLog }) { + console.log(`Selfbot ready as ${client.user.tag} (${client.user.id})`); + // Add dummy data for demonstration purposes + actionsLog.unshift({ + message: "Responded to Alice in", + channel: "#gaming", + icon: "💬", + }); + actionsLog.unshift({ + message: "Joined Voice Chat", + channel: "#general", + icon: "🎤", + }); + + const now = new Date(); + logItem(activityLog, { + message: `[${now.toLocaleTimeString()}] System initialized and connected.`, + }); + }, +}; diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js new file mode 100644 index 0000000..4c1c2a1 --- /dev/null +++ b/src/routes/authRoutes.js @@ -0,0 +1,16 @@ +import express from "express"; +import authController from "../controllers/authController.js"; + +const router = express.Router(); + +// --- Authentication Routes --- +// These routes handle the OAuth2 flow with Discord. +router.get("/auth/discord", authController.redirectToDiscord); +router.get("/auth/callback", authController.handleCallback); +router.get("/logout", authController.logout); + +// --- Dashboard Route --- +// The main dashboard route is protected by the `checkAuth` middleware. +router.get("/", authController.checkAuth, authController.renderDashboard); + +export default router; diff --git a/views/index.ejs b/views/index.ejs index d102145..c09db4a 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -45,7 +45,7 @@

Recent Actions 12 in last hour

- <% let-i = 0; %> + <% let i = 0; %> <% actionsLog.forEach(action => { %> <% const time = i === 0 ? '5 min ago' : i === 1 ? '12 min ago' : '30 min ago'; i++; %>
@@ -110,5 +110,65 @@
+