User accounts
In this tutorial we are going to learn how to connect to and query a (SQL) database, and how to store the extracted information in a session. We will use these technologies to build a simple app for managing user accounts.
We will use the module
express-session for handling
sessions, and the database abstraction layer
knex for querying the database.
The references for this tutorial are
- The lessons on sessions and on DBALs,
- The
express-sessionreference manual, - The Knex reference manual,
- The MDN documentation on using Promises.
Preparing your workspace
Create a new Node app on Glitch. Install the following packages
through package.json:
body-parserfor handling POST requests,express-sessionfor creating sessions,knexfor querying databases,sqlite3for the engine doing the real work.
Note: The sqlite3 module won’t work if you develop locally on
Windows. Fortunately, Knex is compatible with all flavors of SQL
(MySQL, Postgres, MariaDB, …), so you can have your application talk
with any of these servers with very few code changes. Follow the
instructions in the docs.
Building the schema
For a starter, we need a database, and some tables in it. Glitch supports SQLite natively, so that’s what we’re going to use. However, we cannot create the tables at the moment we start the application, as those instructions would be executed at every restart.
Instead, we are going to write a separate script to initialize the database with the tables we need.
-
Create a script named
db_init.js, containing the following code:var knex = require('knex')({ client: 'sqlite3', connection: { filename: ".data/db.sqlite3" }, debug: true, });
This code creates a SQLite file named db.sqlite3 in the folder
.data (a hidden folder on Unix systems, and a special folder on
Glitch), and initiates a connection. We can now query the connection
by operating on the knex object. For example, we can read the
contents of a table, using the async/await syntax (see the lesson
on using databases):
async function foo() {
...
var rows = await knex.raw('SELECT * FROM bar);
...
}
-
Using a raw
CREATE TABLEstatement indb_init.js, or using the schema builder, create a table namedusers, with the following columns:login: typevarchar(255), primary key,pass: typevarchar(255) NOT NULL.
-
To be sure to terminate the program, add a call to
knex.destroy()after the table creation.
Of course, nothing is going to happen if you don’t run the script. We will use Glitch’s Linux console to do the sysadmin magic. From the main menu (top left, click on the name of your project), select “Advanced Options”, then “Open Console”.
A Linux terminal opens in a new window. Run your script by typing
node db_init.js
Some debug messages will appear. If things have gone well, you can check that your database was created and populated by issuing the command
sqlite3 .data/db.sqlite3 .schema
Damn! I need to update the schema
The schema we chose for our users table was too minimalist: we need
to add more columns. SQL offers a ALTER TABLE command to modify a
table, and we could in principle run it from a Node script, or
directly from the SQLite command line. However, this does not play
very well with the idea of having a script to run just once to
initialize the database. Instead, we are going to modify db_init.js
so that it erases the database and recreates the table. This way, we
can easily update the script and rerun it whenever we need a fresh
state.
-
Modify
db_init.jsto:- first drop the table
users(useDROP TABLE IF EXISTSto avoid errors), - then (re)-create the table users with the following columns:
login: typevarchar(255), primary key,pass: typevarchar(255) NOT NULL,name: typevarchar(255),color1: typevarchar(10),color2: typevarchar(10).
Rerun
db_init.jsvia the console and verify that the tableusersis recreated correctly. - first drop the table
-
The (asynchronous) function
knex('users').columnInfo()returns a description of the columns of theuserstable. Add a call to.columnInfo()todb_init.jsto show the structure of theuserstable after it has been created. -
Add some default users to the table by using a
INSERT INTOstatement. -
Print the contents of the table on the console by using a
SELECTstatement (Note: the rows obtained bySELECTare returned as a list of JavaScript objects by Knex).
The Web app
We are now ready to write our web application. We keep it very simple: we want to be able to create a new user, log in, and show the list of users.
-
Initialize the server with the usual Express skeleton (with
body-parserandnunjucksconfigured). Add a Knex configuration section as done indb_init.js. Add (emtpy) routes for the URLs/,/signin,/logoutand/userlist. -
Write the handler for
/userlist. It must select all rows from theuserstable, and show them in a HTML table. -
Replace the text of the
color1column with a rectangle colored in the same color. Ifcolor1isNULLuse a default color. The result may look something like this:Login Name Color kasparov Garry Kasparov karpov Anatoly Karpov Hint: for the colored rectangle you can use a
<span> with fixed height and width and thedisplay: inline-blockproperty.
Creating users
The /signin route will let us create a new user. For this we use a
HTML form, and a POST request. In the handler for the POST request we
will need to send a INSERT INTO query with user supplied
parameters. We recall that the knex.raw() method takes an optional
second argument, which is a list of replacement values for prepared
statements, like thus:
await knex.raw('INSERT INTO users VALUES (?, ?, ?, ?, ?)',
[ 'karpov', 'checkmate', 'Anatoly Karpov', 'green', 'black' ]);
Alternatively, you can use the query builder.
-
Create a Nunjucks template for a web form for creating a new user, and serve it at the
/signinURL. It must contain a login field, a password field, a name field, a favorite color and a secondary color (trytype=colorfor the color fields!). The form must use the POST method (methodattribute) and it must send the data back to/signin(it is enough to leave theactionattributed empty). -
Create a POST handler for
/signinwhere you take the values sent in the form and insert then in theuserstable, only if the login and the password field are non-empty (an empty field in a form is read as an empty string''inreq.body). After the insertion has succeeded, redirect to/(usingres.redirect()).If one of the fields is empty, do not insert in the database, and send instead the same form template, with a message explaining what error happened (use the template engine to show the message). Using the template engine, you can also pre-fill the fields with the values that had been sent.
Test with the “Network” tool of the browser’s developer tools.
Errors may happen when you do SQL queries, and we cannot let them
crash our application. For example, a user may try to create an
account for a login that already exists (violating the PRIMARY KEY
constraint). To handle errors, you can use a try ... catch:
try {
await knex.raw('INSERT INTO ...');
} catch (error) {
console.error(error);
}
- Handle errors in your code. If the error is a violation of the primary key constraint (inspect the error in the console, and find out the error code for this violation), send back the form with an explanatory message. For any other error, send back the form with a generic message.
Login and sessions
We come to the last part: handling logins. We will show a login form
at the / URL, and redirect to /userlist upon successful login.
Since we are moving between different handlers, we need a way to keep the information that the user is logged in across many requests. We cannot, and do not want to, transfer the username and password with every request, hence we will use a session mechanism.
As seen in the sessions lesson, we must start by configuring the Express app with
var session = require('express-session');
app.use(session({
secret: '12345',
resave: false,
saveUninitialized: false,
}));
Then the session will be available in every handler at req.session.
-
Write a handler for
/that shows a form similar to this one:login: password: -
Like you did for
/signin, write a second handler for POST requests to/. The handler checks whether the pair login-password corresponds to a user in the database, then:-
if login and password are wrong, it shows again the form with an error message;
-
if they are correct, it saves the login, the name and the colors in the session, and redirects to
/userlist.
-
-
Modify
/so that it immediately redirects to/userlistif the user is already logged in. -
Modify
/userlistso that it redirects to/when the user is not logged in, and so that it shows in bold the current user. -
Write the
/logoutroute to log the user out. For that, it is enough to put a special value in the session (for example, set the login tonull); when a handler detects this special value, it treats the request as an unauthenticated one. -
Add a link to
/logoutin the/userlistview.
Advanced stuff (optional)
Here are some optional questions to better master Knex and sessions.
-
In
/signin, ask for the password twice, and only accept the new user if the two password fields match. -
We have mostly used
knex.raw()in this tutorial, howeverknexhas a very convenient query builder to construct SQL statements with a more object-oriented syntax. Read the docs and replace your raw queries with built ones. You can also try Schema building indb_init.js. -
It is very bad practice to store cleartext passwords in the database: what you if suffer a data breach and your database is divulged?
Instead, it is preferable to store hashed passwords: an irreversible transformation of the cleartext password, so that the cleartext is only seen in transit, but never stored. The hash computation must happen on the server, and must be salted with a secret, otherwise the technique is no safer than storing cleartext passwords.
If this is done properly, not even the administrator will be able to recover the cleartext passwords. A misunderstanding of this technique was at the origin of the famous leak of 150M Adobe passwords in 2013 : http://zed0.co.uk/crossword/.
It’s best not to roll your own crypto, as it is easy to make mistakes. Use instead trusted ready-made solutions, such as the bcrypt module.