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/"
+ }) %>
+
+
+
+
+
+ <% if (req.cookies.alertType) { %>
+ <%- include("../partials/alert.ejs", {
+ alertType: req.cookies.alertType,
+ content: req.cookies.alertContent
+ }) %>
+ <% } %>
+
+
+
+
+ | Bridge ID |
+ Command |
+ Target Server |
+ Processed |
+ Created |
+
+
+
+ <% if (apiData.success == false) { %>
+ <%- include("../partials/alert.ejs", {
+ alertType: "danger",
+ content: apiData.message
+ }) %>
+ <% } else { %>
+ <% apiData.data.forEach(function (bridge) { %>
+
+ | <%= 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/"
+ }) %>
+
+
+
+
+
+
+<%- 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
+ }) %>
+ <% } %>
+
+
+
+
+ | Vote Site Display Name |
+ Vote Site Address |
+ Action |
+
+
+
+ <% if (apiData.success == false) { %>
+ <%- include("../../partials/alert.ejs", {
+ alertType: "danger",
+ content: apiData.message
+ }) %>
+ <% } else { %>
+ <% apiData.data.forEach(function (voteSite) { %>
+
+ | <%= 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
-
- Profile
+
+ Development
-