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

@ -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.

View file

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

View file

@ -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",

View file

@ -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')));

View file

@ -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
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 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 = {

View file

@ -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';
/** /**

View file

@ -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'

View file

@ -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;
} }

View file

@ -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
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 * 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
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 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;

View file

@ -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);

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 * 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);

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 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 {

View file

@ -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);

View file

@ -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;

View file

@ -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)
}; };

View file

@ -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;

View file

@ -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;

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 }} {{ config.title }}
</a> </a>
<ul class="docs-header__menu"> <ul class="docs-header__menu">
<li class="docs-header__menu-add"> {% if isAuthorized == true %}
<a class="docs-header__button" href="/page/new"> <li class="docs-header__menu-add">
{{ svg('plus') }} <a class="docs-header__button" href="/page/new">
Add Page {{ svg('plus') }}
</a> Add Page
</li> </a>
</li>
{% endif %}
{% for option in config.menu %} {% for option in config.menu %}
<li> <li>
<a <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" /> <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" %}

View file

@ -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") }}
<a href="/page/edit/{{ page._id }}" class="page__header-button"> {% if isAuthorized == true %}
Edit <a href="/page/edit/{{ page._id }}" class="page__header-button">
</a> Edit
</a>
{% endif %}
</time> </time>
</header> </header>
<h1 class="page__title"> <h1 class="page__title">

View file

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

1166
yarn.lock

File diff suppressed because it is too large Load diff