Authentication System for Discord Bot Dashboard - javascript

I have a working discord bot and I decided to write a dashboard for it.
Dashboard runs on the separate server then the bot, so it has to make a lot of calls to the express server (where the bot lives as well) to retrieve necessary information, like api/me, api/me/guilds, api/guild/<guild_id>, api/guild/<guild_id>/members etc.
I have the following middleware, that runs before every call on api/guild route and checks if user belongs to the guild he's trying to access.
function isInGuild(req, res, next) {
// I'm storing the OAuth2 token from the login in the session
const token = req.session.token;
const guild_id = req.params.id;
try {
// this function calls discord API /#me/guilds route to retrieve user's guilds with their permissions
const data = await DC.getUserGuilds(token);
const guild = data.find(o => o.id === guild_id);
if(!guild) {
res.status(404).send({ code: 0, message: "Not in the guild" });
return;
}
req.guild = guild;
next();
} catch(error: any) {
res.status(error.status);
res.send({ code: error.data.code, message: error.data.message });
}
}
The problem is that the #me/guilds discord API call has 1 second rate limit, so making it every time I want to get some guild information is very slow. It's the slowest when the user reloads the guild overview or members site, because it has to make api/me/guilds, api/guild/<guild_id> and maybe even api/guild/<guild_id>/members, and all of them will make #me/guilds call, which will take at least few seconds, because of the rate limit.
Bot itself runs with the usage of DiscordJS, so I'm aware that I could check the cache to see if bot is in the guild, and if not just return an error, but I would like to first check if the user itself belongs to the guild (and check their permission, to decide if they even should have access to the dashboard or not) to display some additional information, and I can't check that with the discordjs, if the bot isn't in the guild.
My question is: does making #me/guilds call to authenticate the user every single API call is the only way of authentication and I have to find some way to make fewer calls on the client side, or is there a quicker way to check if user is authenticated, that won't take several seconds every time?

Related

Change bot nickname (Discord js) in a guild where a command was executed

I am making a free public bot soon but I have some troubles rn.
I am trying to use a command (example: -bitcoin) that makes the bot change its name every 30 seconds ONLY in the guild (bot nickname), but when making a function and looping it to make this possible, the bot can't get the guild id again because there's no message being replied in the 2nd and further times.
if(command === 'bitcoin'){
updateNickname(message);
setInterval(updateNickname, 30000);
}
async function updateNickname(message) {
const { data } = await axios.get(`https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur`);
return message.guild.me.setNickname(`BTC = ${data.bitcoin.eur}€`);
}
Can someone help me?
DISCLAIMER: I can't just cache an id, because it wouldn't work on other servers, it would only work in the server id I texted in, so I want it to work on multiple server at the same time (in further development, people will be able to choose their own cypto and currency for individual servers).
I would probably persist this data in a database (since I guess you want this to persist between bot restarts).
Then, I will have 1 interval to run every 30 seconds that will update all the guilds at once. This should also be easier on your bot since you don't have a different interval for each guild this bot is installed on.
if(command === 'bitcoin'){
updateNickname(message.guild, message.data);
await persistInDatabase({
guildId: message.guild.id
coin: message.data
});
}
// run this every 30 seconds.
setInterval(updateNicknames, 30000);
// change nickname for a single guild
async function updateNickname(guild, coin) {
const { data } = await axios.get(`https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur`);
return guild.me.setNickname(`BTC = ${data.bitcoin.eur}€`);
}
// change nickname for ALL guilds this bot is installed on
async function updateNicknames() {
const allGuilds = await getAllFromDb();
for(let dbGuild of allGuilds) {
const guild = client.guilds.get(dbGuild.guildId);
updateNickname(guild, dbGuild.coin);
}
}
I didn't include the database function, but it should be straightforward. Just choose your favorite database :-)
I saved the coin type in the database, but if you don't need it, you can remove it.
You can cache the axios call just so you won't do it for each guild this bot is installed on.

Discord bot log

So, I created a discord.js bot and added the following into index.js:
client.on("guildCreate", guild = {
const logsServerJoin = client.channels.get('757945781352136794');
console.log(`The bot just joined to ${guild.name}, Owned by ${guild.owner.user.tag}`);
client.channels.cache.get('channel id paste here').send(`The bot just joined to ${guild.name}, Owned by ${guild.owner.user.tag}`);
var guildMSG = guild.channels.find('name', 'general');
if (guildMSG) {
guildMSG.send(` Hello there! My original name is \`Bryant\`!\n\ This bot created by **R 1 J 4 N#7686**\n\ For more info
type \`/help\`!\n\ \`Bryant - Official Server:\`
https://discord.gg/UsQFpzy`);
} else {
return;
}
});
// Logs of the bot leaves a server and changed the game of the bot
client.on("guildDelete", guild = {
client.channels.cache.get('757945781352136794').send(`The bot just
left ${guild.name}, Owned by ${guild.owner.user.tag}`);
console.log(`The bot has been left ${guild.name}, Owned by ${guild.owner.user.tag}`);
logsServerLeave.send(`The bot has been left ${guild.name}, Owned by ${guild.owner.user.tag}`);
});
It does not show any error in the terminal. It is supposed to log me where the bot joined and left in the mentioned channel but does not 🤷‍♂️. Can anyone help me out with this?
If you are getting no console log and no message in the channel / an error telling you that the channel could not be found, the issue is most likely in how you are registering the events, make sure client is an instance of the discord.js Client, below is a minimal working example
const { Client } = require("discord.js")
const client = new Client()
client.on("guildCreate", guild => {
console.log(`The bot just joined to ${guild.name}, Owned by ${guild.owner.user.tag}`)
})
client.login("yourtoken")
If you are trying to get the logs from your bot alert you in your home discord server you can do this multiple ways: Getting the channel from cache, Constructing the channel or Using webhooks. Currently you are trying to fetch the channel from cache. Although a decent solution it can fall down when using sharding later down the line. Personally I prefer webhooks as they are the easiest and most isolated.
Channel from cache
This technique is very similar to the way you were doing it.
const channel = client.channels.cache.get('757945781352136794')
channel.send('An Event occurred')
Just place this code anywhere you want to log something and you'll be fine.
Constructing the channel
const guild = new Discord.Guild(client, { id: '757945781352136794' });
const channel = new Discord.TextChannel(guild, { id: '757945781352136794' });
channel.send('An Event occurred')
This method is similar to fetching the channel from cache except it will be faster as you are constructing your home guild and channel and then sending to it. Mind you will need a client which you can get from message.client
Webhooks
My Favourite method uses webhooks. I recommend you read up on how discord webhooks work in both Discord and Discord.js
You will also need to create a webhook. This is very easy. Go into the channel you want the webhooks to be sent to and then go to integrations and create a new webhook. You can change the name and profile as you wish but copy the url and it should look something like this:
https://discord.com/api/webhooks/757945781352136794/OkMsuUHwdStR90k7hrfEi5*********
The last part of the path is the webhook token and the bit before is the channel ID
I recommend you create a helper function that can be called like this:
sendWebhook('An event occurred');
And then write the function to create and then send to the webhook
function sendWebhook(text) {
const webhook = new Discord.WebhookClient('757945781352136794', 'OkMsuUHwdStR90k7hrfEi5*********');
webhook.send(text);
webhook.destroy();
}
This will not be very dynamic and will be a pain to change the channel but for constant logging ( like guild joins and leaves ) I think it is the best solution
The problem is probably that you don't have "Privileged Gateway Intents" enabled. To turn them on, go to https://discord.com/developers, click on your application, then on "Bot" then scroll down and enable "PRESENCE INTENT" and "SERVER MEMBERS INTENT" and save. It should probably work for you now.

UserState showing null in onMembersAdded function

I have logic in my onMembersAdded function to load the user state and see if userData.accountNumber attribute exists. If it does not, a run an auth dialog to get the user's account number. If the attribute does exist, the welcome message should be displayed without a prompt.
When I test on local, this works fine. But when I test on Azure, I always end up in the !userData.accountNumber block. Through checking the console log, I can see that in the onMembersAdded function is showing {} for the userData object. But in auth dialog, even if I skip the prompt (which we allow the user to do), the accountNumber attribute is there in userState (if it had been entered previously).
The only thing I can figure is that somehow using BlobStorage for state, as I do on Azure, is somehow exhibiting different behavior than MemoryStorage which I am using for local testing. I thought it might be a timing issue, but I am awaiting the get user state call, and besides if I do enter an account number in the auth dialog, the console log immediately following the prompt shows the updated account number, no problem.
EDIT: From the comments below, it's apparent that the issue is the different way channels handle onMembersAdded. It seems in emulator both bot and user are added at the same time, but on webchat/directline, user isn't added until the first message is sent. So that is the issue I need a solution to.
Here is the code in the constructor defining the state variables and onMembersAdded function:
// Snippet from the constructor. UserState is passed in from index.js
// Create the property accessors
this.userDialogStateAccessor = userState.createProperty(USER_DIALOG_STATE_PROPERTY);
this.dialogState = conversationState.createProperty(DIALOG_STATE_PROPERTY);
// Create local objects
this.conversationState = conversationState;
this.userState = userState;
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let member of membersAdded) {
if (member.id === context.activity.recipient.id) {
this.appInsightsClient.trackEvent({name:'userAdded'});
// Get user state. If we don't have the account number, run an authentication dialog
// For initial release this is a simple prompt
const userData = await this.userDialogStateAccessor.get(context, {});
console.log('Members added flow');
console.log(userData);
if (!userData.accountNumber) {
console.log('In !userData.accountNumber block');
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
console.log('In userData.accountNumber block');
var welcomeCard = CardHelper.GetHeroCard('',welcomeMessage,menuOptions);
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
}
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
If you want your bot to receive a conversation update from Web Chat with the correct user ID before the user sends a message manually, you have two options:
Instead of connecting to Direct Line with a secret, connect with a token (recommended). Note that this will only work if you provide a user property in the body of your Generate Token request.
Have Web Chat send an initial activity to the bot automatically so the user doesn't have to. This would be in response to DIRECT_LINE/CONNECT_FULFILLED, and it could be an invisible event activity so to the user it still looks like the first activity in the conversation came from the bot.
If you go with option 1, your bot will receive one conversation update with both the bot and the user in membersAdded, and the from ID of the activity will be the user ID. This is ideal because it means you will be able to acess user state.
If you go with option 2, your bot will receive two conversation update activities. The first is the one you're receiving now, and the second is the one with the user ID that you need. The funny thing about that first conversation update is that the from ID is the conversation ID rather than the bot ID. I presume this was an attempt on Web Chat's part to get the bot to mistake it for the user being added, since Bot Framework bots typically recognize that conversation update by checking if the from ID is different from the member being added. Unfortunately this can result in two welcome messages being sent because it's harder to tell which conversation update to respond to.
Conversation updates have been historically unreliable in Web Chat, as evidenced by a series of GitHub issues. Since you may end up having to write channel-aware bot code anyway, you might consider having the bot respond to a backchannel event instead of a conversation update when it detects that the channel is Web Chat. This is similar to option 2 but you'd have your bot actually respond to that event rather than the conversation update that got sent because of the event.
Per Kyle's answer, I was able to resolve the issue. However, the documentation on initiating a chat session via tokens wasn't entirely clear, so I wanted to provide some guidance for others trying to solve this same issue.
First, you need to create an endpoint in your bot to generate the token. The reason I initiated the session from SECRET initially was because I didn't see a point to creating a token when the SECRET was exposed anyway to generate it. What wasn't made clear in the documentation was that you should create a separate endpoint so that the SECRET isn't in the browser code. You can/should further obfuscate the SECRET using environmental variables or key vault. Here is the code for the endpoint I set up (I'm passing in userId from browser, which you'll see in a minute).
server.post('/directline/token', async (req, res) => {
try {
var body = {User:{Id:req.body.userId}};
const response = await request({
url: 'https://directline.botframework.com/v3/directline/tokens/generate',
method: 'POST',
headers: { Authorization: `Bearer ${process.env.DIRECTLINE_SECRET}`},
json: body,
rejectUnauthorized: false
});
const token = response.token;
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
res.write(token);
res.end();
} catch(err) {
console.log(err);
res.setHeader('Content-Type', 'text/plain');
res.writeHead(500);
res.write('Call to retrieve token from Direct Line failed');
res.end();
}
})
You could return JSON here, but I chose to return token only as text. Now to call the function, you'll need to hit this endpoint from the script wherever you are deploying the bot (this is assuming you are using botframework-webchat CDN). Here is the code I used for that.
const response = await fetch('https://YOURAPPSERVICE.azurewebsites.net/directline/token', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({userId:userID})
});
const token = await response.text();
Body of request must be stringified JSON. Fetch returns the response as a stream, so you need to convert it using .text() or .json() depending on how you are sending the response from your bot endpoint (I used .text()). You need to await both the fetch AND the response.text(). My whole script to deploy the webchat is within an async function. Just a note, if you need this to work in IE11 as I do, async/await won't work. I dealt with this by running the entire code through Babel once I was done and it seems to work fine.

ReactJS - What instead of JWT to keep session

For some time I have been writing an application based on this tutorial:
https://jasonwatmore.com/post/2019/04/06/react-jwt-authentication-tutorial-example
My application is already much more developed, it uses other rest API servers, etc. Currently, after positive authentication, a JWT token is sent to the client, which is stored in localStorage. But!
I need to add such functionality: the assumption is that only one user can use one account at a time.
There are two solutions:
a) when the user is logged in, he logs out in the previous place after logging in on the other device/browser.
b) when user trying log in another device/browser get a throw for example "Client already login"
So, From what I learned, I can't do it using JWT. My question is :
What can I use instead of JWT to handle the session because I think it's about?
Thanks.
EDIT : My authenticate funtion from server side :
async function authenticate({ username, password }) {
console.log(username + " " + password);
const user = await User.findOne({ username });
if (user && bcrypt.compareSync(password, user.hash)) {
const { hash, ...userWithoutHash } = user.toObject();
const token = jwt.sign({ sub: user.id }, config.secret, { expiresIn: 300 });
return {
...userWithoutHash,
token
};
}
}
You may use a more traditional session ID based authentication approach. When a user logs in, a session ID (something along the lines of a UUID) gets generated. Then, using the user ID as a key, the session ID gets stored in a hashmap on the server. In all subsequent login attempts, the session ID in the map would be overwritten for the same user.
I suggest kicking out any other session during subsequent login for usability reasons. Imagine logging into your application from some device, but suddenly getting pulled into a meeting somewhere. Now, if we don't kick out previous sessions during login, any attempt on your part to login from a different device would fail, and you would be forced to wait to be able to login.
Note that once the new session ID has been recorded in the hashmap, any access attempts by the first device using the old session ID would fail.
You can use JWT to handle the session itself.
But to handle multiple logins to a single account, you can for example add a locked attribute to your users table in your database, provided you have such thing in your architecture. This would allow you to lock your session.
This solution only makes sense if you want to totally lock your session until logout (and not be able to login from another device for instance). If that is not your intention, I recommend Tim Biegeleisen's answer.
Example:
users || id | name | locked
----- || -- + ---- + ------
|| 1 | John | 0
|| 2 | Bob | 0
Then, we could imagine your session handling to be as such (simple example for an Express app, but see it as pseudo code):
database.query("SELECT locked FROM users WHERE name = $1", [name])
.then((data) => {
if (data.locked === 1) {
res.status(403).json();
} else {
database.query("UPDATE users SET locked = 1 WHERE name = $1", [name])
.then(() => {
// Define token key and object for jwt
// ...
var token = jwt.sign(...);
res.status(200).cookie('token', token).json();
});
}
});
And then, when a user logs out or is logged out by an expired jwt token, you call another endpoint of your API and you set the locked attribute to 0.
Hope this helps!

