瀏覽代碼

Base Project Files

NGPixel 8 年之前
父節點
當前提交
b035a68ca4
共有 20 個文件被更改,包括 727 次插入0 次删除
  1. 3 0
      .babelrc
  2. 17 0
      .gitattributes
  3. 9 0
      .gitignore
  4. 4 0
      .snyk
  5. 30 0
      .travis.yml
  6. 54 0
      config.sample.yml
  7. 94 0
      gulpfile.js
  8. 10 0
      inch.json
  9. 11 0
      locales/en/common.js
  10. 11 0
      locales/fr/common.js
  11. 34 0
      middlewares/auth.js
  12. 28 0
      middlewares/security.js
  13. 34 0
      models/config.js
  14. 158 0
      models/db/user.js
  15. 53 0
      models/mongodb.js
  16. 41 0
      models/redis.js
  17. 95 0
      package.json
  18. 18 0
      server.js
  19. 11 0
      tests/index.js
  20. 12 0
      wiki.sublime-project

+ 3 - 0
.babelrc

@@ -0,0 +1,3 @@
+{
+  "presets": ["es2015"]
+}

+ 17 - 0
.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

+ 9 - 0
.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

+ 4 - 0
.snyk

@@ -0,0 +1,4 @@
+failThreshold: high
+version: v1.5.2
+ignore: {}
+patch: {}

+ 30 - 0
.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'

+ 54 - 0
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

+ 94 - 0
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);
+});

+ 10 - 0
inch.json

@@ -0,0 +1,10 @@
+{
+  "files": {
+    "included": [
+      "controllers/**/*.js",
+      "middlewares/**/*.js",
+      "models/**/*.js",
+    ],
+    "excluded": []
+  }
+}

+ 11 - 0
locales/en/common.js

@@ -0,0 +1,11 @@
+{
+	"wiki": "Wiki",
+	"headers": {
+		"overview": "Overview"
+	},
+	"footer": {
+		"poweredby": "Powered by",
+		"home": "Home",
+		"admin": "Administration"
+	}
+}

+ 11 - 0
locales/fr/common.js

@@ -0,0 +1,11 @@
+{
+	"wiki": "Wiki",
+	"headers": {
+		"overview": "Vue d'ensemble"
+	},
+	"footer": {
+		"poweredby": "Propulsé par",
+		"home": "Accueil",
+		"admin": "Administration"
+	}
+}

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

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

+ 34 - 0
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
+	});
+
+};

+ 158 - 0
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<Boolean>}  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<String>}  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);

+ 53 - 0
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;
+
+};

+ 41 - 0
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;
+
+};

+ 95 - 0
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"
+  }
+}

+ 18 - 0
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);

+ 11 - 0
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');

+ 12 - 0
wiki.sublime-project

@@ -0,0 +1,12 @@
+{
+	"folders":
+	[
+		{
+			"file_exclude_patterns":
+			[
+				"wiki.sublime-project"
+			],
+			"path": "."
+		}
+	]
+}