DEV Community

Asterki
Asterki

Posted on

Create a basic dashboard for your Discord bot using Next.js 💯

With a dashboard, it makes it easier for your bot's users to configure your bot from a web interface, instead of typing commands to configure everything, so in this tutorial I'm gonna show how to make a basic dashboard using Next.js

Things to take in consideration before starting this tutorial

  • I will use MongoDB as database, if you use a different database, you must change the code accordingly.
  • You must already have a discord app registered, if you don't know how to register one, you can learn here on how to create one.
  • You must have an intermediate knowledge of Next.js and Node.js.
  • The dashboard will run on a separated server by default, but you can add your bot's code to the dashboard's code and make them both run in the same server.
  • If you use a file-based database, such as quick.db, you must host the dashboard and bot in the same server, else it won't work.

For this project I'm gonna use the following technologies:

  • Next.js
  • MongoDB
  • Passport.js
  • Express.js

Setting up our Discord application

  • First we need to go to our app in Discord Developer Portal.
  • We need to get our client secret, and client id, save them somewhere, we'll need them later.
  • Head to OAuth2, go to general, and add your domain with the /api/auth/callback, an example: https://example.com/api/auth/callback This is where the users will be redirected when they press the authorize button, the route that will process the code given by Discord.

It looks like this now
Dev portal

As it says there, ** it must exactly match one of the URIs you enter here**, so please use your domain name.


Creating the Next.js app

For this example, we're gonna use SSR (Server side rendering), so I'd recommend using the Next.js template on SSR, to do this, we're gonna run the next command:

npx create-next-app --example custom-server-express discord-bot-dashboard
Enter fullscreen mode Exit fullscreen mode

And we need to install our dependencies, we can install them app by using the following command:

npm install --save mongoose passport passport-discord express-session connect-mongo cookie-parser body-parser compression helmet axios discord.js 
Enter fullscreen mode Exit fullscreen mode

After that is done, we open our folder in our IDE of preference and start working.

Currently our project looks like this:

Project structure 1

Now, follow the next steps to set up our file structure:

  • Create a folder called src
  • Move the pages folder into the src folder (routing will still work this way, learn more)
  • Inside the src folder, create a folder called styles

  • In the root directory of the project, create a folder called server.

  • Move the file named server.js into the server folder, then rename it to index.js, and since we changed the main file, we should also change it on our package.json, change the dev script to point to the index.js file inside the server folder.

  • Inside the server folder, create 3 folders: api,models and config.

  • Inside the newly created api folder, create a file called servers.js, this will be used for our server related info requests, and a file named auth.js, which will handle our auth api routes such as login, logout, etc.

  • Inside the config folder, create a file called auth.js, one called routes.js, one called middleware.js and another one called databases.js.

New folder structure

After that, our folder structure now looks like this

Project structure


Environment variables

For this, we'll use a .env file at the root of our project, this one haves to have the following entries:

PORT, the port our Express.js app will listen to
HOST, the url of our server, for example, http://localhost:8080 (It shouldn't have the / at the end)

MONGODB_URI, our MongoDB database connection string (including authentication)

SESSION_SECRET, The secret in which our sessions will be encrypted
COOKIE_SECURE, wether our site uses SSL
COOKIE_MAX_AGE, How long should the sessions last (in miliseconds)

CLIENT_ID, Your bot's ID
CLIENT_SECRET, Your bot's secret
Enter fullscreen mode Exit fullscreen mode

So an example for this .env file would be this:

.env

PORT=8080
HOST="http://localhost:8080"
MONGODB_URI="mongodb://localhost:27017/test-bot"

SESSION_SECRET="Keyboard cat"
COOKIE_SECURE="false"
COOKIE_MAX_AGE=604800000

CLIENT_ID=516688206020739243
CLIENT_SECRET=your client secret
Enter fullscreen mode Exit fullscreen mode

Basic server configuration

We're now gonna open the index.js file inside the server folder, it already haves some content, generated by the npx command.

Now, we're gonna write the following code into our files

server/index.js

const express = require("express");
const next = require("next");

const port = parseInt(process.env.PORT, 10) || 8080;
const dev = process.env.NODE_ENV !== "production";

