Browse Source

Formatted the entire project with prettier

Signed-off-by: Dušan Mitrović <dusan@dusanmitrovic.xyz>
master
Dušan Mitrović 1 month ago
parent
commit
a8cb8943cb
Signed by: dusan GPG Key ID: 92C38C4382AE469C
44 changed files with 12025 additions and 10942 deletions
  1. +7
    -7
      config/handlebars.js
  2. +19
    -23
      config/multer.js
  3. +7
    -7
      config/nodemailer.js
  4. +1
    -1
      config/redis.js
  5. +35
    -35
      config/rss-feed.js
  6. +9
    -9
      config/session.js
  7. +12
    -12
      db/connection.js
  8. +18
    -35
      db/migrations/20200707003006_UserMigration.js
  9. +24
    -41
      db/migrations/20200707003939_PostMigration.js
  10. +8
    -14
      db/migrations/20201213191639_AddSlugToPosts.js
  11. +18
    -18
      db/seeds/UserSeeder.js
  12. +12
    -16
      ecosystem.config.js
  13. +34
    -34
      knexfile.js
  14. +3
    -3
      middleware/404-handler.js
  15. +4
    -4
      middleware/auth.js
  16. +11
    -14
      middleware/multer-handler.js
  17. +2
    -3
      middleware/session.js
  18. +16
    -16
      models/Post.js
  19. +16
    -16
      models/User.js
  20. +9668
    -9692
      package-lock.json
  21. +45
    -44
      package.json
  22. +191
    -191
      routes/blog.js
  23. +82
    -91
      routes/contact.js
  24. +3
    -3
      routes/home.js
  25. +34
    -47
      routes/images.js
  26. +44
    -46
      routes/login.js
  27. +8
    -8
      routes/rss.js
  28. +36
    -36
      services/image-conversion-service.js
  29. +20
    -20
      services/login-service.js
  30. +19
    -19
      services/mailer-service.js
  31. +141
    -141
      services/post-service.js
  32. +14
    -14
      services/rss-feed-service.js
  33. +4
    -4
      static/js/main.js
  34. +1213
    -13
      static/js/prism.js
  35. +16
    -16
      static/js/ui-functions.js
  36. +5
    -5
      utilities/capitalize.js
  37. +87
    -85
      utilities/form-antispam.js
  38. +4
    -4
      utilities/get-app-uri.js
  39. +3
    -3
      utilities/markdown-2-html.js
  40. +12
    -23
      validations/contact-validations.js
  41. +4
    -8
      validations/login-validations.js
  42. +35
    -37
      validations/post-validations.js
  43. +22
    -22
      views/helpers/compare.js
  44. +59
    -62
      views/view-data/home.js

+ 7
- 7
config/handlebars.js View File

@@ -8,13 +8,13 @@ const { dirname } = require('path');
const compareHandlebarsHelper = require('../views/helpers/compare');

const handlebarsConfig = {
extname: 'hbs',
defaultLayout: 'main',
partialsDir: dirname(require.main.filename) + '/views/partials/',
layoutsDir: dirname(require.main.filename) + '/views/layouts/',
helpers: {
compare: compareHandlebarsHelper
}
extname: 'hbs',
defaultLayout: 'main',
partialsDir: dirname(require.main.filename) + '/views/partials/',
layoutsDir: dirname(require.main.filename) + '/views/layouts/',
helpers: {
compare: compareHandlebarsHelper,
},
};

module.exports = handlebarsConfig;

+ 19
- 23
config/multer.js View File

@@ -16,37 +16,33 @@ const { v4: uuidv4 } = require('uuid');
* @param {function} callback
*/
const fileFilter = (req, file, callback) => {
const allowedFileTypes = [
'image/jpg',
'image/jpeg',
'image/png'
];
const allowedFileTypes = ['image/jpg', 'image/jpeg', 'image/png'];

if (file.size > 1024 * 1024 * 8) {
callback(new Error('File size must be less than 8MiB.'), false);
return;
}
if (file.size > 1024 * 1024 * 8) {
callback(new Error('File size must be less than 8MiB.'), false);
return;
}

if (!allowedFileTypes.includes(file.mimetype)) {
callback(new Error('File must be an image.'), false);
return;
}
if (!allowedFileTypes.includes(file.mimetype)) {
callback(new Error('File must be an image.'), false);
return;
}

callback(null, true);
callback(null, true);
};

const storage = multer.diskStorage({
destination: (req, file, callback) => {
callback(null, '/tmp');
},
filename(req, file, callback) {
callback(null, uuidv4() + path.extname(file.originalname));
},
destination: (req, file, callback) => {
callback(null, '/tmp');
},
filename(req, file, callback) {
callback(null, uuidv4() + path.extname(file.originalname));
},
});

const uploader = multer({
storage,
fileFilter,
storage,
fileFilter,
});

module.exports = uploader;
module.exports = uploader;

+ 7
- 7
config/nodemailer.js View File

@@ -5,13 +5,13 @@
* @summary Nodemailer configuration
*/
const transporterOptions = {
port: process.env.MAILER_PORT,
host: process.env.MAILER_HOST,
auth: {
user: process.env.MAILER_USERNAME,
pass: process.env.MAILER_PASSWORD
},
secure: parseInt(process.env.MAILER_SECURE) !== 0,
port: process.env.MAILER_PORT,
host: process.env.MAILER_HOST,
auth: {
user: process.env.MAILER_USERNAME,
pass: process.env.MAILER_PASSWORD,
},
secure: parseInt(process.env.MAILER_SECURE) !== 0,
};

module.exports = transporterOptions;

