mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-19 13:19:42 +02:00
Authentication (#22)
* Authorization added * added secret to password, md5 hashing, removed promise from verifyToken, deleted links when not authorized * added dbinsert script * turned verifyToken to middleware, added description for dbinsert, added hidden csrf field in auth form * added middlewares, user model and controller * JSDoc fix * wrong password processing fix * added comments to dbinsert script, moved salt and passHash to singe db doc * Moved salt to .env, upgradedscript for generating password was, fixed comments and JSDoc * Deleted using salt (now user is only one), changed verifying password to bcrypt.compare, added httpyOnly property to jwt cookie
This commit is contained in:
parent
718be6d2f6
commit
58d3892d8f
33 changed files with 1464 additions and 58 deletions
|
@ -42,3 +42,9 @@ $ yarn lint
|
||||||
```
|
```
|
||||||
$ yarn test
|
$ yarn test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
To manage pages you need to authorize (available on `/auth`).
|
||||||
|
|
||||||
|
To generate password use `yarn generatePassword [password]` command.
|
||||||
|
|
|
@ -15,7 +15,8 @@ if (fs.existsSync(path.resolve(__dirname, configPath))) {
|
||||||
} else {
|
} else {
|
||||||
config = {
|
config = {
|
||||||
database: '.db',
|
database: '.db',
|
||||||
port: 3000
|
port: 3000,
|
||||||
|
secret: 'secret'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
45
generatePassword.js
Normal file
45
generatePassword.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
let { password: db } = require('./src/utils/database');
|
||||||
|
const program = require('commander');
|
||||||
|
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const saltRounds = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script for generating password, that will be used to create and edit pages in CodeX.Docs.
|
||||||
|
* Hashes password with bcrypt and inserts it to the database.
|
||||||
|
* @see {https://github.com/tj/commander.js | CommanderJS}
|
||||||
|
*/
|
||||||
|
program
|
||||||
|
.description('Application for generating password, that will be used to create and edit pages in CodeX.Docs.')
|
||||||
|
.usage('[password]')
|
||||||
|
.arguments('<password>')
|
||||||
|
.action(async function (password) {
|
||||||
|
bcrypt.hash(password, saltRounds, async (error, hash) => {
|
||||||
|
if (error) {
|
||||||
|
return 'Hash generating error';
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDoc = { passHash: hash };
|
||||||
|
|
||||||
|
await db.remove({}, {multi: true});
|
||||||
|
await db.insert(userDoc);
|
||||||
|
|
||||||
|
console.log('Password was successfully generated');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
program.on('--help', () => {
|
||||||
|
console.log('');
|
||||||
|
console.log('Example:');
|
||||||
|
console.log('yarn generatePassword qwerty');
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
if (process.argv.length !== 3) {
|
||||||
|
console.error('Invalid command: %s\nSee --help or -h for a list of available commands.', program.args.join(' '));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "codex.docs",
|
"name": "codex.docs",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"bin": { "generatePassword": "bin/generatePassword.js"},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=production nodemon ./bin/www",
|
"start": "cross-env NODE_ENV=production nodemon ./bin/www",
|
||||||
|
@ -8,20 +9,26 @@
|
||||||
"test": "cross-env NODE_ENV=testing mocha --recursive ./test",
|
"test": "cross-env NODE_ENV=testing mocha --recursive ./test",
|
||||||
"lint": "eslint --fix --cache ./src/**/*.js",
|
"lint": "eslint --fix --cache ./src/**/*.js",
|
||||||
"build": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --watch",
|
"build": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --watch",
|
||||||
"precommit": "yarn lint && yarn test --exit"
|
"precommit": "yarn lint && yarn test --exit",
|
||||||
|
"generatePassword": "node ./generatePassword.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "^7.0.0",
|
"@babel/polyfill": "^7.0.0",
|
||||||
|
"bcrypt": "^3.0.3",
|
||||||
"body-parser": "latest",
|
"body-parser": "latest",
|
||||||
"codex.editor": "^2.8.1",
|
"codex.editor": "^2.8.1",
|
||||||
"codex.editor.delimiter": "^1.0.2",
|
"codex.editor.delimiter": "^1.0.2",
|
||||||
"codex.editor.embed": "^2.1.2",
|
"codex.editor.embed": "^2.1.2",
|
||||||
"codex.editor.header": "^2.0.5",
|
"codex.editor.header": "^2.0.5",
|
||||||
|
"commander": "^2.19.0",
|
||||||
"codex.editor.image": "^2.0.3",
|
"codex.editor.image": "^2.0.3",
|
||||||
"codex.editor.quote": "^2.1.5",
|
"codex.editor.quote": "^2.1.5",
|
||||||
"codex.editor.raw": "^2.0.2",
|
"codex.editor.raw": "^2.0.2",
|
||||||
"cookie-parser": "~1.4.3",
|
"cookie-parser": "~1.4.3",
|
||||||
|
"csurf": "^1.9.0",
|
||||||
"debug": "~4.1.0",
|
"debug": "~4.1.0",
|
||||||
|
"dotenv": "^6.2.0",
|
||||||
|
"jsonwebtoken": "^8.4.0",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"express": "~4.16.0",
|
"express": "~4.16.0",
|
||||||
"http-errors": "~1.7.1",
|
"http-errors": "~1.7.1",
|
||||||
|
|
|
@ -19,7 +19,7 @@ require('./utils/twig');
|
||||||
|
|
||||||
app.use(logger('dev'));
|
app.use(logger('dev'));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({extended: true}));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.static(path.join(__dirname, '../public')));
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Pages {
|
||||||
* @param {string} parent - id of current page
|
* @param {string} parent - id of current page
|
||||||
* @returns {Promise<Page[]>}
|
* @returns {Promise<Page[]>}
|
||||||
*/
|
*/
|
||||||
static async getAllExceptChildrens(parent) {
|
static async getAllExceptChildren(parent) {
|
||||||
let pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
|
let pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
|
||||||
|
|
||||||
return pagesAvailable.filter((item) => item !== null);
|
return pagesAvailable.filter((item) => item !== null);
|
||||||
|
|
21
src/controllers/users.js
Normal file
21
src/controllers/users.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
const Model = require('../models/user');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class Users
|
||||||
|
* @classdesc Users controller
|
||||||
|
*/
|
||||||
|
class Users {
|
||||||
|
/**
|
||||||
|
* @static
|
||||||
|
* Find and return user model.
|
||||||
|
*
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
static async get() {
|
||||||
|
const userDoc = await Model.get();
|
||||||
|
|
||||||
|
return userDoc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Users;
|
|
@ -2,6 +2,7 @@ import hljs from 'highlight.js/lib/highlight';
|
||||||
import javascript from 'highlight.js/lib/languages/javascript';
|
import javascript from 'highlight.js/lib/languages/javascript';
|
||||||
import xml from 'highlight.js/lib/languages/xml';
|
import xml from 'highlight.js/lib/languages/xml';
|
||||||
import json from 'highlight.js/lib/languages/json';
|
import json from 'highlight.js/lib/languages/json';
|
||||||
|
// eslint-disable-next-line
|
||||||
import style from 'highlight.js/styles/atom-one-dark.css';
|
import style from 'highlight.js/styles/atom-one-dark.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +14,7 @@ export default class CodeStyler {
|
||||||
* @param {string} selector - CSS selector for code blocks
|
* @param {string} selector - CSS selector for code blocks
|
||||||
* @param {string[]} languages - list of languages to highlight, see hljs.listLanguages()
|
* @param {string[]} languages - list of languages to highlight, see hljs.listLanguages()
|
||||||
*/
|
*/
|
||||||
constructor({selector, languages = [ 'javascript', 'xml', 'json' ]}) {
|
constructor({ selector, languages = ['javascript', 'xml', 'json'] }) {
|
||||||
this.codeBlocksSelector = selector;
|
this.codeBlocksSelector = selector;
|
||||||
this.languages = languages;
|
this.languages = languages;
|
||||||
this.langsAvailable = {
|
this.langsAvailable = {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import Delimiter from 'codex.editor.delimiter';
|
||||||
import InlineCode from 'codex.editor.inline-code';
|
import InlineCode from 'codex.editor.inline-code';
|
||||||
import List from 'codex.editor.list';
|
import List from 'codex.editor.list';
|
||||||
import RawTool from 'codex.editor.raw';
|
import RawTool from 'codex.editor.raw';
|
||||||
import ImageTool from 'codex.editor.image';
|
|
||||||
import Embed from 'codex.editor.embed';
|
import Embed from 'codex.editor.embed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class Writing {
|
||||||
* Init code highlighting
|
* Init code highlighting
|
||||||
*/
|
*/
|
||||||
async createCodeStyling() {
|
async createCodeStyling() {
|
||||||
const {default: CodeStyler} = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
|
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
|
||||||
|
|
||||||
return new CodeStyler({
|
return new CodeStyler({
|
||||||
selector: '.block-code'
|
selector: '.block-code'
|
||||||
|
|
|
@ -64,7 +64,8 @@ export default class Writing {
|
||||||
|
|
||||||
if (this.nodes.removeButton) {
|
if (this.nodes.removeButton) {
|
||||||
this.nodes.removeButton.addEventListener('click', () => {
|
this.nodes.removeButton.addEventListener('click', () => {
|
||||||
const isUserAgree = confirm("Are you sure?");
|
const isUserAgree = window.confirm('Are you sure?');
|
||||||
|
|
||||||
if (!isUserAgree) {
|
if (!isUserAgree) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +84,7 @@ export default class Writing {
|
||||||
* @return {Promise<Editor>}
|
* @return {Promise<Editor>}
|
||||||
*/
|
*/
|
||||||
async loadEditor() {
|
async loadEditor() {
|
||||||
const {default: Editor} = await import(/* webpackChunkName: "editor" */ './../classes/editor');
|
const { default: Editor } = await import(/* webpackChunkName: "editor" */ './../classes/editor');
|
||||||
|
|
||||||
const editorConfig = this.page ? {
|
const editorConfig = this.page ? {
|
||||||
data: this.page.body
|
data: this.page.body
|
||||||
|
@ -119,6 +120,7 @@ export default class Writing {
|
||||||
|
|
||||||
/** get ordering selector value */
|
/** get ordering selector value */
|
||||||
let putAbovePageId = null;
|
let putAbovePageId = null;
|
||||||
|
|
||||||
if (this.nodes.putAboveIdSelector) {
|
if (this.nodes.putAboveIdSelector) {
|
||||||
putAbovePageId = this.nodes.putAboveIdSelector.value;
|
putAbovePageId = this.nodes.putAboveIdSelector.value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ class Page {
|
||||||
/**
|
/**
|
||||||
* Return PageData object
|
* Return PageData object
|
||||||
*
|
*
|
||||||
* @returns {PageData}
|
* @return {PageData}
|
||||||
*/
|
*/
|
||||||
get data() {
|
get data() {
|
||||||
return {
|
return {
|
||||||
|
|
36
src/models/user.js
Normal file
36
src/models/user.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const { password: db } = require('../utils/database/index');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class User
|
||||||
|
* @class User model
|
||||||
|
*
|
||||||
|
* @property {string} passHash - hashed password
|
||||||
|
*/
|
||||||
|
class User {
|
||||||
|
/**
|
||||||
|
* Find and return model of user.
|
||||||
|
* User is only one.
|
||||||
|
*
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
static async get() {
|
||||||
|
const data = await db.findOne({});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new User(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* @param {Object} userData
|
||||||
|
*/
|
||||||
|
constructor(userData) {
|
||||||
|
this.passHash = userData.passHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = User;
|
|
@ -8,6 +8,7 @@ const PagesOrder = require('../../controllers/pagesOrder');
|
||||||
*
|
*
|
||||||
* Return PageData of page with given id
|
* Return PageData of page with given id
|
||||||
*/
|
*/
|
||||||
|
|
||||||
router.get('/page/:id', async (req, res) => {
|
router.get('/page/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const page = await Pages.get(req.params.id);
|
const page = await Pages.get(req.params.id);
|
||||||
|
@ -52,8 +53,8 @@ router.get('/pages', async (req, res) => {
|
||||||
*/
|
*/
|
||||||
router.put('/page', multer.any(), async (req, res) => {
|
router.put('/page', multer.any(), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {title, body, parent} = req.body;
|
const { title, body, parent } = req.body;
|
||||||
const page = await Pages.insert({title, body, parent});
|
const page = await Pages.insert({ title, body, parent });
|
||||||
|
|
||||||
/** push to the orders array */
|
/** push to the orders array */
|
||||||
await PagesOrder.push(parent, page._id);
|
await PagesOrder.push(parent, page._id);
|
||||||
|
@ -76,10 +77,10 @@ router.put('/page', multer.any(), async (req, res) => {
|
||||||
* Update page data in the database
|
* Update page data in the database
|
||||||
*/
|
*/
|
||||||
router.post('/page/:id', multer.any(), async (req, res) => {
|
router.post('/page/:id', multer.any(), async (req, res) => {
|
||||||
const {id} = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {title, body, parent, putAbovePageId, uri} = req.body;
|
const { title, body, parent, putAbovePageId, uri } = req.body;
|
||||||
const pages = await Pages.getAll();
|
const pages = await Pages.getAll();
|
||||||
let page = await Pages.get(id);
|
let page = await Pages.get(id);
|
||||||
|
|
||||||
|
@ -87,12 +88,13 @@ router.post('/page/:id', multer.any(), async (req, res) => {
|
||||||
await PagesOrder.move(page._parent, parent, id);
|
await PagesOrder.move(page._parent, parent, id);
|
||||||
} else {
|
} else {
|
||||||
if (putAbovePageId && putAbovePageId !== '0') {
|
if (putAbovePageId && putAbovePageId !== '0') {
|
||||||
const unordered = pages.filter( _page => _page._parent === page._parent).map(_page => _page._id);
|
const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id);
|
||||||
|
|
||||||
await PagesOrder.update(unordered, page._id, page._parent, putAbovePageId);
|
await PagesOrder.update(unordered, page._id, page._parent, putAbovePageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
page = await Pages.update(id, {title, body, parent, uri});
|
page = await Pages.update(id, { title, body, parent, uri });
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: page
|
result: page
|
||||||
|
@ -119,12 +121,13 @@ router.delete('/page/:id', async (req, res) => {
|
||||||
const pageAfterId = parentPageOrder.getPageAfter(page._id);
|
const pageAfterId = parentPageOrder.getPageAfter(page._id);
|
||||||
|
|
||||||
let pageToRedirect;
|
let pageToRedirect;
|
||||||
|
|
||||||
if (pageBeforeId) {
|
if (pageBeforeId) {
|
||||||
pageToRedirect = await Pages.get(pageBeforeId);
|
pageToRedirect = await Pages.get(pageBeforeId);
|
||||||
} else if (pageAfterId) {
|
} else if (pageAfterId) {
|
||||||
pageToRedirect = await Pages.get(pageAfterId);
|
pageToRedirect = await Pages.get(pageAfterId);
|
||||||
} else {
|
} else {
|
||||||
pageToRedirect = page._parent !== "0" ? await Pages.get(page._parent) : null;
|
pageToRedirect = page._parent !== '0' ? await Pages.get(page._parent) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,10 +136,12 @@ router.delete('/page/:id', async (req, res) => {
|
||||||
* @param startFrom
|
* @param startFrom
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function deleteRecursively(startFrom) {
|
const deleteRecursively = async function (startFrom) {
|
||||||
let order = [];
|
let order = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const children = await PagesOrder.get(startFrom);
|
const children = await PagesOrder.get(startFrom);
|
||||||
|
|
||||||
order = children.order;
|
order = children.order;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
@ -148,7 +153,7 @@ router.delete('/page/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await PagesOrder.remove(startFrom);
|
await PagesOrder.remove(startFrom);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
};
|
||||||
|
|
||||||
await deleteRecursively(req.params.id);
|
await deleteRecursively(req.params.id);
|
||||||
|
|
||||||
|
|
54
src/routes/auth.js
Normal file
54
src/routes/auth.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const router = express.Router();
|
||||||
|
const Users = require('../controllers/users');
|
||||||
|
const config = require('../../config/index');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const csrf = require('csurf');
|
||||||
|
const csrfProtection = csrf({ cookie: true });
|
||||||
|
const parseForm = bodyParser.urlencoded({ extended: false });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization page
|
||||||
|
*/
|
||||||
|
router.get('/auth', csrfProtection, function (req, res) {
|
||||||
|
res.render('auth', {
|
||||||
|
title: 'Login page',
|
||||||
|
header: 'Enter password',
|
||||||
|
csrfToken: req.csrfToken()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process given password
|
||||||
|
*/
|
||||||
|
router.post('/auth', parseForm, csrfProtection, async (req, res) => {
|
||||||
|
let userDoc = await Users.get();
|
||||||
|
|
||||||
|
const passHash = userDoc.passHash;
|
||||||
|
|
||||||
|
bcrypt.compare(req.body.password, passHash, async (err, result) => {
|
||||||
|
if (err || result === false) {
|
||||||
|
res.render('auth', {
|
||||||
|
title: 'Login page',
|
||||||
|
header: 'Wrong password',
|
||||||
|
csrfToken: req.csrfToken()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({
|
||||||
|
'iss': 'Codex Team',
|
||||||
|
'sub': 'auth',
|
||||||
|
'iat': Date.now()
|
||||||
|
}, passHash + config.secret);
|
||||||
|
|
||||||
|
res.cookie('authToken', token, { httpOnly: true });
|
||||||
|
|
||||||
|
res.redirect('/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -1,9 +1,10 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const verifyToken = require('./middlewares/token');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
/* GET home page. */
|
/* GET home page. */
|
||||||
router.get('/', function (req, res, next) {
|
router.get('/', verifyToken, async (req, res) => {
|
||||||
res.render('pages/index');
|
res.render('pages/index', { isAuthorized: res.locals.isAuthorized });
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -3,6 +3,7 @@ const router = express.Router();
|
||||||
|
|
||||||
const home = require('./home');
|
const home = require('./home');
|
||||||
const pages = require('./pages');
|
const pages = require('./pages');
|
||||||
|
const auth = require('./auth');
|
||||||
const aliases = require('./aliases');
|
const aliases = require('./aliases');
|
||||||
const api = require('./api');
|
const api = require('./api');
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ const pagesMiddleware = require('./middlewares/pages');
|
||||||
|
|
||||||
router.use('/', pagesMiddleware, home);
|
router.use('/', pagesMiddleware, home);
|
||||||
router.use('/', pagesMiddleware, pages);
|
router.use('/', pagesMiddleware, pages);
|
||||||
|
router.use('/', pagesMiddleware, auth);
|
||||||
router.use('/api', api);
|
router.use('/api', api);
|
||||||
router.use('/', aliases);
|
router.use('/', aliases);
|
||||||
|
|
||||||
|
|
13
src/routes/middlewares/locals.js
Normal file
13
src/routes/middlewares/locals.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Middleware for checking locals.isAuthorized property, which allows to edit/create pages
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
module.exports = function allowEdit(req, res, next) {
|
||||||
|
if (res.locals.isAuthorized) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.redirect('/auth');
|
||||||
|
}
|
||||||
|
};
|
|
@ -22,14 +22,15 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
|
||||||
* otherwise just find all pages includes parent tree
|
* otherwise just find all pages includes parent tree
|
||||||
*/
|
*/
|
||||||
let ordered = [];
|
let ordered = [];
|
||||||
|
|
||||||
if (childrenOrder) {
|
if (childrenOrder) {
|
||||||
ordered = childrenOrder.order.map( pageId => {
|
ordered = childrenOrder.order.map(pageId => {
|
||||||
return pages.find( page => page._id === pageId);
|
return pages.find(page => page._id === pageId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const unordered = pages.filter( page => page._parent === parentPageId);
|
const unordered = pages.filter(page => page._parent === parentPageId);
|
||||||
const branch = [...new Set([...ordered, ...unordered])];
|
const branch = [ ...new Set([...ordered, ...unordered]) ];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stop recursion when we got the passed max level
|
* stop recursion when we got the passed max level
|
||||||
|
@ -41,12 +42,11 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
|
||||||
/**
|
/**
|
||||||
* Each parents children can have subbranches
|
* Each parents children can have subbranches
|
||||||
*/
|
*/
|
||||||
return branch.filter(page => page && page._id).map( page => {
|
return branch.filter(page => page && page._id).map(page => {
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1)
|
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1)
|
||||||
}, page.data);
|
}, page.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,15 +55,17 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
|
||||||
* @param res
|
* @param res
|
||||||
* @param next
|
* @param next
|
||||||
*/
|
*/
|
||||||
module.exports = asyncMiddleware(async function (req, res, next) {
|
module.exports = asyncMiddleware(async (req, res, next) => {
|
||||||
/**
|
/**
|
||||||
* Pages without parent
|
* Pages without parent
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
const parentIdOfRootPages = '0';
|
const parentIdOfRootPages = '0';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pages = await Pages.getAll();
|
const pages = await Pages.getAll();
|
||||||
const pagesOrder = await PagesOrder.getAll();
|
const pagesOrder = await PagesOrder.getAll();
|
||||||
|
|
||||||
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
|
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Can not load menu:', error);
|
console.log('Can not load menu:', error);
|
||||||
|
|
25
src/routes/middlewares/token.js
Normal file
25
src/routes/middlewares/token.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
const config = require('../../../config/index');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const Users = require('../../controllers/users');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware for checking jwt token
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
module.exports = async function verifyToken(req, res, next) {
|
||||||
|
let token = req.cookies.authToken;
|
||||||
|
const userDoc = await Users.get();
|
||||||
|
|
||||||
|
if (!userDoc) {
|
||||||
|
res.locals.isAuthorized = false;
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, userDoc.passHash + config.secret, (err, decodedToken) => {
|
||||||
|
res.locals.isAuthorized = !(err || !decodedToken);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
|
@ -3,10 +3,13 @@ const router = express.Router();
|
||||||
const Pages = require('../controllers/pages');
|
const Pages = require('../controllers/pages');
|
||||||
const PagesOrder = require('../controllers/pagesOrder');
|
const PagesOrder = require('../controllers/pagesOrder');
|
||||||
|
|
||||||
|
const verifyToken = require('./middlewares/token');
|
||||||
|
const allowEdit = require('./middlewares/locals');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new page form
|
* Create new page form
|
||||||
*/
|
*/
|
||||||
router.get('/page/new', async (req, res) => {
|
router.get('/page/new', verifyToken, allowEdit, async (req, res, next) => {
|
||||||
let pagesAvailable = await Pages.getAll();
|
let pagesAvailable = await Pages.getAll();
|
||||||
|
|
||||||
res.render('pages/form', {
|
res.render('pages/form', {
|
||||||
|
@ -18,7 +21,7 @@ router.get('/page/new', async (req, res) => {
|
||||||
/**
|
/**
|
||||||
* Edit page form
|
* Edit page form
|
||||||
*/
|
*/
|
||||||
router.get('/page/edit/:id', async (req, res, next) => {
|
router.get('/page/edit/:id', verifyToken, allowEdit, async (req, res, next) => {
|
||||||
const pageId = req.params.id;
|
const pageId = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -40,7 +43,7 @@ router.get('/page/edit/:id', async (req, res, next) => {
|
||||||
/**
|
/**
|
||||||
* View page
|
* View page
|
||||||
*/
|
*/
|
||||||
router.get('/page/:id', async (req, res, next) => {
|
router.get('/page/:id', verifyToken, async (req, res, next) => {
|
||||||
const pageId = req.params.id;
|
const pageId = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* @param fn
|
* @param fn
|
||||||
* @return {function(*=, *=, *=)}
|
* @return {function(*=, *=, *=)}
|
||||||
*/
|
*/
|
||||||
module.exports = function asyncMiddlware(fn) {
|
module.exports = function asyncMiddleware(fn) {
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
Promise.resolve(fn(req, res, next))
|
Promise.resolve(fn(req, res, next))
|
||||||
.catch(next);
|
.catch(next);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const Datastore = require('nedb');
|
const Datastore = require('nedb');
|
||||||
const config = require('../../../config');
|
const config = require('../../../config');
|
||||||
|
|
||||||
const db = new Datastore({filename: `./${config.database}/aliases.db`, autoload: true});
|
const db = new Datastore({ filename: `./${config.database}/aliases.db`, autoload: true });
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const pages = require('./pages');
|
const pages = require('./pages');
|
||||||
|
const password = require('./password');
|
||||||
const aliases = require('./aliases');
|
const aliases = require('./aliases');
|
||||||
const pagesOrder = require('./pagesOrder');
|
const pagesOrder = require('./pagesOrder');
|
||||||
|
|
||||||
|
@ -145,6 +146,7 @@ class Database {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
class: Database,
|
class: Database,
|
||||||
pages: new Database(pages),
|
pages: new Database(pages),
|
||||||
|
password: new Database(password),
|
||||||
aliases: new Database(aliases),
|
aliases: new Database(aliases),
|
||||||
pagesOrder: new Database(pagesOrder)
|
pagesOrder: new Database(pagesOrder)
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const Datastore = require('nedb');
|
const Datastore = require('nedb');
|
||||||
const config = require('../../../config');
|
const config = require('../../../config');
|
||||||
|
|
||||||
const db = new Datastore({filename: `./${config.database}/pages.db`, autoload: true});
|
const db = new Datastore({ filename: `./${config.database}/pages.db`, autoload: true });
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const Datastore = require('nedb');
|
const Datastore = require('nedb');
|
||||||
const config = require('../../../config');
|
const config = require('../../../config');
|
||||||
const db = new Datastore({filename: `./${config.database}/pagesOrder.db`, autoload: true});
|
const db = new Datastore({ filename: `./${config.database}/pagesOrder.db`, autoload: true });
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|
6
src/utils/database/password.js
Normal file
6
src/utils/database/password.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const Datastore = require('nedb');
|
||||||
|
const config = require('../../../config');
|
||||||
|
|
||||||
|
const db = new Datastore({ filename: `./${config.database}/password.db`, autoload: true });
|
||||||
|
|
||||||
|
module.exports = db;
|
11
src/views/auth.twig
Normal file
11
src/views/auth.twig
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>{{header}}</h1>
|
||||||
|
<form method="post" action="/auth">
|
||||||
|
<input type="hidden" name="_csrf" value={{csrfToken}}>
|
||||||
|
<input type="password" name="password" placeholder="Password">
|
||||||
|
<input type="submit" value="login">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -3,12 +3,14 @@
|
||||||
{{ config.title }}
|
{{ config.title }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="docs-header__menu">
|
<ul class="docs-header__menu">
|
||||||
|
{% if isAuthorized == true %}
|
||||||
<li class="docs-header__menu-add">
|
<li class="docs-header__menu-add">
|
||||||
<a class="docs-header__button" href="/page/new">
|
<a class="docs-header__button" href="/page/new">
|
||||||
{{ svg('plus') }}
|
{{ svg('plus') }}
|
||||||
Add Page
|
Add Page
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% for option in config.menu %}
|
{% for option in config.menu %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include "components/header.twig" %}
|
{% include "components/header.twig" with res.locals.isAuthorized %}
|
||||||
<div class="docs">
|
<div class="docs">
|
||||||
<aside class="docs__aside">
|
<aside class="docs__aside">
|
||||||
{% include "components/aside.twig" %}
|
{% include "components/aside.twig" %}
|
||||||
|
|
|
@ -18,9 +18,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<time class="page__header-time">
|
<time class="page__header-time">
|
||||||
Last edit {{ (page.body.time / 1000) | date("M d Y") }}
|
Last edit {{ (page.body.time / 1000) | date("M d Y") }}
|
||||||
|
{% if isAuthorized == true %}
|
||||||
<a href="/page/edit/{{ page._id }}" class="page__header-button">
|
<a href="/page/edit/{{ page._id }}" class="page__header-button">
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</time>
|
</time>
|
||||||
</header>
|
</header>
|
||||||
<h1 class="page__title">
|
<h1 class="page__title">
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
const {app} = require('../bin/www');
|
const { app } = require('../bin/www');
|
||||||
const chai = require('chai');
|
const chai = require('chai');
|
||||||
const chaiHTTP = require('chai-http');
|
const chaiHTTP = require('chai-http');
|
||||||
const {expect} = chai;
|
const { expect } = chai;
|
||||||
|
|
||||||
chai.use(chaiHTTP);
|
chai.use(chaiHTTP);
|
||||||
|
|
||||||
describe('Express app', () => {
|
describe('Express app', () => {
|
||||||
it('App is available', (done) => {
|
it('App is available', async () => {
|
||||||
chai
|
let agent = chai.request.agent(app);
|
||||||
.request(app)
|
|
||||||
.get('/')
|
const result = await agent
|
||||||
.end((err, res) => {
|
.get('/');
|
||||||
expect(err).to.be.null;
|
|
||||||
expect(res).to.have.status(200);
|
expect(result).to.have.status(200);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue