Create a basic dashboard for your Discord bot using Next.js 💯
Tags: discord, javascript, tutorial, webdev
Published: 2022-10-22 15:21:52

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](https://www.xda-developers.com/how-to-create-discord-bot/) 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](https://discord.com/developers/applications).
- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vq4m3jwyqdk8x9eywdzf.png)

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
```

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 
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pklwevdvp554ljdq7yee.png)

### 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](https://nextjs.org/docs/advanced-features/src-directory))
- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3zsocbhlh6lciv9m6ti9.png)

---

## 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
```

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
```

---

## 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**

```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 }
```

**server/config/routes.js**
```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);
}
```

**server/config/databases.js**
```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 }
```

**server/config/auth.js**
```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);
        }
    )
);
```

**server/config/middleware.js**
```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);
}
```

---

## 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**
```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;
```

---

## 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**
```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;
```

**src/pages/index.js**
```jsx
const Home = () => {
    return (
        <div>
            <h1>Redirect to discord's login page</h1>
            <a href="/api/auth/redirect">Login</a>
        </div>
    );
};

export default Home;
```

### 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
```

And now we go to `http://example.com` or your domain
![http://example.com](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xw2tn4lmziofkvmlibhf.png)

We press the login button and we get redirected to this page
![Discord auth page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/stlbe0q48veautucoads.png)

We press authorize, and then we check our console.

![Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q86e9dgavscvn6rwxy9o.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c33vgluu97c3qnxfpihr.png)

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](https://nextjs.org/docs/routing/introduction), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a8vs8mbp16vz8ec9cn4l.png)

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**
```jsx
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;
```

If we login, and go to http://example.com/dashboard we will see all our servers
![Server list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0lx7wam3agvx1qbk2qx7.png)

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**
```jsx
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;
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8i15lbfs38jikmyzpv7r.png)

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

**src/pages/dashboard/[server].js**
```jsx
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;
```

If we check the page, we can now see the name of the server, an input and a button:
![Resulting dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1sv9b3bm4h9hq900sey.png)

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**
```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;
```
 
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);
}
```

I created a test configuration document on MongoDB for this tutorial
![MongoDB](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8g6b8z12m6co9l5zy46l.png)

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**
```jsx
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;
```

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

![Final dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cowge6cfhyeynwxku0ul.png)

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

![Console final dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5y00uiysm6l0ihxrn2s4.png)

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

![MongoDB Compass](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/got0wqqgjo7cwk3o1poz.png)

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](https://dev.to/asterki/how-to-make-your-site-multi-language-using-locale-480k).