diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..af0f0c3d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..198a7c45 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5148e527..cceafa60 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ coverage # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release +# Deployment builds +dist + # Dependency directories node_modules jspm_packages @@ -35,3 +38,9 @@ jspm_packages # Optional REPL history .node_repl_history + +# SublimeText Files +*.sublime-workspace + +# Config Files +config.yml \ No newline at end of file diff --git a/.snyk b/.snyk new file mode 100644 index 00000000..03699e69 --- /dev/null +++ b/.snyk @@ -0,0 +1,4 @@ +failThreshold: high +version: v1.5.2 +ignore: {} +patch: {} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..4de6fef6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +language: node_js +node_js: +- '6' +- '5' +- '4.4' +services: +- redis-server +- mongodb +cache: + directories: + - node_modules +before_script: +- npm install -g snyk +before_deploy: +- npm install -g gulp +- gulp deploy +- snyk auth $SNYK_TOKEN +- snyk monitor +deploy: + provider: releases + file: + - dist/requarks-wiki.zip + - dist/requarks-wiki.tar.gz + skip_cleanup: true + overwrite: true + on: + branch: master + repo: requarks/wiki + tags: true + node: '6' \ No newline at end of file diff --git a/config.sample.yml b/config.sample.yml new file mode 100644 index 00000000..221a3dbb --- /dev/null +++ b/config.sample.yml @@ -0,0 +1,54 @@ +################################################### +# REQUARKS WIKI - CONFIGURATION # +################################################### + +# ------------------------------------------------- +# Title of this site +# ------------------------------------------------- + +title: Wiki + +# ------------------------------------------------- +# Full path to the site, without the trailing slash +# ------------------------------------------------- + +host: http://localhost + +# ------------------------------------------------- +# Port the server should listen to (80 by default) +# ------------------------------------------------- +# To use process.env.PORT, comment the line below: + +port: 80 + +# ------------------------------------------------- +# MongoDB Connection String +# ------------------------------------------------- +# Full explanation + examples in the documentation (https://opsstatus.readme.io/) + +db: mongodb://localhost/wiki + +# ------------------------------------------------- +# Redis Connection Info +# ------------------------------------------------- +# Full explanation + examples in the documentation (https://opsstatus.readme.io/) + +redis: + host: localhost + port: 6379 + db: 0 + +# ------------------------------------------------- +# Secret key to use when encrypting sessions +# ------------------------------------------------- +# Use a long and unique random string (256-bit keys are perfect!) + +sessionSecret: 1234567890abcdefghijklmnopqrstuvxyz + +# ------------------------------------------------- +# Administrator email +# ------------------------------------------------- +# An account will be created using the email specified here. +# The password is set to "admin123" by default. Change it immediately upon login!!! + +admin: admin@company.com \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..e387b620 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,94 @@ +var gulp = require("gulp"); +var merge = require('merge-stream'); +var babel = require("gulp-babel"); +var uglify = require('gulp-uglify'); +var concat = require('gulp-concat'); +var nodemon = require('gulp-nodemon'); +var plumber = require('gulp-plumber'); +var zip = require('gulp-zip'); +var tar = require('gulp-tar'); +var gzip = require('gulp-gzip'); +var sass = require('gulp-sass'); +var cleanCSS = require('gulp-clean-css'); +var include = require("gulp-include"); + +/** + * Paths + * + * @type {Object} + */ +var paths = { + scriptlibs: { + + }, + scriptapps: [ + './client/js/components/*.js', + './client/js/app.js' + ], + scriptappswatch: [ + './client/js/**/*.js' + ], + csslibs: [ + + ], + cssapps: [ + './client/css/app.scss' + ], + cssappswatch: [ + './client/css/**/*.scss' + ], + fonts: [ + './node_modules/font-awesome/fonts/*-webfont.*', + '!./node_modules/font-awesome/fonts/*-webfont.svg' + ], + deploypackage: [ + './**/*', + '!node_modules', '!node_modules/**', + '!coverage', '!coverage/**', + '!client/js', '!client/js/**', + '!dist', '!dist/**', + '!tests', '!tests/**', + '!gulpfile.js', '!inch.json', '!config.json', '!wiki.sublime-project' + ] +}; + +/** + * TASK - Starts server in development mode + */ +gulp.task('server', ['scripts', 'css', 'fonts'], function() { + nodemon({ + script: './server', + ignore: ['public/', 'client/', 'tests/'], + ext: 'js json', + env: { 'NODE_ENV': 'development' } + }); +}); + +/** + * TASK - Start dev watchers + */ +gulp.task('watch', function() { + gulp.watch([paths.scriptappswatch], ['scripts-app']); + gulp.watch([paths.cssappswatch], ['css-app']); +}); + +/** + * TASK - Starts development server with watchers + */ +gulp.task('default', ['watch', 'server']); + +/** + * TASK - Creates deployment packages + */ +gulp.task('deploy', ['scripts', 'css', 'fonts'], function() { + var zipStream = gulp.src(paths.deploypackage) + .pipe(zip('requarks-wiki.zip')) + .pipe(gulp.dest('dist')); + + var targzStream = gulp.src(paths.deploypackage) + .pipe(tar('requarks-wiki.tar')) + .pipe(gzip()) + .pipe(gulp.dest('dist')); + + return merge(zipStream, targzStream); +}); \ No newline at end of file diff --git a/inch.json b/inch.json new file mode 100644 index 00000000..0d4476cb --- /dev/null +++ b/inch.json @@ -0,0 +1,10 @@ +{ + "files": { + "included": [ + "controllers/**/*.js", + "middlewares/**/*.js", + "models/**/*.js", + ], + "excluded": [] + } +} \ No newline at end of file diff --git a/locales/en/common.js b/locales/en/common.js new file mode 100644 index 00000000..24e15c2c --- /dev/null +++ b/locales/en/common.js @@ -0,0 +1,11 @@ +{ + "wiki": "Wiki", + "headers": { + "overview": "Overview" + }, + "footer": { + "poweredby": "Powered by", + "home": "Home", + "admin": "Administration" + } +} \ No newline at end of file diff --git a/locales/fr/common.js b/locales/fr/common.js new file mode 100644 index 00000000..a6ef181e --- /dev/null +++ b/locales/fr/common.js @@ -0,0 +1,11 @@ +{ + "wiki": "Wiki", + "headers": { + "overview": "Vue d'ensemble" + }, + "footer": { + "poweredby": "Propulsé par", + "home": "Accueil", + "admin": "Administration" + } +} \ No newline at end of file diff --git a/middlewares/auth.js b/middlewares/auth.js new file mode 100644 index 00000000..aa85db52 --- /dev/null +++ b/middlewares/auth.js @@ -0,0 +1,34 @@ +"use strict"; + +var Promise = require('bluebird'), + moment = require('moment-timezone'); + +/** + * Authentication middleware + * + * @param {Express Request} req Express Request object + * @param {Express Response} res Express Response object + * @param {Function} next Next callback function + * @return {any} void + */ +module.exports = (req, res, next) => { + + // Is user authenticated ? + + if (!req.isAuthenticated()) { + return res.redirect('/login'); + } + + // Set i18n locale + + req.i18n.changeLanguage(req.user.lang); + res.locals.userMoment = moment; + res.locals.userMoment.locale(req.user.lang); + + // Expose user data + + res.locals.user = req.user; + + return next(); + +}; \ No newline at end of file diff --git a/middlewares/security.js b/middlewares/security.js new file mode 100644 index 00000000..fdf7403a --- /dev/null +++ b/middlewares/security.js @@ -0,0 +1,28 @@ +/** + * Security Middleware + * + * @param {Express Request} req Express request object + * @param {Express Response} res Express response object + * @param {Function} next next callback function + * @return {any} void + */ +module.exports = function(req, res, next) { + + //-> Disable X-Powered-By + app.disable('x-powered-by'); + + //-> Disable Frame Embedding + res.set('X-Frame-Options', 'deny'); + + //-> Re-enable XSS Fitler if disabled + res.set('X-XSS-Protection', '1; mode=block'); + + //-> Disable MIME-sniffing + res.set('X-Content-Type-Options', 'nosniff'); + + //-> Disable IE Compatibility Mode + res.set('X-UA-Compatible', 'IE=edge'); + + return next(); + +}; \ No newline at end of file diff --git a/models/config.js b/models/config.js new file mode 100644 index 00000000..513b249f --- /dev/null +++ b/models/config.js @@ -0,0 +1,34 @@ +"use strict"; + +var fs = require('fs'), + yaml = require('js-yaml'), + _ = require('lodash'); + +/** + * Load Application Configuration + * + * @param {String} confPath Path to the configuration file + * @return {Object} Application Configuration + */ +module.exports = (confPath) => { + + var appconfig = {}; + + try { + appconfig = yaml.safeLoad(fs.readFileSync(confPath, 'utf8')); + } catch (ex) { + winston.error(ex); + process.exit(1); + } + + return _.defaultsDeep(appconfig, { + title: "Requarks Wiki", + host: "http://localhost", + port: process.env.PORT, + db: "mongodb://localhost/wiki", + redis: null, + sessionSecret: null, + admin: null + }); + +}; \ No newline at end of file diff --git a/models/db/user.js b/models/db/user.js new file mode 100644 index 00000000..25c93b7d --- /dev/null +++ b/models/db/user.js @@ -0,0 +1,158 @@ +"use strict"; + +var modb = require('mongoose'); +var bcrypt = require('bcryptjs-then'); +var Promise = require('bluebird'); +var _ = require('lodash'); + +/** + * User Schema + * + * @type {Object} + */ +var userSchema = modb.Schema({ + + email: { + type: String, + required: true, + index: true, + minlength: 6 + }, + password: { + type: String, + required: true + }, + firstName: { + type: String, + required: true, + minlength: 1 + }, + lastName: { + type: String, + required: true, + minlength: 1 + }, + timezone: { + type: String, + required: true, + default: 'UTC' + }, + lang: { + type: String, + required: true, + default: 'en' + }, + rights: [{ + type: String, + required: true + }] + +}, +{ + timestamps: {} +}); + +/** + * VIRTUAL - Full Name + */ +userSchema.virtual('fullName').get(function() { + return this.firstName + ' ' + this.lastName; +}); + +/** + * INSTANCE - Validate password against hash + * + * @param {string} uPassword The user password + * @return {Promise} Promise with valid / invalid boolean + */ +userSchema.methods.validatePassword = function(uPassword) { + let self = this; + return bcrypt.compare(uPassword, self.password); +}; + +/** + * MODEL - Generate hash from password + * + * @param {string} uPassword The user password + * @return {Promise} Promise with generated hash + */ +userSchema.statics.generateHash = function(uPassword) { + return bcrypt.hash(uPassword, 10); +}; + +/** + * MODEL - Create a new user + * + * @param {Object} nUserData User data + * @return {Promise} Promise of the create operation + */ +userSchema.statics.new = function(nUserData) { + + let self = this; + + return self.generateHash(nUserData.password).then((passhash) => { + return this.create({ + _id: db.ObjectId(), + email: nUserData.email, + firstName: nUserData.firstName, + lastName: nUserData.lastName, + password: passhash, + rights: ['admin'] + }); + }); + +}; + +/** + * MODEL - Edit a user + * + * @param {String} userId The user identifier + * @param {Object} data The user data + * @return {Promise} Promise of the update operation + */ +userSchema.statics.edit = function(userId, data) { + + let self = this; + + // Change basic info + + let fdata = { + email: data.email, + firstName: data.firstName, + lastName: data.lastName, + timezone: data.timezone, + lang: data.lang, + rights: data.rights + }; + let waitTask = null; + + // Change password? + + if(!_.isEmpty(data.password) && _.trim(data.password) !== '********') { + waitTask = self.generateHash(data.password).then((passhash) => { + fdata.password = passhash; + return fdata; + }); + } else { + waitTask = Promise.resolve(fdata); + } + + // Update user + + return waitTask.then((udata) => { + return this.findByIdAndUpdate(userId, udata, { runValidators: true }); + }); + +}; + +/** + * MODEL - Delete a user + * + * @param {String} userId The user ID + * @return {Promise} Promise of the delete operation + */ +userSchema.statics.erase = function(userId) { + return this.findByIdAndRemove(userId); +}; + +module.exports = modb.model('User', userSchema); \ No newline at end of file diff --git a/models/mongodb.js b/models/mongodb.js new file mode 100644 index 00000000..46c5fa33 --- /dev/null +++ b/models/mongodb.js @@ -0,0 +1,53 @@ +"use strict"; + +var modb = require('mongoose'), + fs = require("fs"), + path = require("path"), + _ = require('lodash'); + +/** + * MongoDB module + * + * @param {Object} appconfig Application config + * @return {Object} Mongoose instance + */ +module.exports = function(appconfig) { + + modb.Promise = require('bluebird'); + + let dbModels = {}; + let dbModelsPath = path.join(ROOTPATH, 'models/db'); + + // Event handlers + + modb.connection.on('error', (err) => { + winston.error('Failed to connect to MongoDB instance.'); + }); + modb.connection.once('open', function() { + winston.log('Connected to MongoDB instance.'); + }); + + // Store connection handle + + dbModels.connection = modb.connection; + dbModels.ObjectId = modb.Types.ObjectId; + + // Load Models + + fs + .readdirSync(dbModelsPath) + .filter(function(file) { + return (file.indexOf(".") !== 0); + }) + .forEach(function(file) { + let modelName = _.upperFirst(_.split(file,'.')[0]); + dbModels[modelName] = require(path.join(dbModelsPath, file)); + }); + + // Connect + + dbModels.connectPromise = modb.connect(appconfig.db); + + return dbModels; + +}; \ No newline at end of file diff --git a/models/redis.js b/models/redis.js new file mode 100644 index 00000000..1cad1fa9 --- /dev/null +++ b/models/redis.js @@ -0,0 +1,41 @@ +"use strict"; + +var Redis = require('ioredis'), + _ = require('lodash'); + +/** + * Redis module + * + * @param {Object} appconfig Application config + * @return {Redis} Redis instance + */ +module.exports = (appconfig) => { + + let rd = null; + + if(_.isArray(appconfig.redis)) { + rd = new Redis.Cluster(appconfig.redis, { + scaleReads: 'master', + redisOptions: { + lazyConnect: false + } + }); + } else { + rd = new Redis(_.defaultsDeep(appconfig.redis), { + lazyConnect: false + }); + } + + // Handle connection errors + + rd.on('error', (err) => { + winston.error('Failed to connect to Redis instance(s). [err-1]'); + }); + + rd.on('node error', (err) => { + winston.error('Failed to connect to Redis instance(s). [err-2]'); + }); + + return rd; + +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..1eb381b7 --- /dev/null +++ b/package.json @@ -0,0 +1,95 @@ +{ + "name": "wiki", + "version": "1.0.0", + "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown", + "main": "server.js", + "scripts": { + "start": "node server", + "dev": "gulp", + "test": "snyk test && istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec ./tests/index.js && cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage && rm -rf ./coverage" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Requarks/wiki.git" + }, + "keywords": [ + "wiki", + "wikis", + "docs", + "documentation", + "markdown", + "guides" + ], + "author": "Nicolas Giard", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/Requarks/wiki/issues" + }, + "homepage": "https://github.com/Requarks/wiki#readme", + "engines": { + "node": ">=4.4.5" + }, + "dependencies": { + "auto-load": "^2.1.0", + "bluebird": "^3.4.1", + "body-parser": "^1.15.2", + "compression": "^1.6.2", + "connect-flash": "^0.1.1", + "connect-redis": "^3.1.0", + "cookie-parser": "^1.4.3", + "express": "^4.14.0", + "express-brute": "^0.7.0-beta.0", + "express-brute-redis": "0.0.1", + "express-session": "^1.14.0", + "express-validator": "^2.20.8", + "gridlex": "^2.1.1", + "i18next": "^3.4.1", + "i18next-express-middleware": "^1.0.1", + "i18next-node-fs-backend": "^0.1.2", + "ioredis": "^2.3.0", + "js-yaml": "^3.6.1", + "lodash": "^4.15.0", + "markdown-it": "^7.0.1", + "moment": "^2.14.1", + "moment-timezone": "^0.5.5", + "mongoose": "^4.5.9", + "mongoose-delete": "^0.3.4", + "node-bcrypt": "0.0.1", + "passport": "^0.3.2", + "passport-local": "^1.0.0", + "pug": "^2.0.0-beta5", + "serve-favicon": "^2.3.0", + "simplemde": "^1.11.2", + "validator": "^5.5.0", + "validator-as-promised": "^1.0.2", + "winston": "^2.2.0" + }, + "devDependencies": { + "babel-preset-es2015": "^6.13.2", + "chai": "^3.5.0", + "chai-as-promised": "^5.3.0", + "codacy-coverage": "^2.0.0", + "font-awesome": "^4.6.3", + "gridlex": "^2.1.1", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "gulp-clean-css": "^2.0.12", + "gulp-concat": "^2.6.0", + "gulp-gzip": "^1.4.0", + "gulp-include": "^2.3.1", + "gulp-nodemon": "^2.1.0", + "gulp-plumber": "^1.1.0", + "gulp-sass": "^2.3.2", + "gulp-tar": "^1.9.0", + "gulp-uglify": "^2.0.0", + "gulp-zip": "^3.2.0", + "istanbul": "^0.4.4", + "jquery": "^3.1.0", + "merge-stream": "^1.0.0", + "mocha": "^3.0.2", + "mocha-lcov-reporter": "^1.2.0", + "nodemon": "^1.10.0", + "snyk": "^1.18.0", + "vue": "^1.0.26" + } +} diff --git a/server.js b/server.js new file mode 100644 index 00000000..1442ed9e --- /dev/null +++ b/server.js @@ -0,0 +1,18 @@ +// =========================================== +// REQUARKS WIKI +// 1.0.0 +// Licensed under AGPLv3 +// =========================================== + +// ---------------------------------------- +// Load modules +// ---------------------------------------- + +global.winston = require('winston'); +winston.info('Requarks Wiki is initializing...'); + +global.ROOTPATH = __dirname; + +var appconfig = require('./models/config')('./config.yml'); +global.db = require('./models/db')(appconfig); +global.red = require('./models/redis')(appconfig); \ No newline at end of file diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 00000000..aa739f15 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,11 @@ +"use strict"; + +let path = require('path'), + fs = require('fs'); + +// ======================================== +// Load global modules +// ======================================== + +global._ = require('lodash'); +global.winston = require('winston'); \ No newline at end of file diff --git a/wiki.sublime-project b/wiki.sublime-project new file mode 100644 index 00000000..0a644944 --- /dev/null +++ b/wiki.sublime-project @@ -0,0 +1,12 @@ +{ + "folders": + [ + { + "file_exclude_patterns": + [ + "wiki.sublime-project" + ], + "path": "." + } + ] +}