Create a basic dashboard for your Discord bot using Next.js 💯
Asterki
Posted on October 22, 2022
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.
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:
Now, follow the next steps to set up our file structure:
- Create a folder called
src
- Move the
pages
folder into thesrc
folder (routing will still work this way, learn more) Inside the
src
folder, create a folder calledstyles
In the root directory of the project, create a folder called
server
.Move the file named
server.js
into theserver
folder, then rename it toindex.js
, and since we changed the main file, we should also change it on our package.json, change the dev script to point to theindex.js
file inside theserver
folder.Inside the
server
folder, create 3 folders:api
,models
andconfig
.Inside the newly created
api
folder, create a file calledservers.js
, this will be used for our server related info requests, and a file namedauth.js
, which will handle our auth api routes such as login, logout, etc.Inside the config folder, create a file called
auth.js
, one calledroutes.js
, one calledmiddleware.js
and another one calleddatabases.js
.
New folder structure
After that, our folder structure now looks like this
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
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
// 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
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
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
// 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
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
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
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
We press the login button and we get redirected to this page
We press authorize, and then we check our 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.
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
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;
If we login, and go to http://example.com/dashboard we will see all our servers
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;
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:
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;
If we check the page, we can now see the name of the server, an input and a button:
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;
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
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;
Now if we go to the site, we can see that it sets the default values to the actual configuration.
If we changed the prefix, pressed save, and then check the console, we'll see that it was saved successfully:
And to test if it was changed in the database, we can go, in this case, to MongoDB Compass and check it:
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.
Posted on October 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.