Node.js: Creating a REST server for user information

The user information service is a REST server to handle user information data and authentication. Our goal is, of course, to integrate that with the Notes application, but in a real project, such a user information service could be integrated with several web applications. The REST service will provide functions we found useful while developing the user login/logout support in Notes, which we’ll show later in the chapter.

In the package.json file, change the main tag to the following line of code:

“main”: “user-server.mjs”,

This declares that the module we’re about to create, user-server.mjs, is the main package of this project.

Make sure the scripts section contains the following script:

“start”: “cross-env DEBUG=users:* PORT=5858

SEQUELIZE_CONNECT=sequelize-sqlite.yaml node ./user-server.mjs”

Clearly, this is how we’ll start our server. It uses the configuration file from the previous section and specifies that we’ll listen on port 5858.

Then, create a file named user-server.mjs containing the following code:

import restify from ‘restify’;

import * as util from ‘util’;

import { SQUser, connectDB, userParams, findOneUser, createUser, sanitizedUser } from ‘./users-sequelize.mjs’;

import DBG from ‘debug’;

const log = DBG(‘users:service’);

const error = DBG(‘users:error’);

///////////// Set up the REST server

var server = restify.createServer({

name: “User-Auth-Service”,

version: “0.0.1”

});

server.use(restify.plugins.authorizationParser());

server.use(check);

server.use(restify.plugins.queryParser());

server.use(restify.plugins.bodyParser({

mapParams: true

}));

server.listen(process.env.PORT, “localhost”, function() {

log(server.name +’ listening at ‘+ server.url);

});

process.on(‘uncaughtException’, function(err) {

console.error(“UNCAUGHT EXCEPTION – “+ (err.stack || err));

process.exit(1);

});

process.on(‘unhandledRejection’, (reason, p) => {

console.error(‘UNHANDLED PROMISE REJECTION: ${util.inspect(p)}

reason: ${reason}’);

process.exit(1);

});

We’re using Restify, rather than Express, to develop this server. Obviously, the Restify API has similarities with Express, since both point to the Ruby framework Sinatra for inspiration. We’ll see even more similarities when we talk about the route handler functions.

What we have here is the core setup of the REST server. We created the server object and added a few things that, in Express, were called middleware, but what Restify simply refers to as handlers. A Restify handler function serves the same purpose as an Express middleware function. Both frameworks let you define a function chain to implement the features of your service. One calls it a middleware function and the other calls it a handler function, but they’re almost identical in form and function.

We also have a collection of listener functions that print a startup message and handle uncaught errors. You do remember that it’s important to catch the uncaught errors?

An interesting thing is that, since REST services are often versioned, Restify has built- in support for handling version numbers. Restify supports semantic versioning (SemVer) version matching in the Accept-Version HTTP header.

In the handlers that were installed, they obviously have to do with authorization and parsing parameters from the Uniform Resource Locator (URL) query string and from the HTTP body. The handlers with names starting with restify.plugins are maintained by the Restify team, and documented on their website.

That leaves the handler simply named check. This handler is in user- server.mjs and provides a simple mechanism of token-based authentication for REST clients.

Add the following code to the bottom of user-server.mjs:

// Mimic API Key authentication.

var apiKeys = [

{ user: ‘them’, key: ‘D4ED43C0-8BD6-4FE2-B358-7C0E230D11EF’ } ];

function check(req, res, next) {

if (req.authorization && req.authorization.basic) {

var found = false;

for (let auth of apiKeys) {

if (auth.key === req.authorization.basic.password

&& auth.user === req.authorization.basic.username) {

found = true;

break;

}

}

if (found) next(); else {

res.send(401, new Error(“Not authenticated”));

next(false);

}

} else {

res.send(500, new Error(‘No Authorization Key’));

next(false);

}

}

This handler executes for every request and immediately follows restify.plugins.authorizationParser. It looks for authorization data—specifically, HTTP basic authorization—to have been supplied in the HTTP request. It then loops through the list of keys in the apiKeys array, and if the Basic Auth parameters supplied matched, then the caller is accepted.

This should not be taken as an example of a best practice since HTTP Basic Auth is widely known to be extremely insecure, among other issues. But it demonstrates the basic concept, and also shows that enforcing token-based authorization is easily done with a similar handler.

This also shows us the function signature of a Restify handler function—namely, that it is the same signature used for Express middleware, the request and result objects, and the next callback.

There is a big difference between Restify and Express as to how the next callback is used. In Express, remember that a middleware function calls next unless that middleware function is the last function on the processing chain—for example if the function has called res.send (or equivalent) to send a response to the caller. In Restify, every handler function calls next. If a handler function knows it should be the last function on the handler chain, then it uses next(false); otherwise, it calls next(). If a handler function needs to indicate an error, it calls next(err), where err is an object where instanceof Error is true.

Consider the following hypothetical handler function:

server.use((req, res, next) => {

// … processing

if (foundErrorCondition) {

next(new Error(‘Describe error condition’));

} else if (successfulConclusion) {

res.send(results);

next(false);

} else {

// more processing must be required next();

}

});

This shows the following three cases:

  1. Errors are indicated with next(new Error(‘Error description’)).
  2. Completion is indicated with next(false).
  3. The continuation of processing is indicated with next().

We have created the starting point for a user information data model and the matching REST service. The next thing we need is a tool to test and administer the server.

What we want to do in the following sections is two things. First, we’ll create the REST handler functions to implement the REST API. At the same time, we’ll create a command-line tool that will use the REST API and let us both test the server and add or delete users.

1. Creating a command-line tool to test and administer the user authentication server

To give ourselves assurance that the user authentication server works, let’s write a tool with which to exercise the server that can also be used for administration. In a typical project, we’d create not only a customer-facing web user interface, but also an administrator-facing web application to administer the service. Instead of doing that here, we’ll create a command-line tool.

The tool will be built with Commander, a popular framework for developing command-line tools in Node.js. With Commander, we can easily build a command- line interface (CLI) tool supporting the program verb –option optionValue parameter pattern.

Any command-line tool looks at the process.argv array to know what to do. This array contains strings parsed from what was given on the command line. The concept for all this goes way back to the earliest history of Unix and the C programming language.

By using Commander, we have a simpler path of dealing with the command line. It uses a declarative approach to handling command-line parameters. This means we use Commander functions to declare the options and sub-commands to be used by this program, and then we ask Commander to parse the command line the user supplies. Commander then calls the functions we declare based on the content of the command line.

Create a file named cli.mjs containing the following code:

import { default as program } from ‘commander’;

import { default as restify } from ‘restify-clients’;

import * as util from ‘util’;

var client_port;

var client_host;

var client_version = ‘*’;

var client_protocol;

var authid = ‘them’;

var authcode = ‘D4ED43C0-8BD6-4FE2-B358-7C0E230D11EF’;