const app = next({ dev });
const handle = app.getRequestHandler();
const server = express();

app.prepare().then(() => {
    // Load the middleware
    require("./config/middleware");

    // Load the auth system
    require("./config/auth");

    // Connect to the database
    require("./config/databases");

    // Set the app's routes
    require("./config/routes");

    // Next.js routing
    server.all("*", (req, res) => {
        return handle(req, res);
    });

    // Start the server
    server.listen(port, () => {
        console.log(`> Ready on http://localhost:${port}`);
    });
});

// Export the Express.js app, since we'll use it in other files
module.exports = { server }
Enter fullscreen mode Exit fullscreen mode

server/config/routes.js

// Import our app
const { server } = require("../");

try {
    // Add our api endpoints to our app
    server.use("/api/auth", require("../api/auth"));
    server.use("/api/servers", require("../api/servers"));
} catch (err) {
    // Handle errors
    console.log("There was an error trying to load the routes");
    console.error(err);
}
Enter fullscreen mode Exit fullscreen mode

server/config/databases.js

const mongoose = require("mongoose");

// Connect to our MongoDB database, using the connection string
mongoose.connect(process.env.MONGODB_URI);
const mongooseClient = mongoose.connection;

// When the app connects to our MongoDB database 
mongooseClient.once('open', () => {
    console.log("MongoDB database successfully connected")
});

// If there's an error trying to connect with the database
mongooseClient.once('error', (error) => {
    console.log("There was an error trying to connect to MongoDB")
    console.log(error);
});

module.exports = { mongooseClient }
Enter fullscreen mode Exit fullscreen mode

server/config/auth.js

const DiscordStrategy = require("passport-discord").Strategy;
const passport = require("passport");
const expressSession = require("express-session");

const { server } = require("../");

// The scopes we'll need
const scopes = ["identify", "guilds"];

// Serialize users
passport.serializeUser((user, done) => {
    done(null, user);
});

passport.deserializeUser((user, done) => {
    done(null, user);
});

// Add the middleware create sessions
server.use(
    expressSession({
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: true,
        // Store the sessions to our MongoDB database, so users don't have to re login each time the server restarts
        store: mongoStore.create({
            mongoUrl: process.env.MONGODB_URI,
        }),
        // Cookie configuration
        name: "session",
        cookie: {
            secure: process.env.COOKIE_SECURE == "true",
            maxAge: parseInt(process.env.COOKIE_MAX_AGE) || 604800000,
            sameSite: true,
        },
    })
);

// Add passport.js middleware to our server
server.use(passport.initialize());
server.use(passport.session());

// Use passport.js's Discord strategy
passport.use(
    new DiscordStrategy(
        {
            // Set passport.js's configuration for the strategy
            clientID: process.env.CLIENT_ID,
            clientSecret: process.env.CLIENT_SECRET,
            callbackURL: `${process.env.HOST}/api/auth/callback`,
            scope: scopes,
        },
        (accessToken, refreshToken, profile, cb) => {
            console.log(profile) // <== To log the authorized user
            // Login the user
            return cb(null, profile);
        }
    )
);
Enter fullscreen mode Exit fullscreen mode

server/config/middleware.js

// Dependencies
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser");
const compression = require("compression");
const helmet = require("helmet");

const { server } = require("../");

try {
    // Requests
    server.use(
        bodyParser.urlencoded({
            extended: true,
        })
    );
    server.use(bodyParser.json());
    server.use(cookieParser());
    server.use(compression());

    // Basic security, which is disabled in development mode
    if (process.env.NODE_ENV == "production") {
        server.use(helmet.contentSecurityPolicy());
        server.use(
            helmet.crossOriginEmbedderPolicy({
                policy: "require-corp",
            })
        );
        server.use(
            helmet.crossOriginOpenerPolicy({
                policy: "same-origin",
            })
        );
        server.use(
            helmet.crossOriginResourcePolicy({
                policy: "same-origin",
            })
        );
        server.use(
            helmet.dnsPrefetchControl({
                allow: false,
            })
        );
        server.use(
            helmet.expectCt({
                maxAge: 0,
            })
        );
        server.use(
            helmet.frameguard({
                action: "sameorigin",
            })
        );
        server.use(
            helmet.hsts({
                maxAge: 15552000,
                includeSubDomains: true,
            })
        );
        server.use(
            helmet.permittedCrossDomainPolicies({
                permittedPolicies: "none",
            })
        );
        server.use(
            helmet.referrerPolicy({
                policy: "no-referrer",
            })
        );
        server.use(helmet.ieNoOpen());
        server.use(helmet.hidePoweredBy());
        server.use(helmet.noSniff());
        server.use(helmet.originAgentCluster());
        server.use(helmet.xssFilter());
    }

    console.log(`Middleware loaded`);
} catch (err) {
    console.log(`There was an error loading the middleware`);
    console.log(err);
}
Enter fullscreen mode Exit fullscreen mode

