diff --git a/.gitignore b/.gitignore index d6b2614..aa48b01 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,4 @@ typings/ # keep config and blog posts out of this config.json -posts/ \ No newline at end of file +static-text.json \ No newline at end of file diff --git a/config.example.json b/config.example.json index 5da6bc6..98a25e6 100644 --- a/config.example.json +++ b/config.example.json @@ -2,18 +2,14 @@ "http": { "port": 8080 }, - "blog": { - "authors": [ - { - "name": "redducks", - "description": "passionate and violence loving programmer", - "image": "https://img00.deviantart.net/a0f8/i/2012/356/3/3/generic_anime_girl__1_by_light1523-d5ou547.png" - } - ] - }, "database": { "url": "mongodb://localhost:27017/pretendo_website" }, + "contactWebhook": { + "port": 443, + "host": "host.com", + "path": "/webhookurl" + }, "secrets": { "session": "session secret here" } diff --git a/helpers/common.js b/helpers/api.js similarity index 82% rename from helpers/common.js rename to helpers/api.js index 11f55f3..4b939c1 100644 --- a/helpers/common.js +++ b/helpers/api.js @@ -1,17 +1,12 @@ /* -common.js - -common page functionality. +api.js - +common api returns */ -// shows 404 template. -function sendDefault404(res) { - res.status(404).send('404'); -} - // use for any api return. it has basic layout used for every return. -function sendApiReturn(res, data, errors) { +function sendReturn(res, data, errors) { res.status(200).json( // combine 2 objects Object.assign({ @@ -64,8 +59,7 @@ function sendApiError(res, code, errors) { } module.exports = { - sendDefault404, - sendApiReturn, + sendReturn, sendApi404, sendApiGenericError, sendApiError, diff --git a/helpers/blog-helper.js b/helpers/blog-helper.js deleted file mode 100644 index 1d12366..0000000 --- a/helpers/blog-helper.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - -blog-helper.js - -blog post helper functionality - -*/ - -// imports -const fs = require('fs'); -const authors = require('../config.json').blog.authors; -const showdown = require('showdown'); -const converter = new showdown.Converter({metadata: true}); - - -function getAuthorByID(authorId) { - return authors[authorId]; -} - -function getBlogPostAsMarkdown(postId) { - try { - return fs.readFileSync(`posts/${postId}.md`); - } catch(exception) { - return null; - } -} - -function getBlogPostAsHtml(postId) { - const markdown = getBlogPostAsMarkdown(postId); - if (!markdown) return; - return converter.makeHtml(markdown); -} - -function getBlogPostExpressReady(postId) { - const markdown = getBlogPostAsMarkdown(postId); - if (!markdown) return; - const html = converter.makeHtml(markdown); - const metadata = converter.getMetadata(); - const hbsObject = { - content: html, - date: metadata.releaseDate, - category: metadata.category, - author: getAuthorByID(metadata.authorId) - }; - - return hbsObject; -} - -function writeMarkdownToFile(text) { - const length = fs.readdirSync('posts').length; - fs.writeFileSync(`posts/${length}.md`, text); -} - -module.exports = { - getBlogPostAsHtml, - getBlogPostAsMarkdown, - getBlogPostExpressReady, - getAuthorByID, - writeMarkdownToFile -}; diff --git a/helpers/util.js b/helpers/util.js new file mode 100644 index 0000000..cff7eba --- /dev/null +++ b/helpers/util.js @@ -0,0 +1,15 @@ +/* + +util.js - +small commonly used utilities + +*/ + +// shows 404 template. takes express response object +function sendDefault404(res) { + res.status(404).send('404'); +} + +module.exports = { + sendDefault404 +}; \ No newline at end of file diff --git a/middleware/admin-authentication.js b/middleware/admin-authentication.js index 4717352..777fb45 100644 --- a/middleware/admin-authentication.js +++ b/middleware/admin-authentication.js @@ -6,23 +6,23 @@ Middleware file for authentication checking */ // imports -const common = require('../helpers/common'); +const apiHelper = require('../helpers/api'); // middleware to use if admin authentication is required -function adminAuthenticationRequired(req, res, next) { +function adminAuthNeeded(req, res, next) { if (req.isAuthenticated() && req.user.role && req.user.role === 'admin') { return next(); } else { - common.sendApiAuthError(res); + apiHelper.sendApiAuthError(res); } } -// middleware to use if authentication -function authenticationOptional(req, res, next) { +// middleware to use if authentication is optional +function authOptional(req, res, next) { return next(); } module.exports = { - adminAuthenticationRequired, - authenticationOptional + adminAuthNeeded, + authOptional }; \ No newline at end of file diff --git a/models/admin-user.js b/models/admin-user.js index 5240dbc..84a7127 100644 --- a/models/admin-user.js +++ b/models/admin-user.js @@ -20,6 +20,7 @@ const adminUserSchema = new mongoose.Schema({ unique: true, trim: true }, + // non hashed, gets hashed at save password: { type: String, required: [true, 'Password is required.'], diff --git a/models/blog-post.js b/models/blog-post.js new file mode 100644 index 0000000..fbf3804 --- /dev/null +++ b/models/blog-post.js @@ -0,0 +1,104 @@ +/* + +blog-post.js - +file containing the model file for a blog post + +*/ + +// imports +const mongoose = require('mongoose'); +const postAuthor = require('./post-author').postAuthorModel; +const showdown = require('showdown'); +const moment = require('moment'); +const converter = new showdown.Converter(); +converter.setFlavor('github'); + +// admin user database layout +const blogPostSchema = new mongoose.Schema({ + // html in content + content: { + type: String, + required: [true, 'Content is required.'], + trim: true + }, + // title of blog post + name: { + type: String, + required: [true, 'Name is required'] + }, + // short description of blog post + short: { + type: String, + required: [true, 'Short version is required'] + }, + meta: { + slug: { + type: String, + required: [true, 'Author is required'], + trim: true + }, + author: { + type: String, + required: [true, 'Author is required'], + trim: true + }, + date: { + type: Date, + default: () => { + return new Date(moment(new Date(), 'YYYY-MM-DD')); + } + }, + category: { + type: String, + required: [true, 'category is required'], + trim: true + } + } +}); + +blogPostSchema.methods.postTemplate = function(callback) { + const self = this; + postAuthor.findById(this.meta.author, function (err, author) { + callback(err, { + content: self.content, + title: self.name, + date: self.meta.date, + category: self.meta.category, + author: author.authorTemplate() + }); + }); +}; +blogPostSchema.methods.postShortTemplate = function() { + return { + content: this.short, + title: this.name, + url: moment(this.meta.date, 'YYYY-MM-DD') + '/' + this.meta.slug + }; +}; + +blogPostSchema.statics.markdownToHtml = function(markdown) { + return converter.makeHtml(markdown); +}; +blogPostSchema.statics.getPost = function(date, slug, callback) { + return blogPostModel.findOne({ + 'meta.date': date, + 'meta.slug': slug + }, callback); +}; +// not tested +blogPostSchema.statics.latestPostsShortTemlate = function(amount, callback) { + blogPostModel.find({}).sort({'meta.date': 'desc'}).exec(function(err, posts) { + if (err) return callback(err); + let out = []; + for (let i = 0, l = posts.length; i < ( amount+1 < l ? amount+1 : l); i++) + out += posts[i].postShortTemplate(); + callback(err, out); + }); +}; + +const blogPostModel = mongoose.model('blogPost', blogPostSchema); + +module.exports = { + blogPostModel, + blogPostSchema +}; \ No newline at end of file diff --git a/models/post-author.js b/models/post-author.js new file mode 100644 index 0000000..4f7287a --- /dev/null +++ b/models/post-author.js @@ -0,0 +1,43 @@ +/* + +post-author.js - +file containing the model for authors + +*/ + +// imports +const mongoose = require('mongoose'); + +// post author database layout +const postAuthorSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, 'Name is required'], + trim: true + }, + description: { + type: String, + required: [true, 'Description is required'], + trim: true + }, + // profile picture image link + image: { + type: String, + trim: true + } +}); + +postAuthorSchema.methods.authorTemplate = function() { + return { + name: this.name, + description: this.description, + profilePicture: this.image + }; +}; + +const postAuthorModel = mongoose.model('postAuthor', postAuthorSchema); + +module.exports = { + postAuthorModel, + postAuthorSchema +}; \ No newline at end of file diff --git a/models/progress-list.js b/models/progress-list.js new file mode 100644 index 0000000..def6205 --- /dev/null +++ b/models/progress-list.js @@ -0,0 +1,36 @@ +/* + +progress-list.js - +file containing the model for progress list + +*/ + +// imports +const mongoose = require('mongoose'); + +// progress text schema +const progressListSchema = new mongoose.Schema({ + state: { + type: Number, /* 0 - nothing, 1 - no support, 2 - partially working, 3 - works */ + default: 0 + }, + isGame: { + type: Boolean, /* true - is game, false - is backend service */ + required: [true, 'isGame is required'] + }, + title: { + type: String, + required: [true, 'Title is required'] + }, + description: { + type: String, + required: [true, 'Description is required'] + } +}); + +const progressListModel = mongoose.model('progress', progressListSchema); + +module.exports = { + progressListModel, + progressListSchema +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c3a9751..205475e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1746,6 +1746,11 @@ } } }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, "mongodb": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.6.tgz", diff --git a/package.json b/package.json index c7b1948..fff669b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "express": "^4.16.2", "express-handlebars": "^3.0.0", "express-session": "^1.15.6", + "moment": "^2.22.2", "mongoose": "^5.3.2", "mongoose-unique-validator": "^2.0.2", "passport": "^0.4.0", diff --git a/passport.config.js b/passport.config.js index dfbbed9..782e09c 100644 --- a/passport.config.js +++ b/passport.config.js @@ -42,7 +42,7 @@ module.exports = (app) => { }); } )); - //Configuring app to have sessions + //Configuring app to have sessions, dont change since it would break everything passport.serializeUser(function(user, done) { done(null, user.id); }); diff --git a/routes/admin.js b/routes/admin.js index 6d2197e..5332ca9 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,18 +1,24 @@ /* admin.js - -file for handling admin panel routes +file for handling admin api. */ // imports const router = require('express').Router(); const passport = require('passport'); -const common = require('../helpers/common'); +const moment = require('moment'); +const apiHelper = require('../helpers/api'); const adminUserMiddleware = require('../middleware/admin-authentication'); -const adminUser = require('../models/admin-user'); -// display admin panel +// database models +const adminUser = require('../models/admin-user'); +const blogPost = require('../models/blog-post'); +const postAuthor = require('../models/post-author'); +const progressList = require('../models/progress-list'); + +// renders admin.hbs router.get('/admin', (req, res) => { res.render('admin'); }); @@ -23,20 +29,21 @@ router.get('/admin', (req, res) => { * signs admin user in * * post { -* username - username of admin account -* password - password of admin account +* username +* password * } * return { * code: http code -* success: boolean - true if login succesfull -* username: undefined | string - username if login was successfull +* success: boolean +* username: undefined | string - only if login was successfull * role: undefined | string - role of user if login was successfull * errors: Strings[messages] - not yet :( * } */ // TODO make login somehow display errors in correct format. +// middleware does the authentication work. this just returns a success router.post('/admin/api/v1/login', passport.authenticate('adminUserStrategy'), function (req, res) { - common.sendApiReturn(res, { + apiHelper.sendReturn(res, { username: req.user.username, role: req.user.role ? req.user.role : undefined }); @@ -60,10 +67,10 @@ router.post('/admin/api/v1/login', passport.authenticate('adminUserStrategy'), f * errors: Strings[messages] * } */ -router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthenticationRequired, (req, res) => { +router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthNeeded, (req, res) => { if (!req.body) { // no post body - common.sendApiGenericError(res); + apiHelper.sendApiGenericError(res); return; } @@ -73,20 +80,80 @@ router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthenticationReq password }); + // saving to database newUser.save().then(() => { - // successfull - common.sendApiReturn(res, { + apiHelper.sendReturn(res, { username: req.user.username, role: req.user.role ? req.user.role : undefined }); return; }).catch((rejection) => { // TODO format exception so it doesnt have a huge list of errors - common.sendApiError(res, 500, [rejection]); + apiHelper.sendApiError(res, 500, [rejection]); return; }); }); +/* +* /admin/api/v1/removeadmin +* - requires admin auth +* +* registers a new admin user +* +* post { +* id - id of the admin user +* } +* return { +* code: httpcode +* success: boolean - true if delete was successull +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/removeadmin', adminUserMiddleware.adminAuthNeeded, (req, res) => { + if (!req.body) { + // no post body + apiHelper.sendApiGenericError(res); + return; + } + + const { id } = req.body; + adminUser.adminUserModel.findByIdAndDelete(id, (err) => { + if (err) return apiHelper.sendApiError(res, 500, [err]); + // successfull + apiHelper.sendReturn(res, {}); + }); +}); + +/* +* /admin/api/v1/listadmins +* - requires admin auth +* +* gets list of admins +* +* return { +* code: httpcode +* success: boolean - true if delete was successull +* errors: Strings[messages] +* } +*/ +router.get('/admin/api/v1/listadmins', adminUserMiddleware.adminAuthNeeded, (req, res) => { + adminUser.adminUserModel.find({}, (err, admins) => { + // TODO format exception so it doesnt have a huge list of errors + if (err) return apiHelper.sendApiError(res, 500, [err]); + + // formats admin list and removes password hash + const output = []; + for (let i = 0, l = admins.length; i < l; i++) { + admins[i].password = undefined; + output.push(admins[i]); + } + + apiHelper.sendReturn(res, { + admins: output + }); + }); +}); + /* * /admin/api/v1/check * @@ -100,8 +167,8 @@ router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthenticationReq * errors: Strings[messages] * } */ -router.get('/admin/api/v1/check', adminUserMiddleware.authenticationOptional, (req, res) => { - common.sendApiReturn(res, { +router.get('/admin/api/v1/check', adminUserMiddleware.authOptional, (req, res) => { + apiHelper.sendReturn(res, { isAuthed: req.user ? true : false, role: req.user ? (req.user.role ? req.user.role : undefined) : undefined }); @@ -118,9 +185,283 @@ router.get('/admin/api/v1/check', adminUserMiddleware.authenticationOptional, (r * errors: Strings[messages] * } */ -router.get('/admin/api/v1/logout', adminUserMiddleware.adminAuthenticationRequired, (req, res) => { +router.get('/admin/api/v1/logout', adminUserMiddleware.adminAuthNeeded, (req, res) => { req.logout(); - common.sendApiReturn(res, {}); + apiHelper.sendReturn(res, {}); +}); + +/* +* /admin/api/v1/newpost +* +* posts a new blog post +* +* post { +* content - content of the blog post in markdown +* title - title of the blog post +* author - id of the author +* short - short description of content in plain text +* category - category name of the blog post +* +* } +* return { +* code: http code +* success: boolean - true if login succesfull +* url: string | undefined - url of the blog post if successfull +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/newpost', adminUserMiddleware.adminAuthNeeded, function (req, res) { + + if (!req.body) return apiHelper.sendApiGenericError(res); + + const { content, title, author, category, short } = req.body; + const newBlogPost = new blogPost.blogPostModel({ + content: blogPost.blogPostModel.markdownToHtml(content), + name: title, + short, + meta: { + author, + category, + slug: title // convert title to slug + .trim() + .replace(/\s/g, '-') + .replace(/[^A-z0-9-]/g, '') + .toLowerCase() + } + }); + + // saving post to database + newBlogPost.save().then((post) => { + apiHelper.sendReturn(res, { + url: moment(post.meta.date, 'YYYY-MM-DD') + '/' + post.meta.slug + }); + }).catch((rejection) => { + // TODO format exception so it doesnt have a huge list of errors + apiHelper.sendApiError(res, 500, [rejection]); + return; + }); +}); + +/* +* /admin/api/v1/editpost +* +* edits a blog post +* +* post { +* id - id of the blog post to be edited +* content - content of the blog post IN HTML +* title - title of the blog post +* author - id of the author +* short - short description of content in plain text +* category - category name of the blog post +* } +* return { +* code: http code +* success: boolean - true if login succesfull +* url: string | undefined - url of the blog post if successfull +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/editpost', adminUserMiddleware.adminAuthNeeded, function (req, res) { + + if (!req.body) return apiHelper.sendApiGenericError(res); + + const { id, content, title, author, category, short } = req.body; + blogPost.blogPostModel.findByIdAndUpdate(id, { + 'content': content, + 'name': title, + 'short': short, + 'meta.author': author, + 'meta.category': category + }, (err, post) => { + if (err) return apiHelper.sendApiError(res, 500, [err]); + + apiHelper.sendReturn(res, { + url: moment(post.meta.date, 'YYYY-MM-DD') + '/' + post.meta.slug + }); + }); +}); + +/* +* /admin/api/v1/newauthor +* +* creates new author +* +* post { +* name - author name +* description - author description +* image - image url for profile picture +* } +* return { +* code: http code +* success: boolean - true if author succesfull +* id: string | undefined - id of the new author if successfull +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/newauthor', adminUserMiddleware.adminAuthNeeded, function (req, res) { + + if (!req.body) return apiHelper.sendApiGenericError(res); + + const { name, description, image } = req.body; + const newAuthor = new postAuthor.postAuthorModel({ + name, + description, + image + }); + + // saving author to database + newAuthor.save().then((author) => { + apiHelper.sendReturn(res, { + id: author.id + }); + }).catch((rejection) => { + // TODO format exception so it doesnt have a huge list of errors + apiHelper.sendApiError(res, 500, [rejection]); + return; + }); +}); + +/* +* /admin/api/v1/editauthor +* +* edit an existing author +* +* post { +* id - id of author to edit +* name - author name +* description - author description +* image - image url for profile picture +* } +* return { +* code: http code +* success: boolean - true if author succesfull +* id: String - id of the edited author +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/editauthor', adminUserMiddleware.adminAuthNeeded, function (req, res) { + + if (!req.body) return apiHelper.sendApiGenericError(res); + + const { id, name, description, image } = req.body; + + // updating author in database + postAuthor.postAuthorModel.findByIdAndUpdate(id, { + name, + description, + image + }, (err, author) => { + // TODO format exception so it doesnt have a huge list of errors + if (err) return apiHelper.sendApiError(res, 500, [err]); + apiHelper.sendReturn(res, { + id: author.id + }); + }); +}); + +/* +* /admin/api/v1/newprogress +* +* creates a new progress entry +* +* post { +* title - progress entry name +* description - progress entry description +* state - 0: backend service entry, 1: no support, 2: partial support, 3: fullly working +* } +* return { +* code: http code +* success: boolean - true if progress succesfull +* id: String | undefined - sends if successfull +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/newprogress', adminUserMiddleware.adminAuthNeeded, function (req, res) { + + if (!req.body) return apiHelper.sendApiGenericError(res); + + // parses state and isGame to be valid + const { title, description } = req.body; + let { state } = req.body; + let isGame = false; + if (state != '1' && state != '2' && state != '3') { + state = undefined; + } else { + state = parseInt(state); + isGame = true; + } + + const newProgress = new progressList.progressListModel({ + title, + description, + isGame, + state + }); + + // saving progress to database + newProgress.save().then((progress) => { + apiHelper.sendReturn(res, { + id: progress.id + }); + }).catch((rejection) => { + // TODO format exception so it doesnt have a huge list of errors + apiHelper.sendApiError(res, 500, [rejection]); + return; + }); +}); + +/* +* /admin/api/v1/editprogress +* +* edit an existing progress entry +* +* post { +* title - progress entry name +* description - progress entry description +* state - 0: backend service entry, 1: no support, 2: partial support, 3: fullly working +* id - id of entry you want to edit +* } +* return { +* code: http code +* success: boolean - true if progress succesfull +* id: String - id of the edited progress entry +* errors: Strings[messages] +* } +*/ +router.post('/admin/api/v1/editprogress', adminUserMiddleware.adminAuthNeeded, function (req, res) { + + if (!req.body) return apiHelper.sendApiGenericError(res); + + // parsing state and isGame to be valid + const { title, description, id } = req.body; + let { state } = req.body; + let isGame = false; + if (state != '1' && state != '2' && state != '3') { + state = undefined; + } else { + state = parseInt(state); + isGame = true; + } + + // updating progress in database + progressList.progressListModel.findByIdAndUpdate(id, { + title, + description, + state, + isGame + }, (err, progress) => { + // TODO format exception so it doesnt have a huge list of errors + if (err) return apiHelper.sendApiError(res, 500, [err]); + apiHelper.sendReturn(res, { + id: progress.id + }); + }); +}); + +// api 404 +router.use('/admin/api', (req, res) => { + apiHelper.sendApi404(res); }); // export the router diff --git a/routes/blog.js b/routes/blog.js index f6dff60..7284e92 100644 --- a/routes/blog.js +++ b/routes/blog.js @@ -7,25 +7,99 @@ file for handling routes regarding blog posts. // imports const router = require('express').Router(); -const blogHelper = require('../helpers/blog-helper.js'); +const apiHelper = require('../helpers/api'); +const utilHelper = require('../helpers/util'); +const blogPostModel = require('../models/blog-post').blogPostModel; +const postAuthorModel = require('../models/post-author').postAuthorModel; -// display blog post -router.get('/news/:id', async (req, res) => { - if (isNaN(req.params.id)) { - res.statusCode = 404; - res.render('404'); - return; +// display single blog post +router.get('/news/:date/:title', (req, res) => { + // date format YYYY-MM-DD + if (/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(req.params.date) && /([a-z]|[0-9]|-)+/.test(req.params.title.toLowerCase())) { + // params are correct format + blogPostModel.getPost(new Date(req.params.date), req.params.title.toLowerCase(), (err, post) => { + // error exists or no post exists with the date and name + if (err || !post) { + console.log('error: ' + err + ' and post: ' + post); + return utilHelper.send404(res); + } + + // render blogpost + post.postTemplate((err, postTemplate) => { + if (err) return utilHelper.send404(res); + res.render('post', { + post: postTemplate + }); + }); + }); + } else { + // params are incorrect + utilHelper.send404(res); } +}); - const hbsObject = blogHelper.getBlogPostExpressReady(req.params.id); +// display latest blogposts +router.get('/news', (req, res) => { + // sort blogposts on date descending + blogPostModel.find({}).sort({'meta.date': 'desc'}).exec(function(err, posts) { + if (err || !posts) { + return utilHelper.send404(res); + } - if (!hbsObject) { - res.statusCode = 404; - res.render('404'); - return; - } + // makes posts template ready + const postCollection = []; + for (let i = 0, l = posts.length; i < l; i++) { + postCollection.push(posts[i].postShortTemplate()); + } - res.render('post', hbsObject); + res.render('post-collection', { + posts: postCollection + }); + }); +}); + +/* +* /api/v1/listauthors +* +* gets a list of all authors +* +* return { +* code: http code +* success: boolean - true if author succesfull +* authorList: Objects[{_id, name, description, image}] - list of authors with information +* errors: Strings[messages] +* } +*/ +router.get('/api/v1/listauthors', function (req, res) { + postAuthorModel.find({}, (err, authors) => { + // TODO format exception so it doesnt have a huge list of errors + if (err) return apiHelper.sendApiError(res, 500, [err]); + apiHelper.sendReturn(res, { + authorList: authors + }); + }); +}); + +/* +* /api/v1/listblog +* +* gets a list of all posts +* +* return { +* code: http code +* success: boolean - true if post succesfull +* postList: Objects[{_id, content, meta}] - list of posts with information +* errors: Strings[messages] +* } +*/ +router.get('/api/v1/listblog', function (req, res) { + blogPostModel.find({}, (err, posts) => { + // TODO format exception so it doesnt have a huge list of errors + if (err) return apiHelper.sendApiError(res, 500, [err]); + apiHelper.sendReturn(res, { + postList: posts + }); + }); }); // export router diff --git a/routes/contact.js b/routes/contact.js new file mode 100644 index 0000000..26c8bd3 --- /dev/null +++ b/routes/contact.js @@ -0,0 +1,79 @@ +/* + +contact.js - +file for handling routes regarding contact + +*/ + +// imports +const router = require('express').Router(); +const apiHelper = require('../helpers/api'); +const config = require('../config.json'); +const https = require('https'); + +// display contact page +router.get('/contact', (req, res) => { + res.render('contact'); +}); + +/* +* /api/v1/sendmessage +* +* registers a new admin user +* +* post { +* email - email of sender +* subject - subject of message +* message - actual message +* } +* return { +* code: httpcode +* success: boolean - true if sending was successull +* errors: Strings[messages] +* } +*/ +router.post('/api/v1/sendmessage', function (req, res) { + if (!req.body) return apiHelper.sendApiGenericError(res); + + + const { email, subject, message } = req.body; + if (email && subject && message && message.length < 2000) { + // request body has everything + const postData = JSON.stringify({ + content: 'email: ' + email + ' \n subject: ' + subject + ' \n\n' + message + }); + + // request object + const request = https.request({ + hostname: config.contactWebhook.host, + port: config.contactWebhook.port, + path: config.contactWebhook.path, + method : 'POST', + headers : { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + 'Content-Length': postData.length + } + }, () => { + // sends success + apiHelper.sendReturn(res, {}); + }); + + // error handling + request.on('error', (e) => { + apiHelper.sendApiGenericError(res); + console.log('request errored' + e); + }); + + // write post data to request + request.write(postData); + request.end(); + } else { + // TODO give more detailed response + return apiHelper.sendApiGenericError(res); + } + +}); + +// export router +module.exports = router; diff --git a/routes/home.js b/routes/home.js index 38c9045..5af1ab2 100644 --- a/routes/home.js +++ b/routes/home.js @@ -14,10 +14,5 @@ router.get('/', (req, res) => { res.render('home'); }); -// display contact page -router.get('/contact', (req, res) => { - res.render('contact'); -}); - // export the router module.exports = router; diff --git a/routes/progress.js b/routes/progress.js new file mode 100644 index 0000000..254ae6e --- /dev/null +++ b/routes/progress.js @@ -0,0 +1,56 @@ +/* + +progress.js - +file for handling routes regarding progress + +*/ + +// imports +const router = require('express').Router(); +const apiHelper = require('../helpers/api'); +const utilHelper = require('../helpers/util'); +const staticText = require('../static-text.json'); +const progressListModel = require('../models/progress-list').progressListModel; + +// display progress +router.get('/progress', (req, res) => { + + progressListModel.find({}, (err, progress) => { + if (err) return utilHelper.send404(res); + + // filtering games and backend + const games = progress.filter(i => i.isGame); + const backends = progress.filter(i => !i.isGame); + + res.render('progress', { + games, + backends, + summary: staticText.progressSummary + }); + }); +}); + +/* +* /api/v1/listprogress +* +* gets a list of progress +* +* return { +* code: http code +* success: boolean - true if progress succesfull +* progressList: Objects[{_id, title, description, state}] - list of progress with information +* errors: Strings[messages] +* } +*/ +router.get('/api/v1/listprogress', function (req, res) { + progressListModel.find({}, (err, progress) => { + // TODO format exception so it doesnt have a huge list of errors + if (err) return apiHelper.sendApiError(res, 500, [err]); + apiHelper.sendReturn(res, { + progressList: progress + }); + }); +}); + +// export router +module.exports = router; diff --git a/server.js b/server.js index 3a3d87b..416e809 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ const mongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const bodyParser = require('body-parser'); const config = require('./config.json'); -const common = require('./helpers/common'); +const utilHelper = require('./helpers/util'); const passportconfig = require('./passport.config.js'); // setup console colors @@ -45,6 +45,7 @@ app.use(session({ collection: 'sessions' }) })); +// setups up passportjs authentication passportconfig(app); // handlebars templating setup @@ -59,17 +60,29 @@ app.set('view engine', '.hbs'); const locations = { home: require('./routes/home'), posts: require('./routes/blog'), - admin: require('./routes/admin') + admin: require('./routes/admin'), + contact: require('./routes/contact'), + progress: require('./routes/progress') }; // static files app.use('/assets', express.static('assets')); // page map app.use('/', locations.home); +app.use('/', locations.contact); app.use('/', locations.posts); app.use('/', locations.admin); +app.use('/', locations.progress); app.use((req, res) => { - common.sendDefault404(res); + utilHelper.send404(res); +}); + +// TODO improve error handling +// TODO remove param decoding errors from logs example: "host/test/%" +// 4 parameters required to read the error, cant help the eslint error +app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars + console.error(err.stack); + return res.status(500).send('Something broke!'); }); // startup diff --git a/setupAdminUser.js b/setupAdminUser.js new file mode 100644 index 0000000..3b4fe24 --- /dev/null +++ b/setupAdminUser.js @@ -0,0 +1,12 @@ +const adminUser = require('./models/admin-user'); + +const newUser = new adminUser.adminUserModel({ + username: 'admin', + password: 'root' +}); + +newUser.save().then(() => { + console.log('success, user admin pass root'); +}).catch((rejection) => { + console.log(rejection); +}); \ No newline at end of file diff --git a/static-text.example.json b/static-text.example.json new file mode 100644 index 0000000..aa61ea6 --- /dev/null +++ b/static-text.example.json @@ -0,0 +1,4 @@ +{ + "progressSummary": "This is a progress summary", + "headerText": "A work-in-progress server replacement for nintendo 3ds and wiiu" +} \ No newline at end of file diff --git a/static-text.json b/static-text.json new file mode 100644 index 0000000..aa61ea6 --- /dev/null +++ b/static-text.json @@ -0,0 +1,4 @@ +{ + "progressSummary": "This is a progress summary", + "headerText": "A work-in-progress server replacement for nintendo 3ds and wiiu" +} \ No newline at end of file diff --git a/views/admin.hbs b/views/admin.hbs index 1d4185f..c328c18 100644 --- a/views/admin.hbs +++ b/views/admin.hbs @@ -20,7 +20,107 @@ +