const client = (program) => {

if (typeof process.env.PORT === ‘string’)

client_port = Number.parseInt(process.env.PORT);

if (typeof program.port === ‘string’)

client_port = Number.parseInt(program.port);

if (typeof program.host === ‘string’) client_host = program.host;

if (typeof program.url === ‘string’) {

let purl = new URL(program.url);

if (purl.host && purl.host !== ”) client_host = purl.host;

if (purl.port && purl.port !== ”) client_port = purl.port;

if (purl.protocol && purl.protocol !== ”) client_protocol = purl.protocol;

}

let connect_url = new URL(‘http://localhost:5858’);

if (client_protocol) connect_url.protocol = client_protocol;

if (client_host) connect_url.host = client_host;

if (client_port) connect_url.port = client_port;

let client = restify.createJsonClient({

url: connect_url.href, version: client_version

});

client.basicAuth(authid, authcode); return client;

}

program

.option(‘-p, –port <port>’,

‘Port number for user server, if using localhost’)

.option(‘-h, –host <host>’,

‘Port number for user server, if using localhost’)

.option(‘-u, –url <url>’,

‘Connection URL for user server, if using a remote server’);

This is just the starting point of the command-line tool. For most of the REST handler functions, we’ll also implement a sub-command in this tool. We’ll take care of that code in the subsequent sections. For now, let’s focus on how the command-line tool is set up.

The Commander project suggests we name the default import program, as shown in the preceding code block. As mentioned earlier, we declare the command-line options and sub-commands by calling methods on this object.

In order to properly parse the command line, the last line of code in cli.mjs must be as follows:

program.parse(process.argv);

The process.argv variable is, of course, the command-line arguments split out into an array. Commander, then, is processing those arguments based on the options’ declarations.

For the REST client, we use the restify-clients package. As the name implies, this is a companion package to Restify and is maintained by the Restify team.

At the top of this script, we declare a few variables to hold connection parameters. The goal is to create a connection URL to access the REST service. The connect_url variable is initialized with the default value, which is port 5858 on the localhost.

The function named client looks at the information Commander parses from the command line, as well as a number of environment variables. From that data, it deduces any modification to the connect_url variable. The result is that we can connect to this service on any server from our laptop to a faraway cloud-hosted server.

We’ve also hardcoded the access token and the use of Basic Auth. Put on the backlog a high-priority task to change to a stricter form of authentication.

Where do the values of program.port, program.host, and program.url come from? We declared those variables—that’s where they came from.

Consider the following line of code:

program.option(‘-p, –port <port>’, ‘Long Description of the option’);

This declares an option, either -p or –port, that Commander will parse out of the command line. Notice that all we do is write a text string and, from that, Commander knows it must parse these options. Isn’t this easy?

When it sees one of these options, the <port> declaration tells Commander that this option requires an argument. It will parse that argument out of the command line, and then assign it to program.port.

Therefore, program.port, program.host, and program.url were all declared in a similar way. When Commander sees those options, it will create the matching variables, and then our client function will take that data and modify connect_url appropriately.

One of the side effects of these declarations is that Commander can generate help text automatically. The result we’ll achieve is being able to type the following code:

$ node cli.mjs –help

Usage: cli.mjs [options] [command]

 Options:

-p, –port <port> Port number for user server, if using localhost

-h, –host <host> Port number for user server, if using localhost

-u, –url <url> Connection URL for user server, if using a remote server

-h, –help output usage information

 Commands:

add [options] <username> Add a user to the user server

find-or-create [options] <username> Add a user to the user server

update [options] <username> Add a user to the user server

destroy <username> Destroy a user on the user server

find <username> Search for a user on the user server

list-users List all users on the user server 

The text comes directly from the descriptive text we put in the declarations. Likewise, each of the sub-commands also takes a –help option to print out corresponding help text.

With all that out of the way, let’s start creating these commands and REST functions.

2. Creating a user in the user information database

We have the starting point for the REST server, and the starting point for a command- line tool to administer the server. Let’s start creating the functions—and, of course, the best place to start is to create an SQUser object.

In user-server.mjs, add the following route handler:

server.post(‘/create-user’, async (req, res, next) => {

try {

await connectDB();

let result = await createUser(req);

res.contentType = ‘json’;

res.send(result);

next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

This handles a POST request on the /create-user URL. This should look very similar to an Express route handler function, apart from the use of the next callback. Refer back to the discussion on this. As we did with the Notes application, we declare the handler callback as an async function and then use a try/catch structure to catch all errors and report them as errors.

The handler starts with connectDB to ensure the database is set up. Then, if you refer back to the createUser function, you see it gathers up the user data from the request parameters and then uses SQUser.create to create an entry in the database. What we will receive here is the sanitized user object, and we simply return that to the caller.

Let’s also add the following code to user-server.mjs:

server.post(‘/find-or-create’, async (req, res, next) => {

try {

await connectDB();

let user = await findOneUser(req.params.username);

if (!user) {

user = await createUser(req);

if (!user) throw new Error(‘No user created’);

}

res.contentType = ‘json’;

res.send(user);

return next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

This is a variation on creating an SQUser. While implementing login support in the Notes application, there was a scenario in which we had an authenticated user that may or may not already have an SQUser object in the database. In this case, we look to see whether the user already exists and, if not, then we create that user.

Let’s turn now to cli.mjs and implement the sub-commands to handle these two REST functions, as follows:

program

.command(‘add <username>’)

.description(‘Add a user to the user server’)

.option(‘–password <password>’, ‘Password for new user’)

.option(‘–family-name <familyName>’,

‘Family name, or last name, of the user’)

.option(‘–given-name <givenName>’, ‘Given name, or first name, of the user’)

.option(‘–middle-name <middleName>’, ‘Middle name of the user’)

.option(‘–email <email>’, ‘Email address for the user’)

.action((username, cmdObj) => { const topost = {

username, password: cmdObj.password, provider: “local”, familyName: cmdObj.familyName,

givenName: cmdObj.givenName, middleName: cmdObj.middleName, emails: [], photos: []

};

if (typeof cmdObj.email !== ‘undefined’) topost.emails.push(cmdObj.email);

client(program).post(‘/create-user’, topost,

(err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(‘Created ‘+ util.inspect(obj));

});

});

By using program.command, we are declaring a sub-command—in this case, add. The <username> declaration says that this sub-command takes an argument.

Commander will provide that argument value in the username parameter to the function passed in the action method.

The structure of a program.command declaration is to first declare the syntax of the sub-command. The description method provides user-friendly documentation. The option method calls are options for this sub-command, rather than global options. Finally, the action method is where we supply a callback function that will be invoked when Commander sees this sub-command in the command line.

Any arguments declared in the program.command string end up as parameters to that callback function.

Any values for the options for this sub-command will land in the cmdObj object. By contrast, the value for global options is attached to the program object.

With that understanding, we can see that this sub-command gathers information from the command line and then uses the client function to connect to the server. It invokes the /create-user URL, passing along the data gathered from the command line. Upon receiving the response, it will print either the error or the result object.

Let’s now add the sub-command corresponding to the /find-or-create URL, as follows:

program

.command(‘find-or-create <username>’)

.description(‘Add a user to the user server’)

.option(‘–password <password>’, ‘Password for new user’)

.option(‘–family-name <familyName>’,

‘Family name, or last name, of the user’)

.option(‘–given-name <givenName>’, ‘Given name, or first name, of the user’)

.option(‘–middle-name <middleName>’, ‘Middle name of the user’)

.option(‘–email <email>’, ‘Email address for the user’)

.action((username, cmdObj) => {

const topost = {

username, password: cmdObj.password, provider: “local”,

familyName: cmdObj.familyName, givenName: cmdObj.givenName, middleName: cmdObj.middleName, emails: [], photos: []

};

if (typeof cmdObj.email !== ‘undefined’) topost.emails.push(cmdObj.email);

client(program).post(‘/find-or-create’, topost, (err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(‘Found or Created ‘+ util.inspect(obj));

});

});

This is very similar, except for calling /find-or-create.

We have enough here to run the server and try the following two commands:

$ npm start

 > user-auth-server@1.0.0 start /home/david/Chapter08/users

> DEBUG=users:* PORT=5858 SEQUELIZE_CONNECT=sequelize-sqlite.yaml node

./user-server.mjs

users:service User-Auth-Service listening at http://127.0.0.1:5858

+0ms 

We run this in one command window to start the server. In another command window, we can run the following command:

$ node cli.mjs add –password w0rd –family-name Einarrsdottir — given-name Ashildr –email me@stolen.tardis me

Created {

id: ‘me’,

username: ‘me’,

provider: ‘local’,

familyName: ‘Einarrsdottir’,

givenName: ‘Ashildr’,

middleName: null,

emails: [ ‘me@stolen.tardis’ ],

photos: []

} 

Over in the server window, it will print a trace of the actions taken in response to this. But it’s what we expect: the values we gave on the command line are in the database, as shown in the following code block:

$ node cli.mjs find-or-create –password foooo –family-name Smith — given-name John –middle-name Snuffy –email snuffy@example.com snuffy-smith

Found or Created {

id: ‘snuffy-smith’,

username: ‘snuffy-smith’,

provider: ‘local’,

familyName: ‘Smith’,

givenName: ‘John’,

middleName: ‘Snuffy’,

emails: [ ‘snuffy@example.com’ ],

photos: []

} 

Likewise, we have success with the find-or-create command.

That gives us the ability to create SQUser objects. Next, let’s see how to read from the database.

3. Reading user data from the user information service

The next thing we want to support is to look for users in the user information service. Instead of a general search facility, the need is to retrieve an SQUser object for a given username. We already have the utility function for this purpose; it’s just a matter of hooking up a REST endpoint.

In user-server.mjs, add the following function:

server.get(‘/find/:username’, async (req, res, next) => {

try {

await connectDB();

const user = await findOneUser(req.params.username);

if (!user) {

res.send(404, new Error(“Did not find “+ req.params.username));

} else {

res.contentType = ‘json’;

res.send(user);

}

next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

And, as expected, that was easy enough. For the /find URL, we need to supply the username in the URL. The code simply looks up the SQUser object using the existing utility function.

A related function retrieves the SQUser objects for all users. Add the following code to user-server.mjs:

server.get(‘/list’, async (req, res, next) => {

try {

await connectDB();

let userlist = await SQUser.findAll({});

userlist = userlist.map(user => sanitizedUser(user));

if (!userlist) userlist = [];

res.contentType = ‘json’;

res.send(userlist);

next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

We know from the previous chapter that the findAll operation retrieves all matching objects and that passing an empty query selector such as this causes findAll to match every SQUser object. Therefore, this performs the task we described, to retrieve information on all users.

Then, in cli.mjs, we add the following sub-command declarations:

program

.command(‘find <username>’)

.description(‘Search for a user on the user server’)

.action((username, cmdObj) => {

client(program).get(‘/find/${username}’,

(err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(‘Found ‘+ util.inspect(obj));

});

});

program

.command(‘list-users’)

.description(‘List all users on the user server’)

.action((cmdObj) => {

client(program).get(‘/list’, (err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(obj);

});

});

This is similarly easy. We pass the username provided on our command line in the /find URL and then print out the result. Likewise, for the list-users sub- command, we simply call /list on the server and print out the result.

After restarting the server, we can test the commands, as follows:

$ node cli.mjs find me Found {

id: ‘me’,

username: ‘me’,

provider: ‘local’,

familyName: ‘Einarrsdottir’,

givenName: ‘Ashildr’,

middleName: null,

emails: [ ‘me@stolen.tardis’ ],

photos: []

}

$ node cli.mjs list-users [

{

id: ‘snuffy-smith’,

username: ‘snuffy-smith’,

provider: ‘local’,

familyName: ‘Smith’,

givenName: ‘John’,

middleName: ‘Snuffy’,

emails: [ ‘snuffy2@gmail.com’ ],

photos: []

},

{

id: ‘me’,

username: ‘me’,

provider: ‘local’,

familyName: ‘Einarrsdottir’,

givenName: ‘Ashildr’,

middleName: null,

emails: [ ‘me@stolen.tardis’ ],

photos: []

}

]

And, indeed, the results came in as we expected.

The next operation we need is to update an SQUser object.

4. Updating user information in the user information service

The next functionality to add is to update user information. For this, we can use the Sequelize update function, and simply expose it as a REST operation.

To that end, add the following code to user-server.mjs:

server.post(‘/update-user/:username’, async (req, res, next) => {

try {

await connectDB();

let toupdate = userParams(req);

await SQUser.update(toupdate, { where: { username: req.params.username }});

const result = await findOneUser(req.params.username);

res.contentType = ‘json’;

res.send(result);

next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

The caller is to provide the same set of user information parameters, which will be picked up by the userParams function. We then use the update function, as expected, and then retrieve the modified SQUser object, sanitize it, and send it as the result.

To match that function, add the following code to cli.mjs:

program

.command(‘update <username>’)

.description(‘Add a user to the user server’)

.option(‘–password <password>’, ‘Password for new user’)

.option(‘–family-name <familyName>’,

‘Family name, or last name, of the user’)

.option(‘–given-name <givenName>’, ‘Given name, or first name, of the user’)

.option(‘–middle-name <middleName>’, ‘Middle name of the user’)

.option(‘–email <email>’, ‘Email address for the user’)

.action((username, cmdObj) => { const topost = {

username,

password: cmdObj.password,

familyName: cmdObj.familyName,

givenName: cmdObj.givenName,

middleName: cmdObj.middleName,

emails: [], photos: []

};

if (typeof cmdObj.email !== ‘undefined’) topost.emails.push(cmdObj.email);

client(program).post(‘/update-user/${username}’, topost, (err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(‘Updated ‘+ util.inspect(obj));

});

});

As expected, this sub-command must take the same set of user information parameters. It then bundles those parameters into an object, posting it to the

/update-user endpoint on the REST server.

Then, to test the result, we run the command, like so:

$ node cli.mjs update –password fooooey –family-name Smith –given- name John –middle-name Snuffy –email snuffy3@gmail.com snuffy-smith Updated {

id: ‘snuffy-smith’,

username: ‘snuffy-smith’,

provider: ‘local’,

familyName: ‘Smith’,

givenName: ‘John’,

middleName: ‘Snuffy’,

emails: [ ‘snuffy3@gmail.com’ ],

photos: []

} 

And, indeed, we managed to change Snuffy’s email address. The next operation is to delete an SQUser object.

5. Deleting a user record from the user information service

Our next operation will complete the create, read, update, and delete (CRUD) operations by letting us delete a user.

Add the following code to user-server.mjs:

server.del(‘/destroy/:username’, async (req, res, next) => {

try {

await connectDB();

const user = await SQUser.findOne({

where: { username: req.params.username } });

if (!user) { res.send(404,

new Error(‘Did not find requested ${req.params.username} to delete’));

} else { user.destroy();

res.contentType = ‘json’;

res.send({});

}

next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

This is simple enough. We first look up the user to ensure it exists, and then call the destroy function on the SQUser object. There’s no need for any result, so we send an empty object.

To exercise this function, add the following code to cli.mjs:

program

.command(‘destroy <username>’)

.description(‘Destroy a user on the user server’)

.action((username, cmdObj) => {

client(program).del(‘/destroy/${username}’,

(err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(‘Deleted – result= ‘+ util.inspect(obj));

});

});

This is simply to send a DELETE request to the server on the /destroy URL. And then, to test it, run the following command:

$ node cli.mjs destroy snuffy-smith

Deleted – result= {}

$ node cli.mjs find snuffy-smith

finding snuffy-smith

NotFoundError: {}

at Object.createHttpErr (/home/david/Chapter08/users/node_modules/restify- clients/lib/helpers/errors.js:91:26)

at ClientRequest.onResponse (/home/david/Chapter08/users/node_modules/restify- clients/lib/HttpClient.js:309:26)

at Object.onceWrapper (events.js:428:26)

at ClientRequest.emit (events.js:321:20)

at HTTPParser.parserOnIncomingClient [as onIncoming] (_http_client.js:602:27)

at HTTPParser.parserOnHeadersComplete (_http_common.js:116:17)

at Socket.socketOnData (_http_client.js:471:22)

at Socket.emit (events.js:321:20)

at addChunk (_stream_readable.js:305:12)

at readableAddChunk (_stream_readable.js:280:11) 

First, we deleted Snuffy’s user record, and it gave us an empty response, as expected. Then, we tried to retrieve his record and, as expected, there was an error.

While we have completed the CRUD operations, we have one final task to cover.

6. Checking the user’s password in the user information service

How can we have a user login/logout service without being able to check their password? The question is: Where should the password check occur? It seems, without examining it too deeply, that it’s better to perform this operation inside the user information service. We earlier described the decision that it’s probably safer to never expose the user password beyond the user information service. As a result, the password check should occur in that service so that the password does not stray beyond the service.

Let’s start with the following function in user-server.mjs:

server.post(‘/password-check’, async (req, res, next) => {

try {

await connectDB();

const user = await SQUser.findOne({

where: { username: req.params.username } });

let checked; if (!user) {

checked = {

check: false,

username: req.params.username,

message: “Could not find user”

};

} else if (user.username === req.params.username

&& user.password === req.params.password) {

checked = { check: true, username: user.username };

} else {

checked = {

check: false,

username: req.params.username,

message: “Incorrect password”

};

}

res.contentType = ‘json’;

res.send(checked);

next(false);

} catch(err) {

res.send(500, err);

next(false);

}

});

This lets us support the checking of user passwords. There are three conditions to check, as follows:

Whether there is no such user Whether the passwords matched Whether the passwords did not match

The code neatly determines all three conditions and returns an object indicating, via the check field, whether the user is authenticated. The caller is to send username and password parameters that will be checked.

To check it out, let’s add the following code to cli.mjs:

program

.command(‘password-check <username> <password>’)

.description(‘Check whether the user password checks out’)

.action((username, password, cmdObj) => {

client(program).post(‘/password-check’, { username, password }, (err, req, res, obj) => {

if (err) console.error(err.stack);

else console.log(obj);

});

});

And, as expected, the code to invoke this operation is simple. We take the username and password parameters from the command line, send them to the server, and then print the result.

To verify that it works, run the following command:

$ node cli.mjs password-check me w0rd

{ check: true, username: ‘me’ }

$ node cli.mjs password-check me w0rdy

{ check: false, username: ‘me’, message: ‘Incorrect password’ } 

Indeed, the correct password gives us a true indicator, while the wrong password gives us false.

We’ve done a lot in this section by implementing a user information service. We successfully created a REST service while thinking about architectural choices around correctly handling sensitive user data. We were also able to verify that the REST service is functioning using an ad hoc testing tool. With this command-line tool, we can easily try any combination of parameters, and we can easily extend it if the need arises to add more REST operations.

Now, we need to start on the real goal of the chapter: changing the Notes user interface to support login/logout. We will see how to do this in the following sections.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *