In this Tutorial, we will learn to build your own Content Management System (CMS). This Tutorial iS for Beginners as well as for Expert Developers

Until now, we have been heavily dependent on external web services to handle all our backend server-side work. Now, we will build our own backend using MongoDB, ExpressJS, angularjs, and Node.js; all these together are also popularly known as the MEAN stack.

This Tutorial will focus more on making angularjs work smoothly with a backend system.

As we get through this tutorial , some of the interesting things that we’ll learn are as follows:

  • Building RESTful web services using Node.js and ExpressJS
  • Saving and reading data from MongoDB
  • Working with ExpressJS and angularjs routes within the same application

 

Download full code

Why the MEAN stack?

An obvious question would be why the choice of MongoDB, Node.js, and Express, when we could use any other stack.

To be politically correct, we could use any other technology, such as Java, PHP, ASP.NET, or even Ruby on Rails, to build the backend part of this project, and angularjs would work just as fine.

The main reason to choose this stack is that all the tools within this stack use a single language, which is JavaScript. Other than this, each of the following tools offers certain unique benefits that make it equally suitable to build this application:

  • Node.js: This is the most important tool in this stack. It allows us to build event-driven, nonblocking I/O applications using JavaScript. Thanks to Node.js, we are now able to write server-side applications in JavaScript.
  • ExpressJS: This is a lightweight web application framework that allows us to build a server-side application on Node.js using the Model View Controller (MVC) design pattern.
  • MongoDB: This is a very popular NoSQL database. It uses JavaScript to read and modify data, and the data is stored in the Binary JSON (BSON) format.
  • MongooseJS: This is an object modeling tool for MongoDB. It provides a schema-based approach to model our data and also a much easier way to validate and query data in MongoDB.

Getting started with the MEAN stack

Let’s start by installing the various tools that we’ll need to build our application.

By this time, you probably already have Node.js installed and have become reasonably comfortable with starting and stopping the web servers.

As we proceed, take a moment to verify your current version of Node.js and upgrade it if necessary. The status of the latest version and how to upgrade it can be found on the Node.js site at http://nodejs.org/.

Setting up MongoDB

Depending on your operating system, MongoDB can be installed in multiple ways.

Perform the steps mentioned at the following links to install MongoDB on your operating system:

Once you have installed MongoDB, the next most important step is to create the folder to store your data.

Create an empty folder named data/db on the root using the following command line:

mkdir /data/db

You can also create the folder directly in the C: as follows:

c:md data
c:md datadb

Next, we’ll connect to the MongoDB database using the following command:

mongod

Tip

You will need to either give read or write permissions to the data/db folder or use the sudo or admin privilege to run the MongoDB command with root-level privileges.

In the following steps, we will start the mongo shell and create a new database named angcms.

With MongoDB running in a terminal window, we will open a new terminal window and fire the following commands.

mongo
use angcms

MongoDB comes with a default test database; one can also use this to test and play around with some MongoDB command

Setting up ExpressJS and MongooseJS

In case you don’t have ExpressJS yet, you can install it using the following command:

npm install -g express-generator

The next step is to create your ExpressJS project folder, which will be done using the following command:

express angcms

This will create a folder named angcms and put the boilerplate Express files into it. Note that we still don’t have ExpressJS installed; we will need to install it with the following command from the terminal:

npm install

We’ll now install MongooseJS as a devdependency along with ExpressJS.

Save the file, cd, into the angcms folder, and run the following command:

npm install –-save mongoose

Go to the angcms/node_modules folder, and verify that we have the express, jade, and mongoose folders within it.

Let’s also check whether our server is working by firing the following command in the terminal:

npm start

Open the browser and run http://localhost:3000; you should get the Welcome to Express message.

 

 

Building the server-side app

We’ll start by building the server-side section of the app. We’ll build a series of routes that will provide Create, Read, Update, Delete (CRUD) operations on our MongoDB database. We will expose these as REST APIs.

Let’s write our models and custom routes into a separate route file to keep things clean.

Creating the Mongoose schemas

We first start by loading the mongoose library and establishing a connection to the angcms database. We add the following highlighted code in the angcms/app.js file:

var app = express();    
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/angcms');
var db = mongoose.connecetion;

For this application, we are going to need two schemas: the Pages schema and the Admin Users schema. Let’s create these now.

We’ll create a new folder named models, and create our page.js file with the following code in it:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
    var Page = new Schema({
        title: String,
        url: {type:String, index:{unique:true}},
        content: String,
        menuIndex: Number,
        date: Date    });
    var Page = mongoose.model('Page', Page);
    module.exports=Page;

The following table gives a description for the fields in the schema:

Fields Description
title The title of the content page.
url The SEO-friendly alias that will be used to identify the page. Note that we are setting its index to unique as we don’t want duplicate URL aliases.
content The content of the page.
menuIndex An integer that defines the menu sequence of the pages in the navigation bar.
date The date when this document was last updated.

Next, we create the schema for our admin users in the models/admin-users.js file as follows:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var adminUser = new Schema({
        username: String,
        password: String
    });
    var adminUser = mongoose.model('adminUser', adminUser);
module.exports=adminUser;

As you can see, we are keeping things very simple, with our admin user’s schema only storing the username and password.

Creating CRUD routes

Now, we’ll start writing the routes for the CRUD operations; we’ll start by generating the listing page.

Create a new file, routes/api.js, in the routes folder, and add the following code:

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Page= require('../models/page.js');
var adminUser= require('../models/admin-users.js');

/* User Routes. */
router.get('/', function(req, res) {
  res.send('Welcome to the API zone');
});

router.get('/pages', function(request, response) {

        return Page.find(function(err, pages) {
            if (!err) {
                return response.send(pages);
            } else {
                return response.send(500, err);
            }
        });
    });

module.exports = router;

What the preceding code does is that it runs the find() method on the Page schema and returns the list of pages found. In case of an error, it would return a status code of 500 and display the error message. We need to get back to our app.js file and add the following lines to create these routes:

var api = require('./routes/api');

app.use('/api', api);

Add the preceding two lines within the respective sections of the app.js file.

Make sure that app.use('/api', api); is called before app.use('/', routes);. This will ensure that the /api routes get higher priority than the others.

On the terminal, stop and restart the npm using the npm start command. Note that you need to restart the web server every time you make a change to the server-side code.

On the browser, navigate to http://localhost:3000/api/pages.

You should see empty square brackets. This means that our current collection is empty.

Adding a new entry to the collection

Next, let’s write the route to add data to our collection. We will continue adding it to the routes/api.js file as follows:

router.post('/pages/add', function(request, response) {
    var page = new Page({
        title: request.body.title,
        url: request.body.url,
        content: request.body.content,
        menuIndex: request.body.menuIndex,
        date: new Date(Date.now())
    });

    page.save(function(err) {
        if (!err) {
            return response.send(200, page);

        } else {
            return response.send(500,err);
        }
    });
});

As we need to pass data to our server script, we will use the post method instead of get. Next, we create a new instance of our page object and pass the request parameters from our post data. We then call the save method, which does the actual task of saving this data into the collection.

We can test this route by simulating the post action using either the browser’s developer tools console or Firebug console. Alternatively, there are quite a few REST clients available as browser extensions and add-ons that can help you simulate the post action.

Try to create a couple of pages using this method, and run http://localhost:3000/api/pages to verify that this data is being saved and returned as a JSON response. You’ll also notice an additional key named _id being saved along with each of these nodes. We will be using the _id key for our delete and update operations

 

Updating a collection

Once we have the route to save a new entry, the next logical step is to create our route that will allow us to update an entry. We’ll continue to write the code to modify a collection item in our angcms/routes/api.js file as follows:

router.post('/pages/update', function(request, response) {
    var id = request.body._id;

    Page.update({
        _id: id
    }, {
        $set: {
            title: request.body.title,
            url: request.body.url,
            content: request.body.content,
            menuIndex: request.body.menuIndex,
            date: new Date(Date.now())
        }
    }).exec();
    response.send("Page updated");
});

Deleting a collection item

Next comes the route to delete an item; while continuing to work on the same file, we add the following code:

router.get('/pages/delete/:id', function(request, response) {
    var id = request.params.id;
    Page.remove({
        _id: id
    }, function(err) {
        return console.log(err);
    });
    return response.send('Page id- ' + id + ' has been deleted');
});

Ensure that the code is working by testing it with a REST client or typing in the route URL in a browser window along with a valid ID.

Displaying a single record

Next, we will write the route to fetch the data for an individual page on the admin side.

We’ll continue by adding the following code to our api.js file:

router.get('/pages/admin-details/:id', function(request, response) {
    var id = request.params.id;
    Page.findOne({
        _id: id
    }, function(err, page) {
        if (err)
            return console.log(err);
        return response.send(page);
    });
});

We use the get method here and pass the ID as a request parameter. We then run the findOne method to pull up a single record that matches the ID and return that as a response.

You can easily verify this route by simply appending the ID to the URL endpoint as follows:

http://localhst:3000/api/pages/view/<_id>.

On similar lines, we will also create another route to fetch the page contents for the frontend. Here, in the following code, we will use the URL as a parameter to fetch the data because we would like our frontend to show SEO-friendly URLs:

router.get('/pages/details/:url', function(request, response) {
    var url = request.params.url;
    Page.findOne({
        url: url
    }, function(err, page) {
        if (err)
            return console.log(err);
        return response.send(page);
    });
});

Securing your admin section

Now, it’s time to secure the admin section so that only authorized users can log in.

An important thing to note here is that we will need to secure both the client-side admin section and also our server-side APIs, because it is relatively easy to bypass client-side validations.

We will start with securing our server-side code. ExpressJS comes with its own session management and encryption modules.

We will enable cookieParser in our app by adding the following line to our angcms/app.js file:

app.use(express.cookieParser('secret'));

Using bcrypt to encrypt passwords

To encrypt confidential data such as passwords, we will use a popular utility called bcrypt to hash the password before it is stored in the database.

Let’s download and install the bcrypt-nodejs package using the following terminal command from the root of the project folder:

npm install bcrypt-nodejs

Next, we will include this in our ExpressJS app. As we will be securing our routes, we’ll include the bcrypt module in our angcms/routes/api.js file as follows:

var bcrypt = require('bcrypt-nodejs');

Adding a new admin user

Along with this, we will create our route to add in a new admin user as follows:

router.post('/add-user', function(request, response) {
    var salt, hash, password;
    password = request.body.password;
    salt = bcrypt.genSaltSync(10);
    hash = bcrypt.hashSync(password, salt);

    var AdminUser = new adminUser({
        username: request.body.username,
        password: hash
    });
    AdminUser.save(function(err) {
        if (!err) {
            return response.send('Admin User successfully created');

        } else {
            return response.send(err);
        }
    });
});

Here, we first start by defining our password, salt, and hash variables.

Then, using bcrypt and salt, we generate the hash string of the password.

Note

Using the salt variable is optional with bcrypt, but it is recommended, as it makes it difficult for potential hackers to decrypt the hashed password.

We then create a new instance of the AdminUser object, store the username and hashed password, and run the save method to save this information in the AdminUser document in MongoDB.

Creating the route for authenticating login

Next, we create the route for login. Add the following code to the api.js file:

router.post('/login', function(request, response) {
  var username = request.body.username;
  var password = request.body.password;

  adminUser.findOne({
    username: username
  }, function(err, data) {
    if (err | data === null) {
      return response.send(401, "User Doesn't exist");
    } else {
      var usr = data;

      if (username == usr.username && bcrypt.compareSync(password, usr.password)) {

        request.session.regenerate(function() {
          request.session.user = username;
          return response.send(username);

        });
      } else {
        return response.send(401, "Bad Username or Password");
      }
    }
  });
});

The code piece, although long, is fairly straightforward.

We capture the username and password as variables from the post data. We then check to see if the username is present, and if it is, then using the compare method of bcrypt, we check to see if the password entered matches that stored in the database.

Once the username and password match, we create the user session and redirect the user to the page’s listing page.

In case the username or password doesn’t exist, we return back with a status code 401 and a relevant error message.

We will be using this status code in our angularjs side to redirect the users in case of session time outs and so on.

Creating the logout route

After the login function, we create the logout function as follows:

router.get('/logout', function(request, response) {
    request.session.destroy(function() {
        return response.send(401, 'User logged out');

    });
});

The function will simply destroy the session.

Writing the sessionCheck middleware

The next step is to create our middleware function that does a session check.