+ 1
- 1
config/redis.js View File

@@ -8,4 +8,4 @@ const redis = require('redis');

const redisClient = redis.createClient();

module.exports = redisClient;
module.exports = redisClient;

+ 35
- 35
config/rss-feed.js View File

@@ -9,45 +9,45 @@ const markdown2Html = require('../utilities/markdown-2-html');
const moment = require('moment');

const rssFeedOptions = {
title: 'Dušan\'s blog',
description: 'An RSS feed of my personal blog.',
id: getAppURI(),
link: getAppURI() + '/rss',
language: 'en',
image: getAppURI() + '/static/images/avatar.png',
favicon: getAppURI() + '/static/images/favicon.png',
copyright: `${new Date().getFullYear()} All rights reversed.`,
feedLinks: {
atom: getAppURI() + '/rss'
},
author: {
name: 'Dušan Mitrović',
email: 'dusan@dusanmitrovic.xyz',
link: getAppURI()
}
title: "Dušan's blog",
description: 'An RSS feed of my personal blog.',
id: getAppURI(),
link: getAppURI() + '/rss',
language: 'en',
image: getAppURI() + '/static/images/avatar.png',
favicon: getAppURI() + '/static/images/favicon.png',
copyright: `${new Date().getFullYear()} All rights reversed.`,
feedLinks: {
atom: getAppURI() + '/rss',
},
author: {
name: 'Dušan Mitrović',
email: 'dusan@dusanmitrovic.xyz',
link: getAppURI(),
},
};

const rssFeedPost = (post) => {
const date = moment(post.created_at).format('YYYY-MM-DD');
const date = moment(post.created_at).format('YYYY-MM-DD');

return {
id: `${getAppURI()}/blog/post/${date}/${post.slug}`,
title: post.title,
description: post.description,
link: `${getAppURI()}/blog/post/${date}/${post.slug}`,
content: markdown2Html(post.content),
author: [
{
name: `${post.first_name} ${post.last_name}`,
email: post.email,
link: getAppURI()
}
],
date: post.created_at,
}
return {
id: `${getAppURI()}/blog/post/${date}/${post.slug}`,
title: post.title,
description: post.description,
link: `${getAppURI()}/blog/post/${date}/${post.slug}`,
content: markdown2Html(post.content),
author: [
{
name: `${post.first_name} ${post.last_name}`,
email: post.email,
link: getAppURI(),
},
],
date: post.created_at,
};
};

module.exports = {
rssFeedOptions,
rssFeedPost,
};
rssFeedOptions,
rssFeedPost,
};

+ 9
- 9
config/session.js View File

@@ -9,15 +9,15 @@ const RedisStore = require('connect-redis')(session);
const redisClient = require('./redis');

const redisSession = {
store: new RedisStore({
client: redisClient
}),
secret: process.env.SESSION_SECRET,
saveUninitialized: true,
resave: false,
cookie: {
secure: true
}
store: new RedisStore({
client: redisClient,
}),
secret: process.env.SESSION_SECRET,
saveUninitialized: true,
resave: false,
cookie: {
secure: true,
},
};

module.exports = session(redisSession);

+ 12
- 12
db/connection.js View File

@@ -7,18 +7,18 @@
const Knex = require('knex');

const knex = Knex({
client: process.env.DB_DRIVER,
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
pool: {
min: 2,
max: 10
}
client: process.env.DB_DRIVER,
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
pool: {
min: 2,
max: 10,
},
});

module.exports = knex;

+ 18
- 35
db/migrations/20200707003006_UserMigration.js View File

