Browse Source

begin transition to php environment for rebuild

develop
Ro 3 weeks ago
parent
commit
05ff48a51c
43 changed files with 5 additions and 16232 deletions
  1. +1
    -0
      .prettierignore
  2. +0
    -1
      .prettierrc
  3. +0
    -235
      brain/api/v1/auth.js
  4. +0
    -107
      brain/api/v1/backup.js
  5. +0
    -83
      brain/api/v1/mailer.js
  6. +0
    -268
      brain/api/v1/pages.js
  7. +0
    -250
      brain/api/v1/settings.js
  8. +0
    -94
      brain/app.js
  9. +0
    -105
      brain/data/Auth.js
  10. +0
    -273
      brain/data/Book.js
  11. +0
    -75
      brain/data/Navigation.js
  12. +0
    -304
      brain/data/Render.js
  13. +0
    -184
      brain/data/Settings.js
  14. +0
    -243
      brain/data/Utils.js
  15. +0
    -62
      brain/routes/dash/index.js
  16. +0
    -79
      brain/routes/dash/nav.js
  17. +0
    -172
      brain/routes/dash/pages.js
  18. +0
    -94
      brain/routes/dash/settings.js
  19. +0
    -57
      brain/views/book-index.pug
  20. +0
    -94
      brain/views/email/base.pug
  21. +0
    -8
      brain/views/error.pug
  22. +0
    -42
      brain/views/frame.pug
  23. +0
    -9
      brain/views/index.pug
  24. +0
    -52
      brain/views/init.pug
  25. +0
    -16
      brain/views/navigation.pug
  26. +0
    -57
      brain/views/page-edit.pug
  27. +0
    -7
      brain/views/partials/dash-nav.pug
  28. +0
    -37
      brain/views/partials/editor.pug
  29. +0
    -30
      brain/views/partials/front.pug
  30. +0
    -7
      brain/views/partials/login.pug
  31. +0
    -25
      brain/views/partials/mailforms.pug
  32. +0
    -13
      brain/views/partials/options.pug
  33. +0
    -94
      brain/views/settings.pug
  34. +1
    -0
      config/folks.json
  35. +0
    -0
      config/init/folks-template.json
  36. +0
    -0
      config/init/index-template.md
  37. +0
    -0
      config/init/settings-template.json
  38. +1
    -0
      config/settings.json
  39. +1
    -0
      config/tags.json
  40. +1
    -0
      index.php
  41. +0
    -92
      init.js
  42. +0
    -12895
      package-lock.json
  43. +0
    -68
      package.json

+ 1
- 0
.prettierignore View File

@@ -3,4 +3,5 @@ README.md
*.pug
*.sass
*.json
*.php


+ 0
- 1
.prettierrc View File

@@ -5,7 +5,6 @@
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"parser": "babel",
"proseWrap": "preserve",
"requirePragma": false,
"semi": true,


+ 0
- 235
brain/api/v1/auth.js View File

@@ -1,235 +0,0 @@
import * as DataEvent from '../../../src/com/events/DataEvent';
import mdparser from 'markdown-yaml-metadata-parser';
const uuidv4 = require('uuid/v4');
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const fs = require('fs-extra');
const _ = require('lodash');
const crypto = require('crypto'); // for setting up new accounts
const secret_key = '58d5aeec3c604e2837aef70bc1606f35131ab8fea9731925558f5acfaa00da60';
const moment = require('moment');

/**
* Get Auth Status
*/
router.get('/', function (req, res) {
var token = req.headers['x-access-token'];
if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });

jwt.verify(token, 'super-secret-string', function (err, decoded) {
if (err)
return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
res.status(200).send(decoded);
});
});

/**
* Get Auth Status
*/
router.get('/status', function (req, res) {
if (req.session.user) {
let session = req.session;
res.json({
type: DataEvent.API_REQUEST_GOOD,
message: 'Auth is Good',
token: session.hashToken
});
} else {
res.json({
type: DataEvent.API_REQUEST_LAME,
message: 'NOT AUTHORIZED'
});
}
});
/**
* Login Member and return token
*/
router.post('/login', function (req, res) {
fs.readJson('site/folks.json').then(folks => {
let found = _.find(folks, { handle: req.body.handle });
if (found) {
if (!isValidPassword(found, req.body.password)) {
res.json({
type: DataEvent.REQUEST_LAME,
message: 'CHECK YOUR PASSWORD'
});
}

let token = jwt.sign({ id: found.id }, found.key, {
expiresIn: 86400 // expires in 24 hours
});

let session = req.session;
session.user = found;
session.token = token;
session.hashToken = hashToken(token);
res.json({
type: DataEvent.REQUEST_GOOD,
message: 'Welcome Back',
token: session.hashToken
});
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: 'Need to see some id, champ.'
});
}
});
});

/**
* Initial Site Setup
*/
router.post('/init', function (req, res) {
let body = req.body;
let re = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
// check email
if (!re.test(body.new_member_email)) {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'Need a valid email address'
});
}
//check handle is being passed
if (body.new_member_handle === null || body.new_member_handle === '') {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'No handle. Kinda need that.'
});
}
// check password match
if (
body.new_member_pass !== body.new_member_pass2 ||
body.new_member_pass === '' ||
body.new_member_pass2 === ''
) {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'Passwords do not match.'
});
}

if (body.new_member_title === null || body.new_member_title === '') {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'No title. Gotta call it something.'
});
}

let key = crypto
.createHash('sha256')
.update(body.new_member_pass + secret_key)
.digest('hex');

// set up config files
fs.readJson('site/init/settings-template.json').then(fresh => {
fresh.global.title = body.new_member_title;
fs.writeJSON('site/settings.json', fresh);
});

fs.readJson('site/init/folks-template.json').then(folks => {
folks[0].id = 1;
folks[0].handle = body.new_member_handle;
folks[0].email = body.new_member_email;
folks[0].password = bcrypt.hashSync(body.new_member_pass, bcrypt.genSaltSync(10), null);
folks[0].key = key;
folks[0].role = 'hnic';
folks[0].created = moment(Date.now()).format();
folks[0].updated = moment(Date.now()).format();
fs.writeJSON('site/folks.json', folks);
});

fs.writeJson('site/tags.json', { tags: [] });

//set up index file as first page

fs.readFile('site/init/index-template.md', { encoding: 'utf8' }).then(file => {
let index = mdparser(file);
let data = index.metadata;
data.uuid = uuidv4();
data.path = moment().format('YYYY') + '/' + moment().format('MM');
data.author = body.new_member_handle;
data.created = moment(Date.now()).format();
data.updated = moment(Date.now()).format();

var init =
'---\n' +
'id: ' +
data.id +
'\n' +
'uuid: ' +
data.uuid +
'\n' +
'title: ' +
data.title +
'\n' +
'feature: ' +
data.feature +
'\n' +
'path: ' +
moment(Date.now()).format('YYYY') +
'/' +
moment(Date.now()).format('MM') +
'\n' +
'layout: ' +
'index' +
'\n' +
'tags: ' +
data.tags +
'\n' +
'author: ' +
body.new_member_handle +
'\n' +
'created: ' +
moment(Date.now()).format() +
'\n' +
'updated: ' +
moment(Date.now()).format() +
'\n' +
'deleted: ' +
'false' +
'\n' +
'menu: ' +
data.menu +
'\n' +
'featured: ' +
data.featured +
'\n' +
'published: ' +
data.published +
'\n' +
'slug: ' +
data.slug +
'\n' +
'---\n' +
index.content;

fs.ensureDir('content/pages/').then(() => {
fs.writeFile('content/pages/index.md', init)
.then(() => {
//console.log('index file created');
})
.catch(() => {
//console.log('ERROR', err);
});
});
});

res.json({
type: DataEvent.API_INIT_GOOD,
message: 'All Set Up'
});
});

//router.post('/logout', function(req, res) {});
module.exports = router;

function isValidPassword(user, password) {
return bcrypt.compareSync(password, user.password);
}

function hashToken(token) {
return bcrypt.hashSync(token, bcrypt.genSaltSync(10), null);
}

+ 0
- 107
brain/api/v1/backup.js View File

@@ -1,107 +0,0 @@
import * as DataEvent from '../../../src/com/events/DataEvent';
import Auth from '../../data/Auth';
import Utils from '../../data/Utils';
const express = require('express');
const router = express.Router();
const multer = require('multer');
const auth = new Auth();
const utils = new Utils();

var backup_upload = multer().array('backup_upload');
var backup_restore = multer().any();

