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
|
@ -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')));
|
||||
|
||||
|
|
|
@ -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
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 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 = {
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ class Page {
|
|||
/**
|
||||
* Return PageData object
|
||||
*
|
||||
* @returns {PageData}
|
||||
* @return {PageData}
|
||||
*/
|
||||
get data() {
|
||||
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
|
||||
*/
|
||||
|
||||
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
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 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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
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
|
||||
*/
|
||||
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);
|
||||
|
|
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 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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
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 }}
|
||||
</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
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue