Registration & Login Function

Introduction

In today’s lesson, I’m going to implement registration and login functions. Each function will be available as an API endpoint in the app server built in a previous post.

To follow along, take a look at these posts:

Development Environment
Node Setup
App Server
Database Creation

API Structure

At this point, I’ve setup my node.js development environment, created an app server with a health check API endpoint, and finally setup a database with an accounts table inside it. I’m ready to write the registration & login handlers.

I’m going to open my app server file, base-server.js, in VS Code. It looks like this (as plain text outside the IDE):

'use strict'

const express	    = require('express')
const app 	        = express()
const http 	        = require('http').createServer(app)
const httpPort 	    = '8080'
const bodyParser 	= require('body-parser')

// serve static files from a directory named public that is in the same
// level as the server currently running, i.e. this file
app.use(express.static('public'))

// validate all user-controlled input before trusting it for further prorcessing
app.use(bodyParser.urlencoded({ 
    extended: true 
    })
)

app.get('/api/healthcheck', function (req, res) {
        res.write('The server is running\n')
        res.end()
})

// APIs and other business logic goes here

// finally we start the listener process bound to the port
// defined in the httpPort const at the top of this server file
http.listen(httpPort, function () {
  console.log('HTTP server listening on port', httpPort)
})

Database Connection

In order to take any CRUD - Create, Read, Update, Delete - actions in the database, I need an open connection to the database. This is done using the node mysql module.

Install it with npm, being sure to use the -S flag to save the mysql requirement to package.json.

npm install -S mysql2

If you run into any difficulties, take a look at the module’s NPM page here https://www.npmjs.com/package/mysql2

Once it’s installed, I add it to my list of module requirements at the top of my base-server.js file like this:

const mysql = require('mysql2')

Next, I add a block to provision a connection to the members database that I created earlier. I add this directly below my const at the top of the server. You can place it anywhere prior to the point in your server where you want to use the pool const to perform queries.

const pool = mysql.createPool({
  host: 'localhost',
  user: 'databaseUser',
  password: 'secretPAssword',
  database: 'members',
  connectionLimit: 100,
 })

Registration Function

I’m going to place my two new APIs in the area that currently holds the APIs go here comment.

First, I remove that comment. I’ll use the healthcheck API as a starter for this work, so I’ll copy the healthcheck API code block into my clipboard & then insert it into that now empty space.

Next, I’ll make a quick edit to the API name and the HTTP method used. For a registration function, I will be passing in a few data elements - the new member’s username, email and password at a minimum - to the server. The POST method is well-suited to handle these elements as data in the body of the post. There is no strictly right or wrong method to use; if you had a preference to use GET and pass the incoming member data as query paramemters, you could do that.

The POST method is more aligned with a write action, so I use that for any APIs that write to my database.

Here’s what the registration function looks like so far:

app.post('/api/registration', function (req, res) {

    let action 	    	= req.body.action 
    let account_id      = req.body.account_id 
    let email 	    	= req.body.email 
    let account_type    = req.body.account_type 
    let member_name 	= req.body.member_name 
    let password     	= req.body.password
    let role            = req.body.role
    let salt 	    	= bcrypt.genSaltSync(10);

    // check if there's an action with the incoming request & whether it is a create action
    if ( action && action == 'create') { 

	// validate that you have the important parameters.  this checks for empty & undefined values. you can add other checks here but this covers the core

        if ((typeof member_name == 'undefined') || (member_name == '') || (member_name == null) || (typeof password == 'undefined') || (password == '') || (password == null) || (typeof email == 'undefined') || (email == '') || (email == null)) {


	    // if there are errors, return the JSONified message and end the response

            return res.json({ error: true, message: 'Please supply appropriate parameters' });

        } else {

	    // since I store the user's password for later logins, I hash it first.  This keeps the plaintext password from landing in my database

            let hash = bcrypt.hashSync(password, salt);

	    // during development, I like to see what my workflow does. using this technique allows me to set the DEBUG flag as a const 
	    // at the top of the server once and then log messages to the console if the value is YES
	    // you will want to only log important stuff in production and never log any plaintext credential values

	    // the console in this case is the server's console, not the browser's console

            if (DEBUG == 'YES') { console.log('Hash: ', hash) }

	    // setup the SQL statement using the incoming request's BODY parameters
	    // these vars are set at the top of the registration function
            
            let create_sql = "INSERT INTO accounts (member_name, email, password, account_type, account_creation, account_modification_time, account_status, role) VALUES (\"" +member_name+ "\", \"" +email+ "\", \"" +hash+ "\", \"" +account_type+ "\", CURDATE(), CURDATE(), \"active\", \"" +role+ "\")";
            
            pool.query(create_sql, function(err, row) {

                if (DEBUG == 'YES') { console.log('SQL:',create_sql) }

                if (err) {

		    // do some errror handling

                    let _Error = JSON.stringify(err);

                    if (_Error.match(/Duplicate entry/g)) {

                        return res.json({ error: true, message: 'Duplicate member name or email address.  Please select another' });

                    }

                } else if ( typeof row === "undefined") {

                    return res.json({ error: true, message: 'Unknown sql error' });

                } else {

		    // if there are no errors, return a success message
		    // then end the response with res.end()

                    if (DEBUG == 'YES') { console.log('account created') }

                    res.write('Account created successfully')
                    res.end()
                    
                } 
            })
        }
    } 
})

Login Function

Now that the registration API is complete, a login API can be implemented. This is a simple login function with error checking.

app.get('/api/login', function(req, res) {

    let member_name  	= req.query.member_name;
    let password     	= req.query.password;

    if ( member_name && password ) {
	    
        let login_sql = "SELECT * FROM accounts WHERE member_name = '" +member_name+ "'";

        if (DEBUG == 'YES') { console.log('SQL used: ' + login_sql); }

        pool.query(login_sql, function(err, row) {

            if (err) {
                if (DEBUG == 'YES') { console.log(err);}
                res.status(500);

            } else if (!row.length) {

                return res.json({ success: false, message: 'Unknown account' });

            } else {

                let account_id = row[0].account_id;
                let hashed_password = row[0].password;
                
                if (bcrypt.compareSync(password, hashed_password)) {
                    
                    res.write('Successful login')
                    res.status(200).send;

                } else {

                    res.json({success: false, message: 'There was a problem verifying your credentials'});

                }
            }
            res.end();
        });

    } else {

        if (DEBUG == 'YES') { console.log("Request received but no parameters: " +req.body); }

        return res.json({ success: false, message: 'Please supply appropriate parameters.' });

    }
});

Once a member logs in successfully, I will set a token that can be used to provide access to services that map to their profile.

Testing

The registration API can be easily tested using curl.

I ensure my mysql server is running & then I start my node server. Refer to my previous app server post if you need a refresher on that.

Once the app server is running, I can run this curl command to register a new member to whatever service I’m building:

curl -X POST http://localhost:8080/api/registration -d "action=create&member_name=santa&email=jbminn@jbminn.com&password=f00Bar123&account_type=basic&role=developer"

This makes a POST to the registration API in my app server running on port 8080. It sends a POST body consisting of data elements that I passed to the -d parameter.

If there are no errors, I will see this message in my shell:

Account created successfully

I can also use curl to test the login. For example:

/Users/jbminn\>curl "http://localhost:8080/api/login?member_name=santa&password=f00Bar123"
Successful login

Conclusion

This lesson showed how to implement registration & login functions that are exposed as APIs in a node.js app server. These are the foundations of any user-facing service that must authenticate & authorize members.

0%