Emit event for particular user if login functionality in application in Socket.io with Node.js

I have used methods socket.on and io.emit, And i got response to all users. But, i want to get response for particular user.
But my application contains login functionality and i followed this post on stackoverflow, and they are saying we need unique userId and socketId in an object for a particular user to emit an event for a particular user.
But i am getting the userId after login, But we want it when user connect to app.
So can anyone please help me with the same?
In your node.js, create a global array 'aryUser', each element contains the socketid and loginid.
node.js onConnect (new connection), add a new element to the array with the socketid and set loginid = empty.
after the user login, emit an event from client to the server, e.g:
socket.emit('userloginok', loginid)
in node.js, define a function:
socket.on('userloginok', loginid)
and in this function, search the aryUser with the socketid and replace the empty loginid inside the array element with the parm loginid.
in node.js, define the function:
socket.on('disconnect')
and in this function, search the aryUser, use aryUser.splice(i,1) to remove the user just disconnected.
that means, aryUser contains all users connected, some of them logined, some of them not logined. And you can use the socketid of the array to send message to particular user, and/or all users.
Example Source Code:
server.js
http://www.zephan.top/server.js
server.html
http://www.zephan.top/server.html.txt
rename server.html.txt to server.html, put server.html and server.js in the same directory, and run:
node server.js
Yes, you definitely need socketId in order to send and receive messages between two specific users.
UserId is required just to keep track of socketId associated with the particular user or you can manage it with some other way as well that's up to you.
As per your question, you have userId of the user and you need socketId of that user! So, in this case, you can pass userId when that particular connects to a socket server from the client side as shown in below snippet,
const socket = io(this.SOCKET_SERVER_BASE_URL, { query: `userId=${userId}` });
And you can read this user on nodejs server like this,
const userId= socket.request._query['userId'],
const socketId= socket.id
Now store this socketId in somewhere, for example, Redis or some sort of caching mechanism again up to you, just make sure fetching and retrieval should be fast.
Now while sending a message just pull the socketId from your cache and emit the message on that socketId by using below code,
io.to(socket.id).emit(`message-response`, {
message: 'hello'
});
I have written a complete blog post on this topic on both Angular and AngularJs, you can refer those as well.
Edit 1:
Part 1 =>
When your user completes the login request, then make the connection to the socket server.
Assuming you are using React Or Angular After a successful login you will redirect your user to home component(page). On the Home component(page) make the socket server connect by passing the userId just like this,
const socket = io(SOCKET_SERVER_BASE_URL, { query: `userId=${userId}` });
P.S. you can get userID from URL or maybe using a cookie that is up to you.
Once you receive this socket connection request on the server, then you can read the userID query and you can get socketId associated with it and store it in cache like this,
io.use( async (socket, next) => {
try {
await addSocketIdInCache({
userId: socket.request._query['userId'],
socketId: socket.id
});
next();
} catch (error) {
// Error
console.error(error);
}
});
Part 2 =>
Now, let's say you have a list of the users on the client side, and you want to send a message to particular users.
socket.emit(`message`, {
message: 'hello',
userId: userId
});
On the server side, fetch the socketId from the cache using UserId. Once you get the socketId from cache send a specific message like this,
io.to(socketId).emit(`message-response`, {
message: 'hello'
});
Hope this helps.

Categories

Resources