Setting up MongoDB's models

This is why we created the models folder, if you use mongoose you will be familiar with this type of folder, but if you don't, basically is a way to easily create and get entries in our database.

So, we're gonna get inside that folder and create a file named server-config.js, if you use MongoDB in your bot, this file must be the same as the one you use in your bot.

This is how we're gonna "connect" the bot and the dashboard, by using the database

If you don't have a server.js file, we're gonna create one and give it a basic example configuration:

server/models/server-config.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

// Create the schema
const ServerConfig = new Schema({
    serverID: {
        type: String,
        required: true,
        unique: true,
    },
    prefix: {
        type: String,
        unique: false,
        default: "!",
    },
});

// Create the model using the schema, in this order: Model name, Schema, and collection we're gonna use
const model = mongoose.model("ServerConfig", ServerConfig, "servers");

// Export our model
module.exports = model;
Enter fullscreen mode Exit fullscreen mode

Setting up the auth system

Now that we've configured our server, we need to add the API routes for our app, for this, we're gonna start on the server/api/auth.js file

server/api/auth.js

const express = require("express");
const passport = require("passport");

// Create our router
const router = express.Router();

// Redirect to the discord's auth page
router.get("/redirect", passport.authenticate("discord"));

// When returning from that page, authenticate and generate a session
router.get(
    "/callback",
    // Specify the auth strategy that we declared in the server/config/auth
    passport.authenticate("discord", {
        failureRedirect: "/", // If the auth failed
    }),
    function (req, res) {
        res.redirect("/dashboard"); // Successful auth
    }
);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

src/pages/index.js

