1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-08 06:55:26 +02:00

Merge branch 'master' into feature/transport

This commit is contained in:
Taly 2019-03-11 13:38:26 +03:00
commit d6a39ea177
32 changed files with 1541 additions and 164 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

@ -16,7 +16,8 @@ if (fs.existsSync(path.resolve(__dirname, configPath))) {
config = { config = {
database: '.db', database: '.db',
port: 3000, port: 3000,
uploads: 'public/uploads' uploads: 'public/uploads',
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,27 +1,29 @@
{ {
"name": "codex.docs", "name": "codex.docs",
"version": "0.0.0", "version": "0.0.0",
"private": true, "bin": {
"generatePassword": "bin/generatePassword.js"
},
"scripts": { "scripts": {
"start": "cross-env NODE_ENV=production nodemon ./bin/www", "start": "cross-env NODE_ENV=production nodemon ./bin/www",
"start:dev": "cross-env NODE_ENV=development nodemon ./bin/www", "start:dev": "cross-env NODE_ENV=development nodemon ./bin/www",
"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 --mode=production",
"precommit": "yarn lint && yarn test --exit" "build:dev": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --mode=development --watch",
"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", "commander": "^2.19.0",
"codex.editor.delimiter": "^1.0.2",
"codex.editor.embed": "^2.1.2",
"codex.editor.header": "^2.0.5",
"codex.editor.image": "^2.0.3",
"codex.editor.quote": "^2.1.5",
"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",
"file-type": "^10.7.1", "file-type": "^10.7.1",
@ -41,17 +43,23 @@
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.1.0",
"@editorjs/code": "^2.4.0",
"@editorjs/delimiter": "^1.1.0",
"@editorjs/editorjs": "^2.11.7",
"@editorjs/embed": "^2.2.0",
"@editorjs/header": "^2.2.0",
"@editorjs/image": "^2.1.0",
"@editorjs/inline-code": "^1.3.0",
"@editorjs/list": "^1.3.0",
"@editorjs/marker": "^1.2.0",
"@editorjs/quote": "^2.2.0",
"@editorjs/raw": "^2.1.1",
"autoprefixer": "^9.1.3", "autoprefixer": "^9.1.3",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.2", "babel-loader": "^8.0.2",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-http": "^4.0.0", "chai-http": "^4.0.0",
"codex.editor.code": "^2.3.1",
"codex.editor.image": "^2.0.3",
"codex.editor.inline-code": "^1.2.0",
"codex.editor.list": "^1.2.3",
"codex.editor.marker": "^1.0.1",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"cssnano": "^4.1.0", "cssnano": "^4.1.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -1,18 +1,17 @@
import CodeXEditor from 'codex.editor'; import EditorJS from '@editorjs/editorjs';
/** /**
* Tools for the Editor * Tools for the Editor
*/ */
import Header from 'codex.editor.header'; import Header from '@editorjs/header';
import Quote from 'codex.editor.quote'; import Quote from '@editorjs/quote';
import CodeTool from 'codex.editor.code'; import Marker from '@editorjs/marker';
import Delimiter from 'codex.editor.delimiter'; import CodeTool from '@editorjs/code';
import InlineCode from 'codex.editor.inline-code'; import Delimiter from '@editorjs/delimiter';
import Marker from 'codex.editor.marker'; import InlineCode from '@editorjs/inline-code';
import ListTool from 'codex.editor.list'; import List from '@editorjs/list';
import RawTool from 'codex.editor.raw'; import RawTool from '@editorjs/raw';
import ImageTool from 'codex.editor.image'; import Embed from '@editorjs/embed';
import Embed from 'codex.editor.embed';
/** /**
* Class for working with Editor.js * Class for working with Editor.js
@ -92,7 +91,7 @@ export default class Editor {
} }
}; };
this.editor = new CodeXEditor(Object.assign(defaultConfig, editorConfig)); this.editor = new EditorJS(Object.assign(defaultConfig, editorConfig));
} }
/** /**

View file

@ -1,11 +1,11 @@
@import url('../../../node_modules/normalize.css'); @import 'normalize.css';
@import url('vars.pcss'); @import './vars.pcss';
@import url('layout.pcss'); @import './layout.pcss';
@import url('components/header.pcss'); @import './components/header.pcss';
@import url('components/aside.pcss'); @import './components/aside.pcss';
@import url('components/writing.pcss'); @import './components/writing.pcss';
@import url('components/page.pcss'); @import './components/page.pcss';
@import url('components/landing.pcss'); @import './components/landing.pcss';
body { body {
font-family: system-ui, Helvetica, Arial, Verdana; font-family: system-ui, Helvetica, Arial, Verdana;

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

@ -3,13 +3,14 @@ const router = express.Router();
const Aliases = require('../controllers/aliases'); const Aliases = require('../controllers/aliases');
const Pages = require('../controllers/pages'); const Pages = require('../controllers/pages');
const Alias = require('../models/alias'); const Alias = require('../models/alias');
const verifyToken = require('./middlewares/token');
/** /**
* GET /* * GET /*
* *
* Return document with given alias * Return document with given alias
*/ */
router.get('*', async (req, res) => { router.get('*', verifyToken, async (req, res) => {
try { try {
const alias = await Aliases.get(req.originalUrl.slice(1)); // Cuts first '/' character const alias = await Aliases.get(req.originalUrl.slice(1)); // Cuts first '/' character

View file

@ -9,6 +9,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);

62
src/routes/auth.js Normal file
View file

@ -0,0 +1,62 @@
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();
if (!userDoc) {
res.render('auth', {
title: 'Login page',
header: 'Password not set',
csrfToken: req.csrfToken()
});
}
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

@ -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,12 +21,12 @@ 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 {
const page = await Pages.get(pageId); const page = await Pages.get(pageId);
const pagesAvailable = await Pages.getAllExceptChildrens(pageId); const pagesAvailable = await Pages.getAllExceptChildren(pageId);
const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true); const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true);
res.render('pages/form', { res.render('pages/form', {
@ -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,5 +1,6 @@
const pages = require('./pages'); const pages = require('./pages');
const files = require('./files'); const files = require('./files');
const password = require('./password');
const aliases = require('./aliases'); const aliases = require('./aliases');
const pagesOrder = require('./pagesOrder'); const pagesOrder = require('./pagesOrder');
@ -146,6 +147,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),
files: new Database(files) files: new Database(files)

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,16 +3,18 @@
{{ 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
{% if child.uri %} {% if option.uri %}
href="{{ option.uri }}" href="{{ option.uri }}"
{% else %} {% else %}
href="/page/{{ option._id }}" href="/page/{{ option._id }}"

View file

@ -3,10 +3,13 @@
<head> <head>
<title>{{ config.title }}</title> <title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" /> <link rel="stylesheet" href="/dist/main.css" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ page.title }}" />
<meta property="article:modified_time" content="{{ (page.body.time / 1000) | date("c") }}" />
<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

@ -47,7 +47,7 @@
<span class="writing-header__save" name="js-submit-save">Save</span> <span class="writing-header__save" name="js-submit-save">Save</span>
</header> </header>
<div class="writing-editor"> <div class="writing-editor">
<div id="codex-editor"></div> <div id="editorjs"></div>
</div> </div>
<div class="writing-buttons"> <div class="writing-buttons">
{% if page._id is not empty %} {% if page._id is not empty %}

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") }}
{% 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">

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

View file

@ -70,7 +70,6 @@ module.exports = (env) => {
}) })
], ],
optimization: { optimization: {
minimize: true,
splitChunks: false splitChunks: false
} }
}; };

1253
yarn.lock

File diff suppressed because it is too large Load diff