delete admin

+
+

id

+ + +
+ +

create blog post

+
+

content

+ +

short version

+ +

category

+ +

title

+ +

author id

+ + +
+ +

edit blog post

+
+

content

+ +

short version

+ +

id

+ +

category

+ +

title

+ +

author id

+ + +
+ +

new author

+
+

name

+ +

description

+ +

image link

+ + +
+ +

edit author

+
+

id

+ +

name

+ +

description

+ +

image link

+ + +
+ +

new progress entry

+
+

title

+ +

description

+ +

state

+

0 - backend service
1 - no support
2 - partial support
3 - fully working

+ + +
+ +

edit progress entry

+
+

id

+ +

title

+ +

description

+ +

state

+

0 - backend service
1 - no support
2 - partial support
3 - fully working

+ + +
+ +
+ List of admin users +
+ List of blog posts +
+ List of progress +
+ List of authors +
Check if logged in +
+ Logout {{> footer }} diff --git a/views/contact.hbs b/views/contact.hbs index bea14ac..51fd7a7 100644 --- a/views/contact.hbs +++ b/views/contact.hbs @@ -5,7 +5,16 @@ {{> navbar }} -

contact form here

+

contact form

+
+

content

+ +

subject

+ +

email

+ + +
{{> footer }} diff --git a/views/post-collection.hbs b/views/post-collection.hbs new file mode 100644 index 0000000..ca42724 --- /dev/null +++ b/views/post-collection.hbs @@ -0,0 +1,7 @@ +

Latest posts

+{{#each posts }} +{{ this.title }} +

{{ this.content }}

+link +

+{{/each}} \ No newline at end of file diff --git a/views/post.hbs b/views/post.hbs index b986f49..e20f425 100644 --- a/views/post.hbs +++ b/views/post.hbs @@ -5,17 +5,18 @@ {{> navbar }} -

blog post

-

{{ date }}

-

{{ category }}

+

{{ post.title }}

+

{{ post.date }}

+

{{ post.category }}

- {{{ content }}} + {{{ post.content }}}
-

{{ author.name }} - {{ author.description }}

- +

{{ post.author.name }} - {{ post.author.description }}

+ {{> footer }} + diff --git a/views/progress.hbs b/views/progress.hbs new file mode 100644 index 0000000..63019f7 --- /dev/null +++ b/views/progress.hbs @@ -0,0 +1,15 @@ +

progress summary

+

{{ summary }}

+
+

game support

+{{#each games }} +{{ this.title }} +

{{ this.description }}

+

state: {{ this.state }}

+{{/each}} +

+

backend support

+{{#each backends }} +{{ this.title }} +

{{ this.description }}

+{{/each}} \ No newline at end of file