const Home = () => {
    return (
        <div>
            <h1>Redirect to discord's login page</h1>
            <a href="/api/auth/redirect">Login</a>
        </div>
    );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Run the server

So, basically what we've setted up, is that when we go to http://example.com we're gonna see the src/pages/index.js file, it haves a header and a link that redirects to Discord's auth page.

After the user authorizes access to their account, it will log to console their information, so, lets test it out.

Run the server on development mode

npm run dev
Enter fullscreen mode Exit fullscreen mode

And now we go to http://example.com or your domain
http://example.com

We press the login button and we get redirected to this page
Discord auth page

We press authorize, and then we check our console.

Console

We get their information, and the servers they're in, now we can delete the console.log in the server/config/auth.js file.

If we see our browser, we were redirected to http://example.com/dashboard, but that page doesn't exist.

404 page

So lets create it.


Creating the server selection page

Lets get inside src/pages and get use of Next.js's routing system, learn more, and we'll create a folder called dashboard.

Inside the dashboard folder, we'll create 2 files: index.js and [server].js.

Our src file now looks like this
Folder structure 3

The [server].js file is where our users will configure our bot, and the index.js file is where our users will select the server they want to configure.

And now we follow this code

src/pages/dashboard/index.js

export const getServerSideProps = async (context) => {
    // Check if the user is logged in, if not, redirect to the login page
    if (!context.req.user)
        return {
            redirect: {
                destination: "/api/auth/redirect",
                permanent: false,
            },
        };

    return {
        // Pass the props to the page
        props: {
            user: context.req.user,
            userServers: context.req.user.guilds,
        },
    };
};

const Dashboard = (props) => {
    // Generate a map of the user's servers
    const servers = props.userServers.map((server) => {
        return (
            <li key={server.id}>
                <a href={`/dashboard/${server.id}`}>{server.name}</a>
            </li>
        );
    });

    return (
        <div>
            <h1>Please select a server</h1>

            <ul>{servers}</ul>
        </div>
    );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

If we login, and go to http://example.com/dashboard we will see all our servers
Server list

But, we dont want to show the servers the user is not an admin in, so we can sort them our by using discord.js permissions:

src/pages/dashboard/index.js

import discord from "discord.js"

export const getServerSideProps = async (context) => {
    // Check if the user is logged in, if not, redirect to the login page
    if (!context.req.user)
        return {
            redirect: {
                destination: "/api/auth/redirect",
                permanent: false,
            },
        };

    return {
        // Pass the props to the page
        props: {
            user: context.req.user,
            userServers: context.req.user.guilds,
        },
    };
};

const Dashboard = (props) => {
    // Generate a map of the user's servers
    const servers = props.userServers.map((server) => {
        // Sort out the servers in which the user doesn't have permissions
        let permissions = new discord.Permissions(server.permissions).toArray();
        if (!permissions.has("MANAGE_GUILD "))

        return (
            <li key={server.id}>
                <a href={`/dashboard/${server.id}`}>{server.name}</a>
            </li>
        );
    });

    return (
        <div>
            <h1>
                Hello {props.user.username}#{props.user.discriminator} Please select a server
            </h1>

            <ul>{servers}</ul>
        </div>
    );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

And now if we go to the same url, we'll see that the number has decreased to show only the ones we have the MANAGE_GUILD, after that, we will see that the number of servers has decreased to the ones we moderate.

So, lets click on one.


Making the server dashboard page

We will be redirected to http://example/dashboard/server_id, like this:
Resulting url

But the page doesn't have any content yet, so we'll add it

src/pages/dashboard/[server].js

export const getServerSideProps = async (context) => {
    // Check if the user is logged in, if not, redirect to the login page
    if (!context.req.user)
        return {
            redirect: {
                destination: "/api/auth/redirect",
                permanent: false,
            },
        };

    // Select the server given the id in the url
    const selectedServer = context.req.user.guilds.find((server) => {
        return server.id == context.params.server;
    });

    const serverPermissions = new discord.Permissions(server.permissions);

    // Check if the server is not in the user's session and if the user doesn't have permission to edit that server, if so, redirect out of the page
    if (!selectedServer || !serverPermissions.has("MANAGE_GUILD"))
        return {
            redirect: {
                destination: "/dashboard",
                permanent: false,
            },
        };

    return {
        // Pass the props to the page
        props: {
            user: context.req.user,
            selectedServer: selectedServer,
        },
    };
};

const Dashboard = (props) => {
    return (
        <div>
            <h1>
                Hello {props.user.username}#{props.user.discriminator}, you can configure {props.selectedServer.name} here
            </h1>

            <input type="text" placeholder="Prefix" />
            <button>Save</button>
        </div>
    );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

If we check the page, we can now see the name of the server, an input and a button:
Resulting dashboard

But we need to make those elements work, but first, we need to add a backend to them.


Saving and getting configurations

Remember the server/api/servers.js we created at the start of the tutorial? Now we will use it

server/api/servers.js

const express = require("express");
const ServerConfigs = require("../models/server-config");

const router = express.Router();

router.post("/get-server-config", async (req, res) => {
    // If the user is not authenticated
    if (!req.isAuthenticated()) return res.status(403).send("unauthorized");

    // Check parameters
    if (!req.body.serverID || typeof req.body.serverID !== "string") return res.status(400).send("invalid-parameters");

    // Find the server in the bot's database using the provided id
    const serverConfig = await ServerConfigs.findOne({ serverID: req.body.serverID });

    // If there isn't a server, return
    if (!serverConfig) return res.status(400).send("server-not-found");

    // Send the configuration
    return res.send(serverConfig);
});

router.post("/save-server-config", async (req, res) => {
    // If the user is not authenticated
    if (!req.isAuthenticated()) return res.status(403).send("unauthorized");

    // Check parameters
    if (!req.body.newPrefix || !req.body.serverID) return res.status(400).send("missing-parameters");
    if (typeof req.body.newPrefix !== "string" || typeof req.body.serverID !== "string") return res.status(400).send("invalid-parameters");

    // Save the configuration
    await ServerConfigs.updateOne({ serverID: req.body.serverID }, { prefix: req.body.newPrefix });

    // Send the configuration
    return res.send("success");
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Now we need to enable this api route, to do this, just put this into your server/config/routes.js file
server/config/routes.js

// Import our app
const { server } = require("../");

try {
    // Add our api endpoints to our app
    server.use("/api/auth", require("../api/auth"));
    server.use("/api/servers", require("../api/servers"));
} catch (err) {
    // Handle errors
    console.log("There was an error trying to load the routes");
    console.error(err);
}
Enter fullscreen mode Exit fullscreen mode

I created a test configuration document on MongoDB for this tutorial
MongoDB

And now we make an interface for it, to connect both the server and the front-end I'm gonna use axios

src/pages/dashboard/[server].js

import axios from "axios";

export const getServerSideProps = async (context) => {
    // Check if the user is logged in, if not, redirect to the login page
    if (!context.req.user)
        return {
            redirect: {
                destination: "/api/auth/redirect",
                permanent: false,
            },
        };

    // Select the server given the id in the url
    const selectedServer = context.req.user.guilds.find((server) => {
        return server.id == context.params.server;
    });

    const serverPermissions = new discord.Permissions(server.permissions);

    // Check if the server is not in the user's session and if the user doesn't have permission to edit that server, if so, redirect out of the page
    if (!selectedServer || !serverPermissions.has("MANAGE_GUILD"))
        return {
            redirect: {
                destination: "/dashboard",
                permanent: false,
            },
        };

    // Get the server configuration
    const response = await axios({
        method: "POST",
        url: `${process.env.HOST}/api/servers/get-server-config`,
        headers: context.req.headers, // <== This part is important, if we don't add it, we won't pass our credentials, and so we wont the able to be authorized to get that information
        data: {
            serverID: selectedServer.id,
        },
    });

    return {
        // Pass the props to the page
        props: {
            user: context.req.user,
            selectedServer: selectedServer,
            selectedServerConfig: response.data, // This assuming there is a server with that
            host: process.env.HOST, // <== Important to save changes
        },
    };
};

const Dashboard = (props) => {
    const saveChanges = async (event) => {
        event.preventDefault();

        // Get the new prefix
        let newPrefix = document.querySelector("#prefix-input").value;

        // Post changes
        const response = await axios({
            method: "POST",
            url: `${props.host}/api/servers/get-server-config`,
            data: {
                serverID: props.selectedServer.id,
                newPrefix: newPrefix,
            },
        });

        // show the results
        console.log(response);
    };

    return (
        <div>
            <h1>
                Hello {props.user.username}#{props.user.discriminator}, you can configure {props.selectedServer.name} here
            </h1>

            <input defaultValue={props.selectedServerConfig.prefix} type="text" id="prefix-input" placeholder="Prefix" />
            <button onClick={saveChanges}>Save</button>
        </div>
    );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

Now if we go to the site, we can see that it sets the default values to the actual configuration.

Final dashboard

If we changed the prefix, pressed save, and then check the console, we'll see that it was saved successfully:

Console final dashboard

And to test if it was changed in the database, we can go, in this case, to MongoDB Compass and check it:

MongoDB Compass

And as we can see, the value changed


How to implement it with your bot

After all of this is done, you might want to change the things in the models, add more configurations, and styles, this is a very very basic dashboard only made to update one value, but it can be scaled to be whatever you want, since we're connecting the bot and the dashboard using the database, the only limitant on how much you want to control your bot from the dashboard is your imagination.


Conclussion

Thanks for reading, it's been a while since I did anything related to Discord and I remember struggling to add a dashboard, the purpose of this tutorial is to make that process simple, and learn more in the way.

All the source code for this project is in my repository at Github if you want to check it out, or clone it so you don't have to copy paste all the code in this project:
https://github.com/Asterki/basic-discord-bot-dashboard

And, I got another thing for you, if you want to support multiple languages, I already did a post on that here on dev.to, you can read it here.

Top comments (0)