Chat App - basic login and registration
You can look at here to check your code against mine.
Note: Above GitHub URL has final code and might not look like exactly as described below. We will add more code as series progresses.
For brevity, I have removed CSS and error handling.
In this we should be able to do basic registration and login without maintaining any session.
Creating HTTP server
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Server is working'));
app.listen(3000);
Go ahead and type node index.js
in your command line or use nodeman
. You can configure package.json to use npm start
command. Go in your browser and check localhost:3000
to see if everything is working.
Express automatically create the HTTP server for you and start listening on the given port.
Setting up MongoDB
We need database to store credential and later on verify those credentials. You can install MongoDB or you can use docker. I used docker because I don't wanted to install software for one time purpose. You may want to install docker-compose to make your life easier.
Side Note: docker is basically like a lightweight virtual machine (not really). For our purpose this is good enough.
Once installed, create a docker-compose.yml
file in your chat application root directory like here. Copy the following
version: '3.7'
services:
mongodb_container:
image: mongo:latest
ports:
- 27017:27017
volumes:
- mongodb_data_container:/data/db
volumes:
mongodb_data_container:
Notice: we created mongodb_data_container volume to persist data.
By default docker does not persist data.
In your command line fire docker-compose up -d
. It may take some time to download and running the MongoDB. It will only happen initially and after that docker is going to use local cache and it will be a breeze.
Connecting Node.js to MongoDB
For this I am using mongoose framework to make our life simpler.
const moongose = require('mongoose');
// if app db will not exist in our MongoDB, it will automatically create it when we write it first time to the db.
moongose.connect("mongodb://localhost:27017/app", {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
})
.then(res => console.log('connected to db'))
.catch(error => console.error(error));
exports.db = moongose.connection
Whenever we write require('mongoose')
in different file, it is going to use same connection.
Side Note: You may not want to catch error here.
If connection fail, we may want to crash our app here.
I skipped password protection for MongoDB here. You may want to set that up.
Setting up User collection schema
In User collection, we are going to store username and password documents for each user.
// file: models/user.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
require: true,
trim: true
},
password: {
type: String,
required: true
}
});
var User = mongoose.model('User', UserSchema);
module.exports = User;
Creating login/registration page
To save time, I used same page for registration and login. Create a login.html
file.
<html>
<head>
<script async src="/static/js/login.js"></script>
</head>
<body>
<form>
<input type="text" name="username" id="username"/>
<input type="password" name="password" id="password"/>
<input id="login" type="button" value="Login"/>
<input id="register" type="button" value="Register"/>
</form>
</body>
</html>
//login.js
const register = document.getElementById('register');
const login = document.getElementById('login');
function sendRequest(url) {
const form = document.querySelector('form');
const formData = new FormData(form);
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: formData.get('username'),
password: formData.get('password')
})
})
.then(res => res.json())
.catch((error) => {
console.error('Error:', error);
});
}
register.addEventListener('click', () => {
sendRequest('/register').then(({ registered }) => {
if (registered) window.location.href = '/chat';
});;
});
login.addEventListener('click', () => {
sendRequest('/login').then(({ authenticated }) => {
if (authenticated) window.location.href = '/chat';
});
});
Reason for sending the data using fetch API instead of submission because this way I can have separate endpoints for login and registration.
On server-side create two endpoints:
// For brevity, I removed validation middleware which basically checks if name or password field are not set.
app.post('/login', authenticateUser);
app.post('/register', registerUser);
// In original code, I used then instead of async and await.
exports.registerUser = async (req, res, next) => {
const userData = {
username: req.body.username,
password: req.body.password
}
try {
await User.create(userData);
res.json({
registered: true
});
} catch(err => {
// error handling
});
};
To serve static file such as /static/js/login.js
we are going to use express.static
controller. This way we do not have to write app.get
for each file.
app.use('/static', express.static(path.join(__dirname, 'public')));
Basically we are saying, for /static
URL use express.static
and in express.static
we are passing the directory to handle static content. So actual location of login.js
is public/js/login.js
.
At this point you should be able to register user. You can check the data in database.
Encrypting password
This is good but we are storing password as text. This is problematic if some get access to our data dump. To encrypt password we are going to use bcrypt library. We are going to add following code in the User schema file.
// file: models/user.js
const bcrypt = require('bcrypt');
UserSchema.pre('save', function(next) {
const user = this;
bcrypt.hash(user.password, 10)
.then(hash => {
user.password = hash;
next();
})
.catch(e => next(e));
});
pre
is a mongoose middleware defined on the collection schema. Here, we are saying before saving run bcrypt hash method and replace user.password with its hash value. So in User collection, hash value is getting stored.
10 in bcrypt.hash
is a saltsRound. Higher the number, more time will it take to brute force the data. All the relevant information is attached to the encrypted hash.
Try to register the user again and check password field in the database. You will now see an encrypted hash.
Login User
For login, we need a mechanism to compare plain text password against encrypted password. For this we will create a static method on the User model and use bcrypt.compare
for comparison.
// using promise here. In the code I used callback approach.
UserSchema.statics.authenticate = async function(username, password) {
const user = await User.findOne({ username }).exec();
if (!user) return false;
return await bcrypt.compare(password, user.password);
}
Remember we used authenticateUser
for post login request.
//modified code from the original because now I am using promises.
exports.authenticateUser = async (req, res) => {
const result = await User.authenticate(req.body.username, req.body.password);
const status = result ? 200 : 401;
res.status(status).json({
authenticated: result
});
};
You can check the status in browser network tool.
Issue: For each privileged request client has to send the password because we are not maintaining any session info.