Building A Content Management System Using The MEAN Stack - 2 (Create Controller Modules 1)
utopian-io·@gotgame·
0.000 HBDBuilding A Content Management System Using The MEAN Stack - 2 (Create Controller Modules 1)
### Repository [https://github.com/nodejs/node](https://github.com/nodejs/node ) #### What Will I Learn The codebase for this tutorial is based on the [MEANie](https://github.com/cornflourblue/meanie) an open source content management system by [Jason Watmore](http://jasonwatmore.com/). In the first tutorial for this series we covered the creation of the web application server and all helper modules required for this application plus the database config file. In this tutorial we are going to work on the controller modules for the application features including 1. Admin Controller 2. Blog Controller 3. Install Controller 4. Login Controller N.B;- LINK TO THE FIRST TUTORIAL IN THIS SERIES CAN BE FOUND AT THE END OF THIS POST ### Requirements - [NodeJS and NPM](https://nodejs.org/en/download/package-manager/), - [Angular](https://angular.io/) - [MongoDB](https://www.mongodb.com/) - Text Editor ### Difficulty - Intermediate ### Tutorial Contents We already made reference to all controller modules in the `server.js` file, the controllers used in this application include all in the following list. - Install Controller - Login Controller - Admin Controller - Blog Controller - Contact Form Controller - Pages Controller - Posts Controller - Redirect Controller - Users Controller In this tutorial we will cover the first four and conclude the remainder in later tutorials. To create the controller modules, in the server directory create a new directory with the name `controllers`. #### 1. Admin Controller The first controller we'll work on is the `admin` controller. In the `controllers` directory create a new file `admin.controller.js`. In our newly created file add the following code ``` var express = require('express'); var router = express.Router(); var path = require('path'); var multer = require('multer'); var slugify = require('helpers/slugify'); var fileExists = require('helpers/file-exists'); router.use('/', ensureAuthenticated); router.post('/upload', getUpload().single('upload'), upload); // handle file upload router.use('/', express.static('../client/admin')); // serve admin front end files from '/admin' module.exports = router; /* ROUTE FUNCTIONS ---------------------------------------*/ function upload(req, res, next) { // respond with ckeditor callback res.status(200).send( '<script>window.parent.CKEDITOR.tools.callFunction(' + req.query.CKEditorFuncNum + ', "/_content/uploads/' + req.file.filename + '");</script>' ); } /* MIDDLEWARE FUNCTIONS ---------------------------------------*/ function ensureAuthenticated(req, res, next) { // use session auth to secure the front end admin files if (!req.session.token) { return res.redirect('/login?returnUrl=' + encodeURIComponent('/admin' + req.path)); } next(); } /* HELPER FUNCTIONS ---------------------------------------*/ function getUpload() { // file upload config using multer var uploadDir = '../client/blog/_content/uploads'; var storage = multer.diskStorage({ destination: uploadDir, filename: function (req, file, cb) { var fileExtension = path.extname(file.originalname); var fileBase = path.basename(file.originalname, fileExtension); var fileSlug = slugify(fileBase) + fileExtension; // ensure file name is unique by adding a counter suffix if the file exists var fileCounter = 0; while (fileExists(path.join(uploadDir, fileSlug))) { fileCounter += 1; fileSlug = slugify(fileBase) + '-' + fileCounter + fileExtension; } cb(null, fileSlug); } }); var upload = multer({ storage: storage }); return upload; } ``` The admin controller handles all requests and returns the appropriate responses for each request made by the blog administrator. The first part of this file `imports` all required dependencies for this controller module. We import express using the `var express = require('express');`. To import the express router for app routing we add the following line of code `var router = express.Router();` We need to use the path module also so we import that using `var path = require('path');`. To handle file uploads we need to import the `multer` dependency and the line `var multer = require('multer');` will help us in that aspect. On the next line we also import one of the helper modules `slugify` and we do that using the following line `var slugify = require('helpers/slugify');`. Finally we have another helper module `fileExists` imported using the `var fileExists = require('helpers/file-exists');`. To implement the express router module we added the block ``` router.use('/', ensureAuthenticated); router.post('/upload', getUpload().single('upload'), upload); // handle file upload router.use('/', express.static('../client/admin')); // serve admin front end files from '/admin' ``` The first line `router.use('/', ensureAuthenticated);` ensures that the router is put to use in the admin area and the middleware function `ensureAuthenticated` which handles user authentication is implemented whenever a user tries to access the `admin` area of the application. The next line `router.post('/upload', getUpload().single('upload'), upload);` ensures that every file uploaded by the admin passes through the `/upload` router and is . For each file uploaded through the `/upload` router, the `getUpload()` method is implemented. We'll go into the details of the `getUpload()` method in a bit. The method `single(upload)` is a multer middleware that allows only one file to be uploaded per upload. The last parameter `upload` is defined at the end of the function. `router.use('/', express.static('../client/admin'));` will set the `client/admin` directory as the front end folder for the area. We also export the router module `module.exports = router;`. We need to add the route functions for the admin area, to do that we create a new function `upload`. The function has three parameters `req` which is the request object, `res` which is the response object and `next` which helps to execute the next middleware after the current one. Whenever the function accepts any request it returns a response. ``` res.status(200).send( '<script>window.parent.CKEDITOR.tools.callFunction(' + req.query.CKEditorFuncNum + ', "/_content/uploads/' + req.file.filename + '");</script>' ); ``` If the request is accepted and runs without any error it returns a status code of 200 and then proceed to send the response using the `send()` method. The method sends an `API` call enclosed in a `<script></script>` tag. The call is performed on the CKEDITOR tool which is a text editor that is used in the application for adding, posting and uploading pages, posts and media contents. This call is responsible for setting the upload route for uploaded images. Earlier we made reference to a function `ensureAuthenticated` which uses session authentication to secure front end admin files in the function we have an `if` statement with the condition `!req.session.token` which is actually interpreted as **if there is no authentication token returned for the session**, the code in the curly braces will run. That is, the user is redirected to the login page whenever the authentication token runs. The function `getUpload` accepts no parameters but it contains the configuration for each file upload process using the `multer` dependency. In the `getUpload()` function we have a variable `uploadDir` which sets the directory where the uploaded media will be saved. We set up `multer` by adding a new variable in the function `storage`. The variable is assigned an object value with an associated method `multer.diskStorage({})`. In this method we set `destination` of the uploaded image to the value of `uploadDir` which was defined earlier. Since multer doesn't handle addition of file extension automatically that has to be done manually. This leads us to `filename` function with three parameters `req, file, cb` which will provide data for the file naming and extension. We use the `path` module to define some of the required characteristics for naming and setting file extensions for our uploads. To set these characteristics we add a first variable `fileExtension` whose value is the extension from the value of `file.originalname`. We make use of the method `path.extname()` in this case. `var fileBase` is assigned a method `path.basename()`which will extract the uploaded file name from the upload path. The extracted file name is then assigned to the method `slugify()` and concatenated with the value of `fileExtension`to create a unique SEO friendly name for the uploaded image. We also need to make sure that each uploaded image has a unique name identified with only one image. In such cases where more than one image shares the same name we need to add a block of code that adds a counter suffix at the end of the file name for the later uploaded images in order to differentiate between the files. For the purpose of differentiating described above, we add a new variable `fileCounter = 0`. The while loop below this variable uses the `fileExists()` module to determine if a file with specified file name exists in the upload directory. If it returns `true`, the value of `fileCounter` increases by one for that particular image. The `fileSlug` variable for that image then gets a new value added after the file name, just before adding the `fileExtension`. The counter and file name is also separated by the dash symbol. The third variable parameter for `filename` i.e `cb` which is a callback function then accepts and returns the value `null` and the new value for the variable `fileSlug`. A new variable is then created `upload` which stores the value of the `storage` variable created earlier in a property also known as `storage`. The value of `upload` variable is returned by using `return upload` which brings us to the end of the `admin.controller.js` file. #### 2. Blog Controller The next controller we'll work on is the blog controller which will help us intercept and disburse all requests from the blog section of this application. To add our blog controller, we create a new file and call it `blog.controller.js` In our file we have the following code ``` var express = require('express'); var _ = require('lodash'); var moment = require('moment'); var path = require('path'); var router = express.Router(); var request = require('request'); var fs = require('fs'); var config = require('config.json'); var pageService = require('services/page.service'); var postService = require('services/post.service'); var redirectService = require('services/redirect.service'); var slugify = require('helpers/slugify'); var pager = require('helpers/pager'); var basePath = path.resolve('../client/blog'); var indexPath = basePath + '/index'; var metaTitleSuffix = ""; var oneWeekSeconds = 60 * 60 * 24 * 7; var oneWeekMilliseconds = oneWeekSeconds * 1000; /* STATIC ROUTES ---------------------------------------*/ router.use('/_dist', express.static(basePath + '/_dist')); router.use('/_content', express.static(basePath + '/_content', { maxAge: oneWeekMilliseconds })); /* MIDDLEWARE ---------------------------------------*/ // check for redirects router.use(function (req, res, next) { var host = req.get('host'); var url = req.url.toLowerCase(); // redirects entered into cms redirectService.getByFrom(url) .then(function (redirect) { if (redirect) { // 301 redirect to new url return res.redirect(301, redirect.to); } next(); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); }); // add shared data to vm router.use(function (req, res, next) { var vm = req.vm = {}; vm.loggedIn = !!req.session.token; vm.domain = req.protocol + '://' + req.get('host'); vm.url = vm.domain + req.path; postService.getAll() .then(function (posts) { // if admin user is logged in return all posts, otherwise return only published posts vm.posts = vm.loggedIn ? posts : _.filter(posts, { 'publish': true }); // add urls to posts vm.posts.forEach(function (post) { post.url = '/post/' + moment(post.publishDate).format('YYYY/MM/DD') + '/' + post.slug; post.publishDateFormatted = moment(post.publishDate).format('MMMM DD YYYY'); }); loadYears(); loadTags(); next(); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); // load years and months for blog month list function loadYears() { vm.years = []; // get all publish dates var dates = _.pluck(vm.posts, 'publishDate'); // loop through dates and create list of unique years and months _.each(dates, function (dateString) { var date = moment(dateString); var year = _.findWhere(vm.years, { value: date.format('YYYY') }); if (!year) { year = { value: date.format('YYYY'), months: [] }; vm.years.push(year); } var month = _.findWhere(year.months, { value: date.format('MM') }); if (!month) { month = { value: date.format('MM'), name: moment(date).format('MMMM'), postCount: 1 }; year.months.push(month); } else { month.postCount += 1; } }); } function loadTags() { // get unique array of all tags vm.tags = _.chain(vm.posts) .pluck('tags') .flatten() .uniq() .sort() .filter(function (el) { return el; }) // remove undefined/null values .map(function (tag) { return { text: tag, slug: slugify(tag) }; }) .value(); } }); /* ROUTES ---------------------------------------*/ // home route router.get('/', function (req, res, next) { var vm = req.vm; var currentPage = req.query.page || 1; vm.pager = pager(vm.posts.length, currentPage); vm.posts = vm.posts.slice(vm.pager.startIndex, vm.pager.endIndex + 1); render('home/index.view.html', req, res); }); // post by id route (permalink used by disqus comments plugin) router.get('/post', function (req, res, next) { var vm = req.vm; if (!req.query.id) return res.status(404).send('Not found'); // find by post id or disqus id (old post id) var post = _.find(vm.posts, function (p) { return p._id.toString() === req.query.id; }); if (!post) return res.status(404).send('Not found'); // 301 redirect to main post url var postUrl = '/post/' + moment(post.publishDate).format('YYYY/MM/DD') + '/' + post.slug; return res.redirect(301, postUrl); }); // post details route router.get('/post/:year/:month/:day/:slug', function (req, res, next) { var vm = req.vm; postService.getByUrl(req.params.year, req.params.month, req.params.day, req.params.slug) .then(function (post) { if (!post) return res.status(404).send('Not found'); post.url = '/post/' + moment(post.publishDate).format('YYYY/MM/DD') + '/' + post.slug; post.publishDateFormatted = moment(post.publishDate).format('MMMM DD YYYY'); post.permalink = vm.domain + '/post?id=' + post._id; vm.post = post; // add post tags and tag slugs to viewmodel vm.postTags = _.map(post.tags, function (tag) { return { text: tag, slug: slugify(tag) }; }); // meta tags vm.metaTitle = vm.post.title + metaTitleSuffix; vm.metaDescription = vm.post.summary; render('posts/details.view.html', req, res); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); }); // posts for tag route router.get('/posts/tag/:tag', function (req, res, next) { var vm = req.vm; // filter posts by specified tag vm.posts = _.filter(vm.posts, function (post) { if (!post.tags) return false; // loop through tags to find a match var tagFound = false; _.each(post.tags, function (tag) { var tagSlug = slugify(tag); if (tagSlug === req.params.tag) { // set vm.tag and title here to get the un-slugified version for display vm.tag = tag; tagFound = true; // meta tags vm.metaTitle = 'Posts tagged "' + vm.tag + '"' + metaTitleSuffix; vm.metaDescription = 'Posts tagged "' + vm.tag + '"' + metaTitleSuffix; } }); return tagFound; }); // redirect to home page if there are no posts with tag if (!vm.posts.length) return res.redirect(301, '/'); render('posts/tag.view.html', req, res); }); // posts for month route router.get('/posts/:year/:month', function (req, res, next) { var vm = req.vm; vm.year = req.params.year; vm.monthName = moment(req.params.year + req.params.month + '01').format('MMMM'); // filter posts by specified year and month vm.posts = _.filter(vm.posts, function (post) { return moment(post.publishDate).format('YYYYMM') === req.params.year + req.params.month; }); // meta tags vm.metaTitle = 'Posts for ' + vm.monthName + ' ' + vm.year + metaTitleSuffix; vm.metaDescription = 'Posts for ' + vm.monthName + ' ' + vm.year + metaTitleSuffix; render('posts/month.view.html', req, res); }); // page details route router.get('/page/:slug', function (req, res, next) { var vm = req.vm; pageService.getBySlug(req.params.slug) .then(function (page) { if (!page) return res.status(404).send('Not found'); vm.page = page; // meta tags vm.metaTitle = vm.page.title + metaTitleSuffix; vm.metaDescription = vm.page.description + metaTitleSuffix; render('pages/details.view.html', req, res); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); }); // archive route router.get('/archive', function (req, res, next) { var vm = req.vm; // meta tags vm.metaTitle = 'Archive' + metaTitleSuffix; vm.metaDescription = 'Archive' + metaTitleSuffix; render('archive/index.view.html', req, res); }); /* PRIVATE HELPER FUNCTIONS ---------------------------------------*/ // render template function render(templateUrl, req, res) { var vm = req.vm; vm.xhr = req.xhr; vm.templateUrl = templateUrl; // render view only for ajax request or whole page for full request var renderPath = req.xhr ? basePath + '/' + vm.templateUrl : indexPath; return res.render(renderPath, vm); } // proxy file from remote url for page speed score function proxy(fileUrl, filePath, req, res) { // ensure file exists and is less than 1 hour old fs.stat(filePath, function (err, stats) { if (err) { // file doesn't exist so download and create it updateFileAndReturn(); } else { // file exists so ensure it's not stale if (moment().diff(stats.mtime, 'minutes') > 60) { updateFileAndReturn(); } else { returnFile(); } } }); // update file from remote url then send to client function updateFileAndReturn() { request(fileUrl, function (error, response, body) { fs.writeFileSync(filePath, body); returnFile(); }); } // send file to client function returnFile() { res.set('Cache-Control', 'public, max-age=' + oneWeekSeconds); res.sendFile(filePath); } } ``` So in our file we first of all import all of the dependencies that is required for this module. We import `express` first which is a generally required dependency module. We import `lodash` to help perform special utility functions. The `moment` module can be used to wrap a JavaScript date object into a moment object. It will come in handy while calculating date and time of blog posts. We also import the `path` module for file path handling. We add the Express Router by adding a variable `router`. To make the handling of HTTP requests easier we import a the NodeJS `request` module which does a better work than the traditional `http`. We use the `fs` module to check for the existence of files in our application database, hence it is imported by assigning it to the variable `fs`. We'll be needing the `config.json` file we crated in the earlier tutorials so we import that also. Three variables `pageService` , `postService` and `redirectService` are also imported, all set the set the service files for pages, posts and redirects in our application as requirements in this module. Both files are yet to be created, that will be covered in a later tutorial along with other `services` needed in the application. We also import two of our helper files in the name of `slugify` and `pager`. The variable `basePath` will set an absolute path for the blog section using the `path.resolve('../client/blog')` method. `indexPath` uses the value of `basePath` concatenated with the with the string `/index` to specify a path to the subdirectory `index` in the blog section of the application. ``` router.use('/_dist', express.static(basePath + '/_dist')); router.use('/_content', express.static(basePath + '/_content', { maxAge: oneWeekMilliseconds })); ``` The block above will tell the server where the static front end files for the blog section is located. In both methods `express.static()` uses the value of the variable `basePath` concatenated with `'/_dist'` and `'/_content'` respectively to set the path to the `'/_dist'` and `'/_content'` sub-directories. We add a new function that helps us check for redirects in the application, the following block helps us get that done ``` router.use(function (req, res, next) { var host = req.get('host'); var url = req.url.toLowerCase(); // redirects entered into cms redirectService.getByFrom(url) .then(function (redirect) { if (redirect) { // 301 redirect to new url return res.redirect(301, redirect.to); } next(); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); }); ``` What the above block does is that whenever a request is made from a page, the full `url` of the request page is gotten from the request body and is converted to lower case. Once the full url has been gotten, it is then passed as parameter through a function created in the redirect service for our application `redirectService.getByFrom(url)`. After the method must have run its course, then another function checks if the request is a redirect, this is determined from the result of running `redirectService.getByFrom(url)`. If it returns true, the function returns a response consisting a `301` redirect and another value `redirect.to` which provides the destination being redirected to. If it returns false the function returns an error. Next we need to add shared data to the vm so the blog and admin can view published and all(published and unpublished) posts respectively. ``` router.use(function (req, res, next) { var vm = req.vm = {}; vm.loggedIn = !!req.session.token; vm.domain = req.protocol + '://' + req.get('host'); vm.url = vm.domain + req.path; vm.googleAnalyticsAccount = config.googleAnalyticsAccount; postService.getAll() .then(function (posts) { // if admin user is logged in return all posts, otherwise return only published posts vm.posts = vm.loggedIn ? posts : _.filter(posts, { 'publish': true }); // add urls to posts vm.posts.forEach(function (post) { post.url = '/post/' + moment(post.publishDate).format('YYYY/MM/DD') + '/' + post.slug; post.publishDateFormatted = moment(post.publishDate).format('MMMM DD YYYY'); }); loadYears(); loadTags(); next(); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); // load years and months for blog month list function loadYears() { vm.years = []; // get all publish dates var dates = _.pluck(vm.posts, 'publishDate'); // loop through dates and create list of unique years and months _.each(dates, function (dateString) { var date = moment(dateString); var year = _.findWhere(vm.years, { value: date.format('YYYY') }); if (!year) { year = { value: date.format('YYYY'), months: [] }; vm.years.push(year); } var month = _.findWhere(year.months, { value: date.format('MM') }); if (!month) { month = { value: date.format('MM'), name: moment(date).format('MMMM'), postCount: 1 }; year.months.push(month); } else { month.postCount += 1; } }); } function loadTags() { // get unique array of all tags vm.tags = _.chain(vm.posts) .pluck('tags') .flatten() .uniq() .sort() .filter(function (el) { return el; }) // remove undefined/null values .map(function (tag) { return { text: tag, slug: slugify(tag) }; }) .value(); } }); ``` We first of all set the parameters for the view model of the blog section of the application. We set patterns for the view models of logged in users/admin, domain and url of the blog. In order to collate all the posts stored in the database we make reference to a method created in the `postService` module `getAll()`. This method returns an array of all posts stored in the database. After executing the `getAll()` function another function is run which filters the posts that can be seen by certain, if the user is logged in all posts are returned, otherwise only published posts are returned. In order to set a pattern for each blog post url the `forEach()` function is implemented. Given an array of posts, for each post we execute a function that sets the pattern for the post url which comprises of the string `/post/` added to the date the post was published followed by the post slug. If any error occurs in the process, it is caught and rendered through the view. Next is setting the routes for the blog section. the first route included is the home route ``` router.get('/', function (req, res, next) { var vm = req.vm; var currentPage = req.query.page || 1; vm.pager = pager(vm.posts.length, currentPage); vm.posts = vm.posts.slice(vm.pager.startIndex, vm.pager.endIndex + 1); render('home/index.view.html', req, res); }); ``` Using the router we set the home route using the `get()` method. The above function will render the file `home/index.view.html` as the home page. ``` // post by id route (permalink used by disqus comments plugin) router.get('/post', function (req, res, next) { var vm = req.vm; if (!req.query.id) return res.status(404).send('Not found'); // find by post id or disqus id (old post id) var post = _.find(vm.posts, function (p) { return p._id.toString() === req.query.id; }); if (!post) return res.status(404).send('Not found'); // 301 redirect to main post url var postUrl = '/post/' + moment(post.publishDate).format('YYYY/MM/DD') + '/' + post.slug; return res.redirect(301, postUrl); }); ``` The block above will attempt to find a post through its unique `id`, if it happens that the post id doesn't exist in the database it returns a `404` error status. Else, if the post exists the user is redirected using a 301 redirect to the post url where they can view the full contents of the post. We also attempt to get the details for each post by getting the post date and slug and render the post detail through `posts/details.view.html` file. The block below will help get that done ``` router.get('/post/:year/:month/:day/:slug', function (req, res, next) { var vm = req.vm; postService.getByUrl(req.params.year, req.params.month, req.params.day, req.params.slug) .then(function (post) { if (!post) return res.status(404).send('Not found'); post.url = '/post/' + moment(post.publishDate).format('YYYY/MM/DD') + '/' + post.slug; post.publishDateFormatted = moment(post.publishDate).format('MMMM DD YYYY'); post.permalink = vm.domain + '/post?id=' + post._id; vm.post = post; // add post tags and tag slugs to viewmodel vm.postTags = _.map(post.tags, function (tag) { return { text: tag, slug: slugify(tag) }; }); // meta tags vm.metaTitle = vm.post.title + metaTitleSuffix; vm.metaDescription = vm.post.summary; render('posts/details.view.html', req, res); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); }); ``` Using the post service the server requests for the year, month, day and slug for the post and use these values to execute a callback function. The function tries to verify if the post in question exists, if it doesn't a `404` status error is returned else the function returns post url, the publish date, post permalink, post tags, post title and summary. If an error occurs the post catches the error and renders it. Just like we tried to find a post through its unique `id` we would like to find and filter posts by returning post with identical tags. ``` router.get('/posts/tag/:tag', function (req, res, next) { var vm = req.vm; // filter posts by specified tag vm.posts = _.filter(vm.posts, function (post) { if (!post.tags) return false; // loop through tags to find a match var tagFound = false; _.each(post.tags, function (tag) { var tagSlug = slugify(tag); if (tagSlug === req.params.tag) { // set vm.tag and title here to get the un-slugified version for display vm.tag = tag; tagFound = true; // meta tags vm.metaTitle = 'Posts tagged "' + vm.tag + '"' + metaTitleSuffix; vm.metaDescription = 'Posts tagged "' + vm.tag + '"' + metaTitleSuffix; } }); return tagFound; }); ``` For the tag filtering we set a route which renders posts after executing a callback function. For each provided tag or tags, if there is no corresponding post the function returns as `false`. If there are posts matching the specified tags, the function loops through the tags and returns a list of post displaying the post title and summary. The list is rendered on the front end through the `posts/tag.view.html` file. Furthermore, if there are no posts with the specified the user is redirected to the homepage. ``` router.get('/posts/:year/:month', function (req, res, next) { var vm = req.vm; vm.year = req.params.year; vm.monthName = moment(req.params.year + req.params.month + '01').format('MMMM'); // filter posts by specified year and month vm.posts = _.filter(vm.posts, function (post) { return moment(post.publishDate).format('YYYYMM') === req.params.year + req.params.month; }); // meta tags vm.metaTitle = 'Posts for ' + vm.monthName + ' ' + vm.year + metaTitleSuffix; vm.metaDescription = 'Posts for ' + vm.monthName + ' ' + vm.year + metaTitleSuffix; render('posts/month.view.html', req, res); }); ``` The block above will filter through all available posts and return the ones matching the specified month. The route `/posts/:year/:month` indicates that the posts returned are from a specific year and month. The callback function executed after will loop through a list of all posts and display the title and summary for the posts from a specified month which will be rendered through the `posts/month.view.html` file. There is also the route for rendering the page details for each page. The code below handles that ``` router.get('/page/:slug', function (req, res, next) { var vm = req.vm; pageService.getBySlug(req.params.slug) .then(function (page) { if (!page) return res.status(404).send('Not found'); vm.page = page; // meta tags vm.metaTitle = vm.page.title + metaTitleSuffix; vm.metaDescription = vm.page.description + metaTitleSuffix; render('pages/details.view.html', req, res); }) .catch(function (err) { vm.error = err; res.render(indexPath, vm); }); }); ``` In the callback function above, we use a method from the page service module `pageService.getBySlug(req.params.slug)` which returns the slug for the page in question. If a page matching the slug is non-existent the function returns a 404 error. If it corresponds, the function returns the page, title and summary and renders it through the `pages/details.view.html`. ``` router.get('/archive', function (req, res, next) { var vm = req.vm; // meta tags vm.metaTitle = 'Archive' + metaTitleSuffix; vm.metaDescription = 'Archive' + metaTitleSuffix; render('archive/index.view.html', req, res); }); ``` We use the block above to set the route for blog post archive. The callback function returns the page title and description for all published blog posts which is rendered through the `archive/index.view.html` file. #### Install Controller On first installation of our application, the initial user would need a login username and password to gain access to the admin area. We need to create a controller module for that purpose, in our controller directory we will add a new file `install.controller.js`. In the file we have, ``` var express = require('express'); var router = express.Router(); var config = require('config.json'); var fs = require("fs"); var userService = require('services/user.service'); router.get('/', function (req, res) { if (config.installed) { return res.sendStatus(401); } return res.render('install/index'); }); router.post('/', function (req, res) { if (config.installed) { return res.sendStatus(401); } // create user userService.create(req.body) .then(function () { // save installed flag in config file config.installed = true; fs.writeFileSync('./config.json', JSON.stringify(config)); // return to login page with success message req.session.success = 'Installation successful, you can login now.'; return res.redirect('/login'); }) .catch(function (err) { return res.render('install/index', { error: err }); }); }); module.exports = router; ``` In this file the we utilize the following dependencies `express`, `express router`, `fs`. We also require the `config` and `userService` module to achieve our objectives. ``` router.get('/', function (req, res) { if (config.installed) { return res.sendStatus(401); } return res.render('install/index'); }); ``` The block above checks if `config.installed` equals true. If `true` the function returns a `401` unauthorized status code. The function renders the `install/index.html` page. ``` userService.create(req.body) .then(function () { // save installed flag in config file config.installed = true; fs.writeFileSync('./config.json', JSON.stringify(config)); // return to login page with success message req.session.success = 'Installation successful, you can login now.'; return res.redirect('/login'); }) .catch(function (err) { return res.render('install/index', { error: err }); }); ``` If `config.installed` equals false and a new user is created the function changes `config.installed = true`. The function then redirects the user to the login pageFor . #### Login Controller Admin would need to login to the login area of our application, the login feature would also require a controller feature. In the controller directory, create a new file `login.controller.js`. In the file add the following code ``` var express = require('express'); var router = express.Router(); var userService = require('services/user.service'); 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/index', viewData); }); router.post('/', function (req, res) { userService.authenticate(req.body.username, req.body.password) .then(function (token) { // authentication is successful if the token parameter has a value if (token) { // save JWT token in the session to make it available to the angular app req.session.token = token; // redirect to returnUrl var returnUrl = req.query.returnUrl && decodeURIComponent(req.query.returnUrl) || '/admin'; return res.redirect(returnUrl); } else { return res.render('login/index', { error: 'Username or password is incorrect', username: req.body.username }); } }) .catch(function (err) { console.log('error on login', err); return res.render('login/index', { error: err }); }); }); module.exports = router; ``` We import the `express` dependency, use the `express` router and the user service module to reach our objectives in this file. The callback function in our router handles thwe admin login and logout the application. The line `delete req.session.token;` will log any logged in user out of the admin area. The user is returned to the login page and out of the admin area upon successful logout. The processes described above are made possible by the block below ``` 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/index', viewData); }); ``` Whenever a user attempts to login to the admin area, the block of code below is implemented. ``` router.post('/', function (req, res) { userService.authenticate(req.body.username, req.body.password) .then(function (token) { // authentication is successful if the token parameter has a value if (token) { // save JWT token in the session to make it available to the angular app req.session.token = token; // redirect to returnUrl var returnUrl = req.query.returnUrl && decodeURIComponent(req.query.returnUrl) || '/admin'; return res.redirect(returnUrl); } else { return res.render('login/index', { error: 'Username or password is incorrect', username: req.body.username }); } }) .catch(function (err) { console.log('error on login', err); return res.render('login/index', { error: err }); }); }); ``` From the `userService` module the function is authenticated to confirm the provided username and password. Upon successful authentication the function checks if `token`parameter has a value. If the `token` parameter statement returns true the user is redirected to the admin area else the user receives an error message on the login page `'Username or password is incorrect'`. We have come to the end of this tutorial, in the next tutorial we will continue with and conclude all controller modules needed for our application. #### Curriculum 1. [Building A Content Management System Using The MEAN Stack - 1 (Create Server, Config File and Helper Modules)](https://steemit.com/utopian-io/@gotgame/5b98i4-building-a-content-management-system-using-the-mean-stack-1-create-server-config-file-and-helper-modules) 2. [Simple Shopping Cart Using Vue.js and Materialize - 2](https://steemit.com/utopian-io/@gotgame/simple-shopping-cart-using-vue-js-and-materialize-2) 3. [Simple Shopping Cart Using Vue.js and Materialize - 1](https://steemit.com/utopian-io/@gotgame/simple-shopping-cart-using-vue-js-and-materialize) #### Proof Of Work Done [https://github.com/olatundeee/mean-cms](https://github.com/olatundeee/mean-cms )