1
0
Fork 0
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:
Timur Kazantaev 2019-03-06 13:22:57 +03:00 committed by GitHub
parent 718be6d2f6
commit 58d3892d8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1464 additions and 58 deletions

View file

@ -19,7 +19,7 @@ require('./utils/twig');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../public')));

View file

@ -49,7 +49,7 @@ class Pages {
* @param {string} parent - id of current page
* @returns {Promise<Page[]>}
*/
static async getAllExceptChildrens(parent) {
static async getAllExceptChildren(parent) {
let pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
return pagesAvailable.filter((item) => item !== null);

21
src/controllers/users.js Normal file
View 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;

View file

@ -2,6 +2,7 @@ import hljs from 'highlight.js/lib/highlight';
import javascript from 'highlight.js/lib/languages/javascript';
import xml from 'highlight.js/lib/languages/xml';
import json from 'highlight.js/lib/languages/json';
// eslint-disable-next-line
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[]} 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.languages = languages;
this.langsAvailable = {

View file

@ -11,7 +11,6 @@ import Delimiter from 'codex.editor.delimiter';
import InlineCode from 'codex.editor.inline-code';
import List from 'codex.editor.list';
import RawTool from 'codex.editor.raw';
import ImageTool from 'codex.editor.image';
import Embed from 'codex.editor.embed';
/**

View file

@ -27,7 +27,7 @@ export default class Writing {
* Init code highlighting
*/
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({
selector: '.block-code'

View file

@ -64,7 +64,8 @@ export default class Writing {
if (this.nodes.removeButton) {
this.nodes.removeButton.addEventListener('click', () => {
const isUserAgree = confirm("Are you sure?");
const isUserAgree = window.confirm('Are you sure?');
if (!isUserAgree) {
return;
}
@ -83,7 +84,7 @@ export default class Writing {
* @return {Promise<Editor>}
*/
async loadEditor() {
const {default: Editor} = await import(/* webpackChunkName: "editor" */ './../classes/editor');
const { default: Editor } = await import(/* webpackChunkName: "editor" */ './../classes/editor');
const editorConfig = this.page ? {
data: this.page.body
@ -119,6 +120,7 @@ export default class Writing {
/** get ordering selector value */
let putAbovePageId = null;
if (this.nodes.putAboveIdSelector) {
putAbovePageId = this.nodes.putAboveIdSelector.value;
}

View file

@ -89,7 +89,7 @@ class Page {
/**
* Return PageData object
*
* @returns {PageData}
* @return {PageData}
*/
get data() {
return {

36
src/models/user.js Normal file
View 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;

View file

@ -8,6 +8,7 @@ const PagesOrder = require('../../controllers/pagesOrder');
*
* Return PageData of page with given id
*/
router.get('/page/:id', async (req, res) => {
try {
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) => {
try {
const {title, body, parent} = req.body;
const page = await Pages.insert({title, body, parent});
const { title, body, parent } = req.body;
const page = await Pages.insert({ title, body, parent });
/** push to the orders array */
await PagesOrder.push(parent, page._id);
@ -76,10 +77,10 @@ router.put('/page', multer.any(), async (req, res) => {
* Update page data in the database
*/
router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params;
const { id } = req.params;
try {
const {title, body, parent, putAbovePageId, uri} = req.body;
const { title, body, parent, putAbovePageId, uri } = req.body;
const pages = await Pages.getAll();
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);
} else {
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);
}
}
page = await Pages.update(id, {title, body, parent, uri});
page = await Pages.update(id, { title, body, parent, uri });
res.json({
success: true,
result: page
@ -119,12 +121,13 @@ router.delete('/page/:id', async (req, res) => {
const pageAfterId = parentPageOrder.getPageAfter(page._id);
let pageToRedirect;
if (pageBeforeId) {
pageToRedirect = await Pages.get(pageBeforeId);
} else if (pageAfterId) {
pageToRedirect = await Pages.get(pageAfterId);
} 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
* @returns {Promise<void>}
*/
async function deleteRecursively(startFrom) {
const deleteRecursively = async function (startFrom) {
let order = [];
try {
const children = await PagesOrder.get(startFrom);
order = children.order;
} catch (e) {}
@ -148,7 +153,7 @@ router.delete('/page/:id', async (req, res) => {
try {
await PagesOrder.remove(startFrom);
} catch (e) {}
}
};
await deleteRecursively(req.params.id);

54
src/routes/auth.js Normal file
View 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;

View file

@ -1,9 +1,10 @@
const express = require('express');
const verifyToken = require('./middlewares/token');
const router = express.Router();
/* GET home page. */
router.get('/', function (req, res, next) {
res.render('pages/index');
router.get('/', verifyToken, async (req, res) => {
res.render('pages/index', { isAuthorized: res.locals.isAuthorized });
});
module.exports = router;

View file

@ -3,6 +3,7 @@ const router = express.Router();
const home = require('./home');
const pages = require('./pages');
const auth = require('./auth');
const aliases = require('./aliases');
const api = require('./api');
@ -10,6 +11,7 @@ const pagesMiddleware = require('./middlewares/pages');
router.use('/', pagesMiddleware, home);
router.use('/', pagesMiddleware, pages);
router.use('/', pagesMiddleware, auth);
router.use('/api', api);
router.use('/', aliases);

View 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');
}
};

View file

@ -22,14 +22,15 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
* otherwise just find all pages includes parent tree
*/
let ordered = [];
if (childrenOrder) {
ordered = childrenOrder.order.map( pageId => {
return pages.find( page => page._id === pageId);
ordered = childrenOrder.order.map(pageId => {
return pages.find(page => page._id === pageId);
});
}
const unordered = pages.filter( page => page._parent === parentPageId);
const branch = [...new Set([...ordered, ...unordered])];
const unordered = pages.filter(page => page._parent === parentPageId);
const branch = [ ...new Set([...ordered, ...unordered]) ];
/**
* 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
*/
return branch.filter(page => page && page._id).map( page => {
return branch.filter(page => page && page._id).map(page => {
return Object.assign({
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1)
}, page.data);
});
}
/**
@ -55,15 +55,17 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
* @param res
* @param next
*/
module.exports = asyncMiddleware(async function (req, res, next) {
module.exports = asyncMiddleware(async (req, res, next) => {
/**
* Pages without parent
* @type {string}
*/
const parentIdOfRootPages = '0';
try {
const pages = await Pages.getAll();
const pagesOrder = await PagesOrder.getAll();
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
} catch (error) {
console.log('Can not load menu:', error);

View 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();
});
};

View file

@ -3,10 +3,13 @@ const router = express.Router();
const Pages = require('../controllers/pages');
const PagesOrder = require('../controllers/pagesOrder');
const verifyToken = require('./middlewares/token');
const allowEdit = require('./middlewares/locals');
/**
* 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();
res.render('pages/form', {
@ -18,7 +21,7 @@ router.get('/page/new', async (req, res) => {
/**
* 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;
try {
@ -40,7 +43,7 @@ router.get('/page/edit/:id', async (req, res, next) => {
/**
* View page
*/
router.get('/page/:id', async (req, res, next) => {
router.get('/page/:id', verifyToken, async (req, res, next) => {
const pageId = req.params.id;
try {

View file

@ -3,7 +3,7 @@
* @param fn
* @return {function(*=, *=, *=)}
*/
module.exports = function asyncMiddlware(fn) {
module.exports = function asyncMiddleware(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next);

View file

@ -1,6 +1,6 @@
const Datastore = require('nedb');
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;

View file

@ -1,4 +1,5 @@
const pages = require('./pages');
const password = require('./password');
const aliases = require('./aliases');
const pagesOrder = require('./pagesOrder');
@ -145,6 +146,7 @@ class Database {
module.exports = {
class: Database,
pages: new Database(pages),
password: new Database(password),
aliases: new Database(aliases),
pagesOrder: new Database(pagesOrder)
};

View file

@ -1,6 +1,6 @@
const Datastore = require('nedb');
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;

View file

@ -1,5 +1,5 @@
const Datastore = require('nedb');
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;

View 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
View 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 %}

View file

@ -3,12 +3,14 @@
{{ config.title }}
</a>
<ul class="docs-header__menu">
<li class="docs-header__menu-add">
<a class="docs-header__button" href="/page/new">
{{ svg('plus') }}
Add Page
</a>
</li>
{% if isAuthorized == true %}
<li class="docs-header__menu-add">
<a class="docs-header__button" href="/page/new">
{{ svg('plus') }}
Add Page
</a>
</li>
{% endif %}
{% for option in config.menu %}
<li>
<a

View file

@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
{% include "components/header.twig" %}
{% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs">
<aside class="docs__aside">
{% include "components/aside.twig" %}

View file

@ -18,9 +18,11 @@
{% endif %}
<time class="page__header-time">
Last edit {{ (page.body.time / 1000) | date("M d Y") }}
<a href="/page/edit/{{ page._id }}" class="page__header-button">
Edit
</a>
{% if isAuthorized == true %}
<a href="/page/edit/{{ page._id }}" class="page__header-button">
Edit
</a>
{% endif %}
</time>
</header>
<h1 class="page__title">