@@ -1,42 +1,25 @@
exports.up = function (knex) {
return knex.schema.createTable('users', table => {
table
.increments('id');
table
.string('first_name', 100)
.notNullable();
table
.string('last_name', 100)
.notNullable();
table
.string('email', 100)
.notNullable();
table
.string('password', 200)
.notNullable();
table
.timestamp('created_at')
.defaultTo(
knex.raw('CURRENT_TIMESTAMP')
)
.notNullable();
table
.timestamp('updated_at')
.defaultTo(
knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
)
.notNullable();
return knex.schema.createTable('users', (table) => {
table.increments('id');
table.string('first_name', 100).notNullable();
table.string('last_name', 100).notNullable();
table.string('email', 100).notNullable();
table.string('password', 200).notNullable();
table
.timestamp('created_at')
.defaultTo(knex.raw('CURRENT_TIMESTAMP'))
.notNullable();
table
.timestamp('updated_at')
.defaultTo(knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
.notNullable();

table
.index([
'created_at',
])
table.index(['created_at']);

table
.unique(['email']);
});
table.unique(['email']);
});
};

exports.down = function (knex) {
return knex.schema.dropTable('users');
return knex.schema.dropTable('users');
};

+ 24
- 41
db/migrations/20200707003939_PostMigration.js View File

@@ -1,48 +1,31 @@
exports.up = function (knex) {
return knex.schema.createTable('posts', table => {
table
.increments('id');
table
.string('title', 100)
.notNullable();
table
.string('description', 300)
.notNullable();
table
.text('content', 'longtext')
.notNullable();
table
.integer('user_id')
.unsigned()
.notNullable();
table
.timestamp('created_at')
.defaultTo(
knex.raw('CURRENT_TIMESTAMP')
)
.notNullable();
return knex.schema.createTable('posts', (table) => {
table.increments('id');
table.string('title', 100).notNullable();
table.string('description', 300).notNullable();
table.text('content', 'longtext').notNullable();
table.integer('user_id').unsigned().notNullable();
table
.timestamp('created_at')
.defaultTo(knex.raw('CURRENT_TIMESTAMP'))
.notNullable();

table
.timestamp('updated_at')
.defaultTo(
knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
)
.notNullable();
table
.timestamp('updated_at')
.defaultTo(knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
.notNullable();

table
.index([
'created_at',
]);
table.index(['created_at']);

table
.foreign('user_id')
.references('id')
.inTable('users')
.onDelete('CASCADE')
.onUpdate('CASCADE');
});
table
.foreign('user_id')
.references('id')
.inTable('users')
.onDelete('CASCADE')
.onUpdate('CASCADE');
});
};

exports.down = function (knex) {
return knex.schema.dropTable('posts');
};
return knex.schema.dropTable('posts');
};

+ 8
- 14
db/migrations/20201213191639_AddSlugToPosts.js View File

@@ -1,21 +1,15 @@
exports.up = function (knex) {
return knex.schema.alterTable('posts', table => {
table
.string('slug', 120)
.notNullable();
return knex.schema.alterTable('posts', (table) => {
table.string('slug', 120).notNullable();

table.index([
'slug'
]);
});
table.index(['slug']);
});
};

exports.down = function (knex) {
return knex.schema.alterTable('posts', table => {
table.dropIndex([
'slug'
]);
return knex.schema.alterTable('posts', (table) => {
table.dropIndex(['slug']);

table.dropColumn('slug');
});
table.dropColumn('slug');
});
};

+ 18
- 18
db/seeds/UserSeeder.js View File

@@ -1,22 +1,22 @@
const argon2 = require('argon2');

exports.seed = async function(knex) {
const passwordHash = await argon2.hash(process.env.ADMIN_PASSWORD, {
type: argon2.argon2id,
hashLength: 60
exports.seed = async function (knex) {
const passwordHash = await argon2.hash(process.env.ADMIN_PASSWORD, {
type: argon2.argon2id,
hashLength: 60,
});
// Deletes ALL existing entries
return knex('users')
.del()
.then(function () {
// Inserts seed entries
return knex('users').insert([
{
first_name: process.env.ADMIN_FIRST_NAME,
last_name: process.env.ADMIN_LAST_NAME,
email: process.env.ADMIN_EMAIL,
password: passwordHash,
},
]);
});
// Deletes ALL existing entries
return knex('users')
.del()
.then(function() {
// Inserts seed entries
return knex('users').insert([
{
first_name: process.env.ADMIN_FIRST_NAME,
last_name: process.env.ADMIN_LAST_NAME,
email: process.env.ADMIN_EMAIL,
password: passwordHash
}
]);
});
};

+ 12
- 16
ecosystem.config.js View File

@@ -1,17 +1,13 @@
module.exports = {
apps: [{
script: 'app.js',
watch: [
'views',
],
watch_delay: 1000,
ignore_watch: [
'node_modules',
'static',
'certificates'
],
watch_options: {
'followSymlinks': false
},
}],
}
apps: [
{
script: 'app.js',
watch: ['views'],
watch_delay: 1000,
ignore_watch: ['node_modules', 'static', 'certificates'],
watch_options: {
followSymlinks: false,
},
},
],
};

+ 34
- 34
knexfile.js View File

@@ -4,64 +4,64 @@ module.exports = {
development: {
client: process.env.DB_DRIVER,
connection: {
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
},
seeds: {
directory: './db/seeds'
directory: './db/seeds',
},
migrations: {
tableName: 'knex_migrations',
directory: './db/migrations'
tableName: 'knex_migrations',
directory: './db/migrations',
},
pool: {
min: 2,
max: 10
}
min: 2,
max: 10,
},
},
staging: {
client: process.env.DB_DRIVER,
connection: {
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
},
seeds: {
directory: './db/seeds'
directory: './db/seeds',
},
migrations: {
tableName: 'knex_migrations',
directory: './db/migrations'
tableName: 'knex_migrations',
directory: './db/migrations',
},
pool: {
min: 2,
max: 10
}
min: 2,
max: 10,
},
},
production: {
client: process.env.DB_DRIVER,
connection: {
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
},
seeds: {
directory: './db/seeds'
directory: './db/seeds',
},
migrations: {
tableName: 'knex_migrations',
directory: './db/migrations'
tableName: 'knex_migrations',
directory: './db/migrations',
},
pool: {
min: 2,
max: 10
}
}
min: 2,
max: 10,
},
},
};

+ 3
- 3
middleware/404-handler.js View File

@@ -5,9 +5,9 @@
* @summary A middleware that handles all 404 errors
*/
const notFoundHandler = (req, res, next) => {
res.status(404).render('404', {
title: 'Page Not Found'
});
res.status(404).render('404', {
title: 'Page Not Found',
});
};

module.exports = notFoundHandler;

+ 4
- 4
middleware/auth.js View File

@@ -5,11 +5,11 @@
* @summary Authorization middleware
*/
const authorizationMiddleware = (req, res, next) => {
if (req.session.user === undefined) {
return res.redirect('/');
}
if (req.session.user === undefined) {
return res.redirect('/');
}

next();
next();
};

module.exports = authorizationMiddleware;

+ 11
- 14
middleware/multer-handler.js View File

@@ -5,20 +5,17 @@
* @summary Handler for multer errors
*/
const multerHandler = (error, req, res, next) => {
if (error) {
return res.status(422).render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
error: {
message: error.message,
},
});
}
if (error) {
return res.status(422).render('image-upload', {
title: 'Upload an image',
css: ['/static/css/image-upload.css', '/static/css/form.css'],
error: {
message: error.message,
},
});
}