As of ExpressJS Version 4.x, all the middleware, except static, have been removed and need to be installed and included as needed. Thus, we download our session module with the following terminal command:

npm install express-session --save

We then include the following lines in the respective sections of our app.js file:

var session = require('express-session');
app.use (session());

Next, we write our function that will check the user sessions. We add this to the api.js file:

function sessionCheck(request,response,next){

    if(request.session.user) next();
        else response.send(401,'authorization failed');
}

Now, to secure the API routes, we simply need to call the sessionCheck function after the route name, as highlighted in the following code:

router.post('/pages/add', sessionCheck, function(request, response) {

Usually, we’d want to secure the APIs that modify the data, and hence, we will add the sessionCheck function to the add, update, and delete APIs as follows:

  • For the update API, it should be as follows:
    router.post('/pages/update', sessionCheck, function(request, response) {
  • For the delete API, it should be as follows::
    router.get ('/pages/delete/:id', sessionCheck, function(request,response){
  • For the details API, it should be as follows::
    router.get('/pages/admin-details/:id', sessionCheck, function(request, response) {
    
    

    Integrating angularjs with an ExpressJS project

    Now that we have most of our server-side code working, we’ll start working on our angularjs code.

    Let’s download the angular-seed project as a ZIP download from https://github.com/areai51/angular-seed and extract the contents of the ZIP file.

    Now, we will only take the content of the app folder along with the package.json and bower.json files and place it within the public folder of angcms.

    In the terminal, navigate to the angcms/public folder and run the following two commands:

    npm install
    bower install

    Note that we do not run npm start from within the public folder, as we will be using the Express server that runs at port 3000.

    Your folder structure should look something like the following:


    The next step is to define the routes in our ExpressJS app such that all routes are managed by angularjs, except for those that start with a/api/.For this, we will add the following catch-all route at the end of the angcms/routes/index.js file as follows:
    router.get('*', function(request, response) {
        response.sendfile('./public/index.html');
    });

    The routes in ExpressJS are executed sequentially, and hence, the catch-all route needs to be at the end.

    Restart your app.js node application and point the browser URL to http://localhost:3000/index.html. Verify that the page displayed is the default index.html file of angular-seed.

     

     

    Generating SEO-friendly URLs using HTML5 mode

    All this while, all the URLs in our angularjs app have had # in the URLs. When building a CMS, ensuring that the URLs are meaningful and SEO-friendly is quite important.

    To make our site URLs are SEO friendly, we need to turn on the HTML5 mode in $locationProvider by making the following highlighted changes in the angcms/public/js/app.js file:

    .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
      $routeProvider.when('/view1', {templateUrl: 'partials/partial1.html', controller: 'MyCtrl1'});
      $routeProvider.when('/view2', {templateUrl: 'partials/partial2.html', controller: 'MyCtrl2'});
      $routeProvider.otherwise({redirectTo: '/view1'});
      $locationProvider.html5Mode(true);
    }]);

    The next thing to do is set the base URL in our angcms/public/index.html file, as highlighted in the following code:

    <title>AngCMS</title>
          <base href="/">
      <link rel="stylesheet" href="css/bootstrap.min.css"/>

    Refresh the Index page, and you will notice that your URLs are now clean without the # symbol in them.

     

    Building the admin section for CRUD operations

    We will now look to build the admin section of our CMS using Angular JS. The angularjs app will talk to the backend ExpressJS scripts that we just wrote in the preceding section.

    Creating the routes for the admin section

    Ideally, we would like our admin section to be called from within the admin URL, so let’s go ahead and add the routes for the admin section of the angularjs app.

    Add the following routes to the angcms/public/js/app.js file:

    config(['$routeProvider', '$locationProvider',
        function($routeProvider, $locationProvider) {
    
            $routeProvider.when('/admin/login', {
                templateUrl: 'partials/admin/login.html',
                controller: 'AdminLoginCtrl'
            });
            $routeProvider.when('/admin/pages', {
                templateUrl: 'partials/admin/pages.html',
                controller: 'AdminPagesCtrl'
            });
            $routeProvider.when('/admin/add-edit-page/:id', {
                templateUrl: 'partials/admin/add-edit-page.html',
                controller: 'AddEditPageCtrl'
            });
            $routeProvider.otherwise({
                redirectTo: '/'
            });
            $locationProvider.html5Mode(true);
        }
    ]);

    For the admin side, we have three routes: /admin/login is to authenticate the user, /admin/pages will show the list of pages available, and /admin/add-edit-page/:id will be used to add or edit the contents of the page. Note that we will make use of a single route to both add and edit a page.

    Building the factory services

    As we are going to be reading the dynamic data from web services, we will create a factory service that will be used to communicate with the backend web service.

    Let’s create our factory web services that will do the CRUD operations.

    We will add the following methods to our angcms/public/js/services.js file:

    'use strict';
    angular.module('myApp.services', [])
    
    .factory('pagesFactory', ['$http', 
      function($http) {
    
        return {
          getPages: function() {
            return $http.get('/api/pages');
          },
    
          savePage: function(pageData) {
            var id = pageData._id;
    
            if (id === 0) {
              return $http.post('/api/pages/add', pageData);
            } else {
              return $http.post('/api/pages/update', pageData);
            }
          },
          deletePage: function(id) {
            return $http.get('/api/pages/delete/' + id);
          },
          getAdminPageContent: function(id) {
            return $http.get('/api/pages/admin-details/' + id);
          },
          getPageContent: function(url) {
            return $http.get('/api/pages/details/' + url);
          },
        };
      }
    ]);

    The methods to list, delete, and view the details of a page are quite straightforward; we simply make a request to the appropriate ExpressJS route that passes the id parameter where necessary.

    Focusing on the savePage method, you’ll notice that we are using the same method to add a new page or edit the contents of an existing page. What we do here is we check for the id value in our post data. If the id value is set to 0, then it is treated as adding a new record; otherwise, it will try to update the record whose id value is being passed.

    Building the controllers for the admin section

    Now that we have our factory services ready, we’ll get started with writing our controllers.

    We’ll add the following code to the angcms/public/js/controllers.js file:

    'use strict';
    angular.module('myApp.controllers', []).
    controller('AdminPagesCtrl', ['$scope', '$log', 'pagesFactory',
      function($scope, $log, pagesFactory) {
        pagesFactory.getPages().then(
          function(response) {
            $scope.allPages = response.data;
          },
          function(err) {
            $log.error(err);
          });
    
          $scope.deletePage = function(id) {
            pagesFactory.deletePage(id);
          };
    
        }
    ]);

    Tip

    Don’t forget to delete the default controllers that come as a part of the angular-seed package.

    The AdminPagesCtrl controller is primarily used to display the page’s listing.

    We make a request to the getPages method of pagesFactory and populate the allPages scope object using the promise.

    We also define our method to delete a page; the method accepts the id value as an input parameter.

    Setting up the admin page layout

    We’ll now work on building our listing view that will display a list of all the pages, along with the ability to add, edit, or delete a page.

    Before we get to our listing view, let’s first get the groundwork ready on our Index page located at angcms/public/index.html.

    Ensure that your index.html file contains the following code:

    <!doctype html>
    <html lang="en" ng-app="myApp">
    
    <head>
        <meta charset="utf-8">
        <title>Angular CMS</title>
         <base href="/">
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css" />
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" />
        <link rel="stylesheet" href="css/app.css" />
    </head>
    
    <body>
        <div class="container" ng-view></div>
        <script src="bower_components/angular/angular.js"></script>
        <script src="bower_components/angular-route/angular-route.js"></script>
       <script src="js/app.js"></script>
        <script src="js/services.js"></script>
        <script src="js/controllers.js"></script>
        <script src="js/filters.js"></script>
        <script src="js/directives.js"></script>
    </body>
    
    </html>

    We will leverage BootStrap3 to get our styling in place. You can choose to either download Bootstrap from www.getbootstrap.com, call it from any of the CDN, or run the following command in the terminal from within the angcms/public folder:

    bower install bootstrap

    As you can see from the code, we are loading bootstrap and Bootstrap-theme CSS files to take advantage of the default Bootstrap theme.

    The only other change to the index.html file at this stage is adding the container CSS class to our ng-view div. This will act as the container for all the pages that load within it.

    Building the listing view for the admin section

    Next, we’ll create the partial that will display our list of pages stored in the database.

    Create a folder named admin and a new file named pages.html at angcms/public/partials/admin/pages.html, and add the following code:

    <a href="#/admin/add-edit-page/0" class="btn btn-success pull-right"> Add New Page</a>
    <h1>Pages List</h1>
    <hr/>
    <table class="table">
      <thead>
        <tr>
          <th>Menu Index</th>
          <th>Title</th>
          <th>URL</th>
          <th>Edit</th>
          <th>Delete</th>
        </tr>
      </thead>
    
      <tr ng-repeat="page in allPages">
        <td>{{page.menuIndex}}</td>
        <td>{{page.title}}</td>
        <td>{{page.url}}</td>
        <td> <a ng-href="#/admin/add-edit-page/{{page._id}}">Edit</a>
        </td>
        <td> <a ng-href="#" ng-click="deletePage(page._id)">Delete</a>
        </td>
      </tr>
    </table>

    At the top, we have a button to add new pages. It will link to the add-edit-page route and pass a fixed ID of 0. As you might have realized, we are reusing our partial to add and edit the page. We will need to let angularjs know when to call the add endpoint and when to call the edit endpoint. For this reason, we pass 0 as a parameter while adding a new page and the MongoDB-assigned ID while editing a page.

    The next piece of code is the table to display our list of pages with the title and URL fields. Along with it, we also have links to edit or delete the respective page. Both these hyperlinks link to the respective routes that pass the page ID.

    Save the file and point the browser URL to http://localhost:3000/admin/pages . This should show you a list of pages. In case you don’t see any pages, check for any console errors or add some content using a REST Client for the time being, until our add-edit-page route is ready.

    The delete link will not work for now as its API is authenticated.

     

    Setting up authentication in angularjs

    Before we can proceed to build the client-side sections, we’ll need to build the login and session management modules in angularjs. We’ll need to do this now, because the rest of the services for the CRUD operation are secured on the server side.

    Creating our login page

    We will start with the creation of our partial by creating a new file in angcms/public/partials/admin/login.html, and we will put in the following code:

    <h1>Login</h1>
    <hr/>
    
    <form role="form" id="login" ng-submit="login(credentials)">
    
    <div class="form-group">
    <label>Login</label>
    
    <input class="form-control" type="text" ng-model="credentials.username"/>
    </div>
    <div class="form-group">
    <label>Password</label>
    <input class="form-control" type="password"  ng-model=" credentials.password"/>
    </div>
    
    <input type="submit" class="btn btn-success" value="Login">
    </div>
    </form>

    Next, we will create our controller in the angcms/public/js/controllers.js file with the following code.

      .controller('AdminLoginCtrl', ['$scope', '$location', '$cookies', 'AuthService','$log',
          function($scope, $location, $cookies, AuthService, $log) {
            $scope.credentials = {
              username: '',
              password: ''
            };
            $scope.login = function(credentials) {
              AuthService.login(credentials).then(
                function(res, err) {
                  $cookies.loggedInUser = res.data;
                  $location.path('/admin/pages');
                },
                function(err) {
                  $log.log(err);
                });
              };
          }
      ])

    You’ll notice that we have injected $location, AuthService, $scope, $log, and $cookies into our controller function.

    angularjs has a module called ngCookies that allows to read and write to the browser cookie. However, this doesn’t come as a part of the angularjs library and needs to be included separately.

    Run the following command in the terminal to download angular-cookies:

    bower install angular-cookies

    We’ll first need to load the angular-cookies.js file in our angcms/public/index.html file as follows:

    <script type="text/javascript" src="bower_components/angular-cookies/angular-cookies.js"></script>

    Next, we need to include the ngCookies module as a part of our main application. We do this in our angcms/public/js/app.js file, as highlighted in the following code:

    angular.module('myApp', [
        'ngRoute',
        'myApp.filters',
        'myApp.services',
        'myApp.directives',
        'myApp.controllers',
        'ngCookies'
    ])

    Next, we will create the AuthService factory that will contain the login and logout methods. Add the following code in the angcms/public/js/services.js file:

    .factory('AuthService', ['$http', function($http) {
      return {
        login: function(credentials) {
          return $http.post('/api/login', credentials);
        },
        logout: function() {
          return $http.get('/api/logout');
        }
      };
    }])

    Let’s test our login functionality. Open the following URL in the browser, and log in with the correct username and password:

    http://localhost:3000/admin/login

    Using the correct username and password, you should get redirected to the pages listing.

    Note

    Make sure you have a couple of admin users saved; if not, use a REST API Client and create a couple of admin users using the following API URL:

    http://localhost:3000/api/add-user

     

    Setting up authentication in angularjs

    Before we can proceed to build the client-side sections, we’ll need to build the login and session management modules in angularjs. We’ll need to do this now, because the rest of the services for the CRUD operation are secured on the server side.

    Creating our login page

    We will start with the creation of our partial by creating a new file in angcms/public/partials/admin/login.html, and we will put in the following code:

    <h1>Login</h1>
    <hr/>
    
    <form role="form" id="login" ng-submit="login(credentials)">
    
    <div class="form-group">
    <label>Login</label>
    
    <input class="form-control" type="text" ng-model="credentials.username"/>
    </div>
    <div class="form-group">
    <label>Password</label>
    <input class="form-control" type="password"  ng-model=" credentials.password"/>
    </div>
    
    <input type="submit" class="btn btn-success" value="Login">
    </div>
    </form>

    Next, we will create our controller in the angcms/public/js/controllers.js file with the following code.

      .controller('AdminLoginCtrl', ['$scope', '$location', '$cookies', 'AuthService','$log',
          function($scope, $location, $cookies, AuthService, $log) {
            $scope.credentials = {
              username: '',
              password: ''
            };
            $scope.login = function(credentials) {
              AuthService.login(credentials).then(
                function(res, err) {
                  $cookies.loggedInUser = res.data;
                  $location.path('/admin/pages');
                },
                function(err) {
                  $log.log(err);
                });
              };
          }
      ])

    You’ll notice that we have injected $location, AuthService, $scope, $log, and $cookies into our controller function.

    angularjs has a module called ngCookies that allows to read and write to the browser cookie. However, this doesn’t come as a part of the angularjs library and needs to be included separately.

    Run the following command in the terminal to download angular-cookies:

    bower install angular-cookies

    We’ll first need to load the angular-cookies.js file in our angcms/public/index.html file as follows:

    <script type="text/javascript" src="bower_components/angular-cookies/angular-cookies.js"></script>

    Next, we need to include the ngCookies module as a part of our main application. We do this in our angcms/public/js/app.js file, as highlighted in the following code:

    angular.module('myApp', [
        'ngRoute',
        'myApp.filters',
        'myApp.services',
        'myApp.directives',
        'myApp.controllers',
        'ngCookies'
    ])

    Next, we will create the AuthService factory that will contain the login and logout methods. Add the following code in the angcms/public/js/services.js file:

    .factory('AuthService', ['$http', function($http) {
      return {
        login: function(credentials) {
          return $http.post('/api/login', credentials);
        },
        logout: function() {
          return $http.get('/api/logout');
        }
      };
    }])

    Let’s test our login functionality. Open the following URL in the browser, and log in with the correct username and password:

    http://localhost:3000/admin/login

    Using the correct username and password, you should get redirected to the pages listing.

    Note

    Make sure you have a couple of admin users saved; if not, use a REST API Client and create a couple of admin users using the following API URL:

    http://localhost:3000/api/add-user

    Building a custom module for global notification

    As you might have realized by now, our login page works fine as long as we put the correct credentials; however, when you try with an invalid username or password, the page doesn’t do anything.

    Tip

    The developer console should, however, show a 401 Unauthorized failed message.

    We will need to build a notification system that displays a message when invalid credentials are passed. Thinking a few steps ahead, you’ll realize that we are going to need such messages displayed on many occasions, for example, when a new page has been created or updated, or when a page has been deleted.

    In view of this, it is most ideal to build a global notification system that can be used all throughout our application.

    angularjs allows us to create custom modules. These are self-contained modules that can be easily reused across multiple applications. A custom module is simply a wrapper that holds different parts of an angularjs app; these parts can be directives, services, filters, controllers, and so on.

    As you would recall, ngCookies is a similar custom module we just made use of earlier.

    Building and initializing the message.flash module

    We will create a new file named message-flash.js at angcms/public/js/message-flash.js.

    We will initialize it with the following code:

    angular.module('message.flash', [])

    We also need to include this in our app, so let’s include the message-flash.js file in our angcms/public/index.html file, as follows:

    <script src="js/message-flash.js"></script>

    Next, we add the message-flash.js file as a dependency in our main module in the angcms/public/js/app.js file, as highlighted in the following code:

    angular.module('myApp', [
        'ngRoute',
        'myApp.filters',
        'myApp.services',
        'myApp.directives',
        'myApp.controllers',
        'ui.tinymce',
        'ngCookies',
        'message.flash'
    ])

    Building the message.flash factory service

    We will chain our factory to the message.flash module in our angcms/public/js/message-flash.js file, as highlighted in the following code:

    angular.module('message.flash', [])
    .factory('flashMessageService', ['$rootScope',function($rootScope) {
      var message = '';
      return {
        getMessage: function() {
          return message;
        },
        setMessage: function(newMessage) {
          message = newMessage;
        
    }
      };
    }])

    The factory service is quite straightforward. We initialize a variable called message and have two methods, namely, setMessage and getMessage, which assign and read values to the message variable.

    Setting up $broadcasts

    Anybody who has tried to pass variables from one controller to another or to a directive would have realized that it isn’t quite straightforward, and one needs to use either rootScope or set up $watch or $digest to ensure that the scope objects update when the source has changed.

    We will face a similar problem here where the message in our directive wouldn’t update when we pass the message from a controller.

    To overcome this, we will set up $broadcast.

    The broadcast, $broadcast, dispatches an event name to all child scopes. Child scopes use this as a trigger to execute different functions.

    In our case, as we don’t really have a parent-child relation between the directive and our controllers, we will set up a broadcast on rootScope itself

    We add the broadcast event to the setMessage method in the message-flash.js file as highlighted:

    setMessage: function(newMessage) {
      message=newMessage;
      $rootScope.$broadcast('NEW_MESSAGE')
    }

    Now, every time the setMessage function is called, we will broadcast the event called ‘NEW_MESSAGE'.

     

    We will continue to chain our directive to the same module in the message-flash.js file as follows:

    .directive('messageFlash', [function() {
      return {
        controller: function($scope, flashMessageService, $timeout) {
          $scope.$on('NEW_MESSAGE', function() {
            $scope.message = flashMessageService.getMessage();
            $scope.isVisible = true;
            return $timeout(function() {
              $scope.isVisible = false;
              return $scope.message = '';
            }, 2500);
          })
        },
        template: '<p ng-if="isVisible" class="alert alert-info">{{message}}</p>'
        }
      }
    ]);

    The directive code is quite interesting. We first listen for the broadcast event, and on its trigger, we populate $scope.message by calling the getMessage function of flashMessageService.

    It is usually a good usability practice to hide the flash message after a few seconds of being visible; hence, we will add a timeout function that will automatically hide the message in 2500 milliseconds.

    The last piece of code of the directive is the template code that uses the ng-if directive to toggle the display. We also use Bootstrap’s alert CSS classes for some visual elegance.

    Now, let’s add this directive to our main index.html file, as highlighted in the following code:

    <div message-flash> </div>
      <div  class="container" ng-view></div>

    Setting a flash message

    Let’s revisit our AdminLoginCtrl function and set a flash message in case the login fails.

    We add it to our controller.js file, as highlighted.

      .controller('AdminLoginCtrl', ['$scope', '$location', '$cookies', 'AuthService', 'flashMessageService',function($scope, $location, $cookies, AuthService, flashMessageService) {
              $scope.credentials = {
                username: '',
                password: ''
              };
              $scope.login = function(credentials) {
    
                AuthService.login(credentials).then(
                  function(res, err) {
                    $cookies.loggedInUser = res.data;
                    $location.path('/admin/pages');
    
                  },
                  function(err) {
                    flashMessageService.setMessage(err.data);
    
                    console.log(err);
    
                });
              };
        }
      ])

    Let’s test our login page with an invalid username and password, and we should be able to see our flash message.

    Creating our Add-Edit page controller

    Now that we have our global messaging system in place, let’s continue with building the rest of the admin sections

    We’ll start to create our controller for adding and editing pages.

    Create a new controller function in the angcms/public/controllers.js file as follows:

    .controller('AddEditPageCtrl', ['$scope', '$log', 'pagesFactory', '$routeParams', '$location', 'flashMessageService', function($scope, $log, pagesFactory, $routeParams, $location, flashMessageService) {
            $scope.pageContent = {};
            $scope.pageContent._id = $routeParams.id;
            $scope.heading = "Add a New Page";
    
            if ($scope.pageContent._id !== 0) {
              $scope.heading = "Update Page";
              pagesFactory.getAdminPageContent($scope.pageContent._id).then(
                  function(response) {
                    $scope.pageContent = response.data;
                    $log.info($scope.pageContent);
                  },
                  function(err) {
                    $log.error(err);
                  });
            }
    
            $scope.savePage = function() {
              pagesFactory.savePage($scope.pageContent).then(
                function() {
                  flashMessageService.setMessage("Page Saved Successfully");
                  $location.path('/admin/pages');
                },
                function() {
                  $log.error('error saving data');
                }
              );
            };
        }
    ])

    We start by defining our AddEditPageCtrl controller and injecting the necessary dependencies. Besides $scope and $log, we need to inject $routeparams to get the route parameters, the $location module to redirect, flashMessageService to set notifications, and pagesFactory service.

    Next, we check to see if the page ID being passed is 0; this corresponds to an insert or the long MongoDB-generated ID, which means we’ll be doing an update.

    In case if it’s the MongoDB-generated ID, we then need to fetch the data of the page and populate the edit template. For this, we make a call to the getPageContent factory function, and using promises, we populate our pageContent scope with the returned data.

    The next part is writing the savePage function, which will save the contents of the form by posting it to the savePage factory function. When the promise returns with a success, we redirect the user back to the listing page.

     

    Creating our Add-Edit view

    Now that we have the controller in place, let’s work on the form to add and edit the page content.

    Create a new file at angcms/public/partials/add-edit-page.html, and add the following content:

    <h1>{{heading}}</h1>
    <hr/>
    
    <form role="form" id="add-page" ng-submit="savePage()">
    <div class="form-group">
    <label>Page ID</label>
    <input class="form-control" type="text" readonly ng-model="pageContent._id"/>
    </div>
    <div class="form-group">
    <label>Page Title</label>
    
    <input class="form-control" type="text" ng-model="pageContent.title"/>
    </div>
    <div class="form-group">
    <label>Page URL Alias</label>
    <input class="form-control"type="text" ng-model="pageContent.url"/>
    </div>
    
    <div class="form-group">
    <label>Menu Index</label>
    <input class="form-control"type="number" ng-model="pageContent.menuIndex"/>
    </div>
    
    <div class="form-group">
    <label>Page Content</label>
    <textarea rows="15" class="form-control" type="text" ng-model="pageContent.content"></textarea>
    </div>
    <input type="submit" class="btn btn-success" value="Save">
    </div>
    </form>

    Test the add page to ensure that it’s working.

    Writing a custom filter to autogenerate the URL field

    Most CMS tools would autogenerate the URL alias based on the title of the page. While doing this, we will need to ensure that the alias being generated is stripped out of any special characters and all spaces are ideally replaced by a dash.

    We will do this by creating our own custom filter.

    Open up the angcms/public/js/filters.js file, and add the following code.

    'use strict';
    
    /* Filters */
    angular.module('myApp.filters', [])
      .filter('formatURL', [
        function() {
          return function(input) {
            var url = input.replace(/[`~!@#$%^&*()_|+-=?;:'",.<>{}[]/]/gi, '');
            var url = url.replace(/[s+]/g, '-');
            return url.toLowerCase();
    
          };
        }
      ]);

    Here, we are basically creating a filter called formatURL and taking in the input parameters. We first remove any special characters that may be present using regex. We then replace all spaces with a hyphen and return the formatted string in lowercase.

    Now, let’s see how to use it in our code. We will use this filter in our controller, so let’s make the highlighted changes in our controller file located at angcms/public/js/controlllers.js:

    .controller('AddEditPageCtrl', ['$scope', '$log', 'pagesFactory', '$routeParams', '$location', 'flashMessageService','$filter',
        function($scope, $log, pagesFactory, $routeParams, $location, flashMessageService,$filter) {

    As you can see, we are injecting the $filter module into our controller.

    Next, we create a $scope function as follows:

    $scope.updateURL=function(){
      $scope.pageContent.url=$filter('formatURL')($scope.pageContent.title);
    }

    Within the update URL function, we store the value into the pageContent.url property by using the formatURL filter and passing $scope.pageContent.title as an argument to it.

    Next, we need to make the highlighted changes to our partial located at angcms/public/partials/admin/add-edit-page.html, as highlighted:

    <label>Page Title</label>
    <input class="form-control" type="text" ng-change="updateURL()" ng-model="pageContent.title"/>
    </div>
    <div class="form-group">
    <label>Page URL Alias</label>
    <input class="form-control"type="text" readonly ng-model="pageContent.url"/>
    </div>

    Save the files and test the add-edit page in the browser. Notice the URL field getting updated automatically as you enter the title field.

    Adding the WYSIWYG editor

    Most CMS tools would have a What You See Is What You Get (WYSIWYG) editor. This allows the content administrators to easily format the text on a page, for example, add headings, make the text bold or italics, add numbering bullets, and so on.

    We’ll see how to add TinyMCE, a very popular WYSIWYG editor, to our page content text area.

    Angular UI has a ready-to-use module, which makes it very easy to add TinyMCE to any form in an angularjs app.

    The Angular-UI TinyMCE wrapper can be downloaded from GitHub at https://github.com/angular-ui/ui-tinymce.

    Alternatively, we can also use bower to download the files.

    Assuming that you have already installed bower, run the following command in the terminal;

    bower install angular-ui-tinymce --save

    This will create a folder called bower_components and download the files within it.

    Next, let’s include these libraries in our index.html file, as highlighted in the following code:

    <script type="text/javascript" src="bower_components/tinymce/tinymce.min.js"></script>
    <script type="text/javascript" src="lib/angular/angular.js"></script>
    <script type="text/javascript" src="bower_components/angular-ui-tinymce/src/tinymce.js"></script>
    <script src="lib/angular/angular-route.js"></script>

    Next, we will add the TinyMCE module as a dependency to our app in the angcms/public/js/app.js file, as highlighted in the following code:

    angular.module('myApp', [
        'ngRoute',
        'myApp.filters',
        'myApp.services',
        'myApp.directives',
        'myApp.controllers',
        'ui.tinymce',
        'ngCookies',
        'message.flash'
    ]).

    This is all that is required to include TinyMCE in our angularjs app.

    Now, to add the editor to our angcms/public/partials/admin/add-edit-page.html file, we will simply call our directive, as highlighted in the following code:

    <textarea ui-tinymce rows="15" class="form-control" type="text" ng-model="pageContent.content"></textarea>

    Save the file, and now, try to add or edit a page to notice TinyMCE replace the text area.

    Setting up an Interceptor to detect responses

    A use case that we need to consider is what happens if the backend web service’s session timed out and somebody from the frontend is trying to add, edit, or delete a page.

    At the instance when the backend service times out, it would return a 401 status code; we would need to have every angularjs controller check for this status code and redirect the user to the login page in case it gets one.

    Instead of writing this check on each and every controller, we will make use of an Interceptor to check every incoming response, and act accordingly.

    Let’s chain our Interceptor service in our services.js file as follows:

    .factory('myHttpInterceptor', ['$q', '$location', function($q, $location) {
        return {
            response: function(response) {
                return response;
            },
            responseError: function(response) {
                if (response.status === 401) {
                    $location.path('/admin/login');
                    return $q.reject(response);
                }
                return $q.reject(response);
            }
        };
    }]);

    The next step is to push this into $httpProvider.

    We will add the following code to our angcms/public/js/app.js file:

    .config(function ($httpProvider) {
        $httpProvider.interceptors.push('myHttpInterceptor');
    });

    To test whether our Interceptors are working or not, open up a new tab in the browser in Incognito or private browsing mode and try to directly put in the URL to edit a page; it would be something like http://localhost:3000/admin/add-edit-page/<_id>.

    It should automatically redirect you to the login page.

     

    Building the frontend of our CMS

    All this while, we have been working on the backend and admin sections of the CMS.

    Now, we will work on the frontend, the public-facing side of the website.

    As the public-facing side of the website needs to have a neat layout with a logo, navigation bar, content area, footer, and so on, we are going to tweak the index page layout.

    Update the angcms/public/index.html file with the upcoming changes.

    As we would like to control some application-level settings such as the logo, footer, and so on, we first bind AppCtrl to the <body> tag, as shown in the following code:

    <body ng-controller="AppCtrl">

    Next, we add the following markup:

    <div admin-login class="col-md-3 pull-right"></div>
    <div class="container">
        <header>
            <img ng-src="{{site.logo}}">
        </header>
        <div message-flash></div>
        <div class="row">
            <div class="col-md-3" nav-bar></div>
            <div class="col-md-6" ng-view></div>
        </div>
        <footer>{{site.footer}}</footer>
    </div>

    As you can see from the markup, we are calling in two directives: admin-login, which will display a welcome message to the logged-in user, and nav-bar, which will show relevant navigation links on the left-hand side of the window.

    We also plan to have a scope object called site and are displaying the site logo and site footer on this template.

    The next step is to create our AppCtrl function in our controller, which is done as follows:

    .controller('AppCtrl', ['$scope','AuthService','flashMessageService','$location',function($scope,AuthService,flashMessageService,$location) {
            $scope.site = {
                logo: "img/angcms-logo.png",
                footer: "Copyright 2014 Angular CMS"
            };
        }
    ])

    Refresh the page and notice the logo and footer. Needless to say, ensure that you have a logo named angcms-logo.png present in the img folder.

    Building our navigation bar directive

    We would like our navigation bar to display the links for all the pages created via the admin. We would like these links to be displayed in a sequence based on their menuIndex values.

    We would also like this directive to display the admin menu links when the user is in the admin section.

    With these goals in mind, let’s create our directive in the directives.js file as follows:

    directive('navBar', [
      function() {
        return {
          controller: function($scope, pagesFactory, $location) {
            var path = $location.path().substr(0, 6);
            if (path == "/admin") {
              $scope.navLinks = [{
                title: 'Pages',
                url: 'admin'
              }, {
                title: 'Site Settings',
                url: 'admin/site-settings'
              }, ];
            } else {
              pagesFactory.getPages().then(
                function(response) {
                  $scope.navLinks = response.data;
                }, function() {
    
                });
              }
            },
            templateUrl: 'partials/directives/nav.html'
    
          };
      }
    ])

    What we are doing here is using $location.path, we are trying to see whether the user is in the admin section or on the frontend, and based on this, we are populating the navLinks scope object with the relevant menu links.

    Next, let’s create the template for this directive. Create a new file named nav.html in angcms/public/partials/directives/nav.html, and add the following code:

    <ul class="nav-links">
    <li ng-repeat="nav in navLinks | orderBy:'menuIndex'"> <a href="/{{nav.url}}">{{nav.title}}</a>
    </li>
    </ul>

    As you see, we are using ng-repeat to list out our entire page menu and ordering it with the help of menuIndex.

    Building the admin-login directive

    The next directive that we’ll build is the admin login, which will display the Welcome <username> message and have additional links to jump to the admin or log out.

    Let’s add the following directive to the directives.js file:

    .directive('adminLogin', [
      function() {
        return {
          controller: function($scope, $cookies) {
            $scope.loggedInUser = $cookies.loggedInUser;
          },
          templateUrl: 'partials/directives/admin-login.html'
        };
      }
    ]);

    The controller code is straightforward, and it simply assigns the loggedInUser value from the cookie to the scope object.

    We will create it’s template as a new file in partials/directives/admin-login.html as follows:

    <div ng-if=loggedInUser>
        Welcome {{loggedInUser}} |  <a href="admin/pages">My Admin</a> | <a href ng-click='logout()'>Logout</a>
    </div>

    Next, we will quickly write the code for the logout method. As this directive is within the scope of AppCtrl, we will write this method within the AppCtrl function as follows:

    $scope.logout = function() {
      AuthService.logout().then(
        function() {
    
          $location.path('/admin/login');
          flashMessageService.setMessage("Successfully logged out");
    
        }, function(err) {
            console.log('there was an error tying to logout');
        });
    };

    Displaying the content of a page

    The last and most crucial step of this entire project is to display the actual content of the selected page.

    This will require us to create a new route that will accept route params. Let’s get this done first in our public/js/app.js file as follows:

    $routeProvider.when('/:url', {
        templateUrl: 'partials/page.html',
        controller: 'PageCtrl'
    });

    Next, let’s create the partials view as a new file called partials/page.html with the following content:

    <h1>{{pageContent.title}}</h1>
    <div ng-bind-html="pageContent.content"></div>

    We are using the ng-bind-html directive here so that the HTML content is rendered correctly instead of it spitting out the raw HTML as it is.

    Next, let’s create our PageCtrl function in controllers.js as follows:

    .controller('PageCtrl',  [ '$scope','pagesFactory', '$routeParams ', function($scope, pagesFactory, $routeParams) {
        var url = $routeParams.url;
        pagesFactory.getPageContent(url).then(
          function(response) {
            $scope.pageContent = response.data; 
          }, function() {
            console.log('error fetching data');
          });
        }]);

    Save the file, refresh the site, and hit any of the frontend links. You’ll get an error in your console; you will see something like the following screenshot:


    So, what went wrong here? What is $sce?

    Note

    One of the coolest things about testing angularjs apps in Google Chrome is whenever there is an error message, angularjs has a hyperlink that will take you directly to the site that explains what the error is.

    By reading up on the link, you’ll get to know that the Strict Contextual Escaping (SCE) mode of angularjs is turned on by default, and angularjs feels that the HTML markup on the content of our CMS pages is unsafe. To overcome this, we will need to explicitly tell $sce to trust our content. We do this in our controller by adding the following highlighted lines to the PageCtrl function:

    .controller('PageCtrl', ['$scope','pagesFactory', '$routeParams', '$sce', function($scope, pagesFactory, $routeParams,$sce) {
          var url = $routeParams.url;
          pagesFactory.getPageContent(url).then(
            function(response) {
              $scope.pageContent = {};
              $scope.pageContent.title = response.data.title;
              $scope.pageContent.content = $sce.trustAsHtml(response.data.content);
    
            }, function() {
                console.log('error fetching data');
        });
    }])

    Save the file and refresh any of the page URLs. Now, you should be able to see the title and page contents with the HTML formatting.

    Setting the default home page

    Now, our public-facing frontend is working quite well with all the nav links, content, and so on. However, when you launch the site for the first time or hit http://localhost:3000/, we land up with a blank screen.

    To overcome this, we will make sure that our site always has a page titled Home.

    Then, in the page controller, we will simply add the following highlighted line, which will set the default value of the URL to home in case we don’t find a URL param in the current route; we will add this to the PageCtrl function:

            var url = $routeParams.url;
            if(!url) url="home";

    Now, the home page will load by default for the preceding URL link. Alternatively, you can also set the $routeProvider redirect in the public/js/app.js file to, say, the following:

    $routeProvider.otherwise({redirectTo: '/home'});

     

    wrapping up

    We went full stack, right from coding our backend by building REST APIs to saving and reading data from the database. We also built the angularjs frontend that interacts with these backend APIs.

    The key takeaways from this Tutorial are as follows:

    • Building backend web services using Node.js, MongoDB, and ExpressJS
    • Securing API using sessions
    • Making angularjs and ExpressJS work together and build routes that span across both the systems
    • Authenticating on the client side using Interceptors
    • Integrating third-party modules
    • Using custom filters to format and store data
    • Building a custom module for a global notification system
Deven Rathore

Deven is an Entrepreneur, and Full-stack developer, Constantly learning and experiencing new things. He currently runs CodeSource.io and Dunebook.com.

Published by
Deven Rathore

Recent Posts

How to Create the Perfect Branded Website for Your Business

Marketing has now fully gone online so that the first thing you think of as…

2 days ago

Top 20 Tailwind CSS Dashboard Templates and themes

Tailwind CSS framework allows high customization and provides you with construction blocks for you to…

3 days ago

Top 19 Opensource Form Builder projects

Form Builder projects enables you to develop job-specific and customizable forms. They collect responses from…

1 week ago

Top 20 OpenSource Neural networks projects

Are you looking for an open source neural network project? In this article you will…

1 week ago

5 Tools for Effective Academic Writing

Not every student is able to comprehend their assignment. Some struggle with the basic task…

2 weeks ago

PowerPoint Tips for Software Professional

Knowing how to give a successful presentation will assist you in landing a job, making…

2 weeks ago