Building A Content Management System Using The MEAN Stack - 2 (Create Controller Modules 1)

in #utopian-io6 years ago (edited)

Repository

https://github.com/nodejs/node

What Will I Learn

The codebase for this tutorial is based on the MEANie an open source content management system by Jason Watmore.

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

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 fileExtensionto 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 tokenparameter 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)
  2. Simple Shopping Cart Using Vue.js and Materialize - 2
  3. Simple Shopping Cart Using Vue.js and Materialize - 1

Proof Of Work Done

https://github.com/olatundeee/mean-cms

Sort:  

Thank you for your contribution.
After reviewing your tutorial I recommend the following:

  • When using code blocks from other tutorials please put the site link as a reference. I've found blocks of code like here and here.
  • Put your code ident so the user has an easy read.

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Thank you for your review, @portugalcoin!

So far this week you've reviewed 2 contributions. Keep up the good work!

Congratulations @gotgame! You have completed the following achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of posts published

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Do you like SteemitBoard's project? Then Vote for its witness and get one more award!

Hey, @gotgame!

Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Coin Marketplace

STEEM 0.16
TRX 0.15
JST 0.027
BTC 60063.85
ETH 2313.06
USDT 1.00
SBD 2.46