next();
next();
};

module.exports = multerHandler;
module.exports = multerHandler;

+ 2
- 3
middleware/session.js View File

@@ -5,9 +5,8 @@
* @summary Session middleware
*/
const passSessionToHandlebars = (req, res, next) => {
res.locals.session = req.session;
next();
res.locals.session = req.session;
next();
};

module.exports = passSessionToHandlebars;


+ 16
- 16
models/Post.js View File

@@ -7,24 +7,24 @@
const { Model } = require('objection');

class Post extends Model {
static get tableName() {
return 'posts';
}
static get tableName() {
return 'posts';
}

static get relationMappings() {
const User = require('./User');
static get relationMappings() {
const User = require('./User');

return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'posts.user_id',
to: 'users.id',
},
},
};
}
return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'posts.user_id',
to: 'users.id',
},
},
};
}
}

module.exports = Post;

+ 16
- 16
models/User.js View File

@@ -7,24 +7,24 @@
const { Model } = require('objection');

class User extends Model {
static get tableName() {
return 'users';
}
static get tableName() {
return 'users';
}

static get relationMappings() {
const Post = require('./Post');
static get relationMappings() {
const Post = require('./Post');

return {
posts: {
relation: Model.HasManyRelation,
modelClass: Post,
join: {
from: 'users.id',
to: 'posts.user_id',
},
},
};
}
return {
posts: {
relation: Model.HasManyRelation,
modelClass: Post,
join: {
from: 'users.id',
to: 'posts.user_id',
},
},
};
}
}

module.exports = User;

+ 9668
- 9692
package-lock.json
File diff suppressed because it is too large
View File


+ 45
- 44
package.json View File

@@ -1,46 +1,47 @@
{
"name": "dusanmitrovic.xyz",
"version": "1.0.0",
"description": "My website",
"main": "app.js",
"scripts": {
"dev": "nodemon .",
"start": "node .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@types/express": "^4.17.9",
"@types/multer": "^1.4.5",
"@types/node": "^14.14.20",
"nodemon": "^2.0.7"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"argon2": "^0.26.2",
"connect-redis": "^4.0.4",
"dompurify": "^2.2.6",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-handlebars": "^4.0.6",
"express-session": "^1.17.1",
"express-validator": "^6.9.2",
"feed": "^4.2.1",
"gm": "^1.23.1",
"image-size": "^0.9.3",
"jsdom": "^16.4.0",
"knex": "^0.21.15",
"marked": "^2.0.1",
"method-override": "^3.0.0",
"moment": "^2.29.1",
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nodemailer": "^6.4.17",
"objection": "^2.2.6",
"redis": "^3.0.2",
"slugify": "^1.4.6",
"uuid": "^8.3.2"
}
"name": "dusanmitrovic.xyz",
"version": "1.0.0",
"description": "My website",
"main": "app.js",
"scripts": {
"dev": "nodemon .",
"start": "node .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@types/express": "^4.17.9",
"@types/multer": "^1.4.5",
"@types/node": "^14.14.20",
"nodemon": "^2.0.7",
"prettier": "^2.3.2"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"argon2": "^0.26.2",
"connect-redis": "^4.0.4",
"dompurify": "^2.2.6",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-handlebars": "^4.0.6",
"express-session": "^1.17.1",
"express-validator": "^6.9.2",
"feed": "^4.2.1",
"gm": "^1.23.1",
"image-size": "^0.9.3",
"jsdom": "^16.4.0",
"knex": "^0.21.15",
"marked": "^2.0.1",
"method-override": "^3.0.0",
"moment": "^2.29.1",
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nodemailer": "^6.4.17",
"objection": "^2.2.6",
"redis": "^3.0.2",
"slugify": "^1.4.6",
"uuid": "^8.3.2"
}
}

+ 191
- 191
routes/blog.js View File

@@ -9,229 +9,229 @@ const PostService = require('../services/post-service');
const authorizationMiddleware = require('../middleware/auth');
const markdown2Html = require('../utilities/markdown-2-html');
const validations = require('../validations/post-validations');
const {validationResult} = require('express-validator');
const { validationResult } = require('express-validator');
const capitalize = require('../utilities/capitalize');
const moment = require('moment');
const router = express.Router();

router.get('/new', authorizationMiddleware, (req, res) => {
return res.render('newPost', {
title: 'New Post',
form: {
id: 'publish-post',
action: `/blog`,
heading: 'Publish Post',
title: '',
description: '',
content: '',
button: 'Publish'
}
});
return res.render('newPost', {
title: 'New Post',
form: {
id: 'publish-post',
action: `/blog`,
heading: 'Publish Post',
title: '',
description: '',
content: '',
button: 'Publish',
},
});
});

router.get('/post/:date/:slug', async (req, res) => {
try {
const date = req.params.date;
const slug = req.params.slug;
const post = await PostService.getBySlugAndDate(slug, date);

if (null === post) {
return res.render('404', {
title: 'Page Not Found'
});
}

// Font awesome is not needed unless I'm logged in.
const css = [
'/static/css/prism.css',
'/static/css/blog-post.css',
'/static/css/webring.css',
];

if (undefined !== req.session.user) {
css.push('/static/fontawesome-free/css/solid.min.css');
css.push('/static/fontawesome-free/css/fontawesome.min.css');
}

return res.render('blogPost', {
title: post.title,
css,
js: ['/static/js/prism.js'],
id: post.id,
slug: post.slug,
content: markdown2Html(post.content),
created_at: moment(post.created_at).format('MMMM Do YYYY'),
openGraph: {
title: post.title,
type: 'article',
url: `${process.env.HOST}/blog/post/${date}/${post.slug}`,
description: post.description,
}
});
} catch (error) {
console.error(error);
return res.end();
try {
const date = req.params.date;
const slug = req.params.slug;
const post = await PostService.getBySlugAndDate(slug, date);

if (null === post) {
return res.render('404', {
title: 'Page Not Found',
});
}

// Font awesome is not needed unless I'm logged in.
const css = [
'/static/css/prism.css',
'/static/css/blog-post.css',
'/static/css/webring.css',
];

if (undefined !== req.session.user) {
css.push('/static/fontawesome-free/css/solid.min.css');
css.push('/static/fontawesome-free/css/fontawesome.min.css');
}

return res.render('blogPost', {
title: post.title,
css,
js: ['/static/js/prism.js'],
id: post.id,
slug: post.slug,
content: markdown2Html(post.content),
created_at: moment(post.created_at).format('MMMM Do YYYY'),
openGraph: {
title: post.title,
type: 'article',
url: `${process.env.HOST}/blog/post/${date}/${post.slug}`,
description: post.description,
},
});
} catch (error) {
console.error(error);
return res.end();
}
});

router.get('/edit/:id', authorizationMiddleware, async (req, res) => {
const id = req.params.id;
const id = req.params.id;

const post = await PostService.getById(id);
const post = await PostService.getById(id);

if (undefined === post) {
return res.render('404', {
title: 'Page Not Found'
});
}

return res.render('editPost', {
title: 'Edit Post',
form: {
id: 'edit-post',
action: `/blog/${id}?_method=PUT`,
heading: 'Edit Post',
error: {},
title: post.title,
description: post.description,
content: post.content,
button: 'Edit'
}
if (undefined === post) {
return res.render('404', {
title: 'Page Not Found',
});
}

return res.render('editPost', {
title: 'Edit Post',
form: {
id: 'edit-post',
action: `/blog/${id}?_method=PUT`,
heading: 'Edit Post',
error: {},
title: post.title,
description: post.description,
content: post.content,
button: 'Edit',
},
});
});

router.get('/', validations.getPaginatedPosts, async (req, res) => {
try {
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(404).render('404', {
title: 'Page Not Found'
});
}

const page = req.query.page ? req.query.page : 1;
const perPage = req.query.perPage ? req.query.perPage : 4;

const paginatedPosts = await PostService.getPaginated(page, perPage);

for (post of paginatedPosts.posts) {
post.date = moment(post.created_at).format('YYYY-MM-DD');
post.created_at = moment(post.created_at, 'YYYYMMDD').fromNow();
}

return res.render('blogPage', {
title: 'Blog',
css: ['/static/css/blog-post.css'],
posts: paginatedPosts.posts,
totalPages: paginatedPosts.totalPages,
page,
previousPage: parseInt(page) - 1,
nextPage: parseInt(page) + 1,
perPage
});
} catch (error) {
console.error(error);
try {
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(404).render('404', {
title: 'Page Not Found',
});
}

const page = req.query.page ? req.query.page : 1;
const perPage = req.query.perPage ? req.query.perPage : 4;

const paginatedPosts = await PostService.getPaginated(page, perPage);

for (post of paginatedPosts.posts) {
post.date = moment(post.created_at).format('YYYY-MM-DD');
post.created_at = moment(post.created_at, 'YYYYMMDD').fromNow();
}

return res.render('blogPage', {
title: 'Blog',
css: ['/static/css/blog-post.css'],
posts: paginatedPosts.posts,
totalPages: paginatedPosts.totalPages,
page,
previousPage: parseInt(page) - 1,
nextPage: parseInt(page) + 1,
perPage,
});
} catch (error) {
console.error(error);
}
});

router.post(
'/',
authorizationMiddleware,
validations.createAndUpdatePost,
async (req, res) => {
try {
const errors = validationResult(req);
const {title, description, content} = req.body;
const user_id = req.session.user.id;

if (!errors.isEmpty()) {
const errorsArray = errors.array();

return res.status(422).render('newPost', {
title: 'New Post',
form: {
id: 'publish-post',
action: `/blog`,
heading: 'Publish Post',
error: {
message: `${capitalize(errorsArray[0].param)} ${
errorsArray[0].msg
}`
},
title,
description,
content,
button: 'Publish'
}
});
}

await PostService.create(title, description, content, user_id);

return res.redirect('/blog');
} catch (error) {
console.error(error);
return res.end();
}
'/',
authorizationMiddleware,
validations.createAndUpdatePost,
async (req, res) => {
try {
const errors = validationResult(req);
const { title, description, content } = req.body;
const user_id = req.session.user.id;

if (!errors.isEmpty()) {
const errorsArray = errors.array();

return res.status(422).render('newPost', {
title: 'New Post',
form: {
id: 'publish-post',
action: `/blog`,
heading: 'Publish Post',
error: {
message: `${capitalize(errorsArray[0].param)} ${
errorsArray[0].msg
}`,
},
title,
description,
content,
button: 'Publish',
},
});
}

await PostService.create(title, description, content, user_id);

return res.redirect('/blog');
} catch (error) {
console.error(error);
return res.end();
}
}
);

router.put(
'/:id',
authorizationMiddleware,
validations.createAndUpdatePost,
async (req, res) => {
try {
const errors = validationResult(req);
const {title, description, content} = req.body;
const id = req.params.id;
const user_id = req.session.user.id;

if (!errors.isEmpty()) {
const errorsArray = errors.array();

return res.status(422).render('editPost', {
title: 'Edit Post',
form: {
id: 'edit-post',
action: `/blog/${id}?_method=PUT`,
heading: 'Edit Post',
error: {
message: `${capitalize(errorsArray[0].param)} ${
errorsArray[0].msg
}`
},
title,
description,
content,
button: 'Edit'
}
});
}

await PostService.update(title, description, content, user_id, id);

return res.redirect('/blog');
} catch (error) {
console.error(error);
return res.end();
}
'/:id',
authorizationMiddleware,
validations.createAndUpdatePost,
async (req, res) => {
try {
const errors = validationResult(req);
const { title, description, content } = req.body;
const id = req.params.id;
const user_id = req.session.user.id;

if (!errors.isEmpty()) {
const errorsArray = errors.array();

return res.status(422).render('editPost', {
title: 'Edit Post',
form: {
id: 'edit-post',
action: `/blog/${id}?_method=PUT`,
heading: 'Edit Post',
error: {
message: `${capitalize(errorsArray[0].param)} ${
errorsArray[0].msg
}`,
},
title,
description,
content,
button: 'Edit',
},
});
}

await PostService.update(title, description, content, user_id, id);

return res.redirect('/blog');
} catch (error) {
console.error(error);
return res.end();
}
}
);

router.delete('/post/:id', authorizationMiddleware, async (req, res) => {
try {
const id = req.params.id;
const user_id = req.session.user.id;
try {
const id = req.params.id;
const user_id = req.session.user.id;

await PostService.delete(id, user_id);
await PostService.delete(id, user_id);

return res.redirect('/blog');
} catch (error) {
console.error(error);
return res.end();
}
return res.redirect('/blog');
} catch (error) {
console.error(error);
return res.end();
}
});

module.exports = router;

+ 82
- 91
routes/contact.js View File

@@ -15,110 +15,101 @@ const router = express.Router();
const formAntispam = new FormAntispam();

router.get('/', (req, res) => {
return res.render('contact', {
title: 'Contact',
css: [
'/static/css/contact.css',
],
form: {
id: 'contact-form',
action: '/contact',
name: '',
email: '',
subject: '',
message: '',
antispam: formAntispam.generate(),
}
});
return res.render('contact', {
title: 'Contact',
css: ['/static/css/contact.css'],
form: {
id: 'contact-form',
action: '/contact',
name: '',
email: '',
subject: '',
message: '',
antispam: formAntispam.generate(),
},
});
});

router.post('/', validations.contact, async (req, res) => {
const { name, email, subject, message, antispamQuestion, antispamAnswer } = req.body;
const errors = validationResult(req);

if (!errors.isEmpty()) {
const errorsArray = errors.array();
const { name, email, subject, message, antispamQuestion, antispamAnswer } =
req.body;
const errors = validationResult(req);

return res.status(422).render('contact', {
title: 'Contact',
css: [
'/static/css/contact.css',
],
form: {
id: 'contact-form',
action: '/contact',
error: {
message: `${capitalize(errorsArray[0].param)} ${
errorsArray[0].msg
}`
},
success: {},
name,
email,
subject,
message,
antispam: formAntispam.generate(),
}
});
}
if (!errors.isEmpty()) {
const errorsArray = errors.array();

if (parseInt(antispamAnswer) !== formAntispam.solve(antispamQuestion)) {
return res.status(422).render('contact', {
title: 'Contact',
css: [
'/static/css/contact.css',
],
form: {
id: 'contact-form',
action: '/contact',
error: {
message: 'Wrong answer.',
},
success: {},
name,
email,
subject,
message,
antispam: formAntispam.generate(),
}
});
}
return res.status(422).render('contact', {
title: 'Contact',
css: ['/static/css/contact.css'],
form: {
id: 'contact-form',
action: '/contact',
error: {
message: `${capitalize(errorsArray[0].param)} ${errorsArray[0].msg}`,
},
success: {},
name,
email,
subject,
message,
antispam: formAntispam.generate(),
},
});
}

const messageResponse = await MailerService.send(
if (parseInt(antispamAnswer) !== formAntispam.solve(antispamQuestion)) {
return res.status(422).render('contact', {
title: 'Contact',
css: ['/static/css/contact.css'],
form: {
id: 'contact-form',
action: '/contact',
error: {
message: 'Wrong answer.',
},
success: {},
name,
email,
subject,
message
);
message,
antispam: formAntispam.generate(),
},
});
}

const success = {};
const error = {};
const messageResponse = await MailerService.send(
name,
email,
subject,
message
);

if (messageResponse.rejected.length > 0) {
error.message = 'There was a problem sendig this message.';
}
const success = {};
const error = {};

if (messageResponse.accepted.length > 0) {
success.message = 'Message sent successfully.';
}
if (messageResponse.rejected.length > 0) {
error.message = 'There was a problem sendig this message.';
}

return res.render('contact', {
title: 'Contact',
css: [
'/static/css/contact.css',
],
form: {
id: 'contact-form',
action: '/contact',
error,
success,
name: '',
email: '',
subject: '',
message: '',
antispam: formAntispam.generate(),
}
});
if (messageResponse.accepted.length > 0) {
success.message = 'Message sent successfully.';
}

return res.render('contact', {
title: 'Contact',
css: ['/static/css/contact.css'],
form: {
id: 'contact-form',
action: '/contact',
error,
success,
name: '',
email: '',
subject: '',
message: '',
antispam: formAntispam.generate(),
},
});
});

module.exports = router;

+ 3
- 3
routes/home.js View File

@@ -9,9 +9,9 @@ const homeViewData = require('../views/view-data/home');
const router = express.Router();

router.get('/', async (req, res) => {
return res.render('home', {
...homeViewData,
});
return res.render('home', {
...homeViewData,
});
});

module.exports = router;

+ 34
- 47
routes/images.js View File

@@ -13,58 +13,45 @@ const ImageConversionService = require('../services/image-conversion-service');
const path = require('path');
const multer = require('../config/multer');

router.get(
'/images',
authorizationMiddleware,
(req, res) => {
return res.render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
});
}
);
router.get('/images', authorizationMiddleware, (req, res) => {
return res.render('image-upload', {
title: 'Upload an image',
css: ['/static/css/image-upload.css', '/static/css/form.css'],
});
});

router.post(
'/images',
authorizationMiddleware,
multer.single('uploaded_file'),
multerHandler,
(req, res) => {
const {file} = req;
'/images',
authorizationMiddleware,
multer.single('uploaded_file'),
multerHandler,
(req, res) => {
const { file } = req;

if (undefined === file) {
return res.status(422).render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
error: {
message: 'No file received, but it is required.',
},
});
}
if (undefined === file) {
return res.status(422).render('image-upload', {
title: 'Upload an image',
css: ['/static/css/image-upload.css', '/static/css/form.css'],
error: {
message: 'No file received, but it is required.',
},
});
}

const size = imageSize(file.path);
const size = imageSize(file.path);

const imageURI = ImageConversionService.convert({
size: size.width,
inputPath: file.path,
outputName: path.parse(file.filename).name + '.jpg',
});
const imageURI = ImageConversionService.convert({
size: size.width,
inputPath: file.path,
outputName: path.parse(file.filename).name + '.jpg',
});

return res.render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
imageURI,
});
}
return res.render('image-upload', {
title: 'Upload an image',
css: ['/static/css/image-upload.css', '/static/css/form.css'],
imageURI,
});
}
);

module.exports = router;
module.exports = router;

+ 44
- 46
routes/login.js View File

@@ -14,67 +14,65 @@ const LoginService = require('../services/login-service');
const router = express.Router();

router.get('/login', (req, res) => {
if (undefined !== req.session.user) {
return res.redirect('/');
}
if (undefined !== req.session.user) {
return res.redirect('/');
}

return res.render('login', {
title: 'Login'
});
return res.render('login', {
title: 'Login',
});
});

router.post('/login', validations.login, async (req, res) => {
try {
const errors = validationResult(req);
try {
const errors = validationResult(req);

if (!errors.isEmpty()) {
const errorsArray = errors.array();
if (!errors.isEmpty()) {
const errorsArray = errors.array();

return res.status(422).render('login', {
title: 'Login',
error: {
message: `${capitalize(errorsArray[0].param)} ${
errorsArray[0].msg
}`
}
});
}
return res.status(422).render('login', {
title: 'Login',
error: {
message: `${capitalize(errorsArray[0].param)} ${errorsArray[0].msg}`,
},
});
}

const { email, password } = req.body;
const { email, password } = req.body;

const user = await LoginService.login(email, password);
const user = await LoginService.login(email, password);

if (!user) {
return res.status(401).render('login', {
title: 'Login',
error: {
message: 'Wrong email or password.'
}
});
}
if (!user) {
return res.status(401).render('login', {
title: 'Login',
error: {
message: 'Wrong email or password.',
},
});
}

req.session.user = {
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
email: user.email
};
req.session.user = {
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
email: user.email,
};

return res.redirect('/');
} catch (error) {
console.error(error);
return res.end();
}
return res.redirect('/');
} catch (error) {
console.error(error);
return res.end();
}
});

router.get('/logout', authorizationMiddleware, (req, res) => {
req.session.destroy(err => {
if (err) {
console.error(err);
}
});
req.session.destroy((err) => {
if (err) {
console.error(err);
}
});

return res.redirect('/');
return res.redirect('/');
});

module.exports = router;

+ 8
- 8
routes/rss.js View File

@@ -9,14 +9,14 @@ const router = express.Router();
const RssFeedService = require('../services/rss-feed-service');

router.get('/', async (req, res) => {
try {
return res
.contentType('application/rss+xml')
.send(await RssFeedService.generate());
} catch(err) {
console.error(err);
return res.end();
}
try {
return res
.contentType('application/rss+xml')
.send(await RssFeedService.generate());
} catch (err) {
console.error(err);
return res.end();
}
});

module.exports = router;

+ 36
- 36
services/image-conversion-service.js View File

@@ -4,49 +4,49 @@
*
* @summary Does basic image manipulation using imagemagick
*/
const imageMagick = require('gm').subClass({imageMagick: true});
const imageMagick = require('gm').subClass({ imageMagick: true });
const getAppURI = require('../utilities/get-app-uri');
const path = require('path');
const fs = require('fs');

class ImageConversionService {
/**
* Converts an image to JPEG to reduce file size
*/
static convert({inputPath, outputName, size} = options) {
const outputPath = path.join(
__dirname,
'../static/images/uploads/',
outputName
);
/**
* Converts an image to JPEG to reduce file size
*/
static convert({ inputPath, outputName, size } = options) {
const outputPath = path.join(
__dirname,
'../static/images/uploads/',
outputName
);

imageMagick(inputPath)
// Image width, height scales acordingly
.resize(size > 1200 ? 1200 : size)
// Slice the chroma values in half to reduce file size
.samplingFactor('4:2:0', '')
// Quality in range of 1-100
.quality(75)
// Desired colorspace
.colorspace('sRGB')
// interlacing type
.interlace('JPEG')
// Strip off all EXIF metadata
.strip()
.write(outputPath, error => {
if (error) {
console.error(error);
}
imageMagick(inputPath)
// Image width, height scales acordingly
.resize(size > 1200 ? 1200 : size)
// Slice the chroma values in half to reduce file size
.samplingFactor('4:2:0', '')
// Quality in range of 1-100
.quality(75)
// Desired colorspace
.colorspace('sRGB')
// interlacing type
.interlace('JPEG')
// Strip off all EXIF metadata
.strip()
.write(outputPath, (error) => {
if (error) {
console.error(error);
}

fs.unlink(inputPath, error => {
if (error) {
console.error(error);
}
});
});
fs.unlink(inputPath, (error) => {
if (error) {
console.error(error);
}
});
});

return `${getAppURI()}/static/images/uploads/${outputName}`;
}
return `${getAppURI()}/static/images/uploads/${outputName}`;
}
}

