1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-07 22:45:23 +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
```
### 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 = {
database: '.db',
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",
"version": "0.0.0",
"private": true,
"bin": {
"generatePassword": "bin/generatePassword.js"
},
"scripts": {
"start": "cross-env NODE_ENV=production nodemon ./bin/www",
"start:dev": "cross-env NODE_ENV=development nodemon ./bin/www",
"test": "cross-env NODE_ENV=testing mocha --recursive ./test",
"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",
"precommit": "yarn lint && yarn test --exit"
"build": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --mode=production",
"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": {
"@babel/polyfill": "^7.0.0",
"bcrypt": "^3.0.3",
"body-parser": "latest",
"codex.editor": "^2.8.1",
"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",
"commander": "^2.19.0",
"cookie-parser": "~1.4.3",
"csurf": "^1.9.0",
"debug": "~4.1.0",
"dotenv": "^6.2.0",
"jsonwebtoken": "^8.4.0",
"eslint-plugin-standard": "^4.0.0",
"express": "~4.16.0",
"file-type": "^10.7.1",
@ -41,17 +43,23 @@
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.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",
"babel": "^6.23.0",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.2",
"chai": "^4.1.2",
"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",
"css-loader": "^1.0.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
* @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

@ -1,18 +1,17 @@
import CodeXEditor from 'codex.editor';
import EditorJS from '@editorjs/editorjs';
/**
* Tools for the Editor
*/
import Header from 'codex.editor.header';
import Quote from 'codex.editor.quote';
import CodeTool from 'codex.editor.code';
import Delimiter from 'codex.editor.delimiter';
import InlineCode from 'codex.editor.inline-code';
import Marker from 'codex.editor.marker';
import ListTool from 'codex.editor.list';
import RawTool from 'codex.editor.raw';
import ImageTool from 'codex.editor.image';
import Embed from 'codex.editor.embed';
import Header from '@editorjs/header';
import Quote from '@editorjs/quote';
import Marker from '@editorjs/marker';
import CodeTool from '@editorjs/code';
import Delimiter from '@editorjs/delimiter';
import InlineCode from '@editorjs/inline-code';
import List from '@editorjs/list';
import RawTool from '@editorjs/raw';
import Embed from '@editorjs/embed';
/**
* 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 url('vars.pcss');
@import url('layout.pcss');
@import url('components/header.pcss');
@import url('components/aside.pcss');
@import url('components/writing.pcss');
@import url('components/page.pcss');
@import url('components/landing.pcss');
@import 'normalize.css';
@import './vars.pcss';
@import './layout.pcss';
@import './components/header.pcss';
@import './components/aside.pcss';
@import './components/writing.pcss';
@import './components/page.pcss';
@import './components/landing.pcss';
body {
font-family: system-ui, Helvetica, Arial, Verdana;

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

@ -3,13 +3,14 @@ const router = express.Router();
const Aliases = require('../controllers/aliases');
const Pages = require('../controllers/pages');
const Alias = require('../models/alias');
const verifyToken = require('./middlewares/token');
/**
* GET /*
*
* Return document with given alias
*/
router.get('*', async (req, res) => {
router.get('*', verifyToken, async (req, res) => {
try {
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
*/
router.get('/page/:id', async (req, res) => {
try {
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 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

@ -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,12 +21,12 @@ 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 {
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);
res.render('pages/form', {
@ -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,5 +1,6 @@
const pages = require('./pages');
const files = require('./files');
const password = require('./password');
const aliases = require('./aliases');
const pagesOrder = require('./pagesOrder');
@ -146,6 +147,7 @@ class Database {
module.exports = {
class: Database,
pages: new Database(pages),
password: new Database(password),
aliases: new Database(aliases),
pagesOrder: new Database(pagesOrder),
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 }}
</a>
<ul class="docs-header__menu">
{% 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
{% if child.uri %}
{% if option.uri %}
href="{{ option.uri }}"
{% else %}
href="/page/{{ option._id }}"

View file

@ -3,10 +3,13 @@
<head>
<title>{{ config.title }}</title>
<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" />
</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

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

View file

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

View file

@ -6,14 +6,12 @@ const {expect} = chai;
chai.use(chaiHTTP);
describe('Express app', () => {
it('App is available', (done) => {
chai
.request(app)
.get('/')
.end((err, res) => {
expect(err).to.be.null;
expect(res).to.have.status(200);
done();
});
it('App is available', async () => {
let agent = chai.request.agent(app);
const result = await agent
.get('/');
expect(result).to.have.status(200);
});
});

View file

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

1253
yarn.lock

File diff suppressed because it is too large Load diff