const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const request = require('request');
const crypto = require('crypto');
const uuidv4 = require('uuid/v4');
const path = require('path');
const rss = require('rss');
const marked = require('marked');
const cache = require('apicache').middleware;
const templates = require('./templates');
const Op = require('sequelize').Op;
const multer = require('multer');
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, __dirname+'/uploads/')
},
filename: function (req, file, cb) {
var ext = "";
if (file.originalname.includes(".")) {
ext = "." + file.originalname.split(".")[1];
}
return cb(null, 'img-' + Date.now() + ext)
}
})
var upload = multer({ storage: storage })
const server = express();
server.use(cookieParser())
server.use(bodyParser.urlencoded({ extended: true }));
async function addImagesAndTagsToPosts(models, posts) {
for (const post of posts) {
const images = await models.pictures.findAll({ attributes: ["source"], where: { postId: post.id } }).map(x => x.source);
post.images = images;
const tags = await models.tags.findAll({ attributes: ["text"], where: { postId: post.id } }).map(x => x.text);
post.tags = tags;
}
}
function listen(port) {
server.listen(port, () => console.info(`Listening on port ${port}!`));
}
function hashWithSalt(password, salt){
var hash = crypto.createHmac('sha512', salt);
hash.update(password);
return hash.digest("base64");
};
function formatDate(d) {
let month = d.toLocaleString('default', { month: 'long' });
return month + " " + d.getDate() + ", " + (1900+d.getYear())
}
async function formatPostsforSingle(models, postType, postId){
var posts = await models.posts.findAll({
where: {
type: postType,
id: postId
}, order: [['createdAt', 'DESC']]
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
posts.forEach(post => {
post.createdAt = formatDate(post.createdAt)
post.showTitle = post.type != "bread"
})
return posts
}
async function formatPostsForType(models, postType){
var posts = await models.posts.findAll({
where: { type: postType }, order: [['createdAt', 'DESC']]
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
posts.forEach(post => {
post.createdAt = formatDate(post.createdAt)
post.showTitle = post.type != "bread"
})
return posts;
}
function to_sitemap_xml(host, path, updated){
return `${host}${path}${updated}`
}
async function get_routes(models){
let routes = [
{ "route": "/", "updated": new Date().toISOString() },
{ "route": "/bread", "updated": new Date().toISOString() },
{ "route": "/blog", "updated": new Date().toISOString() },
{ "route": "/email", "updated": new Date().toISOString() },
{ "route": "/work-square", "updated": new Date().toISOString() },
{ "route": "/misc", "updated": new Date().toISOString() },
{ "route": "/projects", "updated": new Date().toISOString() },
{ "route": "/about", "updated": new Date().toISOString() },
]
let posts = (await models.posts.findAll()).map(x => x.get({ plain: true }));
posts.forEach(post => {
routes.push(
{ "route": `/posts/${post.type}/${post.id}`, "updated": post.updatedAt.toISOString() }
)
})
let tags = (await models.tags.findAll()).map(x => x.get({ plain: true }));
tags.forEach(tag => {
routes.push(
{ "route": `/tags/${tag.text}`, "updated": tag.updatedAt.toISOString() }
)
})
return routes
}
async function sitemap(models) {
let routes = await get_routes(models)
let host = "https://marks.kitchen"
let urlset = []
routes.forEach(item => {
urlset.push(to_sitemap_xml(host, item.route, item.updated))
})
return `${urlset.join("")}`;
}
function setUpRoutes(models, jwtFunctions, database, templates) {
// Authentication routine
server.use(function (req, res, next) {
if (req.path.toLowerCase().startsWith("/admin")) {
let cookie = req.cookies.authorization
if (!cookie) {
console.debug("Redirecting to login - no cookie")
res.redirect('/login');
return;
}
try {
const decryptedUserId = jwtFunctions.verify(cookie);
models.users.findOne({ where: { username: decryptedUserId } }).then((user, error) => {
if (user) {
res.locals.user = user.get({ plain: true });
} else {
console.debug("Redirecting to login - invalid cookie")
res.redirect('/login');
return;
}
});
} catch (e) {
res.status(400).send(e.message);
}
}
next();
})
// Route logging
server.use(function (req, res, next) {
let cookie = req.cookies.session;
if (!cookie) {
cookie = uuidv4();
res.cookie('session', cookie, { expires: new Date(Date.now() + (1000 * 60 * 60)) });
}
models.requests.create({
createdAt: new Date(), session: cookie, method: req.method, url: req.originalUrl
});
next()
})
server.get('/', cache('5 minutes'), async (req, res) => {
let posts = await formatPostsForType(models, "index")
let body = templates["index"]({posts});
res.status(200).send(body)
})
server.get('/bread', cache('5 minutes'), async (req, res) => {
let posts = await formatPostsForType(models, "bread")
let body = templates["bread"]({posts});
res.status(200).send(body)
})
server.get('/blog', cache('5 minutes'), async (req, res) => {
let posts = await formatPostsForType(models, "blog")
let body = templates["blog"]({posts});
res.status(200).send(body)
})
server.get('/post/:type/:id', cache('5 minutes'), async (req, res) => {
let posts = await formatPostsforSingle(models, req.params.type, req.params.id)
let date = posts[0].createdAt;
let body = templates["blog-single"]({posts, date});
res.status(200).send(body)
})
server.get('/post/like/:type/:id', async (req, res) => {
let type = req.params.type
let id = req.params.id
var post = await models.posts.findOne({
where: { type, id },
});
post.update({likes: post.likes+1})
res.status(200).send({likes: post.likes});
})
server.get('/tags/:name', cache('5 minutes'), async (req, res) => {
const { name } = req.params;
const postsWithTag = await models.tags.findAll({ attributes: ["postId"], where: { text: name } })
.map(function (x) {
return { id: x.postId }
});
var posts = await models.posts.findAll({
where: { [Op.or]: postsWithTag }, order: [['createdAt', 'DESC']]
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
posts.forEach(post => {
post.createdAt = post.createdAt.toString().substring(0, 10)
})
let body = templates["tags"]({posts, name})
res.status(200).send(body)
})
server.get('/robots.txt', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/misc/robots.txt"));
server.get('/admin', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/admin.html"));
server.get('/login', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/login.html"))
server.get('/email', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/email.html"))
server.get('/email-success', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/email-success.html"))
server.get('/email-unsubscribe', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/email-unsubscribe.html"))
server.get('/feed', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/feed.html"));
server.get('/essay', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/essay.html"));
server.get('/word-square', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/word-square.html"));
server.get('/chess', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/html/chess.html"));
server.get('/admin/chess', cache('5 minutes'), async (req, res, next) => res.sendFile(__dirname + "/html/chess.html"));
server.get('/zines', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/public/zines.html"));
server.use('/static', express.static(__dirname + '/public'))
server.get('/hello/:path', async (req, res) => {
await models.hellos.create({page: req.params.path})
res.status(200).send()
})
server.get('/misc', cache('5 minutes'), async (req, res) => {
let body = templates["misc"]();
res.status(200).send(body)
})
server.get('/projects', cache('5 minutes'), async (req, res) => {
let body = templates["projects"]();
res.status(200).send(body)
})
server.get('/about', cache('5 minutes'), async (req, res) => {
let body = templates["about"]();
res.status(200).send(body)
})
server.get('/sitemap.xml', cache('1 day'), async (req, res) => {
res.setHeader('Content-Type', 'text/xml')
res.status(200).send(await sitemap(models))
});
server.get('/wordsquares/best', cache('5 minutes'), async (req, res, next) => {
var best = await database.query("select words, name from wordsquares where best = 1", { type: database.QueryTypes.SELECT })
res.status(200).send({ best: best });
})
server.get('/admin/chess/games', async (req, res, next) => {
const { name } = req.params;
var game = await models.chessgames.findOne({where: {
turn: {
[Op.ne]: {$col: 'userside'}
}
}})
res.status(200).send({game:game});
})
server.get('/chess/:name', async (req, res, next) => {
const { name } = req.params;
var game = await models.chessgames.findOne({where: {name: name}})
res.status(200).send({game:game});
})
server.get('/admin/emails', async (req, res, next) => {
var emails = await models.emails.findAll();
res.status(200).send(emails);
})
server.get('/admin/stats', async (req, res, next) => {
try {
var sessionResult = await database.query("SELECT session, count(id) as c FROM requests GROUP BY session HAVING c > 1", { type: database.QueryTypes.SELECT })
var total = await database.query("select count(distinct session) as t FROM requests", { type: database.QueryTypes.SELECT })
var urlResult = await database.query("SELECT method, url, count(id) as c FROM requests GROUP BY method, url ORDER BY c DESC", { type: database.QueryTypes.SELECT })
let urls = (await get_routes(models)).map(obj => obj.route)
urlResult = urlResult.filter(obj => { return urls.includes(obj.url) })
var logResult = await database.query("SELECT createdAt, session, method, url FROM requests order by createdAt desc limit 15", { type: database.QueryTypes.SELECT })
res.status(200).send({ total: total[0].t, session: sessionResult, url: urlResult, log: logResult });
} catch (e) {
res.status(400).send(e.message);
}
})
server.get('/posts/:type', cache('5 minutes'), async (req, res, next) => {
try {
const { type } = req.params;
var posts = await models.posts.findAll({
where: { type: type }, order: [['createdAt', 'DESC']]
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
res.status(200).send(posts);
next();
} catch (e) {
res.status(400).send(e.message);
}
})
server.get('/posts/:type/:id', cache('5 minutes'), async (req, res, next) => {
try {
const { type, id } = req.params;
var posts = await models.posts.findAll({
where: {
type: type,
id: id
}, order: [['createdAt', 'DESC']]
});
posts = posts.map(x => x.get({ plain: true }));
await addImagesAndTagsToPosts(models, posts)
res.status(200).send(posts);
next();
} catch (e) {
res.status(400).send(e.message);
}
})
server.post('/admin/posts', upload.array('images'), async (req, res, next) => {
try {
const type = req.body.type
req.body.description = marked(req.body.description)
req.body.likes = 0
req.body.sent_email = false
const newPost = await models.posts.create(req.body);
req.files.forEach(async (file) => {
await models.pictures.create({ "source": "uploads/" + file.filename, "postId": newPost.id });
console.log("uploaded ", file.path);
})
if(req.body.tags.trim().length > 0) {
req.body.tags.split(" ").forEach(async (tag) => {
await models.tags.create({ "text": tag, "postId": newPost.id });
})
}
res.redirect(`/${type}`);
next();
} catch (e) {
res.status(400).send(e.message);
}
})
server.delete('/admin/email/:id', async (req, res, next) => {
await models.emails.destroy({ where: { id: req.params.id } });
var emails = await models.emails.findAll();
res.status(200).send(emails);
})
server.post('/login', async (req, res, next) => {
const user = await models.users.findOne({ where: { username: req.body.username} })
const hash = hashWithSalt(req.body.password, user.salt)
if (user.password == hash) {
const token = jwtFunctions.sign(user.username);
res.cookie('authorization', token, { expires: new Date(Date.now() + (1000 * 60 * 60)) });
console.debug("Redirecting to admin - logged in")
res.redirect('/admin');
} else {
console.debug("Redirecting to login - invalid login")
res.redirect('/login');
}
})
server.post('/email', async (req, res, next) => {
const name = req.body.name;
const email = req.body.email;
if (name && email) {
const code = crypto.randomBytes(40).toString('hex').slice(0, 40)
models.emails.create({"name": name, "address": email, "code": code})
res.redirect('/email-success');
} else {
console.debug("Error with email submission")
}
})
server.get('/email/unsubscribe/:code/check', async (req, res, next) => {
res.sendFile(__dirname + "/html/email-confirm.html")
})
server.get('/email/unsubscribe/:code/confirm', async (req, res, next) => {
await models.emails.destroy({ where: {"code": req.params.code}})
res.redirect('/email-unsubscribe');
})
server.post('/wordsquares', async (req, res, next) => {
const words = req.body.words;
const name = req.body.name;
if (name && words) {
models.wordsquares.create({"name": name, "words": words, "best": false})
res.redirect('/wordsquare#success');
} else {
console.debug("Error with wordsquare submission")
}
})
server.post('/chess', async (req, res, next) => {
const game = req.body;
if (game) {
models.chessgames.findOne({where: {name: game.name}})
.then(obj => {
if(obj){
obj.update(game)
} else {
models.chessgames.create(game)
}
})
} else {
console.debug("Error with chess submission")
}
res.status(200).send(game);
})
server.get('/favicon.ico', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/icon/favicon.ico"))
server.get('/favicon.svg', cache('5 minutes'), (req, res) => res.sendFile(__dirname + "/icon/favicon.svg"))
server.get('/css/:id', cache('5 minutes'), (req, res) => {
res.sendFile(__dirname + "/css/" + req.params.id);
});
server.get('/uploads/:id', cache('5 minutes'), (req, res) => {
res.sendFile(__dirname + "/uploads/" + req.params.id);
});
server.get('/essay/:id', cache('5 minutes'), (req, res) => {
res.sendFile(__dirname + "/html/essay/" + req.params.id);
});
server.get('/js/:id', cache('5 minutes'), (req, res) => {
res.sendFile(__dirname + "/js/" + req.params.id);
});
server.get('/res/:id', (req, res) => {
res.sendFile(__dirname + "/res/" + req.params.id);
});
server.get('/feed.xml', cache('1 hour'), async (req, res) => {
var feed = new rss({
title: "Mark's Kitchen",
description: "Posts from marks.kitchen",
feed_url: "https://marks.kitchen/rss",
site_url: "https://marks.kitchen",
webMaster: "webmaster@marks.kitchen (Mark Powers)",
copyright: "Mark Powers"
})
var posts = await models.posts.findAll({
order: [['createdAt', 'DESC']]
});
posts = posts.map(x => x.get({ plain: true }));
posts.forEach(post =>{
feed.item({
title: post.title,
description: post.description,
date: post.createdAt,
url: `https://marks.kitchen/post/${post.type}/${post.id}`,
})
})
res.setHeader('Content-Type', 'text/xml')
res.status(200).send(feed.xml({indent: true}))
})
// Final 404 fallback
server.use(function(req, res) {
res.status(400);
res.sendFile(__dirname + "/html/404.html");
});
}
module.exports = {
listen,
setUpRoutes
};