module.exports = ImageConversionService;
module.exports = ImageConversionService;

+ 20
- 20
services/login-service.js View File

@@ -8,29 +8,29 @@ const User = require('../models/User');
const argon2 = require('argon2');

class LoginService {
/**
* Log the user in
*
* @param {string} email
* @param {string} password
*
* @return {Promise<boolean | User>}
*/
static async login(email, password) {
const user = await User.query()
.select('id', 'first_name', 'last_name', 'email', 'password')
.where('email', '=', email);
/**
* Log the user in
*
* @param {string} email
* @param {string} password
*
* @return {Promise<boolean | User>}
*/
static async login(email, password) {
const user = await User.query()
.select('id', 'first_name', 'last_name', 'email', 'password')
.where('email', '=', email);

if (undefined === user[0]) {
return false;
}

if (!(await argon2.verify(user[0].password, password))) {
return false;
}
if (undefined === user[0]) {
return false;
}

return user[0];
if (!(await argon2.verify(user[0].password, password))) {
return false;
}

return user[0];
}
}

module.exports = LoginService;

+ 19
- 19
services/mailer-service.js View File

@@ -10,26 +10,26 @@ const transporterOptions = require('../config/nodemailer');
const transporter = nodemailer.createTransport(transporterOptions);

class MailerService {
/**
* Send an email
*
* @param {string} name
* @param {string} email
* @param {string} subject
* @param {string} message
*
* @returns {Promise<Object>}
*/
static async send(name, email, subject, message) {
return transporter.sendMail({
from: process.env.MAILER_USERNAME,
to: process.env.MAILER_USERNAME,
subject,
html: `<p>New message from: ${name} &lt;${email}&gt;</p>
/**
* Send an email
*
* @param {string} name
* @param {string} email
* @param {string} subject
* @param {string} message
*
* @returns {Promise<Object>}
*/
static async send(name, email, subject, message) {
return transporter.sendMail({
from: process.env.MAILER_USERNAME,
to: process.env.MAILER_USERNAME,
subject,
html: `<p>New message from: ${name} &lt;${email}&gt;</p>
<p>Subject: ${subject}</p>
<p>Message: ${message}</p>`
});
}
<p>Message: ${message}</p>`,
});
}
}

module.exports = MailerService;

+ 141
- 141
services/post-service.js View File

@@ -8,157 +8,157 @@ const Post = require('../models/Post');
const slugify = require('slugify');

class PostService {
/**
* Return a single post by id
*
* @param {number} id
*
* @returns {Promise<Post>}
*/
static async getById(id) {
return Post.query()
.select('id', 'title', 'description', 'content', 'created_at')
.findById(id);
}

/**
* Find a single post by slug.
*
* @param {string} slug
* @param {string} date
*
* @returns {Promise<Post|null>}
*/
static async getBySlugAndDate(slug, date) {
const posts = await Post.query()
.select('id', 'title', 'slug', 'description', 'content', 'created_at')
.where('slug', '=', slug)
.andWhere('created_at', 'like', `${date}%`);
/**
* Return a single post by id
*
* @param {number} id
*
* @returns {Promise<Post>}
*/
static async getById(id) {
return Post.query()
.select('id', 'title', 'description', 'content', 'created_at')
.findById(id);
}

if (undefined !== posts[0]) {
return posts[0];
}
/**
* Find a single post by slug.
*
* @param {string} slug
* @param {string} date
*
* @returns {Promise<Post|null>}
*/
static async getBySlugAndDate(slug, date) {
const posts = await Post.query()
.select('id', 'title', 'slug', 'description', 'content', 'created_at')
.where('slug', '=', slug)
.andWhere('created_at', 'like', `${date}%`);

return null;
if (undefined !== posts[0]) {
return posts[0];
}

/**
* Return all posts
*
* @returns {Promise<Post[]>}
*/
static async getAll() {
return Post.query()
.select(
'posts.title',
'posts.slug',
'posts.description',
'posts.content',
'posts.created_at',
'user.first_name',
'user.last_name',
'user.email'
)
.leftJoinRelated('user')
.orderBy('posts.created_at', 'DESC')
.limit(100);
}
return null;
}

/**
* @typedef {Object} PaginatedPosts
* @property {Array<Post>} posts
* @property {number} totalPages
*/
/**
* Return all posts
*
* @returns {Promise<Post[]>}
*/
static async getAll() {
return Post.query()
.select(
'posts.title',
'posts.slug',
'posts.description',
'posts.content',
'posts.created_at',
'user.first_name',
'user.last_name',
'user.email'
)
.leftJoinRelated('user')
.orderBy('posts.created_at', 'DESC')
.limit(100);
}

/**
* Get a collection of paginated posts
*
* @param {number} page
* @param {number} perPage
*