Database and Authentication
MongoDB
Intro
It is a NoSQL
which is structured in collections, where each collection would be used to store a particular type of data in the form of documents:
| Blog Collection | | Blog document | | Blog document | | Blog document |
Here each document represent a single item of data, for example, each Blog document
represents one blog. The data is contained inside the documents in a very similar fashion to JSON
objects, so the documents consist of key-value pairs like so:
{
"id": ObjectId(12345),
"title": "Opening party",
"snippet": "All about...",
"body": "Lorem ipsum"
}
Set Up
We can either install MongoDB
locally or we can use a cloud database which is already hosted for us. For the latter we will use MongoDB Atlas.
There we create a cluster and inside this new cluster we create a new collection called Blog
.
Then we create a user accessing the Security -> Database Access
section.
Once we have our user created, we specify a way to connect to the database, by heading to Clusters -> Connect your application
. We then copy the Connection String
that we will use as the database URI
. Observe that this URI
needs you to input your password.
Mongoose
Now we need to actually connect to the database, we could use the MongoDB API
package and use the MongoDB API
, however we will use Mongoose
that makes it easier to interact with the database.
Mongoose
is a ODM
(Object Document Mapping) library, which means that it maps the standard MongoDB API
providing a much easier way to connect to and interact with the database.
It does this by allowing us to create simple data models which have query methods to create, get, delete and update database documents.
For that we first have to create a Schema
for the document which define the structure of a type of data or document. For example:
Blog Schema:
- title(string), required
- snippet(string), required
- body(string), required
Next, what we do is to create a Model
based on that Schema
, the Model
is what actually allows us to communicate with a particular database collection. Each Model
has static methods get
, save
, delete
, etc, that allow us to manage the data.
Installing
$ npm install mongoose
=== Connect to MongoDB
===
So, now, we import the Mongoose
package and we use our database URI
to connect to it, remember to change password
and cluster_name
to the values you specified for your database.
const express = require('express');
const morgan = require('morgan');
const mongoose = require('mongoose');
// express app
const app = express();
// connect to mongodb & listen for requests
const dbURI = "mongodb+srv://user:<password>@test.mongodb.net/<cluster_name>
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(result => app.listen(3000))
.catch(err => console.log(err));
The connect
method is an asynchronous function, so it will execute a callback function when it finished connecting, or an error if the connection failed. In our case, we proceed to start our server when the database is ready.
Create Models & Schemas
Once we have successfully connected to our database, we will create our Blog Schema
. For that, we first create a folder called models
and inside it we create blog.js
that will contain the following code:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const blogSchema = new Schema(
{
title: {
type: String,
required: true,
},
snippet: {
type: String,
required: true,
},
body: {
type: String,
required: true,
},
},
{ timestamps: true }
);
const Blog = mongoose.model("Blog", blogSchema);
module.exports = Blog;
As you can see, we first import mongoose
and the Schema
object that we use to define the Blog Schema
.
In order to create a new Blog Schema
we create a new Schema
object and we specify the different properties and restrictions. We also set and object of options, where we specify that we want MongoDB
to save the timestamps of updates, creations, etc.
Next we created a model that is based in the Schema
we just created with the function model
and we pass it the Model
name (this name is then pluralized, as to then look up the collection that matches it) and the Schema
instance.
Getting/Saving Data
In order to work we data, we must import the Model
we just created.
const express = require('express');
const morgan = require('morgan');
const mongoose = require('mongoose');
const Blog = require('./models/blog');
// express app
const app = express();
// connect to mongodb & listen for requests
const dbURI = "mongodb+srv://user:<password>@test.mongodb.net/<cluster_name>
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(result => app.listen(3000))
.catch(err => console.log(err));
app.get('/blogs', (req, res) => {
Blog.find()
.then(result => {
res.send(result);
})
.catch(err => {
console.log(err);
});
});
app.get('/blogs/:id', (req, res) => {
const id = req.params.id;
Blog.findById(id)
.then(result => {
res.send(result);
})
.catch(err => {
console.log(err);
});
});
Here we use the find
and findById
methods to interact with our database.
In order to create or delete new Blogs
:
app.post("/blogs", (req, res) => {
const blog = new Blog(req.body);
blog
.save()
.then((result) => {
res.redirect("/blogs");
})
.catch((err) => {
console.log(err);
});
});
app.delete("/blogs/:id", (req, res) => {
const id = req.params.id;
Blog.findByIdAndDelete(id)
.then((result) => {
res.json({ redirect: "/blogs" });
})
.catch((err) => {
console.log(err);
});
});
In the POST
method we create a new Blog
object using the objects from the request body, and then we save it in our database. On the other hand, in order to delete a Blog
we pass the id
as a parameter, we search for it on the database and we delete it.
Mocking MongoDB
MongoMemoryServer
As we have mentioned we need MongoMemoryServer
, so we install it as a development depencendy. For that we head to our node app’s root folder and we execute:
$ npm install mongodb-memory-server-core --save-dev
Docker
So, now we create our Dockerfile
, which holds our app source code, and where we install mongodb:
FROM alpine:latest
MAINTAINER albamr09
# Install dependencies
RUN apk add --no-cache nodejs npm
# Install mongodb
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.6/main' >> /etc/apk/repositories
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.6/community' >> /etc/apk/repositories
RUN apk update
RUN apk add mongodb
RUN apk add mongodb-tools
RUN mkdir -p /data/db/
RUN chmod -R 777 /data/db
# Add common user
RUN adduser -D user
#RUN useradd --create-home --shell /bin/bash user
# Create app directory
WORKDIR /home/user/src/
# Change permissions
RUN chown -R user:user /home/user/src/
RUN chmod -R 755 /home/user/src/
USER user
# Copy with user as owner
COPY --chown=user:user ./package*.json ./
# Install app dependencies
RUN npm install
# Copy and override src folder
COPY . .
Note that this version of MongoDB
is 3.4.4
, mainly because we are using the alpine
image. This version may not coincide with our MongoDB Docker
image, and is not desirable. So make sure (or force) that you are installing the save versions.
MongoMemoryServer
Configuration
Also, we only need to install it for those images that are not supported by MongoDB
. Furthermore, if instead of the package mongo-memory-server-core
we install mongo-memory-server
, the latter will include a post-install
hook that will install MongoDB
if it is not already installed on the system.
In case of manually installing MongoDB
we have to let know MongoMemoryServer
where the binary lays. So, within our package.json
file we add:
"config": {
"mongodbMemoryServer": {
"systemBinary": "/usr/bin/mongod",
"version": "3.4.4"
}
Example of Usage
We, now, exemplify how to mock our database in our tests:
const { MongoMemoryServer } = require('mongodb-memory-server-core');
const mongoose = require('mongoose');
const UserModel = require('../../models/user');
const userData = { 'name': 'test', 'email': 'test@test.com', 'password': 'test1234', 'username': 'testname' };
describe('User Model Tests', ()=> {
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri(), {
useNewUrlParser: true,
useUnifiedTopology: true,
}).catch(error => console.log(error));
});
afterAll(async () => {
await mongoServer.stop();
await mongoose.connection.close();
});
afterEach(() => {
mongoose.connection.collections['users'].drop( function() {});
});
it('Create a new user', async ()=> {
const user = new UserModel(userData);
const savedUser = await user.save();
expect(savedUser._id).toBeDefined();
expect(savedUser.name).toBe(userData.name);
expect(savedUser.email).toBe(userData.email);
expect(savedUser.password).toBe(userData.password);
expect(savedUser.username).toBe(userData.username);
})
it('Create a user with invalid fields', async ()=> {
var invalidUserData = {...userData};
delete invalidUserData.email;
const user = new UserModel(invalidUserData);
let error;
try{
const savedUser = await user.save();
error = savedUser;
}catch(err){
error = err;
}
expect(error).toBeInstanceOf(mongoose.Error.ValidationError);
expect(error.errors.email).toBeDefined();
})
it('Create user that already exists', async ()=>{
await new UserModel(userData).save();
let error;
try{
const repeatedUser = new UserModel(userData);
await repeatedUser.save();
}catch(err){
error = err;
}
expect(error).toBeDefined();
expect(error.code).toBe(11000);
})
it('Create user with undefined fields', async ()=>{
var newUserData = {...userData};
delete newUserData.name;
const user = new UserModel(newUserData);
await user.save();
expect(user._id).toBeDefined();
expect(user.name).toBeUndefined();
})
}
JSON Web Tokens
Installation
$ npm install jsonwebtoken
Example of Usage
We first create our Express
application and so, we import express
and jsonwebtoken
. And then we start the server.
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
app.listen(3000, () => {
console.log("nodejs app running...");
});
Now, we define two new endpoints: /api
and /api/login
.
app.get("/api", (req, res) => {
res.json({
mensaje: "Nodejs and JWT",
});
});
app.post("/api/login", (req, res) => {
const user = {
id: 1,
nombre: "Henry",
email: "henry@email.com",
};
jwt.sign({ user }, "secretkey", { expiresIn: "32s" }, (err, token) => {
res.json({
token,
});
});
});
Where we use the sign
method to create a new token.
So, if we want to define an endpoint that requires authentication we do:
// Middleware
function verifyToken(req, res, next) {
const bearerHeader = req.headers["authorization"];
if (typeof bearerHeader !== "undefined") {
const bearerToken = bearerHeader.split(" ")[1];
req.token = bearerToken;
next();
} else {
res.sendStatus(403);
}
}
app.post("/api/posts", verifyToken, (req, res) => {
jwt.verify(req.token, "secretkey", (error, authData) => {
if (error) {
res.sendStatus(403);
} else {
res.json({
mensaje: "Post fue creado",
authData,
});
}
});
});
Where verifyToken
is a middleware function that gets the token from the header, and then we use the verify
method to check if the token is valid.