├── .gitignore ├── README.md ├── api ├── ArticleRoute.js ├── IPRoute.js ├── RoleRoute.js ├── UserRoute.js ├── ViewRoute.js └── index.js ├── app.js ├── config └── config.js ├── middlewares ├── AuthWare.js ├── ParamWare.js └── RoleWare.js ├── models ├── ArticleModel.js ├── IPModel.js ├── RoleModel.js └── UserModel.js ├── package-lock.json ├── package.json ├── perms.txt ├── public └── css │ └── style.css ├── services ├── ArticleService.js ├── IPService.js ├── RoleService.js └── UserService.js ├── utils ├── auth.js ├── tags.js └── validator.js └── views ├── pages └── index.ejs └── partials ├── head.ejs ├── navbar.ejs └── post.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | # JSDoc output 2 | out 3 | #config files 4 | config/jsdoc.json 5 | config/*.json 6 | 7 | # Nodemodules 8 | ./node_modules 9 | node_modules 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # restful-cms 2 | A restful content management system. Offers features such as: 3 | - Role-based access control (RBAC) 4 | - User authentication 5 | - IP address filtering / blocking 6 | 7 | ## Project Structure 8 | - **models**: All of the database models are stored here. 9 | - **services**: The business logic layer. 10 | - **api**: Contains all Express.js routes 11 | - **subscribers**: Event handlers for any asynchronous tasks (TODO) 12 | - **config**: System configuration. 13 | - **middlewares**: Express.js middlewares are stored here. 14 | - **utilities**: Utility modules, such as hashing, email validation. 15 | 16 | ## Request / Response Structure 17 | All API requests and responses follow a project-wide standardized format. By default, the server runs on port `3005`; this can be changed in `config.json`. For any route that requires authorization, a token must be provided in the request's `x-access-token` header. 18 | 19 | Each response is of the same structure: 20 | 21 | { 22 | 'success' : Boolean, 23 | 'message' : String, 24 | 'error' : Object 25 | } 26 | 27 | If the call was made successfully, then error is null. Message is the data recieved from the call. Based on the request being made, the data can be structured in different ways. Usually, the result is sent as an object or an array of objects. 28 | 29 | ## Authentication 30 | A token can be obtained by making a request to `baseurl:3005/login` with the appropriate email and password. A default admin account is provided in the configuration, and will be seeded to the database upon startup. In a production environment, the default user should be replaced. These credentials are stored as `root`. 31 | 32 | ## TODO: 33 | - IPFilterWare and failed login attempt tracking 34 | - Unit testing 35 | - Parameter validation middleware 36 | - User account verification 37 | - Optional request logging -------------------------------------------------------------------------------- /api/ArticleRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ArticleRoute 3 | * @requires express 4 | * ArticleRouter acts on "/blog" baseURL 5 | */ 6 | 7 | /** 8 | * @const express 9 | */ 10 | const express = require('express') 11 | 12 | /** 13 | * @const articleRouter 14 | */ 15 | const router = express.Router() 16 | 17 | const ArticleModel = require('../models/ArticleModel') 18 | const ArticleService = new (require('../services/ArticleService'))(ArticleModel) 19 | 20 | /** 21 | * @memberof module:ArticleRoute 22 | * @name POST/blog/post 23 | * @function 24 | * Creates a new, unapproved article. 25 | */ 26 | router.post('/post', (req,res) => { 27 | const title = req.body.title 28 | const tagline = req.body.tagline 29 | const author = req.body.author 30 | const content = req.body.content 31 | const tags = req.body.tags 32 | 33 | ArticleService.createArticle(title,tagline,author,content,tags) 34 | .then((result) => { 35 | res.send({success:true,message:result}) 36 | }).catch ((e) => { 37 | res.send({success:false, error:e}) 38 | }) 39 | }) 40 | 41 | /** 42 | * @memberof module:ArticleRoute 43 | * @name POST/blog/update 44 | * @function 45 | * Updates the content of a given article, specified by an ID parameter. 46 | */ 47 | router.post('/update', (req,res) => { 48 | const id = req.body.id 49 | const content = req.body.content 50 | 51 | ArticleService.updateArticle(id,content).then((result) => { 52 | res.send({success:true,message:result}) 53 | }).catch((e) => { 54 | res.send({success:false, error:e}) 55 | }) 56 | }) 57 | 58 | /** 59 | * @memberof module:ArticleRoute 60 | * @name POST/blog/delete 61 | * @function 62 | * Removes the given post, as specified by an ID parameter. 63 | */ 64 | router.post('/delete', (req,res) => { 65 | const id = req.body.id 66 | 67 | ArticleService.deleteArticle(id).then((result) => { 68 | res.send({success:true,message:result}) 69 | }).catch((e) => { 70 | res.send({success:false,error:e}) 71 | }) 72 | }) 73 | 74 | /** 75 | * @memberof module:ArticleRoute 76 | * @name GET/blog/id 77 | * @function 78 | * Fetches a post by its given ID 79 | */ 80 | router.get('/:id', (req,res) => { 81 | const id = req.params.id 82 | 83 | ArticleService.getArticleByID(id).then((result) => { 84 | res.send({success:true,message:result}) 85 | }).catch((e) => { 86 | res.send({success:false,error:e}) 87 | }) 88 | }) 89 | 90 | /** 91 | * @memberof module:ArticleRoute 92 | * @name GET/blog/author 93 | * @function 94 | * Get all articles written by the author with the specified ID. 95 | */ 96 | router.get('/author/:id', (req,res) => { 97 | const id = req.params.id 98 | 99 | ArticleService.getArticlesByAuthor(id).then((result) => { 100 | res.send({success:true,message:result}) 101 | }).catch((e) => { 102 | res.send({success:false,error:e}) 103 | }) 104 | }) 105 | 106 | /** 107 | * @memberof module:ArticleRoute 108 | * @name GET/blog/ 109 | * @function 110 | * Fetches the 5 most recent articles. 111 | */ 112 | router.get('/', (req,res) => { 113 | // TODO Change to take param from configuration 114 | ArticleService.getRecentArticles(5).then((result) => { 115 | res.send({success:true,message:result}) 116 | }).catch((e) => { 117 | res.send({success:false,error:e}) 118 | }) 119 | }) 120 | 121 | /** 122 | * @memberof module:ArticleRoute 123 | * @name GET/blog/tags 124 | * @function 125 | * Get all articles that contain the specified tag 126 | */ 127 | router.get('/tags/:tag', (req,res) => { 128 | const tag = req.params.tag 129 | ArticleService.getArticlesByTag(tag).then((result) => { 130 | res.send({success:true,message:result}) 131 | }).catch((e) => { 132 | res.send({success:false,error:e}) 133 | }) 134 | }) 135 | 136 | /** 137 | * @memberof module:ArticleRoute 138 | * @name GET/blog/approve 139 | * @function 140 | * Approve the article, specified by the ID parameter. An approved article 141 | * is visible to the end user. 142 | */ 143 | router.get('/approve/:id', (req,res) => { 144 | const id = req.params.id 145 | ArticleService.approveArticle(id).then((result) => { 146 | res.send({success:true,message:result}) 147 | }).catch((e) => { 148 | res.send({success:false,error:e}) 149 | }) 150 | }) 151 | 152 | /** 153 | * @memberof module:ArticleRoute 154 | * @name POST/blog/comment 155 | * @function 156 | * Posts a new comment to the specified article. 157 | */ 158 | router.post('/comment', (req,res) => { 159 | const articleID = req.params.articleID 160 | const userID = req.params.userID 161 | const content = req.params.content 162 | 163 | ArticleService.postComment(articleID, userID, content).then((result) => { 164 | res.send({success:true,message:result}) 165 | }).catch((error) => { 166 | res.send({success:false, error:error}) 167 | }) 168 | }) 169 | 170 | module.exports = router; -------------------------------------------------------------------------------- /api/IPRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module IPRoute 3 | * @requires express 4 | * @requires module:IPService 5 | * @requires module:ParamWare 6 | * IPRoute router acts on "/ip" baseURL 7 | */ 8 | 9 | /** 10 | * @const ipRouter 11 | */ 12 | const router = require('express').Router() 13 | 14 | const mongoose = require('mongoose') 15 | 16 | const IPModel = require('../models/IPModel') 17 | const IPService = new (require('../services/IPService'))(IPModel) 18 | const ParamWare = new (require('../middlewares/ParamWare'))('body','address') 19 | 20 | // Apply the parameter middleware to both routes 21 | router.use('/ban', ParamWare) 22 | router.use('/unban', ParamWare) 23 | 24 | /** 25 | * POST request to /ip/ban 26 | * @name POST/ip/ban 27 | * @function 28 | * @memberof module:IPRoute 29 | */ 30 | router.post('/ban', (req,res) => { 31 | const address = req.body.address 32 | const reason = req.body.reason 33 | 34 | IPService.banAddress(address,reason).then((result) => { 35 | res.send({success:true, message:result}) 36 | }).catch ((e) => { 37 | res.send({success:false, 'error':e}) 38 | }) 39 | }) 40 | 41 | /** 42 | * POST request to /ip/unban 43 | * @name POST/ip/unban 44 | * @function 45 | * @memberof module:IPRoute 46 | */ 47 | router.post('/unban', (req,res) => { 48 | const address = req.body.address 49 | 50 | IPService.unbanAddress(address).then((result) => { 51 | res.send({success:true, message:result}) 52 | }).catch((e) => { 53 | res.send({success:false, error:e}) 54 | }) 55 | }) 56 | 57 | /** 58 | * GET request to /ip/check 59 | * @name GET/ip/check 60 | * @function 61 | * @memberof module:IPRoute 62 | */ 63 | router.post('/check/', (req,res) => { 64 | const address = req.body.address 65 | 66 | IPService.checkAddress(address).then((result) => { 67 | res.send({success:true, message:result}) 68 | }).catch((e) => { 69 | res.send({success:false, error:e}) 70 | }) 71 | }) 72 | 73 | module.exports = router; -------------------------------------------------------------------------------- /api/RoleRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module RoleRoute 3 | * @requires express 4 | * @requires services/RoleModel 5 | * @requires services/RoleService 6 | * RoleRoute router acts on "/role" baseURL 7 | */ 8 | 9 | /** 10 | * @const express 11 | */ 12 | const express = require('express') 13 | 14 | /** 15 | * @const 16 | */ 17 | const router = express.Router() 18 | 19 | const RoleModel = require('../models/RoleModel') 20 | const RoleService = new (require('../services/RoleService'))(RoleModel) 21 | 22 | /** 23 | * GET request on /role/permissions/:role 24 | * Gets all permissions assigned to a role. 25 | * @name GET/role/permissions/ 26 | * @function 27 | * @memberof module:RoleRoute 28 | */ 29 | router.get('/permissions/:role', (req,res) => { 30 | const role = req.params.role 31 | RoleService.getRolePermissions(role).then((result) => { 32 | res.send({success:true, message:result}) 33 | }).catch((e) => { 34 | res.send({success:false, error:e}) 35 | }) 36 | }) 37 | 38 | /** 39 | * POST request on /role/grant/:role/:permission 40 | * @name POST/role/grant 41 | * @function 42 | * @memberof module:RoleRoute 43 | */ 44 | router.post('/grant/:role/:permission', (req,res) => { 45 | const role = req.params.role 46 | const permission = req.params.permission 47 | 48 | RoleService.grantPermission(role, permission).then((result) => { 49 | res.send({success:true, message:result}) 50 | }).catch((e) => { 51 | res.send({success:false, error:e}) 52 | }) 53 | }) 54 | 55 | /** 56 | * POST request on /role/revoke/:role/:permission 57 | * @name POST/role/revoke 58 | * @function 59 | * @memberof module:RoleRoute 60 | */ 61 | router.post('/revoke/:role/:permission', () => { 62 | const role = req.params.role 63 | const permission = req.params.permission 64 | 65 | RoleService.revokePermission(role,permission).then((result) => { 66 | res.send({success:true,message:result}) 67 | }).catch((e) => { 68 | res.send({success:false, error:e}) 69 | }) 70 | }) 71 | 72 | /** 73 | * POST request on /role/revokeAll/:role 74 | * @name POST/role/revokeAll 75 | * @function 76 | * @memberof module:RoleRoute 77 | */ 78 | router.post('/revokeAll/:role', (req,res) => { 79 | const role = req.params.role 80 | 81 | RoleService.revokeAllPermissions(role).then((result) => { 82 | res.send({success:true, message:result}) 83 | }).catch((e) => { 84 | res.send({success:false, error:e}) 85 | }) 86 | }) 87 | 88 | router.post('/check/:role/:permission', (req,res) => { 89 | const role = req.params.role 90 | const permission = req.params.permission 91 | 92 | RoleService.checkPermission(role,permission).then((result) => { 93 | res.send({success:true, message:result}) 94 | }).catch((e) => { 95 | res.send({success:false, error:e}) 96 | }) 97 | }) 98 | 99 | module.exports = router -------------------------------------------------------------------------------- /api/UserRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module UserRoute 3 | * @requires express 4 | * UserRouter acts on "/" baseURL 5 | */ 6 | 7 | /** 8 | * @const exress 9 | */ 10 | const express = require('express') 11 | 12 | /** 13 | * @const userRouter 14 | */ 15 | const router = express.Router() 16 | 17 | const UserModel = require('../models/UserModel') 18 | const Authenticator = require('../utils/auth') 19 | const UserService = new (require('../services/UserService'))(UserModel, Authenticator) 20 | 21 | const AuthWare = require('../middlewares/AuthWare') 22 | const RoleWare = require('../middlewares/RoleWare') 23 | 24 | router.use('/changePassword', AuthWare) 25 | router.use('/deleteUser', AuthWare) 26 | 27 | router.use('/deleteUser', new RoleWare('deleteUser')) 28 | 29 | /** 30 | * @memberof module:UserRoute 31 | * @name POST/login 32 | * @function 33 | * User authentication route. 34 | */ 35 | router.post('/login', (req,res) => { 36 | const email = req.body.email 37 | const password = req.body.password 38 | UserService.login(email, password) 39 | .then((token) => { 40 | res.cookie('token', token, { 41 | // TODO change token maxAge to take a config variable 42 | maxAge: 3600 * 1000, 43 | httpOnly: true 44 | }) 45 | res.send({success:true, message:token}) 46 | }).catch((e)=> { 47 | res.send({success:false, error:e}) 48 | }) 49 | }) 50 | 51 | /** 52 | * @memberof module:UserRoute 53 | * @name POST/register 54 | * @function 55 | * Registers a new user account, providing a valid email address is provided. 56 | */ 57 | router.post('/register', async (req,res) => { 58 | const email = req.body.email 59 | const username = req.body.username 60 | const password = req.body.password 61 | 62 | UserService.register(username,email,password).then((result) => { 63 | res.send({success:true,message:result}) 64 | }).catch((e) => { 65 | console.log(`Error: ${JSON.stringify(e)}`) 66 | res.send({success:false, error:e}) 67 | }) 68 | }) 69 | 70 | /** 71 | * @memberof module:UserRoute 72 | * @name POST/changePassword 73 | * @function 74 | * Allows a user to change their own password. This route requires a token. 75 | */ 76 | router.post('/changePassword', (req,res) => { 77 | const email = req.body.email 78 | const oldPass = req.body.oldPassword 79 | const newPass = req.body.newPassword 80 | 81 | if (email != res.locals.email) res.status(401).send({success:false, error:"Invalid email."}) 82 | UserService.changePassword(oldPass, newPass, email).then((result) => { 83 | res.send({success:true,message:result}) 84 | }).catch((error) => { 85 | res.send({success:false,error:error}) 86 | }) 87 | }) 88 | 89 | /** 90 | * @memberof module:UserRoute 91 | * @name POST/deleteUser 92 | * @function 93 | * Removes a user from the database. 94 | */ 95 | router.post('/deleteUser', (req,res) => { 96 | 97 | }) 98 | 99 | module.exports = router; -------------------------------------------------------------------------------- /api/ViewRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ViewRoute 3 | * @requires express 4 | * 5 | * View routes acts upon the base url of the blog server, 6 | * and serves html pagesto GET requests using EJS 7 | */ 8 | 9 | /** 10 | * @const express 11 | */ 12 | const express = require('express') 13 | /** 14 | * @const viewRouter 15 | */ 16 | const router = express.Router() 17 | 18 | /** 19 | * @memberof module:ViewRoute 20 | * @name GET/ 21 | * @function 22 | * Serves index.html 23 | */ 24 | router.get('/', (req,res) => { 25 | const post = { 26 | title : "America Now The World's Shithole, Analysts Say", 27 | subtitle : "An Incompetent Administration That Rivals The Worst Governments Ever Seen.", 28 | author : "John L. Carveth", 29 | date : "2020-06-29", 30 | content : "I mean we all know America is a shithole, does it even really need to be said? Anyways, this is the body of the text, so it needs to ramble on and on and on. Sort of how 2020 seems to just go on and on, will it ever end? Will we ever see the sweet release of death?" 31 | } 32 | res.render('pages/index', {'post': post}) 33 | }) 34 | 35 | module.exports = router -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const ArticleRoute = require('./ArticleRoute') 2 | const IPRoute = require('./IPRoute') 3 | const RoleRoute = require('./RoleRoute') 4 | const UserRoute = require('./UserRoute') 5 | const ViewRoute = require('./ViewRoute') 6 | 7 | module.exports = { 8 | 'ArticleRoute' : ArticleRoute, 9 | 'IPRoute' : IPRoute, 10 | 'RoleRoute' : RoleRoute, 11 | 'UserRoute' : UserRoute, 12 | 'ViewRoute' : ViewRoute 13 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author John L. Carveth 3 | * @version 0.6.0 4 | * 5 | * Application entry point. 6 | */ 7 | 8 | // Where it all began... 9 | const express = require('express') 10 | const app = express() 11 | 12 | // Third party middleware 13 | const bodyParser = require('body-parser') 14 | const cookieParser = require('cookie-parser') 15 | const cors = require('cors') 16 | 17 | // First party middleware 18 | const RoleWare = require('./middlewares/RoleWare') 19 | const ParamWare = require('./middlewares/ParamWare') 20 | 21 | // Data persistence 22 | const mongoose = require('mongoose') 23 | 24 | // Database Seeder 25 | const Seeder = require('good-mongoose-seeder') 26 | 27 | // Import Routers 28 | const Routers = require('./api') 29 | 30 | // Initialize the configurator 31 | const Configurator = require('./config/config') 32 | Configurator.init() 33 | 34 | //Set views directory 35 | app.set('views', './views') 36 | // Set the express view engine 37 | app.set('view engine', 'ejs') 38 | 39 | // Add the public folder to express 40 | app.use(express.static('public')) 41 | 42 | // Use the middleware 43 | app.use(cors()) 44 | app.use(bodyParser.json()) 45 | app.use(bodyParser.urlencoded({extended:true})) 46 | 47 | // Connect to the DB specified in the config 48 | // To avoid the 'deprecated url parser' warning... 49 | mongoose.connect(Configurator.get('mongodbURI'), { 50 | useNewUrlParser:true, 51 | useUnifiedTopology:true 52 | }).catch(error => { 53 | throw new Error(error) 54 | }).then(result => { 55 | Seeder.seedData({ 56 | model: 'roles', 57 | documents: Configurator.get('roles') 58 | }) 59 | }) 60 | 61 | // Initialize the routes 62 | app.use('/blog', Routers.ArticleRoute) 63 | app.use('/ip', Routers.IPRoute) 64 | app.use('/role', Routers.RoleRoute) 65 | app.use('/', Routers.UserRoute) 66 | app.use('/', Routers.ViewRoute) 67 | 68 | const server = app.listen(process.env.port) 69 | 70 | server.on('clientError', (response,socket) => { 71 | console.log('Error starting server.') 72 | }) -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Populates the environment variables based on configuration settings. 3 | * 4 | * A local configuration file is created upon system initialization, based upon 5 | * the baseConfig object. As this object transforms with later versions of software, 6 | * the user's configuration file will remain intact, and merge with any new changes 7 | * made to baseConfig. 8 | * 9 | * @module Configurator 10 | * @author John L. Carveth 11 | */ 12 | 13 | const fs = require('fs') 14 | const generateSalt = require('../utils/auth').generateSalt 15 | const deepermerge = require('deeper-merge') 16 | 17 | /** 18 | * The base configuration of the system. If no config/config.js file exists, 19 | * a new configuration file is created with baseConfig. If a config.js file does exist, 20 | * the config file is merged with baseConfig. For any fields that exist in 21 | * both configurations, the value held in /config/config.js is kept. 22 | */ 23 | const baseConfig = { 24 | "development" : { 25 | "port" : 3005, 26 | "mongodbURI" : "", 27 | "secretKey" : generateSalt(), 28 | "root" : { 29 | // A Default Administrator user 30 | 'username' : 'admin', 31 | 'password' : 'admin', 32 | 'role' : 'admin', 33 | 'email' : 'admin@localhost.com' 34 | }, 35 | "roles" : [ 36 | { 37 | "role" : "user", 38 | "permissions" : [ 39 | "commentPost", "votePost" 40 | ] 41 | }, 42 | { 43 | "role" : "admin", 44 | "permissions" : [ 45 | "commentPost", "removeComment", "votePost", "createPost", 46 | "approvePost", "editPostSelf", "editPost", "banip", "unbanip", 47 | "deleteUser", "verifyUser", "modifyRole" 48 | ] 49 | }, 50 | { 51 | "role" : "author", 52 | "permissions" : [ 53 | "commentPost", "votePost", "createPost" 54 | ] 55 | } 56 | ], 57 | "postFetchCount": 5 58 | }, 59 | "production" : { 60 | "port" : 80, 61 | "mongodbURI" : "", 62 | "secretKey" : "", 63 | "roles" : [ 64 | { 65 | "role" : "user", 66 | "permissions" : [ 67 | "commentPost", "votePost" 68 | ] 69 | }, 70 | { 71 | "role" : "admin", 72 | "permissions" : [ 73 | "commentPost", "removeComment", "votePost", "createPost", 74 | "approvePost", "editPostSelf", "editPost", "banip", "unbanip", 75 | "deleteUser", "verifyUser", "modifyRole" 76 | ] 77 | }, 78 | { 79 | "role" : "author", 80 | "permissions" : [ 81 | "commentPost", "votePost", "createPost" 82 | ] 83 | } 84 | ], 85 | "postFetchCount": 5 86 | } 87 | } 88 | 89 | module.exports = { 90 | /** 91 | * Initializes the Configuration module 92 | * @memberof module:Configurator 93 | * @function init 94 | */ 95 | init : function () { 96 | // Create a base config file if it doesn't already exist 97 | this.writeConfig(baseConfig, 'ax') 98 | // Import it 99 | const config = require('./config.json') 100 | // merge baseConfig and config stored on disk. 101 | this.config = deepermerge.merge(baseConfig, this.config) 102 | // update config stored on disk with any new config variables from baseConfig 103 | this.writeConfig(this.config, 'w') 104 | // Detect environment to determine which configuration to use 105 | this.environment = process.env.NODE_ENV || 'development' 106 | this.config = config[this.environment] 107 | // Assign config values to provess.env variables 108 | this.populateEnvironment() 109 | }, 110 | 111 | /** 112 | * Populates process.env with all config variables 113 | * @memberof module:Configurator 114 | * @function populateEnvironment 115 | */ 116 | populateEnvironment : function () { 117 | const keys = Object.keys(this.config) 118 | keys.forEach((key) => { 119 | process.env[key] = this.config[key] 120 | }) 121 | }, 122 | 123 | /** 124 | * Overwrites config.json with new configuration settings 125 | * @memberof module:Configurator 126 | * @function writeConfig 127 | * @param {Object} configuration JSON config object to be written 128 | * @param {String} flag Node FS module flag 129 | */ 130 | writeConfig : function (configuration, flag) { 131 | try { 132 | fs.writeFileSync('./config/config.json', JSON.stringify(configuration, null, 2), {flag: flag}) 133 | } catch (e) { 134 | if (e.code === "EEXIST") { 135 | console.log('Configuration file already exists.') 136 | } else throw new Error(e) 137 | } 138 | }, 139 | 140 | /** 141 | * Merges the hardcoded base configuration setup with another configuration 142 | * object. 143 | * @param {Object} baseConfig 144 | * @param {Object} userConfig 145 | */ 146 | mergeConfig : function (baseConfig, userConfig) { 147 | // For each property in baseConfig, add property to userConfig 148 | // if not contained 149 | for (const key in baseConfig) { 150 | 151 | } 152 | }, 153 | 154 | /** 155 | * Retrieves the value stored at the given key. 156 | * @param {String} key the key of the data to retrieve 157 | * @return {*} the value stored at key 158 | */ 159 | get : function (key) { 160 | return this.config[key] 161 | } 162 | } -------------------------------------------------------------------------------- /middlewares/AuthWare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module AuthWare 3 | * @author John L. Carveth 4 | * @version 0.2.0 5 | * @requires module:Authenticator 6 | * @requires express 7 | * 8 | * ExpressJS middleware that automatically verifies JWTs attached to incoming requests. 9 | * If a token is successfully decoded, the role and email from the token are 10 | * stored in res.locals for later middlewares to access. 11 | * This middleware should be executed before any route that requires authentication 12 | * 13 | * res.locals.role => The role that has been assigned to the authenticated user 14 | * res.locals.email => The email address of the authenticated user 15 | */ 16 | 17 | /** 18 | * @const Authenticator 19 | */ 20 | const Authenticator = require('../utils/auth') 21 | 22 | /** 23 | * @param {Object} req Express.js request object 24 | * @param {Object} res Express.js response object 25 | * @param {Function} next next middleware in the chain 26 | */ 27 | module.exports = function (req,res,next) { 28 | const token = req.headers['x-access-token'] || req.cookies.token 29 | 30 | if (token) { 31 | try { 32 | const result = Authenticator.verifyToken(token) 33 | res.locals.email = result.email 34 | res.locals.role = result.role 35 | next() 36 | } catch (e) { 37 | res.send({success:false, error:e}) 38 | } 39 | } else res.send({success:false, error:"No token provided. Get a token and provide it with the x-access-token header."}) 40 | } -------------------------------------------------------------------------------- /middlewares/ParamWare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ParamWare 3 | * @author John L. Carveth 4 | * @version 1.1.0 5 | * @requires express 6 | * 7 | * Express.js middleware that verifies all required parameters 8 | * are provided with the incoming request. 9 | */ 10 | module.exports = class ParamWare { 11 | /** 12 | * @constructor 13 | * @param {String} paramType Where the parameters are expected to be found. 14 | * Should be either `body` or `params` for the respective parameter types. 15 | * @param {(String|String[])} parameters The name of the parameter(s) expected. 16 | */ 17 | constructor (paramType, parameters) { 18 | // Set any class variables here 19 | this.paramType = paramType 20 | this.params = parameters 21 | 22 | // If only one parameter, make it into an array 23 | if (!Array.isArray(parameters)) this.params = [parameters] 24 | var that = this 25 | return function (req,res,next) { 26 | var valid = true 27 | var undefinedParameters = [] 28 | // The params user provided with the request 29 | const providedParameters = req[that.paramType] 30 | 31 | // For each given parameter, ensure it is defined in req 32 | for (let p of that.params) { 33 | valid = valid && (providedParameters[p] != undefined) 34 | if (providedParameters[p] == undefined) undefinedParameters.push(p) 35 | } 36 | 37 | if (!valid) res.status(422).send({ 38 | success:false, 39 | error:'The following parameters were missing: \`' + undefinedParameters.toString() + '\`' 40 | }) 41 | else next() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /middlewares/RoleWare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @const RoleModel 3 | */ 4 | const RoleModel = require('../models/RoleModel') 5 | const RoleService = new (require('../services/RoleService'))(RoleModel) 6 | 7 | /** 8 | * Middleware that protects routes by only allowing access to 9 | * those with the correct role credentials. 10 | * 11 | * TODO: Allow multiple permissions from one RoleWare middleware 12 | * @module RoleWare 13 | * @author John L. Carveth 14 | * @version 0.4.0 15 | */ 16 | module.exports = class RoleWare { 17 | /** 18 | * @constructor RoleWare 19 | * @param {String} permission permissions required for the route 20 | * @return {Function} the created middleware function 21 | */ 22 | constructor (permission) { 23 | this.permission = permission 24 | this.cache = [] 25 | try { 26 | this.populateCache() 27 | var that = this 28 | return function (req,res,next) { 29 | if (that.checkPermission(that.permission, res.locals.role)) { 30 | next() 31 | } else res.send({success:false, error:'Missing permissions.'}) 32 | } 33 | } catch (error) { 34 | console.error(error) 35 | } 36 | } 37 | 38 | /** 39 | * @memberof module:RoleWare 40 | * @function populateCache 41 | * Stores roles and permissions fetched from the database to a cache obj 42 | */ 43 | populateCache () { 44 | var that = this; 45 | RoleService.getAllRoles().then((roles) => { 46 | that.cache = roles 47 | }).catch((e) => { 48 | console.error(e) 49 | }) 50 | } 51 | 52 | /** 53 | * @memberof module:RoleWare 54 | * @function checkPermission 55 | * @param {*} permission 56 | * @param {*} role 57 | */ 58 | checkPermission (permission, role) { 59 | var contains = false; 60 | this.cache.forEach((item) => { 61 | if (item.role == role) { 62 | item.permissions.forEach((item) => { 63 | if (permission == item) contains = true 64 | }) 65 | } 66 | }) 67 | 68 | return contains 69 | } 70 | } -------------------------------------------------------------------------------- /models/ArticleModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ArticleModel 3 | * @author John L. Carveth 4 | */ 5 | 6 | const mongoose = require('mongoose') 7 | const Schema = mongoose.Schema 8 | /** 9 | * @constructor Article 10 | */ 11 | const Article = new Schema({ 12 | title: { type: String, required: true, trim: true }, 13 | tagline: { type: String, required: false, trim: true }, 14 | // Author refers to the ObjectID of a document in the users collection (a registered user) 15 | author: { 16 | type: mongoose.Types.ObjectId, 17 | required:true 18 | }, 19 | // When the article was published 20 | date: { 21 | type: Date, 22 | default: Date.now 23 | }, 24 | // The content of the article, Markdown format 25 | content: { 26 | type: String, 27 | required: true 28 | }, 29 | // The comments on the specific blog post. Comments can only be made by registered users 30 | comments: [{ 31 | author: { type: mongoose.Types.ObjectId, required: true }, 32 | date: { type: Date, default: Date.now }, 33 | content: { type: String, required: true } 34 | }], 35 | 36 | // For article search / sort / categorization 37 | tags: [{ type: String }], 38 | 39 | // All posts by default, are unapproved, and must be verified manually to become retreivable. 40 | approved: { type: Boolean, default: false }, 41 | 42 | // The rating array tracks users' ratings of the given article. 43 | rating: [{ 44 | // Voter is the registered user's MongoDB/Mongoose ObjectID 45 | voter: { 46 | type: Schema.Types.ObjectId, 47 | required: true 48 | }, 49 | // true/false : +/- 50 | vote: { 51 | type: Boolean, 52 | required: true 53 | } 54 | }], 55 | 56 | // The rating is the approximate score (pos/neg votes) of a blog post. 57 | // The lastUpdate keeps track of when the score is updated (based on counting actual votes) 58 | approxRating: { 59 | rating: { type: Number, default: 1 }, 60 | lastUpdate: { type: Date } 61 | }, 62 | 63 | // For statistical purposes 64 | views: { type: Number, default: 0 } 65 | }) 66 | 67 | module.exports = mongoose.model('articles', Article); -------------------------------------------------------------------------------- /models/IPModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module IPModel 3 | * @author John L. Carveth 4 | */ 5 | const mongoose = require('mongoose') 6 | mongoose.Promise = Promise 7 | 8 | /** 9 | * @constructor IP 10 | */ 11 | const IP = new mongoose.Schema({ 12 | // The IP address, either IPV4 or IPV6 13 | address: { type: String, required: true, unique:true }, 14 | // When the IP was blocked 15 | banDate: { type: Date, default: Date.now()}, 16 | // Reason for the ban (optional) 17 | reason: { type: String} 18 | }) 19 | 20 | module.exports = mongoose.model('ips', IP) -------------------------------------------------------------------------------- /models/RoleModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module RoleModel 3 | * @author John L. Carveth 4 | */ 5 | 6 | const mongoose = require('mongoose') 7 | 8 | /** 9 | * @constructor Role 10 | */ 11 | const Role = new mongoose.Schema({ 12 | role: { 13 | type: String, 14 | default: "user", 15 | unique: true 16 | }, 17 | permissions: { 18 | type: [String] 19 | } 20 | }) 21 | 22 | module.exports = mongoose.model('roles', Role) -------------------------------------------------------------------------------- /models/UserModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module UserModel 3 | * @author John L. Carveth 4 | */ 5 | 6 | const mongoose = require('mongoose') 7 | const isEmail = require('../utils/validator').isEmail 8 | 9 | /** 10 | * @constructor User 11 | */ 12 | const User = new mongoose.Schema({ 13 | email: { 14 | type: String, 15 | required: true, 16 | unique: true, 17 | trim: true, 18 | validate: [isEmail, 'Please provide a valid email.'] 19 | }, 20 | username: { 21 | type: String, 22 | required: true, 23 | unique: true 24 | }, 25 | password: { 26 | type: String, 27 | required: true 28 | }, 29 | salt: { 30 | type: String, 31 | required: true 32 | }, 33 | role: { 34 | type: String, 35 | default: 'user' 36 | }, 37 | verified: { // For user email verification 38 | type: Boolean, 39 | default: false 40 | } 41 | }) 42 | 43 | module.exports = mongoose.model('users', User) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restful-cms", 3 | "version": "0.6.0", 4 | "description": "A restful content management system.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon app.js --ignore config/*.json" 9 | }, 10 | "author": "John L. Carveth", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "cookie-parser": "^1.4.4", 15 | "cors": "^2.8.5", 16 | "deeper-merge": "^0.1.2", 17 | "ejs": "^3.1.7", 18 | "express": "^4.17.1", 19 | "good-mongoose-seeder": "^1.1.2", 20 | "jsonwebtoken": "^8.5.1", 21 | "mongoose": "^5.7.7", 22 | "npm": "^6.14.6" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^1.19.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /perms.txt: -------------------------------------------------------------------------------- 1 | # List of all permissions with a description. 2 | # This is only for reference. This file is not used programmatically. 3 | # Format: 4 | # [permissionName] description 5 | 6 | [postComment] Allows a user to post a comment on a blog post. 7 | [removeComment] Allows a user to remove a comment from a blog post. 8 | [votePost] Allows a user to vote on a blog post. 9 | [createPost] Allows a user to submit a blog post to the system. 10 | [approvePost] Allows a user to approve an unapproved blog post, making it visible to other users. Should be reserved for high-trust users. 11 | [removePost] Removes an article. 12 | [editPostSelf] A user can edit a post they authored themselves. Only some fields may be edited (ex, post score cannot be altered). This includes deleting the post. 13 | [editPost] Allows a user to edit any blog post. As with `editPostSelf`, only some fields may be changed. 14 | [banip] Bans an IP address from accessing the server. 15 | [unbanip] Unbans an IP address from accessing the server. 16 | [checkip] See if an IP address has been banned. 17 | [deleteUser] Removes a user from the collection, revoking access. 18 | [modifyUser] Allows control over the modification of a User document. Can verify a user or change their password. 19 | [modifyRole] Allows a high-level user to alter roles and permissions -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color:#44475a; 3 | } 4 | 5 | a.navbar-brand { 6 | font-family:'Share Tech', sans-serif; 7 | color: #bbe1fa; 8 | } 9 | 10 | a.nav-link { 11 | color: #bbe1fa; 12 | } 13 | 14 | .nav-item:hover { 15 | background-color: #0f4c75 16 | } 17 | 18 | nav.navbar { 19 | background-color: #282a36; 20 | } 21 | 22 | div.card { 23 | background-color: #282a36; 24 | color: #f8f8f2; 25 | } 26 | 27 | p, a.nav-link { 28 | font-family: 'Open Sans', sans-serif 29 | } -------------------------------------------------------------------------------- /services/ArticleService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ArticleService 3 | * @author John L. Carveth 4 | */ 5 | 6 | module.exports = class ArticleService { 7 | /** 8 | * @constructor ArticleService 9 | * @param {module:ArticleModel} articleModel 10 | */ 11 | constructor (articleModel) { 12 | this.articleModel = articleModel 13 | } 14 | 15 | /** 16 | * Creates a new Article 17 | * @memberof module:ArticleService 18 | * @function createArticle 19 | * @param {String} title article title 20 | * @param {String} tagline article tagline / subtitle 21 | * @param {ObjectID} author User ID of article author 22 | * @param {String} content article body content, in Markdown format 23 | * @param {String} tags for categorization, saparated by commas 24 | */ 25 | async createArticle (title, tagline, author, content, tags) { 26 | try { 27 | const article = { 28 | title:title, 29 | tagline:tagline, 30 | author:author, 31 | content:content, 32 | tags:tags 33 | } 34 | 35 | return this.articleModel.create(article) 36 | } catch (e) { 37 | throw new Error(e) 38 | } 39 | } 40 | 41 | /** 42 | * Approves an article, making it visible to the end user 43 | * @memberof module:ArticleService 44 | * @function approveArticle 45 | * @param {ObjectID} id id of the article to be approved 46 | * @return true if the article with given ID is approved. 47 | */ 48 | async approveArticle (id) { 49 | try { 50 | return this.articleModel.updateOne({_id:id}, {approved:true}).exec() 51 | } catch (e) { 52 | throw new Error(e) 53 | } 54 | } 55 | 56 | /** 57 | * Deletes an article with the given ID value 58 | * @memberof module:ArticleService 59 | * @function deleteArticle 60 | * @param {ObjectID} id id of the article to be removed 61 | */ 62 | async deleteArticle (id) { 63 | try { 64 | return this.articleModel.deleteOne({_id:id}).exec() 65 | } catch (e) { 66 | throw new Error(e) 67 | } 68 | } 69 | 70 | /** 71 | * Updates the content of the article with the given ID. 72 | * Also updates the article's date 73 | * @memberof module:ArticleService 74 | * @function updateArticle 75 | * @param {ObjectID} id id of article to be updated 76 | * @param {String} content the edited content, in Markdown format 77 | */ 78 | async updateArticle (id, content) { 79 | try { 80 | return this.articleModel.updateOne({_id:id}, {content:content, date:Date.now()}).exec() 81 | } catch (e) { 82 | throw new Error(e) 83 | } 84 | } 85 | 86 | /** 87 | * Gets the article with the given ID, if it exists 88 | * @memberof module:ArticleService 89 | * @function getArticleByID 90 | * @param {ObjectID} id id of article to fetch 91 | * @return the article with given id 92 | */ 93 | async getArticleByID (id) { 94 | try { 95 | const article = await this.articleModel.findById({_id:id}).exec() 96 | return article 97 | } catch (e) { 98 | throw e 99 | } 100 | } 101 | 102 | /** 103 | * Gets *n* recent articles 104 | * @memberof module:ArticleService 105 | * @function getRecentArticles 106 | * @param {Number} n number of articles to fetch 107 | */ 108 | async getRecentArticles (n) { 109 | try { 110 | return this.articleModel.find({approved:true}).sort({date:-1}).limit(n).exec() 111 | } catch (e) { 112 | throw new Error(e) 113 | } 114 | } 115 | 116 | /** 117 | * Gets all articles published by a given author 118 | * @memberof module:ArticleService 119 | * @function getArticlesByAuthor 120 | * @param {ObjectID} id author id 121 | */ 122 | async getArticlesByAuthor (id) { 123 | try { 124 | return this.articleModel.find({author:id}).exec() 125 | } catch (e) { 126 | throw new Error(e) 127 | } 128 | } 129 | 130 | /** 131 | * Finds all articles with matching tag(s). Multiple tags should work, provided 132 | * they're separated by commas. 133 | * @memberof module:ArticleService 134 | * @function getArticlesByTag 135 | * @param {String} tags tags to search for 136 | */ 137 | async getArticlesByTag (tags) { 138 | try { 139 | const exp = new RegExp(tags, 'g') 140 | return this.articleModel.find({tags: {$regex: exp.toString()}}).exec() 141 | } catch (e) { 142 | throw new Error(e) 143 | } 144 | } 145 | 146 | /** 147 | * Posts a comment onto an article 148 | * @memberof module:ArticleService 149 | * @function postComment 150 | * @param {*} articleID where the comment will be posted 151 | * @param {*} commentAuthor id of author of the comment 152 | * @param {*} content the comment's content 153 | */ 154 | async postComment (articleID, commentAuthor, content) { 155 | try { 156 | const comment = {author: commentAuthor, content:content} 157 | return ArticleModel.findOneAndUpdate({_id:articleID}, { 158 | $push: {comments: comment} 159 | }).exec() 160 | } catch (e) { 161 | throw new Error(e) 162 | } 163 | } 164 | 165 | /** 166 | * Removes all comments on an article posted by commentAuthor 167 | * @memberof module:ArticleService 168 | * @function removeComment 169 | * @param {*} articleID id of article where offending comment was posted 170 | * @param {*} commentAuthor id of author of offending comment 171 | */ 172 | async removeComment (articleID, commentAuthor) { 173 | try { 174 | return ArticleModel.findOneAndUpdate({_id:articleID}, { 175 | $pull: {comments: {author:commentAuthor}}}).exec() 176 | } catch (e) { 177 | throw new Error(e) 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /services/IPService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles the business logic for IP addresses. 3 | * IPService can ban and unban IP addresses, as well as see if an IP address 4 | * has been banned. 5 | * 6 | * @module IPService 7 | * @author John L. Carveth 8 | */ 9 | module.exports = class IPService { 10 | /** 11 | * @constructor IPService 12 | * @param {IPModel} ipModel 13 | */ 14 | constructor (ipModel) { 15 | this.ipModel = ipModel 16 | } 17 | 18 | /** 19 | * Bans an IP address 20 | * @memberof module:IPService 21 | * @function banAddress 22 | * @param {*} address an IPv4 or IPv6 address 23 | * @param {*} reason optional reason for being banned 24 | */ 25 | async banAddress (address, reason) { 26 | return this.ipModel.create({ 27 | address: address, 28 | reason: reason 29 | }) 30 | } 31 | 32 | /** 33 | * Unbans an IP address 34 | * @memberof module:IPService 35 | * @function unbanAddress 36 | * @param {*} address an IPv4 or IPv6 address 37 | */ 38 | async unbanAddress (address) { 39 | return this.ipModel.deleteOne({address:address}).exec() 40 | } 41 | 42 | /** 43 | * Checks if given address is banned 44 | * @memberof module:IPService 45 | * @function checkAddress 46 | * @param {*} address the address to check 47 | * @return true if the address is found in the collection 48 | */ 49 | async checkAddress (address) { 50 | return this.ipModel.findOne({address:address}).exec() 51 | } 52 | } -------------------------------------------------------------------------------- /services/RoleService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module RoleService 3 | * @author John L. Carveth 4 | */ 5 | 6 | module.exports = class RoleService { 7 | 8 | /** 9 | * @constructor RoleService 10 | * @param {models/RoleModel} roleModel 11 | */ 12 | constructor (roleModel) { 13 | this.roleModel = roleModel; 14 | } 15 | 16 | /** 17 | * Fetches an array of all permissions assigned to given role 18 | * @memberof module:RoleService 19 | * @function getRolePermissions 20 | * @param {String} roleName 21 | * @return {Array.} an array of permissions assigned to the role. Empty if the role doesn't exist 22 | */ 23 | async getRolePermissions (roleName) { 24 | return this.roleModel.findAll({ 25 | role: roleName 26 | }).exec() 27 | } 28 | 29 | /** 30 | * Grants a new permission to the given role. If the role doesn't exist, it is created, with its sole permission being `permission` 31 | * @memberof module:RoleService 32 | * @function grantPermission 33 | * @param {String} roleName the name of the role to which a permission will be granted 34 | * @param {String} permission the permission to be granted 35 | */ 36 | async grantPermission (roleName, permission) { 37 | const role = await roleModel.findOne({role:roleName}).exec() 38 | var perms = role.permissions 39 | 40 | perms.concat(','+permission) 41 | 42 | return this.roleModel.update({role:roleName}, {permissions:perms}).exec() 43 | } 44 | 45 | /** 46 | * Revokes access to a permission of roleName 47 | * @memberof module:RoleService 48 | * @function revokePermission 49 | * @param {String} roleName the name of the role whose permission is being revoked 50 | * @param {String} permission the permission to revoke 51 | */ 52 | async revokePermission (roleName, permission) { 53 | const role = await roleModel.findOne({role:roleName}).exec() 54 | var perms = role.permissions 55 | 56 | // Splice the permission 57 | perms = perms.split(',') 58 | const index = perms.indexOf(permission) 59 | if (index != -1) perms.splice(index, 1) 60 | perms = perms.join(',') 61 | 62 | return this.roleModel.update({role:roleName}, {permissions:perms}).exec() 63 | } 64 | 65 | /** 66 | * Revokes all permissions that were granted to `roleName` 67 | * @memberof module:RoleService 68 | * @function revokeAllPermissions 69 | * @param {String} roleName 70 | */ 71 | async revokeAllPermissions (roleName) { 72 | return this.roleModel.update({role:roleName}, {permissions:""}).exec() 73 | } 74 | 75 | /** 76 | * Gets all roles and the permissions they have 77 | * @memberof module:RoleService 78 | * @function getAllRoles 79 | */ 80 | async getAllRoles () { 81 | return this.roleModel.find({}).exec() 82 | 83 | } 84 | 85 | /** 86 | * @memberof module:RoleService 87 | * @function checkPermission 88 | * @param {*} role 89 | * @param {*} permission 90 | */ 91 | async checkPermission (role,permission) { 92 | return this.roleModel.findOne({role}).exec() 93 | 94 | } 95 | } -------------------------------------------------------------------------------- /services/UserService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module UserService 3 | * @author John L. Carveth 4 | */ 5 | module.exports = class UserService { 6 | /** 7 | * @constructor UserService 8 | * @param {module:UserModel} UserModel 9 | * @param {module:Authenticator} auth 10 | */ 11 | constructor (UserModel, auth) { 12 | this.UserModel = UserModel 13 | this.Authenticator = auth 14 | } 15 | 16 | /** 17 | * Registers a new user to the system 18 | * @memberof module:UserService 19 | * @function register 20 | * @param {String} username 21 | * @param {String} email 22 | * @param {String} password 23 | */ 24 | register (username, email, password) { 25 | const hash = this.Authenticator.hashPassword(password) 26 | return this.UserModel.create({ 27 | 'username': username, 28 | 'email': email, 29 | 'password': hash.hash, 30 | 'salt': hash.salt 31 | }) 32 | } 33 | 34 | /** 35 | * @memberof module:UserService 36 | * @function login 37 | * @param {String} email 38 | * @param {String} password 39 | * @return A token generated for the user, or an error if authentication failed 40 | */ 41 | login (email, password) { 42 | return new Promise(async (resolve,reject) => { 43 | const user = await this.UserModel.findOne({email:email}).exec() 44 | const valid = this.Authenticator.comparePassword(password, user.password, user.salt) 45 | 46 | if (valid) { 47 | this.Authenticator.generateToken(email, user.role).then((token) => { 48 | resolve(token) 49 | }) 50 | } else { 51 | reject('Authentication has failed.') 52 | } 53 | }) 54 | } 55 | 56 | /** 57 | * Removes the given user from the system 58 | * @memberof module:UserService 59 | * @function deleteUser 60 | * @param {ObjectID} id the unique ID of the user to delete 61 | */ 62 | deleteUser (id) { 63 | return this.UserModel.deleteOne({_id:id}).exec() 64 | } 65 | 66 | /** 67 | * Change a registered user's existing password. 68 | * Checks to see if oldPass matches the current password before making a change. 69 | * @memberof module:UserService 70 | * @function changePassword 71 | * @param {String} oldPass the user's old password to be changed 72 | * @param {String} newPass the desired new password 73 | * @param {String} email the user's email address. 74 | */ 75 | async changePassword (oldPass, newPass, email) { 76 | try { 77 | const user = this.UserModel.findOne({email:email}).exec() 78 | const valid = user.password == this.Authenticator.hashWithSalt( 79 | oldPass, user.salt 80 | ) 81 | 82 | if (valid) { 83 | const hash = this.Authenticator.hashPassword(newPass) 84 | return UserModel.updateOne({email:email}, {password:hash.hash, salt:hash.salt}).exec() 85 | } else throw new Error('Could not change password.') 86 | } catch (e) { 87 | throw new Error(e) 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Authenticator 3 | * @requires jsonwebtoken 4 | * @requires crypto 5 | * 6 | * A JWT wrapper module that handles password hashing and token generation 7 | */ 8 | 9 | /** 10 | * @const jsonwebtoken OAuth JWT module 11 | */ 12 | const jwt = require('jsonwebtoken') 13 | 14 | /** 15 | * @const crypto Node.js crypto module 16 | */ 17 | const crypto = require('crypto') 18 | 19 | module.exports = { 20 | /** 21 | * Generates a new JWT 22 | * @memberof module:Authenticator 23 | * @function generateToken 24 | * @param {String} email Token holder's email address 25 | * @param {String} role Token holder's assigned role 26 | */ 27 | generateToken : function (email, role) { 28 | return new Promise((resolve,reject) => { 29 | const payload = { 30 | email: email, 31 | role: role 32 | } 33 | 34 | jwt.sign(payload, process.env.secretKey, { 35 | expiresIn: '1h' 36 | }, (err, token) => { 37 | if (err) reject(err) 38 | else resolve(token) 39 | }) 40 | }) 41 | }, 42 | 43 | /** 44 | * Verifies a json-web-token 45 | * @memberof module:Authenticator 46 | * @function verifyToken 47 | * @param {String} token JWT to be verified 48 | * @return the decoded token, or throw an error 49 | */ 50 | verifyToken : function (token) { 51 | try { 52 | return jwt.verify(token, process.env.secretKey) 53 | } catch (e) { 54 | throw e 55 | } 56 | }, 57 | 58 | /** 59 | * encrypts the provided password 60 | * @memberof module:Authenticator 61 | * @function hashPassword 62 | * @param {String} password the string to be hashed 63 | * @return {Object.} object containing the hash and salt 64 | */ 65 | hashPassword : function (password) { 66 | const salt = this.generateSalt() 67 | const hash = crypto.createHmac('sha512', salt).update(password).digest('hex') 68 | return { 69 | hash:hash, 70 | salt:salt 71 | } 72 | }, 73 | 74 | /** 75 | * Hashes a password with a pre-determined salt 76 | * @memberof module:Authenticator 77 | * @function hashWithSalt 78 | * @param {String} password the password to be hashed 79 | * @param {String} salt a random salting string 80 | * @return {Object.} object containing the hash and salt 81 | */ 82 | hashWithSalt : function (password, salt) { 83 | const hash = crypto.createHmac('sha512', salt).update(password).digest('hex') 84 | 85 | return { 86 | hash:hash, 87 | salt:salt 88 | } 89 | }, 90 | 91 | /** 92 | * Compares a plaintext password to a hashed string 93 | * @memberof module:Authenticator 94 | * @function comparePassword 95 | * @param {String} password - String password attempt 96 | * @param {String} hash - hash 97 | * @param {String} salt - salt 98 | * @return {Boolean} true if the password and hash match 99 | */ 100 | comparePassword : function (password, hash, salt) { 101 | return this.hashWithSalt(password, salt).hash == hash 102 | }, 103 | 104 | /** 105 | * Generates a random alphanumeric string 106 | * @memberof module:Authenticator 107 | * @function generateSalt 108 | * @return {String} a 16-byte alphanumeric string 109 | */ 110 | generateSalt : function () { 111 | return crypto.randomBytes(16).toString('hex') 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /utils/tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Tags 3 | */ 4 | 5 | /** 6 | * @memberof module:tags 7 | * @function parseTagString 8 | * @param {*} tagString the string of tags separated by commas 9 | * @return an array of tags 10 | */ 11 | const parseTagString = function (tagString) { 12 | return tagString.split(',') 13 | } 14 | 15 | /** 16 | * @memberof module:tags 17 | * @function compileTagString 18 | * @param {*} tagArray an array of tags 19 | * @return a string of tags, separated by commas 20 | */ 21 | const compileTagString = function (tagArray) { 22 | return tagArray.join(',') 23 | } 24 | 25 | /** 26 | * @memberof module:tags 27 | * @function removeTag 28 | * @param {*} tag the tag to be removed 29 | * @param {*} tagArray the tag array 30 | * @return a string of tags, separated by commas 31 | */ 32 | const removeTag = function (tag, tagArray) { 33 | const index = tagArray.indexOf(tag) 34 | if (index != -1) { 35 | tagArray.splice(index,1) 36 | } 37 | return compileTagString(tagArray) 38 | } 39 | 40 | module.exports = { 41 | parseTagString, 42 | compileTagString, 43 | removeTag 44 | } 45 | -------------------------------------------------------------------------------- /utils/validator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Validator 3 | */ 4 | module.exports = { 5 | /** 6 | * @memberof module:Validator 7 | * @function isEmail 8 | * Function that validates a given string as an email address. 9 | */ 10 | isEmail : function (email) { 11 | var re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/; 12 | return re.test(email) 13 | } 14 | } -------------------------------------------------------------------------------- /views/pages/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include('../partials/head'); -%> 6 | 7 | 8 |
9 | <%- include('../partials/navbar'); -%> 10 |
11 |
12 |
13 | <%- include('../partials/post', {'post' : post}) %> 14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /views/partials/head.ejs: -------------------------------------------------------------------------------- 1 | Blog 2 | 3 | 4 | 7 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /views/partials/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/post.ejs: -------------------------------------------------------------------------------- 1 | 17 |
18 |
19 |

<%- post.title %>

20 |
<%- post.subtitle %>
21 |
<%- post.author %>, <%- post.date %>
22 |
23 |

<%- post.content %>

24 |
25 |
--------------------------------------------------------------------------------