diff --git a/api/internal_redirect/bridge.js b/api/internal_redirect/bridge.js new file mode 100644 index 00000000..3f85bce9 --- /dev/null +++ b/api/internal_redirect/bridge.js @@ -0,0 +1,23 @@ +import { hasPermission, postAPIRequest } from "../common"; + +export default function bridgeRedirectRoute(app, config, lang) { + const baseEndpoint = "/redirect/bridge"; + + app.post(baseEndpoint + "/command/add", async function (req, res) { + if (!hasPermission("zander.web.bridge", req, res)) return; + + // Add userId to req.body + req.body.actioningUser = req.session.user.userId; + + postAPIRequest( + `${process.env.siteAddress}/api/bridge/command/add`, + req.body, + `${process.env.siteAddress}/dashboard/bridge`, + res + ); + + res.redirect(`${process.env.siteAddress}/dashboard/bridge`); + + return res; + }); +} diff --git a/api/internal_redirect/index.js b/api/internal_redirect/index.js index cf6d3448..0fc34fd7 100644 --- a/api/internal_redirect/index.js +++ b/api/internal_redirect/index.js @@ -4,6 +4,8 @@ import webRedirectRoute from "./web"; import announcementsRedirectRoute from "./announcement"; import reportRedirectRoute from "./report"; import vaultRedirectRoute from "./vault"; +import bridgeRedirectRoute from "./bridge"; +import voteRedirectRoute from "./vote"; export default (app, config, lang) => { applicationRedirectRoute(app, config, lang); @@ -12,4 +14,6 @@ export default (app, config, lang) => { webRedirectRoute(app, config, lang); announcementsRedirectRoute(app, config, lang); vaultRedirectRoute(app, config, lang); + bridgeRedirectRoute(app, config, lang); + voteRedirectRoute(app, config, lang); }; diff --git a/api/internal_redirect/vote.js b/api/internal_redirect/vote.js new file mode 100644 index 00000000..909e30d7 --- /dev/null +++ b/api/internal_redirect/vote.js @@ -0,0 +1,77 @@ +import { hasPermission, postAPIRequest } from "../common"; + +export default function voteRedirectRoute(app, config, lang) { + const baseEndpoint = "/redirect/vote"; + + app.post(baseEndpoint + "/cast", async function (req, res) { + if (!hasPermission("zander.web.vote", req, res)) return; + + // Add userId to req.body + req.body.actioningUser = req.session.user.userId; + + postAPIRequest( + `${process.env.siteAddress}/api/vote/cast`, + req.body, + `${process.env.siteAddress}/dashboard/vote`, + res + ); + + res.redirect(`${process.env.siteAddress}/dashboard/vote`); + + return res; + }); + + app.post(baseEndpoint + "/site/create", async function (req, res) { + if (!hasPermission("zander.web.vote", req, res)) return; + + // Add userId to req.body + req.body.actioningUser = req.session.user.userId; + + postAPIRequest( + `${process.env.siteAddress}/api/vote/site/create`, + req.body, + `${process.env.siteAddress}/dashboard/vote`, + res + ); + + res.redirect(`${process.env.siteAddress}/dashboard/vote`); + + return res; + }); + + app.post(baseEndpoint + "/site/edit", async function (req, res) { + if (!hasPermission("zander.web.vote", req, res)) return; + + // Add userId to req.body + req.body.actioningUser = req.session.user.userId; + + postAPIRequest( + `${process.env.siteAddress}/api/vote/site/edit`, + req.body, + `${process.env.siteAddress}/dashboard/vote`, + res + ); + + res.redirect(`${process.env.siteAddress}/dashboard/vote`); + + return res; + }); + + app.post(baseEndpoint + "/site/delete", async function (req, res) { + if (!hasPermission("zander.web.vote", req, res)) return; + + // Add userId to req.body + req.body.actioningUser = req.session.user.userId; + + postAPIRequest( + `${process.env.siteAddress}/api/vote/site/delete`, + req.body, + `${process.env.siteAddress}/dashboard/vote`, + res + ); + + res.redirect(`${process.env.siteAddress}/dashboard/vote`); + + return res; + }); +} diff --git a/api/routes/application.js b/api/routes/application.js index 01d0c18c..51fbf4ff 100644 --- a/api/routes/application.js +++ b/api/routes/application.js @@ -9,9 +9,7 @@ export default function applicationApiRoute(app, config, db, features, lang) { try { function getApplications(dbQuery) { - db.query(dbQuery, function (error, results, fields) { - console.log(results); - + db.query(dbQuery, function (error, results, fields) { if (error) { res.send({ success: false, diff --git a/api/routes/bridge.js b/api/routes/bridge.js new file mode 100644 index 00000000..4efb2aeb --- /dev/null +++ b/api/routes/bridge.js @@ -0,0 +1,162 @@ +import { isFeatureEnabled, required, optional, generateLog } from "../common"; + +export default function bridgeApiRoute(app, config, db, features, lang) { + const baseEndpoint = "/api/bridge"; + + app.get(baseEndpoint + "/get", async function (req, res) { + isFeatureEnabled(features.bridge, res, lang); + const bridgeId = optional(req.query, "id"); + const targetServer = optional(req.query, "targetServer"); + + try { + function getBridge(dbQuery) { + db.query(dbQuery, function (error, results, fields) { + if (error) { + res.send({ + success: false, + message: `${error}`, + }); + } + + if (!results) { + return res.send({ + success: false, + message: `No Bridge requests can be found`, + }); + } + + return res.send({ + success: true, + data: results, + }); + }); + } + + // Get Bridge by ID + if (bridgeId) { + let dbQuery = `SELECT * FROM bridge WHERE bridgeId=${bridgeId};`; + getBridge(dbQuery); + } + + // Get Bridge by Targeted Server + if (targetServer) { + let dbQuery = `SELECT * FROM bridge WHERE targetServer='${targetServer}' AND processed=0;`; + getBridge(dbQuery); + } + + // Return all Bridge requests by default + let dbQuery = `SELECT * FROM bridge;`; + getBridge(dbQuery); + } catch (error) { + res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); + + app.post(baseEndpoint + "/command/add", async function (req, res) { + isFeatureEnabled(features.bridge, res, lang); + + const command = required(req.body, "command", res); + const targetServer = required(req.body, "targetServer", res); + + try { + db.query( + `INSERT INTO bridge (command, targetServer) VALUES (?, ?)`, + [command, targetServer], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res.send({ + success: true, + message: `New Bridge command added for ${targetServer}: ${command}`, + }); + } + ); + } catch (error) { + res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); + + app.post(baseEndpoint + "/clear", async function (req, res) { + isFeatureEnabled(features.bridge, res, lang); + + try { + db.query( + `TRUNCATE bridge;`, + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res.send({ + success: true, + message: `Bridge has now been cleared.`, + }); + } + ); + } catch (error) { + res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); + + app.post(baseEndpoint + "/command/process", async function (req, res) { + isFeatureEnabled(features.bridge, res, lang); + + const bridgeId = required(req.body, "bridgeId", res); + + try { + const fetchURL = `${process.env.siteAddress}/api/bridge/get?id=${bridgeId}`; + const response = await fetch(fetchURL, { + headers: { "x-access-token": process.env.apiKey }, + }); + const bridgeApiData = await response.json(); + + db.query( + `UPDATE bridge SET processed=? WHERE bridgeId=?;`, + [1, bridgeId], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res.send({ + success: true, + message: `Bridge ID ${bridgeId} has been executed.`, + }); + } + ); + } catch (error) { + res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); +} diff --git a/api/routes/index.js b/api/routes/index.js index 1b3db1b8..45ddc9a1 100644 --- a/api/routes/index.js +++ b/api/routes/index.js @@ -9,6 +9,8 @@ import filterApiRoute from "./filter"; import rankApiRoute from "./ranks"; import reportApiRoute from "./report"; import vaultApiRoute from "./vault"; +import bridgeApiRoute from "./bridge"; +import voteApiRoute from "./vote"; export default (app, client, moment, config, db, features, lang) => { announcementApiRoute(app, config, db, features, lang); @@ -22,6 +24,8 @@ export default (app, client, moment, config, db, features, lang) => { rankApiRoute(app, config, db, features, lang); filterApiRoute(app, config, db, features, lang); vaultApiRoute(app, config, db, features, lang); + bridgeApiRoute(app, config, db, features, lang); + voteApiRoute(app, config, db, features, lang); app.get("/api/heartbeat", async function (req, res) { return res.send({ diff --git a/api/routes/user.js b/api/routes/user.js index ba280d2d..06c34390 100644 --- a/api/routes/user.js +++ b/api/routes/user.js @@ -80,6 +80,8 @@ export default function userApiRoute(app, config, db, features, lang) { // TODO: Update docs app.get(baseEndpoint + "/get", async function (req, res) { const username = optional(req.query, "username"); + const discordId = optional(req.query, "discordId"); + const userId = optional(req.query, "userId"); try { if (username) { @@ -101,6 +103,56 @@ export default function userApiRoute(app, config, db, features, lang) { }); } + return res.send({ + success: true, + data: results, + }); + } + ); + } else if (discordId) { + db.query( + `SELECT * FROM users WHERE discordId=?;`, + [discordId], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: error, + }); + } + + if (!results || !results.length) { + return res.send({ + success: false, + message: lang.api.userDoesNotExist, + }); + } + + return res.send({ + success: true, + data: results, + }); + } + ); + } else if (userId) { + db.query( + `SELECT * FROM users WHERE userId=?;`, + [userId], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: error, + }); + } + + if (!results || !results.length) { + return res.send({ + success: false, + message: lang.api.userDoesNotExist, + }); + } + return res.send({ success: true, data: results, diff --git a/api/routes/vote.js b/api/routes/vote.js new file mode 100644 index 00000000..3b7be70f --- /dev/null +++ b/api/routes/vote.js @@ -0,0 +1,208 @@ +import { isFeatureEnabled, required, optional, generateLog } from "../common"; + +export default function voteApiRoute(app, config, db, features, lang) { + const baseEndpoint = "/api/vote"; + + // TODO: Update docs + app.get(baseEndpoint + "/get", async function (req, res) { + isFeatureEnabled(features.vote, res, lang); + + try { + db.query( + `SELECT * FROM votes;`, + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res.send({ + success: true, + data: results, + }); + } + ); + } catch (error) { + res.send({ + success: false, + message: `${error}`, + }); + } + }); + + app.get(baseEndpoint + "/site/get", async function (req, res) { + isFeatureEnabled(features.vote, res, lang); + + try { + db.query(`SELECT * FROM voteSite;`, function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res.send({ + success: true, + data: results, + }); + }); + } catch (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + }); + + app.post(baseEndpoint + "/site/create", async function (req, res) { + isFeatureEnabled(features.vote, res, lang); + + const actioningUser = required(req.body, "actioningUser", res); + const voteSiteDisplayName = required(req.body, "voteSiteDisplayName", res); + const voteSiteRedirect = required(req.body, "voteSiteRedirect", res); + + try { + db.query( + ` + INSERT INTO + voteSite + ( + voteSiteDisplayName, + voteSiteRedirect + ) VALUES (?, ?)`, + [ + voteSiteDisplayName, + voteSiteRedirect, + ], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + generateLog( + actioningUser, + "SUCCESS", + "VOTESITE", + `Created ${voteSiteDisplayName} (${voteSiteRedirect})`, + res + ); + + return res.send({ + success: true, + message: `New vote site added: ${voteSiteDisplayName}`, + }); + } + ); + } catch (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); + + app.post(baseEndpoint + "/site/edit", async function (req, res) { + isFeatureEnabled(features.vote, res, lang); + + const actioningUser = required(req.body, "actioningUser", res); + const voteSiteId = required(req.body, "voteSiteId", res); + const voteSiteDisplayName = required(req.body, "voteSiteDisplayName", res); + const voteSiteRedirect = required(req.body, "voteSiteRedirect", res); + + try { + db.query( + ` + UPDATE + voteSite + SET + voteSiteDisplayName=?, + voteSiteRedirect=? + WHERE + voteSiteId=?`, + [ + voteSiteDisplayName, + voteSiteRedirect, + voteSiteId, + ], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + generateLog( + actioningUser, + "SUCCESS", + "VOTESITE", + `Edited ${voteSiteDisplayName} (${voteSiteRedirect})`, + res + ); + + return res.send({ + success: true, + message: `Vote server edited ${voteSiteDisplayName} (${voteSiteRedirect})`, + }); + } + ); + } catch (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); + + app.post(baseEndpoint + "/site/delete", async function (req, res) { + isFeatureEnabled(features.server, res, lang); + + const actioningUser = required(req.body, "actioningUser", res); + const voteSiteId = required(req.body, "voteSiteId", res); + + try { + db.query( + `DELETE FROM voteSite WHERE voteSiteId=?;`, + [voteSiteId], + function (error, results, fields) { + if (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + generateLog( + actioningUser, + "SUCCESS", + "VOTESITE", + `Deleted ${voteSiteId}`, + res + ); + + return res.send({ + success: true, + message: `Vote site deleted ${voteSiteId}`, + }); + } + ); + } catch (error) { + return res.send({ + success: false, + message: `${error}`, + }); + } + + return res; + }); +} diff --git a/app.js b/app.js index ae17d9de..a0a2a543 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,7 @@ const __dirname = path.dirname(__filename); import("./controllers/discordController.js"); import("./cron/userCodeExpiryCron.js"); +import("./cron/bridgeCleanupCron.js"); // // Website Related diff --git a/commands/bridge.mjs b/commands/bridge.mjs new file mode 100644 index 00000000..a850147a --- /dev/null +++ b/commands/bridge.mjs @@ -0,0 +1,326 @@ +import { Command } from "@sapphire/framework"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Colors, EmbedBuilder } from "discord.js"; +import fetch from "node-fetch"; +import { getUserPermissions, UserGetter } from "../controllers/userController"; +import features from "../features.json" assert { type: "json" }; + +export class BridgeCommand extends Command { + constructor(context, options) { + super(context, { ...options }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName("bridge") + .setDescription("Manage and view bridge status.") + .addSubcommand((subcommand) => + subcommand + .setName("status") + .setDescription("View the current bridge status.") + ) + .addSubcommand((subcommand) => + subcommand + .setName("add") + .setDescription("Add a command to the bridge and targeted server.") + .addStringOption((option) => + option + .setName("command") + .setDescription("The command for the bridge.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("targetedserver") + .setDescription("The targeted server to send to.") + .setRequired(true) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("clear") + .setDescription("Clear all commands on the Bridge.") + ) + ); + } + + async chatInputRun(interaction) { + if (!features.bridge) { + const errorEmbed = new EmbedBuilder() + .setTitle("Feature Disabled") + .setDescription( + `This feature has been disabled by your System Administrator.` + ) + .setColor(Colors.Red); + + return interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + + // Resolve the user to a User ID in the database + const userData = new UserGetter(); + const userGetData = await userData.byDiscordId(interaction.user.id); + + const userPermissions = await getUserPermissions(userGetData); + const hasPermission = userPermissions.includes("zander.web.bridge"); + + if (!hasPermission) { + const errorEmbed = new EmbedBuilder() + .setTitle("No Permission") + .setDescription(`You do not have access to use this command.`) + .setColor(Colors.Red); + + return interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + + // Handle the different subcommands + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === "status") { + try { + const fetchURL = `${process.env.siteAddress}/api/bridge/get`; + const response = await fetch(fetchURL, { + headers: { "x-access-token": process.env.apiKey }, + }); + const apiData = await response.json(); + + if (!apiData.success) { + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Status Error") + .setDescription("There was an error fetching the bridge status.") + .setColor(Colors.Red); + + return interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + + if (!apiData.data || apiData.data.length === 0) { + // No bridge commands found + const noBridgesEmbed = new EmbedBuilder() + .setTitle("Bridge Status") + .setDescription("There are currently no bridge commands available.") + .setColor(Colors.Blurple); + + return interaction.reply({ + embeds: [noBridgesEmbed], + ephemeral: true, + }); + } + + const statusEmbed = new EmbedBuilder() + .setTitle("Bridge Status") + .setColor(Colors.Blurple); + + // Loop through the bridges and add each one to the embed + apiData.data.forEach((bridge) => { + const bridgeInfo = `**Command**: ${`\`${bridge.command}\``}\n**Target Server**: ${ + bridge.targetServer + }\n**Processed**: ${ + bridge.processed === 0 ? "No" : "Yes" + }\n**Date**: ${new Date(bridge.bridgeDateTime).toLocaleString()}`; + + statusEmbed.addFields({ + name: `Bridge ID: ${bridge.bridgeId}`, + value: bridgeInfo, + inline: false, + }); + }); + + return interaction.reply({ embeds: [statusEmbed] }); + } catch (error) { + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Status Error") + .setDescription("There was an error fetching the bridge status.") + .setColor(Colors.Red); + + return interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } + + if (subcommand === "add") { + const command = interaction.options.getString("command"); + const targetedServer = interaction.options.getString("targetedserver"); + + try { + const response = await fetch(`${process.env.siteAddress}/api/bridge/command/add`, { + method: "POST", + headers: { + "x-access-token": process.env.apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + command: command, + targetServer: targetedServer, + }), + }); + + const apiData = await response.json(); + + if (!apiData.success) { + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Add Error") + .setDescription(`Failed to add the bridge: ${apiData.message}`) + .setColor(Colors.Red); + + return interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + + const successEmbed = new EmbedBuilder() + .setTitle("Bridge Command Added") + .setDescription( + `New bridge command added for server: \`${targetedServer}\` with command: \`${command}\`.` + ) + .setColor(Colors.Green); + + return interaction.reply({ embeds: [successEmbed] }); + } catch (error) { + console.log(error); + + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Add Error") + .setDescription("There was an error adding the bridge.") + .setColor(Colors.Red); + + return interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } + + if (subcommand === "clear") { + try { + // Create the confirmation embed + const confirmationEmbed = new EmbedBuilder() + .setTitle("Are You Sure?") + .setDescription( + "Are you sure you want to clear the bridge? This action cannot be undone." + ) + .setColor(Colors.Orange); + + // Create the "Yes" and "No" buttons + const yesButton = new ButtonBuilder() + .setCustomId("clear_bridge_yes") + .setLabel("Yes") + .setStyle(ButtonStyle.Danger); + + const noButton = new ButtonBuilder() + .setCustomId("clear_bridge_no") + .setLabel("No") + .setStyle(ButtonStyle.Secondary); + + // Create an action row to hold the buttons + const actionRow = new ActionRowBuilder().addComponents( + yesButton, + noButton + ); + + // Send the confirmation message with buttons + await interaction.reply({ + embeds: [confirmationEmbed], + components: [actionRow], + ephemeral: true, + }); + + // Create a collector to handle button interactions + const filter = (i) => + i.user.id === interaction.user.id && + (i.customId === "clear_bridge_yes" || + i.customId === "clear_bridge_no"); + + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 15000, // 15 seconds + }); + + collector.on("collect", async (i) => { + if (i.customId === "clear_bridge_yes") { + // User confirmed, clear the bridge + try { + const response = await fetch( + `${process.env.siteAddress}/api/bridge/clear`, + { + method: "POST", + headers: { "x-access-token": process.env.apiKey }, + } + ); + + const apiData = await response.json(); + + if (!apiData.success) { + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Clear Error") + .setDescription("Failed to clear the bridge.") + .setColor(Colors.Red); + + return i.update({ + embeds: [errorEmbed], + components: [], + ephemeral: true, + }); + } + + const successEmbed = new EmbedBuilder() + .setTitle("Bridge Cleared") + .setDescription("The bridge has been cleared.") + .setColor(Colors.Green); + + return i.update({ + embeds: [successEmbed], + components: [], + ephemeral: true, + }); + } catch (error) { + console.log(error); + + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Clear Error") + .setDescription("There was an error clearing the bridge.") + .setColor(Colors.Red); + + return i.update({ + embeds: [errorEmbed], + components: [], + ephemeral: true, + }); + } + } else if (i.customId === "clear_bridge_no") { + // User canceled, respond accordingly + const canceledEmbed = new EmbedBuilder() + .setTitle("Action Canceled") + .setDescription("The bridge clear action has been canceled.") + .setColor(Colors.Grey); + + return i.update({ + embeds: [canceledEmbed], + components: [], + ephemeral: true, + }); + } + }); + + collector.on("end", (collected) => { + if (collected.size === 0) { + // No interaction collected within time limit + interaction.editReply({ + content: "No response received. Action has been canceled.", + components: [], + }); + } + }); + } catch (error) { + console.log(error); + + const errorEmbed = new EmbedBuilder() + .setTitle("Bridge Clear Error") + .setDescription( + "There was an error initiating the bridge clear process." + ) + .setColor(Colors.Red); + + return interaction.reply({ embeds: [errorEmbed], ephemeral: true }); + } + } + } +} diff --git a/controllers/userController.js b/controllers/userController.js index 876fc87c..9dce4c11 100644 --- a/controllers/userController.js +++ b/controllers/userController.js @@ -275,8 +275,6 @@ export async function getUserPermissions(userData) { try { await Promise.all( userRanks.map(async (rank) => { - console.log(rank.rankSlug); - return new Promise((resolve, reject) => { db.query( `SELECT * FROM rankPermissions WHERE rankSlug=?;`, @@ -297,8 +295,6 @@ export async function getUserPermissions(userData) { }) ); - console.log(userPermissions); - resolve(userPermissions); } catch (err) { reject(err); diff --git a/cron/bridgeCleanupCron.js b/cron/bridgeCleanupCron.js new file mode 100644 index 00000000..09c5c2bd --- /dev/null +++ b/cron/bridgeCleanupCron.js @@ -0,0 +1,24 @@ +import cron from "node-cron"; +import db from "../controllers/databaseController"; + +// Schedule the task to run once a day at 00:05 (5 minutes past midnight) +var bridgeCleanupTask = cron.schedule("5 0 * * *", () => { + try { + db.query( + `DELETE FROM bridge WHERE bridgeDatetime <= NOW() - INTERVAL 3 DAY;`, + function (error, results, fields) { + if (error) { + return console.log(`Error: ${error}`); + } + + console.log( + `Bridge cleanup complete. Rows affected: ${results.affectedRows}` + ); + } + ); + } catch (error) { + console.log(`Error: ${error}`); + } +}); + +bridgeCleanupTask.start(); diff --git a/dbinit.sql b/dbinit.sql index 6ec2f796..d9669185 100644 --- a/dbinit.sql +++ b/dbinit.sql @@ -224,6 +224,29 @@ CREATE TABLE vault ( PRIMARY KEY (vaultId) ); +CREATE TABLE bridge ( + bridgeId INT NOT NULL AUTO_INCREMENT, + command TEXT, + targetServer VARCHAR(30), + processed BOOLEAN DEFAULT 0, + bridgeDateTime DATETIME NOT NULL DEFAULT NOW(), + PRIMARY KEY (bridgeId) +); + +CREATE TABLE votes ( + voteId INT NOT NULL AUTO_INCREMENT, + userId INT, + voteSite INT, + PRIMARY KEY (voteId) +); + +CREATE TABLE voteSite ( + voteSiteId INT NOT NULL AUTO_INCREMENT, + voteSiteDisplayName VARCHAR(50), + voteSiteRedirect VARCHAR(200), + PRIMARY KEY (voteSiteId) +); + CREATE TABLE logs ( logId INT NOT NULL AUTO_INCREMENT, creatorId INT NOT NULL, diff --git a/features.json b/features.json index 20760f89..bc0a7037 100644 --- a/features.json +++ b/features.json @@ -12,6 +12,8 @@ "ranks": true, "report": true, "vault": true, + "bridge": true, + "vote": true, "filter": { "link": true, "phrase": true diff --git a/package-lock.json b/package-lock.json index d65cbbb1..48eadcb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "node-fetch": "^3.3.2", "nodemailer": "^6.9.7", "nodemon": "^3.0.1", + "p-limit": "^6.1.0", "path": "^0.12.7", "querystring": "^0.2.1", "zero-md": "^2.3.1" @@ -232,6 +233,33 @@ "p-limit": "^3.1.0" } }, + "node_modules/@fastify/static/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@fastify/static/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@fastify/view": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@fastify/view/-/view-8.2.0.tgz", @@ -1888,14 +1916,15 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz", + "integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==", + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2547,11 +2576,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 59067feb..c8dc2b20 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "node-fetch": "^3.3.2", "nodemailer": "^6.9.7", "nodemon": "^3.0.1", + "p-limit": "^6.1.0", "path": "^0.12.7", "querystring": "^0.2.1", "zero-md": "^2.3.1" diff --git a/routes/dashboard/dashboard.js b/routes/dashboard/dashboard.js index 4b8a2fc5..0df3acaa 100644 --- a/routes/dashboard/dashboard.js +++ b/routes/dashboard/dashboard.js @@ -14,9 +14,9 @@ export default function dashboardSiteRoute(app, config, features, lang) { res, features ); - + if (!permissionBoolean) return; - + return res.view("dashboard/dashboard-index", { pageTitle: `Dashboard`, config: config, @@ -52,4 +52,32 @@ export default function dashboardSiteRoute(app, config, features, lang) { return res; }); + + // + // Bridge + // + app.get("/dashboard/bridge", async function (req, res) { + if (!hasPermission("zander.web.bridge", req, res, features)) return; + + const fetchURL = `${process.env.siteAddress}/api/bridge/get`; + const response = await fetch(fetchURL, { + headers: { "x-access-token": process.env.apiKey }, + }); + const apiData = await response.json(); + + console.log(apiData); + + res.view("dashboard/bridge", { + pageTitle: `Dashboard - Bridge`, + config: config, + apiData: apiData, + features: features, + req: req, + globalImage: getGlobalImage(), + moment: moment, + announcementWeb: await getWebAnnouncement(), + }); + + return res; + }); } diff --git a/routes/dashboard/index.js b/routes/dashboard/index.js index ed675eef..d4f9b8c4 100644 --- a/routes/dashboard/index.js +++ b/routes/dashboard/index.js @@ -3,6 +3,7 @@ import dashboardServersSiteRoute from "./servers"; import dashboardApplicationsSiteRoute from "./applications"; import dashboardAnnouncementSiteRoute from "./announcement"; import dashboardVaultSiteRoute from "./vault"; +import dashboardVoteSiteRoute from "./vote"; export default function dashboardSiteRoutes( app, @@ -19,4 +20,5 @@ export default function dashboardSiteRoutes( dashboardAnnouncementSiteRoute(app, fetch, config, db, features, lang); dashboardApplicationsSiteRoute(app, fetch, config, db, features, lang); dashboardVaultSiteRoute(app, fetch, config, db, features, lang); + dashboardVoteSiteRoute(app, fetch, config, db, features, lang); } diff --git a/routes/dashboard/vote.js b/routes/dashboard/vote.js new file mode 100644 index 00000000..97a29185 --- /dev/null +++ b/routes/dashboard/vote.js @@ -0,0 +1,93 @@ +import { + getGlobalImage, + hasPermission, + isFeatureWebRouteEnabled, +} from "../../api/common"; +import { getWebAnnouncement } from "../../controllers/announcementController"; + +export default function dashboardVoteSiteRoute( + app, + fetch, + config, + db, + features, + lang +) { + // + // Vote + // + app.get("/dashboard/vote", async function (req, res) { + try { + if (!isFeatureWebRouteEnabled(features.vote, req, res, features)) return; + + if (!hasPermission("zander.web.vote", req, res, features)) return; + + const fetchURL = `${process.env.siteAddress}/api/vote/site/get`; + const response = await fetch(fetchURL, { + headers: { "x-access-token": process.env.apiKey }, + }); + const apiData = await response.json(); + + console.log(apiData); + + return res.view("dashboard/vote/vote-list", { + pageTitle: `Dashboard - Vote`, + config: config, + apiData: apiData, + features: features, + req: req, + globalImage: getGlobalImage(), + announcementWeb: await getWebAnnouncement(), + }); + + } catch (error) { + console.log(error); + + + } + }); + + app.get("/dashboard/vote/site/create", async function (req, res) { + if (!isFeatureWebRouteEnabled(features.vote, req, res, features)) return; + + if (!hasPermission("zander.web.vote", req, res, features)) return; + + res.view("dashboard/vote/vote-editor", { + pageTitle: `Dashboard - Vote Creator`, + config: config, + type: "create", + features: features, + globalImage: getGlobalImage(), + req: req, + announcementWeb: await getWebAnnouncement(), + }); + + return res; + }); + + app.get("/dashboard/vote/edit", async function (req, res) { + if (!isFeatureWebRouteEnabled(features.vote, req, res, features)) return; + + if (!hasPermission("zander.web.vote", req, res, features)) return; + + const id = req.query.id; + const fetchURL = `${process.env.siteAddress}/api/vote/site/get?id=${id}`; + const response = await fetch(fetchURL, { + headers: { "x-access-token": process.env.apiKey }, + }); + const voteApiData = await response.json(); + + res.view("dashboard/vote/vote-editor", { + pageTitle: `Dashboard - Vote Editor`, + config: config, + voteApiData: voteApiData.data[0], + type: "edit", + features: features, + globalImage: getGlobalImage(), + req: req, + announcementWeb: await getWebAnnouncement(), + }); + + return res; + }); +} diff --git a/views/dashboard/bridge.ejs b/views/dashboard/bridge.ejs new file mode 100644 index 00000000..074d9499 --- /dev/null +++ b/views/dashboard/bridge.ejs @@ -0,0 +1,93 @@ +<%- include("../modules/header.ejs", { + pageTitle: pageTitle, + pageDescription: "Bridge for sending commands and functions." +}) %> + +<%- include("../modules/navigationBar.ejs") %> + +<%- include("../partials/miniHeader.ejs", { + headerTitle: "Bridge", + backgroundImage: globalImage +}) %> + +
+
+ <%- include("../modules/dashboard/dashboard-sidebar.ejs") %> +
+
+ <%- include("../partials/documentationLink.ejs", { + doclink: "https://modularsoft.org/docs/products/zander/features/bridge/" + }) %> +

+ +
+
+
+ +
+ + + <%- include("../partials/form/inputText.ejs", { + elementName: "command", + elementValue: null, + required: true + }) %> +
+ + +
+ + + <%- include("../partials/form/inputText.ejs", { + elementName: "targetServer", + elementValue: null, + required: true + }) %> +
+
+ + +
+
+

+ + <% if (req.cookies.alertType) { %> + <%- include("../partials/alert.ejs", { + alertType: req.cookies.alertType, + content: req.cookies.alertContent + }) %> + <% } %> + + + + + + + + + + + + + <% if (apiData.success == false) { %> + <%- include("../partials/alert.ejs", { + alertType: "danger", + content: apiData.message + }) %> + <% } else { %> + <% apiData.data.forEach(function (bridge) { %> + + + + + + + + <% }) %> + <% } %> + +
Bridge IDCommandTarget ServerProcessedCreated
<%= bridge.bridgeId %><%= bridge.command %><%= bridge.targetServer %><%= bridge.processed %><%= moment(bridge.bridgeDateTime).startOf('hour').fromNow() %>
+
+
+ +<%- include("../modules/footer.ejs") %> \ No newline at end of file diff --git a/views/dashboard/vote/vote-editor.ejs b/views/dashboard/vote/vote-editor.ejs new file mode 100644 index 00000000..33bdd6bc --- /dev/null +++ b/views/dashboard/vote/vote-editor.ejs @@ -0,0 +1,86 @@ +<%- include("../../modules/header.ejs", { + pageTitle: pageTitle, + pageDescription: "Editor for Voting servers." +}) %> + +<%- include("../../modules/navigationBar.ejs") %> + +<%- include("../../partials/miniHeader.ejs", { + headerTitle: "Vote Editor", + backgroundImage: globalImage +}) %> + +
+
+ <%- include("../../modules/dashboard/dashboard-sidebar.ejs") %> +
+
+ <%- include("../../partials/documentationLink.ejs", { + doclink: "https://modularsoft.org/docs/products/zander/features/vote/" + }) %> +

+ +
+ <% if (type === 'create') { %> +
+ <% } %> + <% if (type === 'edit') { %> + + + <% } %> + +
+ +
+ + + <% if (type === 'create') { %> + <%- include("../../partials/form/inputText.ejs", { + elementName: "voteSiteDisplayName", + elementValue: null, + required: true + }) %> + <% } %> + <% if (type === 'edit') { %> + <%- include("../../partials/form/inputText.ejs", { + elementName: "voteSiteDisplayName", + elementValue: voteSiteApiData.voteSiteDisplayName, + required: true + }) %> + <% } %> +
+ + +
+ + + <% if (type === 'create') { %> + <%- include("../../partials/form/inputText.ejs", { + elementName: "voteSiteRedirect", + elementValue: null, + required: false + }) %> + <% } %> + <% if (type === 'edit') { %> + <%- include("../../partials/form/inputText.ejs", { + elementName: "voteSiteRedirect", + elementValue: voteSiteApiData.voteSiteRedirect, + required: false + }) %> + <% } %> +
+
+ + <% if (type === 'create') { %> + + <% } %> + + <% if (type === 'edit') { %> + + <% } %> +
+
+
+
+ +<%- include("../../modules/footer.ejs") %> \ No newline at end of file diff --git a/views/dashboard/vote/vote-list.ejs b/views/dashboard/vote/vote-list.ejs new file mode 100644 index 00000000..9a27a054 --- /dev/null +++ b/views/dashboard/vote/vote-list.ejs @@ -0,0 +1,70 @@ +<%- include("../../modules/header.ejs", { + pageTitle: pageTitle, + pageDescription: "List of all Voting Servers." +}) %> + +<%- include("../../modules/navigationBar.ejs") %> + +<%- include("../../partials/miniHeader.ejs", { + headerTitle: "Voting", + backgroundImage: globalImage +}) %> + +
+
+ <%- include("../../modules/dashboard/dashboard-sidebar.ejs") %> +
+
+ + <%- include("../../partials/documentationLink.ejs", { + doclink: "https://modularsoft.org/docs/products/zander/features/vote/" + }) %> +

+ + <% if (req.cookies.alertType) { %> + <%- include("../../partials/alert.ejs", { + alertType: req.cookies.alertType, + content: req.cookies.alertContent + }) %> + <% } %> +
+ + + + + + + + + + <% if (apiData.success == false) { %> + <%- include("../../partials/alert.ejs", { + alertType: "danger", + content: apiData.message + }) %> + <% } else { %> + <% apiData.data.forEach(function (voteSite) { %> + + + + + + <% }) %> + <% } %> + +
Vote Site Display NameVote Site AddressAction
<%= voteSite.voteSiteDisplayName %><%= voteSite.voteSiteRedirect %> +
+ + +
+ + + +
+
+
+
+
+
+ +<%- include("../../modules/footer.ejs") %> \ No newline at end of file diff --git a/views/modules/dashboard/dashboard-sidebar.ejs b/views/modules/dashboard/dashboard-sidebar.ejs index 6fec9355..ccbea5e5 100644 --- a/views/modules/dashboard/dashboard-sidebar.ejs +++ b/views/modules/dashboard/dashboard-sidebar.ejs @@ -16,22 +16,18 @@ Announcements Servers Applications + Vote Vault Logs