/***
CREATE BACK UP
*/
router.post('/create', (req, res) => {
auth.authCheck(req)
.then(() => {
utils
.createBackup()
.then(() => {
res.json({
type: DataEvent.API_BACKUP_CREATE,
message: "You're backed up. Hi fives"
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});

/***
RETRIEVE BACKUP
*/
router.get('/download', (req, res) => {
if (req.session.user) {
var filePath = 'content/backup.zip'; // Or format the path using the `id` rest param
var fileName = 'backup.zip'; // The default name the browser will use

res.download(filePath, fileName);
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
});
}

//Move to route?
});

/***
RESTORE BACKUP
*/

router.post('/restore', backup_upload, (req, res) => {
auth.authCheck(req)
.then(() => {
utils
.restoreBackup(req.files[0])
.then(() => {
res.json({
type: DataEvent.API_BACKUP_RESTORE,
message: 'Settings, files and pages restored. Nice work.'
});
})
.catch(err => {
res.json({
type: err.type,
message: 'Backup not restored. Uh oh.'
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});

router.post('/init-restore', backup_restore, (req, res) => {
utils
.verifyBackup(req.files[0], req.body)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
module.exports = router;

+ 0
- 83
brain/api/v1/mailer.js View File

@@ -1,83 +0,0 @@
import Settings, { SETTINGS_FILE } from '../../data/Settings';
import Auth from '../../data/Auth';
var express = require('express');
var router = express.Router();
var nodemailer = require('nodemailer');
var mg = require('nodemailer-mailgun-transport');
const pug = require('pug');
const settings = new Settings();
const auth = new Auth();
router.post('/', function (req, res) {
auth.authCheck(req)
.then(() => {
settings
.load(SETTINGS_FILE)
.then(settings => {
let transport = '';
var auth = '';
switch (settings.email.active) {
case 'option-smtp':
auth = {
host: settings.email.smtp.domain,
port: 587,
secure: false,
auth: {
type: 'login',
user: settings.email.smtp,
pass: settings.email.smtp.password
}
};
transport = nodemailer.createTransport(auth);
break;
case 'option-mg':
auth = {
auth: {
api_key: settings.email.mailgun.key,
domain: settings.email.mailgun.domain
}
};
transport = nodemailer.createTransport(mg(auth));
break;
}
let render = pug.compileFile('brain/views/email/base.pug');
let html = render({
title: settings.global.title,
header: 'a note from ' + settings.global.title,
content: req.body.content,
footer: 'powered by fipamo'
});
transport.sendMail(
{
from: 'control@playvico.us',
to: req.session.user.email, // An array if you have multiple recipients.
subject: 'Hey beautiful',
//You can use "html:" to send HTML email content. It's magic!
html: html
//You can use "text:" to send plain-text content. It's oldschool!
//text: 'Mailgun rocks, pow pow!'
},
function (err, info) {
if (err) {
res.json({
message: 'MAIL ERROR',
desc: err
});
} else {
//console.log(info);
res.json({
message: 'MAIL SENT',
desc: info
});
}
}
);
})
.catch(() => {
//console.error(err);
});
})
.catch(err => {
res.json(err);
});
});
module.exports = router;

+ 0
- 268
brain/api/v1/pages.js View File

@@ -1,268 +0,0 @@
import Book from '../../data/Book';
import Auth from '../../data/Auth';
import Settings, { SETTINGS_FILE } from '../../data/Settings';
import * as DataEvent from '../../../src/com/events/DataEvent';
import Render from '../../data/Render';
const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs-extra');
const moment = require('moment');
const book = new Book();
const auth = new Auth();
const settings = new Settings();
const render = new Render();
const _ = require('lodash');
const uploadPath =
'./public/assets/images/blog/' + moment().format('YYYY') + '/' + moment().format('MM');

var storage = multer.diskStorage({
destination: function (req, file, cb) {
fs.ensureDir(uploadPath, () => {
// dir has now been created, including the directory it is to be placed in
cb(null, uploadPath);
});
},
filename: function (req, file, cb) {
var splice = file.originalname.split(':');
cb(null, splice[0]);
}
});

var feature_upload = multer({
storage: storage
}).array('feature_image');
var post_upload = multer({
storage: storage
}).array('post_image');

/**
* Retrieves a page of a published entries
* @public
*/
router.get('/published/:pageNum?', (req, res) => {
//console.log('PAGE NUM', req.params.pageNum);
let pageNum = req.params.pageNum;
if (pageNum === null || pageNum === '' || !pageNum) pageNum = 1;
let pages = [];
book.getPage().then(result => {
result.sort((a, b) => parseFloat(b.metadata.id) - parseFloat(a.metadata.id));
let displayed = _.filter(result, page => {
return (
page.metadata.deleted === false &&
page.metadata.published === true &&
page.metadata.layout != 'index'
);
});
var pageLimit = 6;
var count = Math.ceil(displayed.length / pageLimit);
if (pageNum > count || isNaN(pageNum))
res.json({ type: DataEvent.REQUEST_LAME, message: "That page doesn't exist, champ." });
var rangeIndex = pageNum * pageLimit - pageLimit;

let meta = [];

for (let index = 0; index < pageLimit; index++) {
const page = displayed[index + rangeIndex];
try {
if (
page.metadata.id != null &&
page.metadata.deleted === false &&
page.metadata.published === true
) {
let entry = page.metadata;
entry.content = page.content;
//console.log('ENTRY', entry);
pages.push({
page: entry,
displayDate: moment(page.metadata.created).fromNow()
});
}
} catch (e) {
//console.log("NO POST", e)
}
}
meta.push({ currentPage: pageNum, totalPages: count });
let data = { pages: pages, meta: meta };
res.json({
type: DataEvent.REQUEST_GOOD,
message: 'This is Page ' + pageNum + ' of ' + count,
data: data
});
});
});

/**
* Retrieves single entry
* @public
*/

router.get('/single/:id', (req, res) => {
let id = req.params.id;
if (id === null || id === '')
res.json({ type: DataEvent.REQUEST_LAME, message: " Nah, this isn't here." });
book.getPage(id)
.then(page => {
let entry = page.metadata;
entry.content = page.content;
res.json({
type: DataEvent.REQUEST_GOOD,
message: 'Found it. Here you go.',
data: entry
});
})
.catch(err => {
res.json({
type: DataEvent.REQUEST_LAME,
message: "This doesn't seem to be here, homie.",
err: err.message
});
});
});

/**
* Add/Update Page
*/
router.post('/write/:task?', feature_upload, (req, res) => {
auth.authCheck(req)
.then(() => {
let body = _.mapValues(req.body);
let feature = '';
let task = '';
req.params.task === 'new'
? (task = DataEvent.API_PAGE_CREATE)
: (task = DataEvent.API_PAGE_WRITE);
if (req.files.length > 0) {
var path = req.files[0].path;
//console.log('NEW FEATURE URL', path);
feature = '/' + path.substring(7, path.length);
} else {
var url = body.feature_image;
//switch this to the new feature path edit
if (url != null || url != undefined || url != '') {
let chunks = url.split('/');
let strip = chunks[0] + '/' + chunks[1] + chunks[2];
feature = url.substr(strip.length + 1, url.length);
} else {
feature = '';
}
}
body.feature = feature;
body.deleted = false;
//if title changes, get rid of a pages with old title
if (body.current_title !== body.slug) {
let path =
moment(body.created).format('YYYY') + '/' + moment(body.created).format('MM');

//remove html page
fs.unlink('public/' + path + '/' + body.current_title + '.html')
.then()
.catch(() => {
//console.log('HTML ERROR', err);
});

//remove markdown
fs.unlink('content/pages/' + path + '/' + body.current_title + '.md')
.then()
.catch(() => {
//console.log('MD ERROR', err);
});
}
book.editPage(body, body.page_uuid, task, req.session.user)
.then(result => {
if (result.type === DataEvent.PAGE_ADDED) {
settings.updatePageIndex();
}
//load all page data and render if render on save flag is set in settings file
getBookData()
.then(result => {
if (result.settings.global.renderOnSave === 'true') {
render
.publishAll(
result.pages,
result.settings.global.theme,
req.session.user.handle
)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: DataEvent.PAGES_NOT_RENDERED,
message: 'Uh oh. Pages not rendered, sport',
error: err
});
});
} else {
//console.log('DONT RENDER PAGES');
}
})
.catch(() => {
//console.log();
});
res.json(result);
})
.catch(err => {
res.json(err);
});
})
.catch(err => {
res.json(err);
});
});

/**
* Soft deletes Page
*/

router.post('/delete', (req, res) => {
auth.authCheck(req)
.then(() => {
book.editPage([], req.body.id, DataEvent.API_PAGE_DELETE, req.session.user)
.then(result => {
//remove item from menu in settings
res.json(result);
})
.catch(err => {
res.json(err);
});
})
.catch(err => {
res.json(err);
});
});

/**
* Uploads image from a Page content
*/

router.post('/add-post-image', post_upload, function (req, res) {
//console.log(req.body);
var image = req.files[0].path;
return res.json({
type: DataEvent.POST_IMAGE_ADDED,
message: 'Added Image',
url: '/' + image.substr(7, image.length)
});
});

module.exports = router;

function getBookData() {
return new Promise((resolve, reject) => {
let getSettings = settings.load(SETTINGS_FILE);
let getBook = book.getPage();
Promise.all([getSettings, getBook])
.then(result => {
const [settings, pages] = result;
let data = { settings: settings, pages: pages };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}

+ 0
- 250
brain/api/v1/settings.js View File

@@ -1,250 +0,0 @@
import * as DataEvent from '../../../src/com/events/DataEvent';
import Auth from '../../data/Auth';
import Render from '../../data/Render';
import Settings, { SETTINGS_FILE, SETTINGS_FOLKS } from '../../data/Settings';
import Navigation from '../../data/Navigation';
import Book from '../../data/Book';
const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs-extra');
const moment = require('moment');
const _ = require('lodash');
const auth = new Auth();
const render = new Render();
const book = new Book();
const settings = new Settings();
const nav = new Navigation();
const uploadPath =
'./public/assets/images/user/' + moment().format('YYYY') + '/' + moment().format('MM');

var storage = multer.diskStorage({
destination: function (req, file, cb) {
fs.ensureDir(uploadPath, () => {
// dir has now been created, including the directory it is to be placed in
cb(null, uploadPath);
});
},
filename: function (req, file, cb) {
var splice = file.originalname.split(':');
cb(null, splice[0]);
}
});
var avatar_upload = multer({
storage: storage
}).array('avatar_upload');
var background_upload = multer({
storage: storage
}).array('background_upload');
//** SYNC POSTS */
router.post('/sync', (req, res) => {
auth.authCheck(req)
.then(() => {
settings
.sync(req, res)
.then(() => {
res.json({
type: DataEvent.SETTINGS_UPDATED,
message: 'Settings Saved'
});
})
.catch(err => {
res.json({
type: DataEvent.REQUEST_LAME,
error: err.message,
message: "Uh oh. Settings didn't take, sport"
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});

router.post('/nav-sync', (req, res) => {
auth.authCheck(req)
.then(() => {
// find removed menu item page and set menu to false
book.getPage(req.body.remove).then(page => {
let body = page.metadata;
body.content = page.content;
body.menu = false;
book.editPage(body, body.uuid, DataEvent.API_PAGE_WRITE, req.session.user);
});
nav.sync(req.body)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: DataEvent.REQUEST_LAME,
message: err
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});

router.post('/publish-pages', (req, res) => {
auth.authCheck(req)
.then(() => {
getBookData()
.then(result => {
render
.publishAll(
result.pages,
result.settings.global.theme,
req.session.user.handle
)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: DataEvent.PAGES_NOT_RENDERED,
message: 'Uh oh. Pages not rendered, sport',
error: err
});
});
})
.catch(err => {
res.json({
type: DataEvent.PAGES_NOT_RENDERED,
message: 'Uh oh. Pages not rendered, sport',
error: err
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});

/***
UPLOAD AVATAR
*/

router.post('/add-avatar', avatar_upload, (req, res) => {
if (req.session.user) {
let user = req.session.user;
settings
.load(SETTINGS_FOLKS)
.then(folks => {
let found = _.find(folks, { handle: user.handle });
if (found) {
var index = found.id - 1;
var path = req.files[0].path;
var image = path.substr(7, path.length);
folks[index].avi = '/' + image;
fs.writeJson('site/folks.json', folks);
user.avi = '/' + image;
res.json({
type: DataEvent.AVATAR_UPLOADED,
message: 'Changed avi. You look great.',
url: '/' + image
});
}
})
.catch(() => {
res.json({
type: DataEvent.REQUEST_LAME,
message: 'Members Not found'
});
});
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
});
}
});

/***
UPLOAD FEATURE BACKGROUND
*/

router.post('/add-feature-background', background_upload, (req, res) => {
if (req.session.user) {
settings
.load(SETTINGS_FILE)
.then(settings => {
var path = req.files[0].path;
var image = path.substr(7, path.length);
settings.global.background = '/' + image;
fs.writeJson('site/settings.json', settings);
res.json({
type: DataEvent.SITE_BACKGROUND_UPLOADED,
message: 'Background Uploaded',
url: '/' + image
});
})
.catch(() => {
//console.log('ERROR', err);
});
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
});
}
});

router.post('/reindex', (req, res) => {
auth.authCheck(req)
.then(() => {
book.reindexPages(req)
.then(response => {
//reset settings index
settings.resetLibraryIndex(response.count + 1);
//return success to front end
res.json(response);
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});

module.exports = router;

function getBookData() {
return new Promise((resolve, reject) => {
let getSettings = settings.load(SETTINGS_FILE);
let getBook = book.getPage();
Promise.all([getSettings, getBook])
.then(result => {
const [settings, pages] = result;
let data = { settings: settings, pages: pages };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}

+ 0
- 94
brain/app.js View File

@@ -1,94 +0,0 @@
var express = require('express');
var path = require('path');
//var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session');
var MemoryStore = require('memorystore')(session);
var flash = require('connect-flash');
var app = express();
// favicon stuff
//app.use(favicon(path.join(__dirname, 'favicons', 'favicon.ico')));

// view engine setup
app.set('views', path.join(__dirname, './views'));
app.set('view engine', 'pug');
app.use(logger('dev'));

app.use(bodyParser.json({ limit: '50mb' }));
app.use(
bodyParser.urlencoded({
extended: false,
limit: '50mb'
})
);
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../public'), { extensions: ['html'] }));

app.use(
session({
store: new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h
}),
secret: '1KqZ18W8KskE1iSw',
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 608800000
}
})
);
app.use(flash());
//sections
//var front = require('./routes/front/index')(session);
var dash = require('./routes/dash/index');
var page = require('./routes/dash/pages');
var settings = require('./routes/dash/settings');
var nav = require('./routes/dash/nav');
//api
var pages = require('./api/v1/pages');
var setting = require('./api/v1/settings');
var mailer = require('./api/v1/mailer');
var auth = require('./api/v1/auth');
var backup = require('./api/v1/backup');
// API PATHS

app.use('/api/v1/page', pages);
app.use('/api/v1/settings', setting);
app.use('/api/v1/auth', auth);
app.use('/api/v1/mailer', mailer);
app.use('/api/v1/backup', backup);
// PAGES
app.use('/@/dashboard', dash);
app.use('/@/dashboard/page', page);
app.use('/@/dashboard/settings', settings);
app.use('/@/dashboard/navigation', nav);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function (err, req, res) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function (err, req, res) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;

+ 0
- 105
brain/data/Auth.js View File

@@ -1,105 +0,0 @@
import * as DataEvent from '../../src/com/events/DataEvent';
const bCrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const _ = require('lodash');

export default class Auth {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
start() {}

/**
* Makes sure access token is legit
* @parameter req
*/

authCheck(req) {
let self = this;
return new Promise((resolve, reject) => {
let hash = req.headers['x-access-token'];
let response = [];
//check to see if user is logged in
if (!req.session.user) {
response = {
status: false,
type: DataEvent.API_REQUEST_LAME,
message: "You're not logged in, champ."
};
reject(response);
}

//Checks if token is a proper hash, if not reject
if (!self.isTokenValid(req.session.token, hash)) {
response = {
status: false,
type: DataEvent.API_REQUEST_LAME,
message: 'No Token Present. Auth Blocked'
};
reject(response);
//res.json();
} else {
var member = req.session.user;
jwt.verify(req.session.token, member.key, function (err, decoded) {
if (err) {
response = {
status: false,
type: DataEvent.API_REQUEST_LAME,
message: 'Invalid Token. Auth Blocked'
};
reject(response);
}
response = {
status: true,
type: DataEvent.API_REQUEST_GOOD,
message: 'Token Verified',
token: decoded
};
resolve(response);
});
}
});
}

verifyCredentials(config, credentials) {
return new Promise((resolve, reject) => {
var found = _.find(config, { handle: credentials.handle });
var response;
if (found) {
if (!this.isValidPassword(found, credentials.pass)) {
response = {
type: DataEvent.REQUEST_LAME,
message: 'CHECK YOUR PASSWORD'
};
reject(response);
}

response = { type: DataEvent.REQUEST_GOOD, message: 'Backup Verified. Restoring' };
resolve(response);
} else {
response = { type: DataEvent.REQUEST_LAME, message: 'Handle not found, boss' };
reject(response);
}
});
}

isValidPassword(user, password) {
return bCrypt.compareSync(password, user.password);
}

/**
* Checks to make sure received token matches
* @parameter token: created token
* @parameter hashedToken: encrypted token
*/
isTokenValid(token, hashedToken) {
return bCrypt.compareSync(token, hashedToken);
}
//--------------------------
// event handlers
//--------------------------
}

+ 0
- 273
brain/data/Book.js View File

@@ -1,273 +0,0 @@
import fh from 'filehound';
import fs from 'fs-extra';
import metadataParser from 'markdown-yaml-metadata-parser';
import _ from 'lodash';
import * as DataEvent from '../../src/com/events/DataEvent';
import Navigation from './Navigation';
import Utils from './Utils';
const moment = require('moment');
const nav = new Navigation();
const utils = new Utils();

/**
* Class for handling blog content pages
*/

export default class Book {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
start() {}
/**
* Retrieves single page or pages
* @parameter id: optional id if requesting a single Page
*/
getPage(id) {
return new Promise((resolve, reject) => {
fh.create()
.paths('content/pages')
.ext('md')
.find()
.then(files => {
let pages = [];
for (let index = 0; index < files.length; index++) {
fs.readFile(files[index], { encoding: 'utf8' }, (err, file) => {
pages.push(metadataParser(file));
});
}
if (id === null || id === null || id === undefined) {
setTimeout(() => {
//TODO: Duct tape solution until something better created
utils.organizeTags(pages);
utils.organizeArchive(pages);
resolve(pages);
}, 100);
} else {
setTimeout(() => {
//TODO: Duct tape solution until something better created

//make check against menu to see if page should be marked as menu item
//if it doesn't exist in menu change, edit page to
let page = _.find(pages, list => {
return list.metadata.uuid === id;
});
resolve(page);
}, 100);
}
})
.catch(err => {
reject(err);
});
});
}
/**
* Edits single page based on id and task
* @parameter body: object that contains all page information
* @parameter id: identifier for page being edited
* @parameter task: type of task being performed - listed in DataEvents Class /src/com/events
* @parameter user: object contain user information
*/
editPage(body, id, task, user) {
return new Promise((resolve, reject) => {
let self = this;
let response = [];
switch (task) {
case DataEvent.API_PAGE_CREATE:
case DataEvent.API_PAGE_WRITE:
var layout = 'page';
var path = '';
fs.ensureDir(
'content/pages/' +
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM') +
'/'
).then(() => {
if (body.menu === 'true') {
body.path =
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM');
nav.editMenu(DataEvent.MENU_ADD_ITEM, body, user);
} else {
nav.editMenu(DataEvent.MENU_DELETE_ITEM, body, user);
}
if (body.layout !== 'page') layout = body.layout;
if (body.layout === null || body.layout === 'null') layout = 'page';
var pageWrite =
'---\n' +
'id: ' +
body.id +
'\n' +
'uuid: ' +
body.uuid +
'\n' +
'title: ' +
body.title +
'\n' +
'feature: ' +
body.feature +
'\n' +
'path: ' +
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM') +
'\n' +
'layout: ' +
layout +
'\n' +
'tags: ' +
body.tags +
'\n' +
'author: ' +
user.handle +
'\n' +
'created: ' +
moment(body.created).format() +
'\n' +
'updated: ' +
moment(Date.now()).format() +
'\n' +
'deleted: ' +
body.deleted +
'\n' +
'menu: ' +
body.menu +
'\n' +
'featured: ' +
body.featured +
'\n' +
'published: ' +
body.published +
'\n' +
'slug: ' +
body.slug +
'\n' +
'---\n' +
body.content;
layout === 'index'
? (path = 'content/pages/index.md')
: (path =
'content/pages/' +
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM') +
'/' +
body.slug +
'.md');
fs.writeFile(path, pageWrite, err => {
// throws an error, you could also catch it here

if (err) {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
}

// success case, the file was saved
if (task === DataEvent.API_PAGE_CREATE) {
// if new file, update settings index and page count
response = {
type: DataEvent.PAGE_ADDED,
message: 'New Page Created',
id: body.uuid
};
resolve(response);
} else {
response = {
type: DataEvent.PAGE_UPDATED,
message: 'Page saved. Nice Work'
};
resolve(response);
}
});
});

break;
case DataEvent.API_PAGE_DELETE:
this.getPage(id)
.then(page => {
let body = _.mapValues(page.metadata);

body.content = page.content;
body.deleted = moment(Date.now()).format();
body.menu = false;

self.editPage(body, body.uuid, DataEvent.API_PAGE_WRITE, user)
.then(() => {
let item = {
title: body.title,
id: body.id,
slug: body.slug,
uuid: body.uuid
};
nav.editMenu(DataEvent.MENU_DELETE_ITEM, item);

response = {
type: DataEvent.PAGE_DELETED,
message: 'Page deleted, sport',
data: { uuid: body.uuid }
};
resolve(response);
})
.catch(err => {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
});
})
.catch(err => {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
});
break;
}
});
}

reindexPages(req) {
var response = '';
var self = this;
return new Promise((resolve, reject) => {
self.getPage()
.then(pages => {
let sorted = [];
for (let i = 0; i < pages.length; i++) {
let body = pages[i].metadata;
body.content = pages[i].content;
sorted.push(body);
}
//resorts pages by date created
let byDate = _.sortBy(sorted, page => {
return page.created;
});
//reassigns id sequentially based on sorted pages
for (let index = 0; index < byDate.length; index++) {
byDate[index].id = index;
self.editPage(
byDate[index],
index,
DataEvent.API_PAGE_WRITE,
req.session.user
);
}
response = {
type: DataEvent.API_REINDEX_PAGES,
message: 'Pages re-sorted. Easy peasy.',
count: byDate.length
};
resolve(response);
})
.catch(err => {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
});
});
}

//--------------------------
// event handlers
//--------------------------
}

+ 0
- 75
brain/data/Navigation.js View File

@@ -1,75 +0,0 @@
import fs from 'fs-extra';
import _ from 'lodash';
import * as DataEvent from '../../src/com/events/DataEvent';
import Settings, { SETTINGS_FILE } from './Settings';
const settings = new Settings();

export default class Navigation {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
sync(body) {
return new Promise((resolve, reject) => {
let response = [];
settings
.load(SETTINGS_FILE)
.then(settings => {
let payload = body;
settings.menu = payload.nav;
fs.writeJson('site/settings.json', settings)
.then(() => {
response = {
type: DataEvent.SETTINGS_UPDATED,
message: 'Menu order saved, champ'
};
resolve(response);
})
.catch(err => {
response = {
type: DataEvent.REQUEST_LAME,
message: err
};
reject(response);
});
})
.catch(err => {
response = {
type: DataEvent.REQUEST_LAME,
message: err
};
reject(response);
});
});
}

editMenu(task, item) {
settings.load(SETTINGS_FILE).then(settings => {
switch (task) {
case DataEvent.MENU_ADD_ITEM:
settings.menu.push({
title: item.title,
id: item.id,
slug: item.slug,
uuid: item.uuid,
path: item.path
});
break;
case DataEvent.MENU_DELETE_ITEM:
settings.menu = _.remove(settings.menu, m => {
return m.uuid != item.uuid;
});

break;
}
fs.writeJSON(SETTINGS_FILE, settings);
});
}

//--------------------------
// event handlers
//--------------------------
}

+ 0
- 304
brain/data/Render.js View File

@@ -1,304 +0,0 @@
import * as DataEvent from '../../src/com/events/DataEvent';
import StringUtils from '../../src/com/utils/StringUtils';
import Settings, { SETTINGS_FILE, SETTINGS_TAG } from './Settings';
import fs from 'fs-extra';
import sanitize from 'sanitize-html';
import Utils from './Utils';
const pug = require('pug');
const md = require('markdown-it')('commonmark');
const _ = require('lodash');
const moment = require('moment');
const settings = new Settings();

export default class Render {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
start() {}

/**
* Renders all pages from markdown to html
* @parameter pages: payload for site pages
* @parameter theme: current theme being used as defined in settings
*/
publishAll(pages, theme, author) {
return new Promise((resolve, reject) => {
settings
.load(SETTINGS_FILE)
.then(config => {
let response = [];
let count = _.filter(pages, page => {
return page.metadata.deleted === false && page.metadata.published === true;
}).length;
let rendered = 0;
let display_count = 0;
let recent = [];
let featured = _.filter(pages, page => {
return (
page.metadata.deleted === false &&
page.metadata.published === true &&
page.metadata.featured === true &&
page.metadata.layout !== 'index'
);
});
for (let index = 0; index < pages.length; index++) {
pages.sort((a, b) => parseFloat(b.metadata.id) - parseFloat(a.metadata.id));
const page = pages[index];
if (page.metadata.deleted === false && page.metadata.published === true) {
if (page.metadata.layout != 'index') {
if (recent.length < config.global.display_limit) {
recent.push({
title: page.metadata.title,
slug: page.metadata.slug,
feature: page.metadata.feature,
created: moment(page.metadata.created).fromNow(),
path: page.metadata.path
});
display_count = ++display_count;
}
}

let writeFile, template;

let path =
'public/' +
moment(page.metadata.created).format('YYYY') +
'/' +
moment(page.metadata.created).format('MM') +
'/';
if (page.metadata.layout === 'index') {
template = 'content/themes/' + theme + '/index.pug';
writeFile = 'public/index.html';
} else {
writeFile = path + page.metadata.slug + '.html';
template = 'content/themes/' + theme + '/page.pug';
}

let buffed = sanitize(page.content, {
allowedTags: ['del', 'a', 'iframe', 'img'],
allowedAttributes: {
a: ['href', 'name', 'target'],
img: ['src'],
iframe: [
'height',
'width',
'src',
'frameborder',
'allow',
'allowfullscreen'
]
}
});
let bag = page.metadata.tags.split(',');
let tags = [];
for (let index = 0; index < bag.length; index++) {
let tag = bag[index].trim();
tags.push({
label: bag[index],
slug: new StringUtils().cleanString(tag)
});
}
buffed = new StringUtils().decodeHTML(buffed);
let html = md.render(buffed, { html: true, xhtmlOut: true });
//add open graph meta variables
let file = pug.renderFile(template, {
title: page.metadata.title,
default_bg: page.metadata.feature,
image: page.metadata.feature,
keywords: page.metadata.tags,
content: html,
tags: tags,
menu: config.menu,
recent_posts: recent,
featured_posts: featured,
meta: {
who: author,
when: moment(page.metadata.created).fromNow(),
tags: tags
},
welcome_message: page.metadata.title
});

fs.ensureDir(path).then(() => {
fs.writeFile(writeFile, file, err => {
// throws an error, you could also catch it here
if (err) {
response = {
type: DataEvent.PAGES_NOT_RENDERED,
message: err
};
reject(response);
}

// success case, the file was saved
});
});
rendered = ++rendered;
if (rendered === count) {
response = {
type: DataEvent.PAGES_RENDERED,
message: 'All Pages Rendered. Sweet.'
};
//move theme assets to public when pages are rendered
new Utils().moveAssets();
resolve(response);
}
} else {
if (count === 0) {
response = {
type: DataEvent.PAGES_RENDERED,
message: 'No page rendering needed'
};
resolve(response);
}
//check to see if deleted pages have been renderered and delete them
if (page.metadata.layout !== 'index') {
fs.unlink(
'public/' +
page.metadata.path +
'/' +
page.metadata.slug +
'.html'
)
.then()
.catch(() => {
//console.log('ERROR', err);
});
}
}
}
})
.catch(err => {
//console.log('ERROR', err);
reject(err);
});
});
}
/**
* Method to extract, group and render tags in page
* @parameter pages: payload for site pages
*/
publishTags(pages) {
let self = this;
return new Promise((resolve, reject) => {
self.loadRenderData()
.then(result => {
let tags = result.tags.tags;
let renderList = [];
for (let index = 0; index < tags.length; index++) {
let tag = tags[index];
//console.log('**TAG**', tag.tag_name);
var pageList = [];
for (let i = 0; i < pages.length; i++) {
let page = pages[i];

//TODO: filter for deleted and unpublished pages
if (
page.metadata.deleted === false &&
page.metadata.published === true
) {
if (_.includes(page.metadata.tags, tag.tag_name)) {
pageList.push({
title: page.metadata.title,
slug: page.metadata.slug,
path: page.metadata.path
});
}
}
}
renderList.push({ tag: tag.tag_name, tag_list: pageList, slug: tag.slug });
}
let response = [];
for (let index = 0; index < renderList.length; index++) {
let item = renderList[index];
let file = pug.renderFile(
'content/themes/' + result.settings.global.theme + '/tags.pug',
{
title: item.tag,
default_bg: result.settings.global.background,
content_tags: 'THESE ARE TAGS',
tag_list: item.tag_list,
menu: result.settings.menu
}
);
fs.ensureDir('public/tags', () => {
fs.writeFile('public/tags/' + item.slug + '.html', file, err => {
// throws an error, you could also catch it here
if (err) {
response = {
type: DataEvent.TAG_PAGES_NOT_RENDERED,
message: err
};
reject(response);
}
// success case, the file was saved
response = {
type: DataEvent.TAG_PAGES_RENDERED,
message: 'Tag Pages ready to go. Good job.'
};
resolve(response);
});
});
}
})
.catch(err => {
reject(err);
});
});
}
/**
* Method to build page that lists all active pages, organized by year and month
* @parameter pages: payload for site pages
*/
publishArchive(archive) {
settings
.load(SETTINGS_FILE)
.then(settings => {
let file = pug.renderFile(
'content/themes/' + settings.global.theme + '/archive.pug',
{
title: 'ARCHIVES',
default_bg: settings.global.background,
content_tags: 'COLD STORAGE',
archives: archive,
menu: settings.menu
}
);

fs.writeFile('public/archives.html', file, err => {
// throws an error, you could also catch it here
if (err) {
//console.log('ERROR', err);
//response = { type: DataEvent.TAG_PAGES_NOT_RENDERED, message: err };
}
// success case, the file was saved
});
})
.catch(() => {
//console.log(err);
});
}
loadRenderData() {
return new Promise((resolve, reject) => {
let getSettings = settings.load(SETTINGS_FILE);
let getTags = settings.load(SETTINGS_TAG);
Promise.all([getSettings, getTags])
.then(result => {
const [settings, tags] = result;
let data = { settings: settings, tags: tags };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}

//--------------------------
// event handlers
//--------------------------
}

+ 0
- 184
brain/data/Settings.js View File

@@ -1,184 +0,0 @@
import * as DataEvent from '../../src/com/events/DataEvent';
import fs from 'fs-extra';
const _ = require('lodash');
export const SETTINGS_FILE = 'site/settings.json';
export const SETTINGS_FOLKS = 'site/folks.json';
export const SETTINGS_TAG = 'site/tags.json';

export default class Settings {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
sync(req) {
let self = this;
return new Promise((resolve, reject) => {
self.loadConfigData()
.then(result => {
let payload = req.body;
//so payload matches loaded config
payload.global.display_limit = result.settings.global.display_limit;
payload.global.port = result.settings.global.port;
payload.global.last_backup = result.settings.global.last_backup;
let user = req.session.user;
let found = _.find(result.folks, { id: user.id });
let needToUpdate = false;
let response = [];
if (found) {
let index = found.id - 1;
if (
result.folks[index].handle != payload.member.handle ||
result.folks[index].email != payload.member.email
) {
user.handle = payload.member.handle;
user.email = payload.member.email;
result.folks[index].handle = payload.member.handle;
result.folks[index].email = payload.member.email;
fs.writeJson('site/folks.json', result.folks);
} else {
//no need to save
}
} else {
let response = {
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
};
reject(response);
}
if (!_.isEqual(result.settings.global, payload.global)) {
let bg = payload.global.background;
let chunks = bg.split('/');
let strip = chunks[0] + '/' + chunks[1] + chunks[2];
payload.global.background = bg.substr(strip.length + 1, bg.length);
result.settings.global = payload.global;
needToUpdate = true;
} else {
//no need to save
}

if (!_.isEqual(result.settings.email, payload.email)) {
result.settings.email = payload.email;
needToUpdate = true;
} else {
//no need to save
}

if (needToUpdate) {
fs.writeJson('site/settings.json', result.settings)
.then(() => {
response = {
type: DataEvent.SETTINGS_UPDATED,
message: 'Settings Saved'
};
resolve(response);
})
.catch(() => {
//console.error(err);
});
} else {
//no need to update
}
})
.catch(err => {
reject(err);
});
});
}
saveTags(tags) {
let self = this;
return new Promise((resolve, reject) => {
self.load(SETTINGS_TAG)
.then(config => {
if (!_.isEqual(config.tags, tags)) {
config.tags = tags;
fs.writeJson('site/tags.json', config)
.then(() => {
let response = {
type: DataEvent.SETTINGS_UPDATED,
message: 'Settings Saved'
};
resolve(response);
})
.catch(err => {
reject(err);
});
} else {
let response = {
type: DataEvent.SETTINGS_NOT_UPDATED,
message: 'Settings Already Saved'
};
resolve(response);
}
})
.catch(err => {
reject(err);
});
});
}

updatePageIndex() {
fs.readJSON('site/settings.json').then(settings => {
settings.library_stats.current_index = ++settings.library_stats.current_index;
setTimeout(() => {
//TODO: Duct tape solution until something better created
fs.writeJSON('site/settings.json', settings)
.then(() => {
//console.log('ALL TO THE GOOD');
})
.catch(() => {
//.log('ERR', err);
});
}, 100);
});
}

resetLibraryIndex(index) {
fs.readJSON('site/settings.json').then(settings => {
settings.library_stats.current_index = index;
setTimeout(() => {
//TODO: Duct tape solution until something better created
fs.writeJSON('site/settings.json', settings)
.then(() => {
//console.log('ALL TO THE GOOD');
})
.catch(() => {
//.log('ERR', err);
});
}, 100);
});
}
load(fileToLoad) {
return new Promise((resolve, reject) => {
fs.readJSON(fileToLoad)
.then(file => {
resolve(file);
})
.catch(err => {
reject(err);
});
});
}

loadConfigData() {
return new Promise((resolve, reject) => {
let getSettings = this.load(SETTINGS_FILE);
let getFolks = this.load(SETTINGS_FOLKS);
Promise.all([getSettings, getFolks])
.then(result => {
const [settings, folks] = result;
let data = { settings: settings, folks: folks };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}

//--------------------------
// event handlers
//--------------------------
}

+ 0
- 243
brain/data/Utils.js View File

@@ -1,243 +0,0 @@
import Settings, { SETTINGS_FILE } from './Settings';
import Render from './Render';
import StringUtils from '../../src/com/utils/StringUtils';
import _ from 'lodash';
import Auth from '../data/Auth';
const settings = new Settings();
const render = new Render();
const stringUtils = new StringUtils();
const moment = require('moment');
const fs = require('fs-extra');
const AdmZip = require('adm-zip');
const auth = new Auth();

export default class Utils {
constructor() {}

/**
* Retrieves single page or pages
* @parameter pages: payload of pages
*/
organizeTags(pages) {
let tags = [];
for (let index = 0; index < pages.length; index++) {
const page = pages[index];
let temp = [];
temp = page.metadata.tags.split(',');
for (let i = 0; i < temp.length; i++) {
let label = temp[i].trim();
if (!_.find(tags, { tag_name: label })) {
tags.push({
tag_name: label,
slug: stringUtils.cleanString(label),
count: 1
});
} else {
_.find(tags, { tag_name: label }).count++;
}
}
}
tags = _.orderBy(tags, ['tag_name'], ['asc']);

settings.saveTags(tags).then(() => {
render
.publishTags(pages)
.then(() => {
//console.log(response);
})
.catch(() => {
//console.log(err);
});
});
}
organizeArchive(pages) {
let years = [];
let archive = [];
for (let index = 0; index < pages.length; index++) {
let page = pages[index].metadata;
if (page.layout !== 'index') {
let year = moment(page.created).format('YYYY');
if (!_.find(years, { year: year })) {
years.push({ year: year, count: 1 });
} else {