From b035a68ca495fcf3994e9bea0cc951bfffa790d2 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Tue, 16 Aug 2016 20:56:55 -0400 Subject: [PATCH] Base Project Files --- .babelrc | 3 + .gitattributes | 17 +++++ .gitignore | 9 +++ .snyk | 4 + .travis.yml | 30 ++++++++ config.sample.yml | 54 ++++++++++++++ gulpfile.js | 94 ++++++++++++++++++++++++ inch.json | 10 +++ locales/en/common.js | 11 +++ locales/fr/common.js | 11 +++ middlewares/auth.js | 34 +++++++++ middlewares/security.js | 28 +++++++ models/config.js | 34 +++++++++ models/db/user.js | 158 ++++++++++++++++++++++++++++++++++++++++ models/mongodb.js | 53 ++++++++++++++ models/redis.js | 41 +++++++++++ package.json | 95 ++++++++++++++++++++++++ server.js | 18 +++++ tests/index.js | 11 +++ wiki.sublime-project | 12 +++ 20 files changed, 727 insertions(+) create mode 100644 .babelrc create mode 100644 .gitattributes create mode 100644 .snyk create mode 100644 .travis.yml create mode 100644 config.sample.yml create mode 100644 gulpfile.js create mode 100644 inch.json create mode 100644 locales/en/common.js create mode 100644 locales/fr/common.js create mode 100644 middlewares/auth.js create mode 100644 middlewares/security.js create mode 100644 models/config.js create mode 100644 models/db/user.js create mode 100644 models/mongodb.js create mode 100644 models/redis.js create mode 100644 package.json create mode 100644 server.js create mode 100644 tests/index.js create mode 100644 wiki.sublime-project 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": "." + } + ] +}