Building A Content Management System Using The MEAN Stack - 2 (Create Controller Modules 1)
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
- Admin Controller
- Blog Controller
- Install Controller
- 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,
- Angular
- MongoDB
- 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
- Building A Content Management System Using The MEAN Stack - 1 (Create Server, Config File and Helper Modules)
- Simple Shopping Cart Using Vue.js and Materialize - 2
- Simple Shopping Cart Using Vue.js and Materialize - 1
Thank you for your contribution.
After reviewing your tutorial I recommend the following:
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!
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!