In this example we'll be using Enmap to store user data in order to authenticate users on a simple Koa application. In order to make this secure, we'll be using bcrypt
to encrypt the passwords, so of course they will not be plain text in the database.
We'll be using koa
as a web server module, along with koa-session
in order to store the session information. Additionally, for ease of use, koa-router
is used to simplify the route code.
This tutorial uses
koa-session
which, by default, is insecure since it stores the entire session data in a browser cookie. This means the password, though encrypted, would be availble in the cookie, and easy to spoof. There are many session stores available for different storage system, but using them is beyond the scope of this example.
To install those requirements, run the following in a new, empty folder for your project:
Once all of those are installed, we're ready to start! We're going to create an index page, login page, and one "protected" page that requires login. Let's start with the top of the file, which is all the required modules:
So now we have all the basics necessary.
Let's create a few functions specifically for the login features, related to enmap. First, a function to create a new user:
This function takes in the following arguments:
username
which obviously is self-explanatory: it's the username entered during login.
name
is the "full name" or "display name", for a friendly display on the page or an email.
plainpw
is the password desired for this account. It has to come in as plain text, obviously, in order to be properly saved in the database.
admin
is a boolean value representing whether the user should be administrator. If creating something like a blog, only an administrator could create other administrators. It's false by default.
The function first checks if the username exists and returns an error if it does. It then generates a salted, hashed version of the password which it stores in the database. Don't let the name fool you, the password is not "encrypted", which implies that it can be decrypted. Instead, it's a "cryptographic hash functions", a unidirectional function that cannot be undone. The only way to verify that a password is correct is to re-hash it again and compared the hashes.
Once the hashed password is obtained, the user itself is stored in the database with all 4 incoming arguments except the password which is the hashed version.
The login function takes in the username and the incoming plain password and verifies that the hashed version corresponds with the one stored in the database.
An important point here is that this function returns a promise in all cases. If the username doesn't exist or the password is blank, a false
response is returned in a promise. Otherwise, the response of bcrypt's compare
function is returned. This function returns true if the passwords match, false if they do not.
There's a few configuration items we need to take care of. First off, the session settings:
Then we need to setup how Koa will handle rendering EJS pages. This is one pretty awesome thing about Koa, that this can be setup automatically and globally, but don't let me gush all over this!
So let's establish our "routes", which is the pages that can be accessed by the browser. With the help of the Router, this can be really straightforward.
Then we have the login route, which does a lot of the bulk of our work. It checks for login, and adds everything it needs to the session in Koa if the authentication is successful:
Let's also create a logout function, that simply destroys the current session and returns the user to the index:
This one is pretty straightforward, so I don't think I need to get into the details, right? ;)
Lastly, we have the route for our "private" page. the one that only works if you're logged in. Now, there are "better" ways to establish protected routes, but let's go with the simplest one for now. We're just going to check for the logged
property of the session to determine if the user is logged in.
At the very end of our file we still have a bit of stuff to add. Mainly starting the server, but also telling the parser and routers to initialize. This would be how it's done:
While templating is slightly beyond the scope of an authentication tutorial, I would be remiss to ignore the fact that logging in without a page would be... let's say a little hard.
Koa's EJS templating configuration, that we did above, means that templates need to appear in the views
folder. There will be a few template files:
template.html
will be the "main" template. It will have the header, footer, and whatever else we want to appear on every page.
index.html
will be the main page everyone can access.
login.html
contains the login page and form
secret.html
has a little secret about something.
Let's start with the template.html
file.
The <%- body %>
tag is where the contents of the other pages appear.
The index.html
is just some welcome thingy that we aren't concerned about:
Then we have the login.html
page which has our form. The form simply posts back to itself, so it'll trigger the .post
endpoint:
And, finally, the secret.html
page we've all been waiting for. Nothing that you haven't heard before, though:
With all said and done, This should be your project. Want a more "advanced" version of this project? Check out My full blogging platform, Koarrots.
This example uses a very, very simple bot made in discord.js to demonstrate how easily Enmap can be used to create a per-server configuration system.
Remember to follow the Installation Instructions before running any of this!
Our first task is of course to initialize the enmap correctly. In this case, we're attaching the settings to our client object so we can use it in different commands.
The main event for our bot, where messages are received. Any error here will probably crash the bot on every message received, so be careful!
This points bot is simple, but functional. Make sure you've followed the Installation Instructions before doing the following.
First, you need to create a new persistent Enmap. Here's how it goes:
That will create a new Enmap under the name of points, and attaches it to the client object so it can be used where ever you have access to the client object.
The obvious goal of a points system is to accumulate fake internet points and gloat about it. So, of course, that's going to be our first focus. In this example implementation, we will make the points guild-specific, and unuseable in DMs. Points will still accumulate even if the user does a command, which simplifies our code a bit.
Our starting point is a very basic message handler with pre-existing commands - such as what we see in the Command with Arguments page on An Idiot's Guide. The code is as such:
We do have a small caveat - we really don't want to react on Direct Messages, so our whole code will be in a block that checks for that:
Our very first step is going to be to initialize a new entry in the enmap for any new user - one we haven't received a message from before. This is done using the enmap.ensure(key, defaultvalue)
method, which can check if a specific key exists in the Enmap, write it if it doesn't (and return the defaultvalue in this case). Note that our keys take the format of guildid-userid
so they're unique to the guild and the user. Also, our data in this case is a complete object, which we'll take advantage of fully.
There's obviously a few ways we could have done this, including some fancy ternary condition or whatever. I will, however, keep this code as simple to read as possible.
The following bit is super simple - Enmap has a method to directly increment a value in Enmap even if it's in an object. Pretty clever if I do say so myself!
Have your own way of incrementing points? No problem! Enmap.math() gives you the ability to add, multiply, and act upon any numerical value or property. To add 10 points, for instance, client.points.math(key, "+", 10, "points")
would be used.
Time to level up! If a user has enough points, they will go up a level. Now we have to do some math here, but don't run off in fear, this one's pretty easy. This is how we calculate the levels:
This line will calculate the square root of currentPoints
then multiplies that result by 0.1 then floors that result for a round number.
Now we should work out if you've amassed enough points to actually level up, by grabbing the current user's level and comparing them. If the new calculated level is higher, it means the user leveled up and we can deal with that, first by sending them a very annoying mee6-inspired message!
Lastly, we want to update the score.level
value with the new level so throw this under the message.reply
.
So here's the whole thing from top to bottom, with bonus comments!
Alright, that's the bulk of the code, you could throw this into your bot and it would work like a charm, however your users wouldn't know how many points, or even their levels, so let's fix that, make a new command called points
, which will also show them their level.
Obviously there's no way for us to know how you're making commands, so again we'll assume you're doing a bot in a single js file. You may need to adjust the code, of course!
So let's re-iterate our current starting position.
The points
command would look like this:
Let's finish this off with a very simple leaderboard
command that will show the top 10 users in the current guild. For this we'll need to filter the Enmap to only get the users for the current guild, then we'll convert the results to an array, sort that, and keep the first 10 results only.
We convert to an array because an Enmap
, just like its underlying Map
structure, is not ordered and thus cannot be sorted. It may seem ordered because it stores by keys, but that's actually a quirk, not a feature.
So here's our leaderboard command: