MEAN Stack User Registration and Login Example & Tutorial
After getting a lot of interest in a previous tutorial I wrote on user registration and login using AngularJS, I thought I'd expand on it a bit further to show a full end to end solution including the server side and database using the MEAN stack (which stands for MongoDB, ExpressJS, AngularJS, NodeJS).
I find one of the best ways to learn how to use a new technology such as the MEAN stack is to tinker with and extend working examples of real world applications. User login and registration, security and account management are extremely common requirements for many applications.
The example project is available on GitHub at https://github.com/cornflourblue/mean-stack-registration-login-example.
For a similar example built with Angular 2/4 you can check out the MEAN Stack 2.0 - User Registration and Login Example & Tutorial.
MEAN Stack Dependencies
To run MEAN stack applications locally you need NodeJS installed and MongoDB running, for a guide on how to do this see Setup the MEAN Stack on Windows.
Project Setup
Once you've downloaded the code you can run the command 'npm install' from the project root folder (where the package.json file is located) to download all node package dependencies.
Then run 'node server.js' from the same location to start the web server and browse to http://localhost:3000 to access the application.
MEAN Stack Tutorial Project Structure
I've split the login and registration pages out from the angular application in order to secure access to the angular client files, so all front end angular files (including javascript, css, images etc) are only available to authenticated users.
It's also possible to include the login and registration pages within the angular application and allow public access to the angular client files, this is how I've built previous examples and is fine as long as you don't have any sensitive data stored in your client files. The main benefit of securing the client files is just a bit of extra piece of mind that you're not going to accidentally leak any secure information in your angular app files.
The express js application is structured using an MVC-ish pattern, there are controllers and views but rather than models I've gone with a services layer for data access & business logic. The services use mongoskin as the mongodb driver, it's a thin wrapper for the native mongodb driver that provides a simpler interface for performing CRUD operations.
UPDATE 18 Apr 2016 - Replaced monk with mongoskin in order to update to the latest mongodb driver, monk still depends on an old version of the mongodb driver which was causing issues for some people.
Here's what the project structure looks like, click on any of the files or folders for the description and code:
AngularJS App Folder
Path: /app
The app folder contains all of the Angular application files, the angular project and code structure is largely based on recommendations from John Papa's angular style guide, along with a few of my own tweaks.
Controllers and views are grouped by function/section (e.g. account, home), code that doesn't belong to a section is placed in a folder with an 'app-' prefix to prevent any name collisions and to make it easy to see what's what. For example css and images are located in the 'app-content' folder and angular services are located in the 'app-services' folder.
AngularJS Account Folder
Path: /app/account
The account folder contains all controllers and views related to the account section of the angular application
AngularJS Account Index Controller
Path: /app/account/index.controller.js
The account index controller is the default controller for the account section of the angular app, it makes the user object available to the view and exposes methods for updating or deleting the current user's account.
(function () {
'use strict';
angular
.module('app')
.controller('Account.IndexController', Controller);
function Controller($window, UserService, FlashService) {
var vm = this;
vm.user = null;
vm.saveUser = saveUser;
vm.deleteUser = deleteUser;
initController();
function initController() {
// get current user
UserService.GetCurrent().then(function (user) {
vm.user = user;
});
}
function saveUser() {
UserService.Update(vm.user)
.then(function () {
FlashService.Success('User updated');
})
.catch(function (error) {
FlashService.Error(error);
});
}
function deleteUser() {
UserService.Delete(vm.user._id)
.then(function () {
// log user out
$window.location = '/login';
})
.catch(function (error) {
FlashService.Error(error);
});
}
}
})();
AngularJS Account Index View
Path: /app/account/index.html
The account index view is the default view for the account section of the angular app, it contains a form for updating or deleting the current user's account.
<h1>My Account</h1>
<div class="form-container">
<form method="post">
<div class="form-group">
<label for="firstName">First name</label>
<input type="text" id="firstName" class="form-control" ng-model="vm.user.firstName" required />
</div>
<div class="form-group">
<label for="lastName">Last name</label>
<input type="text" id="lastName" class="form-control" ng-model="vm.user.lastName" required />
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" ng-model="vm.user.username" required />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" class="form-control" ng-model="vm.user.password" />
</div>
<div class="form-group">
<button class="btn btn-primary" ng-click="vm.saveUser()">Save</button>
<button class="btn btn-danger" ng-click="vm.deleteUser()">Delete</button>
</div>
</form>
</div>
AngularJS App Content Folder
Path: /app/app-content
The app content folder is used for static content such as css, images, fonts etc.
AngularJS App CSS
Path: /app/app-content/app.css
The app css file is a stylesheet containing any custom styles for the angular application.
body {
padding: 20px 0;
}
.flash-message {
margin-top: 20px;
}
AngularJS Services Folder
Path: /app/app-services
The services folder is used for custom AngularJS services / factories. All API access and business logic should be placed in services within this folder in order to keep angular controllers 'thin' and to maintain a clean separation of concerns.
AngularJS Flash Message Service
Path: /app/app-services/flash.service.js
The flash message service is used to display success and error messages in the angular application. It uses the $rootScope to expose the flash message to the main index.html file (/app/index.html) where the html is located for displaying the flash message.
The flash message is cleared on location change so it only displays once, optionally it can be kept after a single location change, this is if you want to display a flash message after redirecting the user.
(function () {
'use strict';
angular
.module('app')
.factory('FlashService', Service);
function Service($rootScope) {
var service = {};
service.Success = Success;
service.Error = Error;
initService();
return service;
function initService() {
$rootScope.$on('$locationChangeStart', function () {
clearFlashMessage();
});
function clearFlashMessage() {
var flash = $rootScope.flash;
if (flash) {
if (!flash.keepAfterLocationChange) {
delete $rootScope.flash;
} else {
// only keep for a single location change
flash.keepAfterLocationChange = false;
}
}
}
}
function Success(message, keepAfterLocationChange) {
$rootScope.flash = {
message: message,
type: 'success',
keepAfterLocationChange: keepAfterLocationChange
};
}
function Error(message, keepAfterLocationChange) {
$rootScope.flash = {
message: message,
type: 'danger',
keepAfterLocationChange: keepAfterLocationChange
};
}
}
})();
AngularJS User Service
Path: /app/app-services/user.service.js
The user service encapsulates interaction with the web api for all user related CRUD operations.
(function () {
'use strict';
angular
.module('app')
.factory('UserService', Service);
function Service($http, $q) {
var service = {};
service.GetCurrent = GetCurrent;
service.GetAll = GetAll;
service.GetById = GetById;
service.GetByUsername = GetByUsername;
service.Create = Create;
service.Update = Update;
service.Delete = Delete;
return service;
function GetCurrent() {
return $http.get('/api/users/current').then(handleSuccess, handleError);
}
function GetAll() {
return $http.get('/api/users').then(handleSuccess, handleError);
}
function GetById(_id) {
return $http.get('/api/users/' + _id).then(handleSuccess, handleError);
}
function GetByUsername(username) {
return $http.get('/api/users/' + username).then(handleSuccess, handleError);
}
function Create(user) {
return $http.post('/api/users', user).then(handleSuccess, handleError);
}
function Update(user) {
return $http.put('/api/users/' + user._id, user).then(handleSuccess, handleError);
}
function Delete(_id) {
return $http.delete('/api/users/' + _id).then(handleSuccess, handleError);
}
// private functions
function handleSuccess(res) {
return res.data;
}
function handleError(res) {
return $q.reject(res.data);
}
}
})();
AngularJS Home Folder
Path: /app/home
The home folder contains all controllers and views for the home section of the angular app.
AngularJS Home Index Controller
Path: /app/home/index.controller.js
The home index controller is the default controller for the home section, it gets the current user and makes it available to the view.
(function () {
'use strict';
angular
.module('app')
.controller('Home.IndexController', Controller);
function Controller(UserService) {
var vm = this;
vm.user = null;
initController();
function initController() {
// get current user
UserService.GetCurrent().then(function (user) {
vm.user = user;
});
}
}
})();
AngularJS Home Index View
Path: /app/home/index.html
The home index view is the default view for the home section, it just displays a welcome message to the user.
<h1>Hi {{vm.user.firstName}}!!</h1>
AngularJS App.js File
Path: /app/app.js
The app.js file is the entry point for the angular application where the app module is declared along with dependencies, and contains configuration and startup logic for when the app is first loaded.
The config function is used to define the routes of the application using the Angular UI Router.
The run function adds the JWT token as the default authorization header for all http requests made by the application, this is required to authenticate to the web api.
The last function in the file is used for manually bootstrapping the angular application after the JWT token is retrieved from the server, this is done to prevent the angular app from starting before the token is available.
(function () {
'use strict';
angular
.module('app', ['ui.router'])
.config(config)
.run(run);
function config($stateProvider, $urlRouterProvider) {
// default route
$urlRouterProvider.otherwise("/");
$stateProvider
.state('home', {
url: '/',
templateUrl: 'home/index.html',
controller: 'Home.IndexController',
controllerAs: 'vm',
data: { activeTab: 'home' }
})
.state('account', {
url: '/account',
templateUrl: 'account/index.html',
controller: 'Account.IndexController',
controllerAs: 'vm',
data: { activeTab: 'account' }
});
}
function run($http, $rootScope, $window) {
// add JWT token as default auth header
$http.defaults.headers.common['Authorization'] = 'Bearer ' + $window.jwtToken;
// update active tab on state change
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
$rootScope.activeTab = toState.data.activeTab;
});
}
// manually bootstrap angular after the JWT token is retrieved from the server
$(function () {
// get JWT token from server
$.get('/app/token', function (token) {
window.jwtToken = token;
angular.bootstrap(document, ['app']);
});
});
})();
AngularJS Index.html File
Path: /app/index.html
The index.html file is the main html file for the angular application, it contains the overall template for the application.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MEAN Stack User Registration and Login Example Application</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
<link href="app-content/app.css" rel="stylesheet" />
</head>
<body class="container">
<!-- header -->
<header>
<ul class="nav nav-tabs">
<li ng-class="{active: activeTab === 'home'}"><a ui-sref="home">Home</a></li>
<li ng-class="{active: activeTab === 'account'}"><a ui-sref="account">Account</a></li>
<li><a href="/login" target="_self">Logout</a></li>
</ul>
<div class="flash-message" ng-if="flash">
<div class="{{'alert alert-' + flash.type}}" ng-bind="flash.message"></div>
</div>
</header>
<!-- main -->
<main ui-view></main>
<!-- footer -->
<footer></footer>
<!-- external scripts -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.min.js"></script>
<!-- application scripts -->
<script src="app.js"></script>
<script src="app-services/user.service.js"></script>
<script src="app-services/flash.service.js"></script>
<script src="home/index.controller.js"></script>
<script src="account/index.controller.js"></script>
</body>
</html>
Express/NodeJS Controllers Folder
Path: /controllers
The express controllers folder contains all server-side controllers for the nodejs application.
Express/NodeJS API Controllers Folder
Path: /controllers/api
The express api controllers folder contains all server-side controllers for the web api.
Express/NodeJS Users API Controller
Path: /controllers/api/users.controller.js
The express users api controller defines the routes responsible for user related operations such as authentication, registration, retrieving, updating and deleting user data.
var config = require('config.json');
var express = require('express');
var router = express.Router();
var userService = require('services/user.service');
// routes
router.post('/authenticate', authenticateUser);
router.post('/register', registerUser);
router.get('/current', getCurrentUser);
router.put('/:_id', updateUser);
router.delete('/:_id', deleteUser);
module.exports = router;
function authenticateUser(req, res) {
userService.authenticate(req.body.username, req.body.password)
.then(function (token) {
if (token) {
// authentication successful
res.send({ token: token });
} else {
// authentication failed
res.sendStatus(401);
}
})
.catch(function (err) {
res.status(400).send(err);
});
}
function registerUser(req, res) {
userService.create(req.body)
.then(function () {
res.sendStatus(200);
})
.catch(function (err) {
res.status(400).send(err);
});
}
function getCurrentUser(req, res) {
userService.getById(req.user.sub)
.then(function (user) {
if (user) {
res.send(user);
} else {
res.sendStatus(404);
}
})
.catch(function (err) {
res.status(400).send(err);
});
}
function updateUser(req, res) {
var userId = req.user.sub;
if (req.params._id !== userId) {
// can only update own account
return res.status(401).send('You can only update your own account');
}
userService.update(userId, req.body)
.then(function () {
res.sendStatus(200);
})
.catch(function (err) {
res.status(400).send(err);
});
}
function deleteUser(req, res) {
var userId = req.user.sub;
if (req.params._id !== userId) {
// can only delete own account
return res.status(401).send('You can only delete your own account');
}
userService.delete(userId)
.then(function () {
res.sendStatus(200);
})
.catch(function (err) {
res.status(400).send(err);
});
}
Express/NodeJS App Controller
Path: /controllers/app.controller.js
The express app controller controls access to the angular app client files. It uses session/cookie authentication to secure the angular files, and also exposes a JWT token to be used by the angular app to make authenticated api requests.
var express = require('express');
var router = express.Router();
// use session auth to secure the angular app files
router.use('/', function (req, res, next) {
if (req.path !== '/login' && !req.session.token) {
return res.redirect('/login?returnUrl=' + encodeURIComponent('/app' + req.path));
}
next();
});
// make JWT token available to angular app
router.get('/token', function (req, res) {
res.send(req.session.token);
});
// serve angular app files from the '/app' route
router.use('/', express.static('app'));
module.exports = router;
Express/NodeJS Login Controller
Path: /controllers/login.controller.js
The express login controller defines routes for displaying the login view and authenticating user credentials. It uses the api to authenticate rather than using the user service directly, this is to keep the api and database layers cleanly separated from the rest of the application so they could easily be split if necessary and run on separate servers.
On successful authentication the jwt token returned from the api is stored in the user session so it can be made available to the angular application when it loads (via the '/token' route defined in the express app controller above).
var express = require('express');
var router = express.Router();
var request = require('request');
var config = require('config.json');
router.get('/', function (req, res) {
// log user out
delete req.session.token;
// move success message into local variable so it only appears once (single read)
var viewData = { success: req.session.success };
delete req.session.success;
res.render('login', viewData);
});
router.post('/', function (req, res) {
// authenticate using api to maintain clean separation between layers
request.post({
url: config.apiUrl + '/users/authenticate',
form: req.body,
json: true
}, function (error, response, body) {
if (error) {
return res.render('login', { error: 'An error occurred' });
}
if (!body.token) {
return res.render('login', { error: 'Username or password is incorrect', username: req.body.username });
}
// save JWT token in the session to make it available to the angular app
req.session.token = body.token;
// redirect to returnUrl
var returnUrl = req.query.returnUrl && decodeURIComponent(req.query.returnUrl) || '/';
res.redirect(returnUrl);
});
});
module.exports = router;
Express/NodeJS Register Controller
Path: /controllers/register.controller.js
The express register controller defines routes for displaying the register view and registering a new user. As with the login controller (above) it uses the api to register new users in order to maintain clean separation from the api and database layers.
var express = require('express');
var router = express.Router();
var request = require('request');
var config = require('config.json');
router.get('/', function (req, res) {
res.render('register');
});
router.post('/', function (req, res) {
// register using api to maintain clean separation between layers
request.post({
url: config.apiUrl + '/users/register',
form: req.body,
json: true
}, function (error, response, body) {
if (error) {
return res.render('register', { error: 'An error occurred' });
}
if (response.statusCode !== 200) {
return res.render('register', {
error: response.body,
firstName: req.body.firstName,
lastName: req.body.lastName,
username: req.body.username
});
}
// return to login page with success message
req.session.success = 'Registration successful';
return res.redirect('/login');
});
});
module.exports = router;
Express/NodeJS Services Folder
Path: /services
The express services folder is for the services layer of the node application, where all business logic and data access is located.
Express/NodeJS User Service
Path: /services/user.service.js
The express user service encapsulates all data access and business logic for users behind a simple interface. It exposes methods for CRUD operations and user authentication.
I've implemented all of the service methods using promises in order to keep the users api controller simple and consistent, so all service methods can be called with the pattern [service method].then(...).catch(...);
Mongoskin is the MongoDB driver used to access to the database, it provides a thin layer over the native mongodb driver that makes it a bit simpler to perform CRUD operations.
var config = require('config.json');
var _ = require('lodash');
var jwt = require('jsonwebtoken');
var bcrypt = require('bcryptjs');
var Q = require('q');
var mongo = require('mongoskin');
var db = mongo.db(config.connectionString, { native_parser: true });
db.bind('users');
var service = {};
service.authenticate = authenticate;
service.getById = getById;
service.create = create;
service.update = update;
service.delete = _delete;
module.exports = service;
function authenticate(username, password) {
var deferred = Q.defer();
db.users.findOne({ username: username }, function (err, user) {
if (err) deferred.reject(err);
if (user && bcrypt.compareSync(password, user.hash)) {
// authentication successful
deferred.resolve(jwt.sign({ sub: user._id }, config.secret));
} else {
// authentication failed
deferred.resolve();
}
});
return deferred.promise;
}
function getById(_id) {
var deferred = Q.defer();
db.users.findById(_id, function (err, user) {
if (err) deferred.reject(err);
if (user) {
// return user (without hashed password)
deferred.resolve(_.omit(user, 'hash'));
} else {
// user not found
deferred.resolve();
}
});
return deferred.promise;
}
function create(userParam) {
var deferred = Q.defer();
// validation
db.users.findOne(
{ username: userParam.username },
function (err, user) {
if (err) deferred.reject(err);
if (user) {
// username already exists
deferred.reject('Username "' + userParam.username + '" is already taken');
} else {
createUser();
}
});
function createUser() {
// set user object to userParam without the cleartext password
var user = _.omit(userParam, 'password');
// add hashed password to user object
user.hash = bcrypt.hashSync(userParam.password, 10);
db.users.insert(
user,
function (err, doc) {
if (err) deferred.reject(err);
deferred.resolve();
});
}
return deferred.promise;
}
function update(_id, userParam) {
var deferred = Q.defer();
// validation
db.users.findById(_id, function (err, user) {
if (err) deferred.reject(err);
if (user.username !== userParam.username) {
// username has changed so check if the new username is already taken
db.users.findOne(
{ username: userParam.username },
function (err, user) {
if (err) deferred.reject(err);
if (user) {
// username already exists
deferred.reject('Username "' + req.body.username + '" is already taken')
} else {
updateUser();
}
});
} else {
updateUser();
}
});
function updateUser() {
// fields to update
var set = {
firstName: userParam.firstName,
lastName: userParam.lastName,
username: userParam.username,
};
// update password if it was entered
if (userParam.password) {
set.hash = bcrypt.hashSync(userParam.password, 10);
}
db.users.update(
{ _id: mongo.helper.toObjectID(_id) },
{ $set: set },
function (err, doc) {
if (err) deferred.reject(err);
deferred.resolve();
});
}
return deferred.promise;
}
// prefixed function name with underscore because 'delete' is a reserved word in javascript
function _delete(_id) {
var deferred = Q.defer();
db.users.remove(
{ _id: mongo.helper.toObjectID(_id) },
function (err) {
if (err) deferred.reject(err);
deferred.resolve();
});
return deferred.promise;
}
Express/NodeJS Views Folder
Path: /views
The express views folder contains all views for the nodejs application, the EJS (embedded javascript) view engine is being used.
Express/NodeJS Partial Views Folder
Path: /views/partials
The express partial views folder contains all partial views for the nodejs application such as the header and footer. The EJS view engine doesn't support layouts which is why I went with partials instead for the header and footer.
Express/NodeJS Footer Partial View
Path: /views/partials/footer.ejs
The footer partial contains the html for the bottom of the page layout, which in this case is simply the closing tags for body and html.
</body>
</html>
Express/NodeJS Header Partial View
Path: /views/partials/header.ejs
The header partial contains the html for the top of the page layout.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Login</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
<style type="text/css">
body {
padding-top: 100px;
}
.form-container {
width: 400px;
margin: auto;
}
</style>
</head>
<body>
Express/NodeJS Login View
Path: /views/login.ejs
The login view contains a simple form for signing into the application.
<%- include('partials/header') %>
<div class="form-container">
<h2>Login</h2>
<% if(locals.error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<% if(locals.success) { %>
<div class="alert alert-success"><%= success %></div>
<% } %>
<form method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" value="<%= locals.username || '' %>" class="form-control" />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" />
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Login</button>
<a href="/register" class="btn btn-link">Register</a>
</div>
</form>
</div>
<%- include('partials/footer') %>
Express/NodeJS Register View
Path: /views/register.ejs
The register view contains a simple form for registering an account with the application.
<%- include('partials/header') %>
<div class="form-container">
<h2>Register</h2>
<% if(locals.error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form method="post">
<div class="form-group">
<label for="firstName">First name</label>
<input type="text" name="firstName" id="firstName" class="form-control" value="<%= locals.firstName || '' %>" required />
</div>
<div class="form-group">
<label for="lastName">Last name</label>
<input type="text" name="lastName" id="lastName" class="form-control" value="<%= locals.lastName || '' %>" required />
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" class="form-control" value="<%= locals.username || '' %>" required />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" required />
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Register</button>
<a href="/login" class="btn btn-link">Cancel</a>
</div>
</form>
</div>
<%- include('partials/footer') %>
Express/NodeJS App Config File
Path: /config.json
The express config file contains configuration data used by the nodejs application.
{
"connectionString": "mongodb://localhost:27017/mean-stack-registration-login-example",
"apiUrl": "http://localhost:3000/api",
"secret": "REPLACE THIS WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
}
Express/NodeJS Server File
Path: /server.js
The express server file is the entry point into the nodejs application, it defines app wide settings, binds controllers to routes and starts the http server.
require('rootpath')();
var express = require('express');
var app = express();
var session = require('express-session');
var bodyParser = require('body-parser');
var expressJwt = require('express-jwt');
var config = require('config.json');
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(session({ secret: config.secret, resave: false, saveUninitialized: true }));
// use JWT auth to secure the api
app.use('/api', expressJwt({ secret: config.secret }).unless({ path: ['/api/users/authenticate', '/api/users/register'] }));
// routes
app.use('/login', require('./controllers/login.controller'));
app.use('/register', require('./controllers/register.controller'));
app.use('/app', require('./controllers/app.controller'));
app.use('/api/users', require('./controllers/api/users.controller'));
// make '/app' default route
app.get('/', function (req, res) {
return res.redirect('/app');
});
// start server
var server = app.listen(3000, function () {
console.log('Server listening at http://' + server.address().address + ':' + server.address().port);
});
Need Some MEAN Stack Help?
Search fiverr for freelance MEAN Stack developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!