commit 2abfa9076596a5cc99a2595331df8b597b62a036 Author: Linus Miller Date: Thu Jul 14 17:15:55 2016 +0200 Initial commit. diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..b7ba315 --- /dev/null +++ b/.babelrc @@ -0,0 +1,16 @@ +{ + "env": { + "server": { + "presets": [ "es2015-node6" ], + "plugins": [ + "add-module-exports" + ] + }, + "client": { + "presets": [ "es2015", "react" ], + "plugins": [ + "add-module-exports" + ] + } + } +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..3665079 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,43 @@ +{ + "extends": "airbnb-base", + + "parserOptions": { + "sourceType": "strict" + }, + + "rules": { + "guard-for-in": 0, + "global-require": 0, + "no-underscore-dangle": 0, + "object-shorthand": 0, + "default-case": 0, + "one-var": 0, + "prefer-rest-params": 0, + "no-unused-vars": [ 2, { "args": "none" } ], + "no-alert": 0, + "quote-props": 0, + "no-nested-ternary": 0, + "no-use-before-define": [2, { "functions": false, "classes": true }], + "consistent-return": 0, + "no-eval": 0, + "prefer-arrow-callback": 0, + "array-bracket-spacing": 0, + "no-console": 0, + "indent": [ 2, 2, { "SwitchCase": 1 }], + "max-len": 0, + "comma-dangle": 0, + "no-param-reassign": 0, + "prefer-template": 0, + "curly": 0, + "func-names": 0, + "no-shadow": 0, + "spaced-comment": 0, + "strict": [ 2, "global" ] + }, + + "globals": { + "ENV": true, + "LOGIN_USER": true, + "PWD": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76772cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# NPM Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +npm-debug.log + +# Bower dependency directory +bower_components + +*.marko.js + +/dump + +/public + +/server/uploads diff --git a/README.md b/README.md new file mode 100644 index 0000000..63cdf98 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Miller Konsult + +## Running + +1. `$ git clone git@gitlab.thecodebureau.com:lohfu/millerkonsult.git && cd millerkonsult` +2. `$ npm install` +3. `$ npm run gulp` + +## Testing + +`$ npm test` + +## Admin Panel + +The admin panel is available under the `/admin` path, ie +`http://localhost:3000/admin` if you run with the default settings. + +## Directories + +### /gulp + +This contains ALL gulp logic. + +### /public + +This directory is maintained by gulp. All static assets get symlinked in here, +and any built code + +This folder should not be edited directly, or commited to git. + +### /sass + +This folder contains all SASS files and is compiled with the gulp sass task, +which puts the output CSS into `/public/css` + +### /server + +This directory contains all JavaScript that only has to do with running your +server instance. + +#### Checking routes + +``` +console.log(routes): +``` + +### /static + +This contains all static content. + +Most content in here should be symlinked into the public folder. The only +content in here that needs to be built in production environment should SVG +files, which should be minified. + +### /src + +This contains all JavaScript not only run in the browser. This includes all +isomorphic code (ie code that runs both in the browser and in the server) and +browser specific code. All Marko templates, components and custom tags are put +in here as well. Some of the code in here is only ment to be run in the +browser, currently this is all the Backbone logic (Backbone views, models, +collections and routers). + diff --git a/bin/create-organization.js b/bin/create-organization.js new file mode 100755 index 0000000..4c97b9a --- /dev/null +++ b/bin/create-organization.js @@ -0,0 +1,44 @@ +#!/bin/env node + +'use strict'; + +const p = require('path'); +const chalk = require('chalk'); + +const MongoClient = require('mongodb').MongoClient; + +const successPrefix = '[' + chalk.green('SUCCESS') + '] '; +const errorPrefix = '[' + chalk.red('ERROR') + '] '; +global.PWD = p.dirname(__dirname); +global.ENV = process.env.NODE_ENV || 'development'; + +console.log(PWD); +function _mongo(collection, cb) { + const mongoConfig = require(p.join(PWD, 'server/config/mongo')); + + MongoClient.connect(mongoConfig.uri, function (err, db) { + if (err) { + console.error(errorPrefix); + console.error(err); + process.exit(1); + } + cb(db.collection(collection), db); + }); +} + +function createOrganization() { + _mongo('organizations', function (orgs, db) { + orgs.insert({ dateCreated: new Date() }, function (err, org) { + if (err) { + console.error(errorPrefix); + console.error(err); + process.exit(1); + } else { + console.log(successPrefix + 'Empty organization created'); + process.exit(0); + } + }); + }); +} + +createOrganization(); diff --git a/bin/create-roles.js b/bin/create-roles.js new file mode 100755 index 0000000..a5e2533 --- /dev/null +++ b/bin/create-roles.js @@ -0,0 +1,47 @@ +#!/bin/env node + +'use strict'; + +const p = require('path'); +const chalk = require('chalk'); + +const MongoClient = require('mongodb').MongoClient; + +const successPrefix = '[' + chalk.green('SUCCESS') + '] '; +const errorPrefix = '[' + chalk.red('ERROR') + '] '; +global.PWD = p.dirname(__dirname); +global.ENV = process.env.NODE_ENV || 'development'; + +console.log(PWD); +function _mongo(collection, cb) { + const mongoConfig = require(p.join(PWD, 'server/config/mongo')); + + MongoClient.connect(mongoConfig.uri, function (err, db) { + if (err) { + console.error(errorPrefix); + console.error(err); + process.exit(1); + } + cb(db.collection(collection), db); + }); +} + +function createRoles(roles) { + const roleNames = (roles ? roles.split(',') : [ 'user', 'admin' ]); + + _mongo('roles', function (roles, db) { + roles.insert(roleNames.map(function (role) { return { name: role }; }), function (err) { + db.close(); + if (err) { + console.error(errorPrefix); + console.error(err); + process.exit(1); + } else { + console.log(successPrefix + 'Created roles: ' + roleNames.join(', ')); + process.exit(0); + } + }); + }); +} + +createRoles(); diff --git a/bin/create-user.js b/bin/create-user.js new file mode 100755 index 0000000..09a4523 --- /dev/null +++ b/bin/create-user.js @@ -0,0 +1,98 @@ +#!/bin/env node + +'use strict'; + +const p = require('path'); +const crypto = require('crypto'); + +const chalk = require('chalk'); + +const MongoClient = require('mongodb').MongoClient; + +const successPrefix = '[' + chalk.green('SUCCESS') + '] '; +const errorPrefix = '[' + chalk.red('ERROR') + '] '; + +global.PWD = p.dirname(__dirname); +global.ENV = process.env.NODE_ENV || 'development'; + +function _mongo(collection, cb) { + const mongoConfig = require(p.join(PWD, 'server/config/mongo')); + + MongoClient.connect(mongoConfig.uri, function (err, db) { + if (err) { + console.error(errorPrefix); + console.error(err); + process.exit(1); + } + cb(db.collection(collection), db); + }); +} + + +const SALT_LENGTH = 16; + +function _hash(password) { + // generate salt + password = password.trim(); + const chars = '0123456789abcdefghijklmnopqurstuvwxyz'; + let salt = ''; + for (let i = 0; i < SALT_LENGTH; i++) { + const j = Math.floor(Math.random() * chars.length); + salt += chars[j]; + } + + // hash the password + const passwordHash = crypto.createHash('sha512').update(salt + password).digest('hex'); + + // entangle the hashed password with the salt and save to the model + return _entangle(passwordHash, salt, password.length); +} + +function _entangle(string, salt, t) { + string = salt + string; + const length = string.length; + + const arr = string.split(''); + for (let i = 0; i < salt.length; i++) { + const num = ((i + 1) * t) % length; + const tmp = arr[i]; + arr[i] = arr[num]; + arr[num] = tmp; + } + + return arr.join(''); +} + +function createUser(email, password, roles) { + if (!email || !password) { + console.log('Usage: bin/create-user.js [email] [password] [?roles]'); + process.exit(1); + } + + + const user = { + email: email, + local: { + password: _hash(password) + }, + roles: roles ? roles.split(',') : [ 'admin' ], + isVerified: true, + dateCreated: new Date() + }; + + _mongo('users', function (users, db) { + users.insert(user, function (err, user) { + db.close(); + if (err) { + console.error(errorPrefix); + console.error(err); + process.exit(1); + } else { + console.log(successPrefix + 'Saved user: ' + user.ops[0].email); + process.exit(0); + } + }); + }); +} + +createUser.apply(null, process.argv.slice(2)); diff --git a/gulp/LICENSE b/gulp/LICENSE new file mode 100644 index 0000000..27e2991 --- /dev/null +++ b/gulp/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Linus Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/gulp/README.md b/gulp/README.md new file mode 100644 index 0000000..f1aefe2 --- /dev/null +++ b/gulp/README.md @@ -0,0 +1,23 @@ +# gulp + +## Tasks + +### browserify.js + +### browser-sync.js + +### fonts.js + +### nodemon.js + +### raster.js + +### sass.js + +### static.js + +### svg.js + +### watch.js + +### wipe.js diff --git a/gulp/config.js b/gulp/config.js new file mode 100644 index 0000000..59dc967 --- /dev/null +++ b/gulp/config.js @@ -0,0 +1,156 @@ +'use strict'; + +const p = require('path'); +const fs = require('fs'); + +const port = fs.existsSync(p.join(PWD, 'server/config/port.js')) ? + require(p.join(PWD, 'server/config/port')) : null; + +module.exports = { + browserSync: { + browser: null, + ghostMode: false, + proxy: 'localhost:' + (port || 10000), + port: 1337, + ui: { + port: 1338 + }, + files: [ + p.join(PWD, 'public/css/**/*.css'), + p.join(PWD, 'public/js/**/*.js'), + ] + }, + + browserify: { + suffix: true, + debug: true, + dest: p.join(PWD, 'public/js'), + extensions: [ '.js', '.jsx', '.json' ], + // PWD/node_modules is added so symlinked ridge does not break. used to + // work without this in browserify 9 + paths: [ p.join(PWD, 'node_modules'), p.join(PWD, 'modules') ], + // outputs only need to be used if output names are different from entries. + // Otherwise the entries array is copied into the outputs array. + entries: [ + 'app.js' + ], + src: p.join(PWD, 'src') + }, + + less: { + suffix: true, + src: [ + p.join(PWD, 'less/**/main.less') + ], + dest: p.join(PWD, 'public/css'), + autoprefixer: { + browsers: [ + 'safari >= 5', + 'ie >= 8', + 'ios >= 6', + 'opera >= 12.1', + 'firefox >= 17', + 'chrome >= 30', + 'android >= 4' + ], + cascade: true + }, + functions: { + 'img-url': function (value) { + const tree = this.context.pluginManager.less.tree; + + return new tree.URL(new tree.Quoted('"', p.join('/img', value.value)), this.index, this.currentFileInfo); + }, + + rem: function (value, context) { + const tree = this.context.pluginManager.less.tree; + + if (value.type === 'Expression') + return new tree.Expression(value.value.map((value) => { + if (value.unit.backupUnit === 'px') + return new tree.Dimension(value.value / 14, 'rem'); + + return new tree.Dimension(value.value, 'rem'); + })); + + return new tree.Dimension(value.value / 14, 'rem'); + }, + + px: function (value) { + const tree = this.context.pluginManager.less.tree; + + if (value.type === 'Expression') + return new tree.Expression(value.value.map((value) => { + if (value.unit.backupUnit === 'px') + return value; + + return new tree.Dimension(value.value * 14, 'px'); + })); + + if (value.unit.backupUnit === 'px') + return value; + + return new tree.Dimension(value.value * 14, 'px'); + } + }, + options: { + paths: [ + p.join(PWD, 'node_modules/spineless/less') + ], + } + }, + + nodemon: { + ext: 'js,marko', + ignore: ['*.marko.js', 'src/**/*.js' ], + watch: [ + 'src', + 'modules', + 'server' + ], + script: fs.existsSync(p.join(PWD, 'package.json')) ? require(p.join(PWD, 'package.json')).main.replace(/^\./, PWD) : 'server/server.js', + env: { + BABEL_ENV: 'server', + // what port you actually put into the browser... when using browser-sync + // this will differ from the internal port used by express + EXTERNAL_PORT: 1337, + // needed to force debug to use colors despite tty.istty(0) being false, + // which it is in a child process + DEBUG_COLORS: true, + // needed to force chalk to use when running gulp nodemon tasks. + FORCE_COLOR: true, + PWD, + NODE_ENV: ENV, + DEBUG: 'hats:contact' + } + }, + + raster: { + src: p.join(PWD, 'img/raster/**/*.{png,gif,jpg}'), + dest: p.join(PWD, 'public/img') + }, + + static: { + src: p.join(PWD, 'static/**/*'), + dest: p.join(PWD, 'public') + }, + + svg: { + src: p.join(PWD, 'img/svg/**/*.svg'), + dest: p.join(PWD, 'public/img') + }, + + tasks: { + development: [ 'wipe', [ 'browserify', 'raster', 'less', 'static', 'svg' ], [ 'nodemon' ], [ 'watch', 'browser-sync' ] ], + production: [ 'wipe', [ 'browserify', 'raster', 'less', 'static', 'svg' ]] + }[ENV], + + watch: { + //sass: p.join(PWD, 'sass/**/*.{sass,scss}') + less: p.join(PWD, 'less/**/*.less') + }, + + wipe: { + src: [ p.join(PWD, 'public') ] + }, +}; diff --git a/gulp/index.js b/gulp/index.js new file mode 100644 index 0000000..f50ced4 --- /dev/null +++ b/gulp/index.js @@ -0,0 +1,30 @@ +'use strict'; + +// modules > 3rd party +const _ = require('lodash'); + +// modules > gulp +const gulp = require('gulp'); + +global.ENV = process.env.NODE_ENV || 'development'; +global.PWD = process.env.PWD; + +const args = process.argv.slice(2); + +// use tasks from arguments list if present, otherwise use tasks from +// configuration (environment specific) +let tasks = args.length > 0 ? args : require('./config').tasks; + +// only require used tasks +_.flatten(tasks, true).forEach((task) => require('./tasks/' + task)); + +tasks = tasks.map((task) => { + if (Array.isArray(task)) { + return gulp.parallel(...task); + } + + return task; +}); + +// set up the 'default' task to use runSequence to run all tasks +gulp.task('default', gulp.series(...tasks)); diff --git a/gulp/tasks/browser-sync.js b/gulp/tasks/browser-sync.js new file mode 100644 index 0000000..21deaaf --- /dev/null +++ b/gulp/tasks/browser-sync.js @@ -0,0 +1,9 @@ +'use strict'; + +const gulp = require('gulp'); +const browserSync = require('browser-sync'); + +const TASK_NAME = 'browser-sync'; +const config = require('../config').browserSync; + +gulp.task(TASK_NAME, () => browserSync(config)); diff --git a/gulp/tasks/browserify.js b/gulp/tasks/browserify.js new file mode 100644 index 0000000..a22b856 --- /dev/null +++ b/gulp/tasks/browserify.js @@ -0,0 +1,113 @@ +'use strict'; + +// native modules +const p = require('path'); +const fs = require('fs'); + +// 3rd party modules +const _ = require('lodash'); +const browserify = require('browserify'); +const buffer = require('vinyl-buffer'); +const chalk = require('chalk'); +const es = require('event-stream'); +const gulp = require('gulp'); +const gutil = require('gulp-util'); +const rename = require('gulp-rename'); +const source = require('vinyl-source-stream'); +const sourcemaps = require('gulp-sourcemaps'); +const uglify = require('gulp-uglify'); +const watchify = require('watchify'); +const babelify = require('babelify'); + +require('marko/compiler').defaultOptions.writeToDisk = false; + +process.env.BABEL_ENV = 'client'; + +const markoify = require('markoify'); + +const config = require('../config').browserify; +const suffix = ENV === 'production' ? '-' + Date.now().toString(16) : ''; + +const errorHandler = require('../util/error-handler'); + +function formatError(err) { + err.task = 'browserify'; + + const matchFileName = err.message.match(/\/[^\s:]*/); + + if (matchFileName) { + let fileName = matchFileName[0]; + + if (fileName.indexOf(PWD) > -1) + fileName = './' + p.relative(PWD, fileName); + + fileName = chalk.yellow(fileName); + + const matchNumbers = err.message.match(/ \((.*)\)/); + + if (matchNumbers) { + const arr = matchNumbers[1].split(':'); + + err.message = err.message.slice(0, matchNumbers.index) + + ' at line ' + arr[0] + ', col ' + arr[1]; + } + + err.message = err.message.split(matchFileName[0]).join(fileName); + } + + err.message = err.message.split(/:\s*/).join('\n'); + + errorHandler.call(this, err); +} + +if (typeof config.entries === 'string') config.entries = [config.entries]; + +const TASK_NAME = 'browserify'; +gulp.task(TASK_NAME, (cb) => { + cb = _.after(config.entries.length, cb); + + if (config.suffix) + fs.writeFileSync(config.dest + '.json', JSON.stringify({ suffix })); + + const tasks = config.entries.map((entry) => { + function bundler(bundle) { + let pipe = bundle.bundle() + .on('error', formatError) + .pipe(source(entry)) + .on('end', () => { + gutil.log(chalk.cyan(TASK_NAME) + ' wrote ' + chalk.magenta(entry) + '.'); + cb(); + }); + + if (ENV !== 'development') + pipe = pipe.pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(uglify()) + .pipe(rename({ suffix })) + .pipe(sourcemaps.write('./')); + + return pipe.pipe(gulp.dest(config.dest)); + } + + const bundle = browserify(_.defaults({ + entries: [ p.join(config.src, entry) ], + cache: {}, + packageCache: {} + }, config)); + + if (ENV !== 'production') + bundle.plugin(watchify, { + ignoreWatch: [ '/public/**', '**/node_modules/**', '**/bower_components/**'] + }); + + bundle.transform(babelify); + bundle.transform(markoify); + + bundle.on('update', () => bundler(bundle)); + + return bundler(bundle); + }); + + // return a single stream from all bundle streams + return es.merge.apply(null, tasks); +}); diff --git a/gulp/tasks/less.js b/gulp/tasks/less.js new file mode 100644 index 0000000..abf0d19 --- /dev/null +++ b/gulp/tasks/less.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('fs'); +const gulp = require('gulp'); +const _less = require('less'); +const less = require('gulp-less'); +const sourcemaps = require('gulp-sourcemaps'); +const rename = require('gulp-rename'); +const mkdirp = require('mkdirp'); + +const postcss = require('gulp-postcss'); +const autoprefixer = require('autoprefixer'); + +const config = require('../config').less; + +const suffix = '-' + Date.now().toString(16); + +const errorHandler = require('../util/error-handler'); + +_less.functions.functionRegistry.addMultiple(config.functions); + +const processors = [ + autoprefixer(config.autoprefixer), +]; + +if (ENV === 'production') { + const csswring = require('csswring'); + processors.push(csswring(config.csswring)); +} + +gulp.task('less', function () { + mkdirp(config.dest); + + if (config.suffix) + fs.writeFile(config.dest + '.json', JSON.stringify({ suffix })); + + let pipe = gulp.src(config.src) + .pipe(sourcemaps.init()) + .pipe(less(config.options).on('error', errorHandler)) + .pipe(postcss(processors)); + + if (config.suffix) + pipe = pipe.pipe(rename({ suffix })); + + return pipe.pipe(sourcemaps.write('./maps')) + .pipe(gulp.dest(config.dest)); +}); diff --git a/gulp/tasks/nodemon.js b/gulp/tasks/nodemon.js new file mode 100644 index 0000000..8217929 --- /dev/null +++ b/gulp/tasks/nodemon.js @@ -0,0 +1,33 @@ +'use strict'; + +const _ = require('lodash'); + +const gulp = require('gulp'); +const browserSync = require('browser-sync'); +const nodemon = require('nodemon'); + +const config = require('../config').nodemon; + +// for some reason, this was needed somewhere before +//process.stdout.isTTY = true; + +gulp.task('nodemon', (cb) => { + // TODO save pipe to variable so onReadable can be replaced by a arrow function + nodemon(_.defaults({ stdout: false }, config)) + .on('readable', function onReadable() { + this.stdout.pipe(process.stdout); + this.stderr.pipe(process.stderr); + + this.stdout.on('data', (chunk) => { + if (/Express server started on port/.test(chunk)) { + if (cb) + cb(); + + cb = null; + + if (browserSync.active) + browserSync.reload(); + } + }); + }); +}); diff --git a/gulp/tasks/raster.js b/gulp/tasks/raster.js new file mode 100644 index 0000000..3e5ad08 --- /dev/null +++ b/gulp/tasks/raster.js @@ -0,0 +1,27 @@ +'use strict'; + +// modules > native +const p = require('path'); + +// modules > 3rd party +const chalk = require('chalk'); + +// modules > gulp:utilities +const gulp = require('gulp'); +const gutil = require('gulp-util'); + +const TASK_NAME = 'raster'; +const config = require('../config').raster; + +gulp.task(TASK_NAME, () => { + let count = 0; + + return gulp.src(config.src) + .pipe(gulp.symlink((file) => { + count++; + return p.join(config.dest); + }, { log: false })) + .on('end', () => { + gutil.log(chalk.cyan(TASK_NAME) + ' done symlinking ' + chalk.bold.blue(count) + ' files'); + }); +}); diff --git a/gulp/tasks/static.js b/gulp/tasks/static.js new file mode 100644 index 0000000..5635c50 --- /dev/null +++ b/gulp/tasks/static.js @@ -0,0 +1,39 @@ +'use strict'; + +// modules > native +const p = require('path'); +const fs = require('fs'); + +// modules > 3rd party +const chalk = require('chalk'); + +// modules > gulp:utilities +const gulp = require('gulp'); +const through = require('through2'); +const gutil = require('gulp-util'); + +const TASK_NAME = 'static'; +const config = require('../config').static; + +gulp.task(TASK_NAME, () => { + let count = 0; + + return gulp.src(config.src) + // we use through so that we can skip directories + // we skip directories because we want to merge file structure + .pipe(through.obj((file, enc, callback) => { + fs.stat(file.path, (err, stats) => { + if (stats.isDirectory()) + file = null; + + callback(null, file); + }); + })) + .pipe(gulp.symlink((file) => { + count++; + return p.join(config.dest); + })) + .on('end', () => { + gutil.log(chalk.cyan(TASK_NAME) + ' done symlinking ' + chalk.bold.blue(count) + ' files'); + }); +}); diff --git a/gulp/tasks/svg.js b/gulp/tasks/svg.js new file mode 100644 index 0000000..edbfbd5 --- /dev/null +++ b/gulp/tasks/svg.js @@ -0,0 +1,27 @@ +'use strict'; + +// modules > native +const p = require('path'); + +// modules > 3rd party +const chalk = require('chalk'); + +// modules > gulp:utilities +const gulp = require('gulp'); +const gutil = require('gulp-util'); + +const TASK_NAME = 'svg'; +const config = require('../config').svg; + +gulp.task(TASK_NAME, () => { + let count = 0; + + return gulp.src(config.src) + .pipe(gulp.symlink((file) => { + count++; + return p.join(config.dest); + })) + .on('end', () => { + gutil.log(chalk.cyan(TASK_NAME) + ' done symlinking ' + chalk.bold.blue(count) + ' files'); + }); +}); diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js new file mode 100644 index 0000000..14af4e1 --- /dev/null +++ b/gulp/tasks/watch.js @@ -0,0 +1,16 @@ +'use strict'; + +// modules > 3rd party +const _ = require('lodash'); + +// modules > gulp:utilities +const gulp = require('gulp'); + +const TASK_NAME = 'watch'; +const config = require('../config').watch; + +gulp.task(TASK_NAME, () => { + _.forIn(config, (value, key) => { + gulp.watch(value, gulp.series(key)); + }); +}); diff --git a/gulp/tasks/wipe.js b/gulp/tasks/wipe.js new file mode 100644 index 0000000..c597d9b --- /dev/null +++ b/gulp/tasks/wipe.js @@ -0,0 +1,44 @@ +'use strict'; + +// native modules +const fs = require('fs'); + +// 3rd party modules +const mkdirp = require('mkdirp'); +const chalk = require('chalk'); +const gulp = require('gulp'); +const gutil = require('gulp-util'); +const rimraf = require('rimraf'); + +const TASK_NAME = 'wipe'; + +const config = require('../config').wipe; + +gulp.task(TASK_NAME, (cb) => { + let count = 0; + config.src.forEach((folder) => { + fs.exists(folder, (exists) => { + if (exists) { + rimraf(folder, (err) => { + if (err) throw err; + gutil.log('Folder ' + chalk.magenta(folder) + ' removed'); + + mkdirp.sync(folder); + + count++; + if (count >= config.src.length) { + cb(); + } + }); + } else { + count++; + + mkdirp.sync(folder); + + if (count >= config.src.length) { + cb(); + } + } + }); + }); +}); diff --git a/gulp/util/error-handler.js b/gulp/util/error-handler.js new file mode 100644 index 0000000..ae0fd11 --- /dev/null +++ b/gulp/util/error-handler.js @@ -0,0 +1,16 @@ +'use strict'; + +const util = require('gulp-util'); +const chalk = require('chalk'); + +module.exports = function (err) { + util.log(chalk.red('ERROR') + (err.task ? ' in task \'' + chalk.cyan(err.task) : ' in plugin \'' + chalk.cyan(err.plugin)) + '\''); + + console.log('\n' + err.message.trim() + '\n'); + + // needed for error handling not thrown by gulp-watch + if (this.emit) { + // Keep gulp from hanging on this task + this.emit('end'); + } +}; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..5882dc4 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,7 @@ +'use strict'; + +require('./gulp'); + +process.on('SIGINT', () => { + process.exit(0); +}); diff --git a/img/svg/form-invalid.svg b/img/svg/form-invalid.svg new file mode 100755 index 0000000..1779fca --- /dev/null +++ b/img/svg/form-invalid.svg @@ -0,0 +1,51 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/img/svg/form-valid.svg b/img/svg/form-valid.svg new file mode 100755 index 0000000..91a3138 --- /dev/null +++ b/img/svg/form-valid.svg @@ -0,0 +1,51 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/less/main.less b/less/main.less new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..02c84c6 --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "title": "Geolets", + "name": "geolets", + "version": "0.0.1", + "description": "Geolets.", + "main": "server/server.js", + "repository": { + "type": "git", + "url": "git@gitlab.thecodebureau.com:lohfu/geolets" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "gulp": "gulp", + "gulp:production": "NODE_ENV=production gulp", + "gulp:development": "NODE_ENV=development gulp" + }, + "license": "MIT", + "dependencies": { + "app-module-path": "^1.1.0", + "body-parser": "^1.15.2", + "chalk": "^1.1.3", + "connect-redis": "^3.1.0", + "cookie-parser": "^1.4.3", + "debug": "^2.2.0", + "express": "^4.14.0", + "express-session": "^1.14.0", + "lodash": "^4.13.1", + "marko": "^3.7.2", + "mongoose": "^4.5.4", + "mongopot": "github:lohfu/mongopot", + "morgan": "^1.7.0", + "nodemailer": "^2.5.0", + "require-dir": "^0.3.0", + "warepot": "github:lohfu/warepot" + }, + "devDependencies": { + "autoprefixer": "^6.3.7", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-preset-es2015": "^6.9.0", + "babel-preset-es2015-node6": "^0.2.0", + "babel-preset-react": "^6.11.1", + "babelify": "^7.3.0", + "browser-sync": "^2.13.0", + "browserify": "^13.0.1", + "chalk": "^1.1.3", + "csswring": "^5.1.0", + "eslint": "^3.0.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-config-airbnb-base": "^4.0.0", + "eslint-plugin-import": "^1.10.3", + "eslint-plugin-jsx-a11y": "^2.0.1", + "eslint-plugin-react": "^5.2.2", + "event-stream": "^3.3.3", + "gulp": "github:gulpjs/gulp#4.0", + "gulp-less": "^3.1.0", + "gulp-postcss": "^6.1.1", + "gulp-rename": "^1.2.2", + "gulp-sourcemaps": "^1.6.0", + "gulp-svgmin": "^1.2.2", + "gulp-uglify": "^1.5.4", + "gulp-util": "^3.0.7", + "marko": "^3.7.2", + "markoify": "^2.1.1", + "mkdirp": "^0.5.1", + "nodemon": "^1.9.2", + "postcss": "^5.1.0", + "pretty-hrtime": "^1.0.2", + "rimraf": "^2.5.3", + "through2": "^2.0.1", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0", + "watchify": "^3.7.0" + } +} diff --git a/server/config/dir.js b/server/config/dir.js new file mode 100644 index 0000000..ee51690 --- /dev/null +++ b/server/config/dir.js @@ -0,0 +1,10 @@ +'use strict'; + +const p = require('path'); + +module.exports = { + root: p.join(PWD, 'server'), + src: p.join(PWD, 'src'), + static: p.join(PWD, 'public'), + uploads: p.join(PWD, 'uploads') +}; diff --git a/server/config/error-handler.js b/server/config/error-handler.js new file mode 100644 index 0000000..b88565a --- /dev/null +++ b/server/config/error-handler.js @@ -0,0 +1,50 @@ +'use strict'; + +const _ = require('lodash'); + +const defaults = { + mystify: { + properties: ['errors', 'message', 'name', 'status', 'statusText'] + }, + + log: { + // if database = true there has to be a mongoose model name ErrorModel + ignore: [], + } +}; + +const ErrorModel = require('mongopot/models/error'); + +function store(error) { + ErrorModel.create(error, (err) => { + // TODO handle errors in error handler better + if (err) { + console.error('ERROR WRITING TO DATABASE'); + console.error(err); + console.log(err.errors); + console.error('ORIGINAL ERROR'); + console.error(error); + } + }); +} + +module.exports = _.merge(defaults, { + development: { + log: { + store: store, + console: true, + } + }, + testing: { + log: { + store: false, + console: false, + }, + }, + production: { + log: { + store: store, + console: false, + } + }, +}[ENV]); diff --git a/server/config/globals.js b/server/config/globals.js new file mode 100644 index 0000000..0550282 --- /dev/null +++ b/server/config/globals.js @@ -0,0 +1,7 @@ +'use strict'; + +global.ENV = process.env.NODE_ENV || 'development'; +global.PWD = process.env.PWD; +//bind.LOGIN_USER = 'username'; +//global.LOGIN_USER = 'linus.miller@thecodebureau.com'; +//bind.LOGIN_USER = 'victor.nilsson@thecodebureau.com'; diff --git a/server/config/membership.js b/server/config/membership.js new file mode 100644 index 0000000..3359c23 --- /dev/null +++ b/server/config/membership.js @@ -0,0 +1,57 @@ +/* + * Contains all configuration for membership + * @module config/membership + * + * @see module:services/invites/invites-middleware + */ + +'use strict'; + +const site = require('./site'); + +module.exports = { + invite: { + from: site.title + ' Robot <' + site.emails.robot + '>', + subject: 'You have been invited to ' + site.title + }, + + paths: { + register: '/admin/register', + login: '/admin/login', + forgot: '/admin/forgot', + reset: '/admin/reset' + }, + + remember: { + // if expires is defined, it will be used. otherwise maxage + expires: new Date('2038-01-19T03:14:07.000Z'), + //expires: Date.now() - 1, + maxAge: 30 * 24 * 60 * 60 * 1000, + }, + + messages: { + login: { + notLocal: 'Account requires external login.', + wrongPassword: 'Wrong password.', + noLocalUser: 'No user registered with that email.', + noExternalUser: 'The account is not connected to this website.', + externalLoginFailed: 'External login failed.', + unverified: 'This account has not been verified.', + banned: 'User is banned.', + blocked: 'User is blocked due to too many login attempts.' + }, + + register: { + notAuthorized: 'The email is not authorized to create an account.', + duplicateEmail: 'The email has already been registered.' + } + }, + + passport: { + local: { + usernameField: 'email' + }, + + scope: [ 'email' ], + } +}; diff --git a/server/config/mongo.js b/server/config/mongo.js new file mode 100644 index 0000000..e377528 --- /dev/null +++ b/server/config/mongo.js @@ -0,0 +1,14 @@ +'use strict'; + +const _ = require('lodash'); + +const defaults = { + uri: 'mongodb://millerkonsult-supreme:revisions-revisions-bloody-revisions@mongo.thecodebureau.com/millerkonsult' + //uri: 'mongodb://localhost/millerkonsult' +}; + +module.exports = _.merge(defaults, { + production: { + uri: 'mongodb://millerkonsult-supreme:revisions-revisions-bloody-revisions@localhost/millerkonsult' + } +}[ENV]); diff --git a/server/config/port.js b/server/config/port.js new file mode 100644 index 0000000..83f5640 --- /dev/null +++ b/server/config/port.js @@ -0,0 +1,10 @@ +'use strict'; + +const basePort = 3050; + +module.exports = { + development: basePort, + testing: basePort + 1, + staging: basePort + 2, + production: basePort + 3 +}[ENV]; diff --git a/server/config/session.js b/server/config/session.js new file mode 100644 index 0000000..0371fbd --- /dev/null +++ b/server/config/session.js @@ -0,0 +1,37 @@ +'use strict'; + +const session = require('express-session'); + +let redisStore; + +const config = { + secret: 'geogeogeogeopoopoopoopoopooooooopPOOOPPPPOOOOOOOOOOOOHNOPOOP;asjldfhaksdjfh', + resave: false, + saveUninitialized: true +}; + +const redisConfig = { + host: 'localhost', + port: 6379 +}; + +if (ENV === 'production') { + const RedisStore = require('connect-redis')(require('express-session')); + + redisStore = new RedisStore(redisConfig); + + redisStore.on('connect', function () { + console.info('Redis connected succcessfully'); + }); + + redisStore.on('disconnect', function () { + throw new Error('Unable to connect to redis. Has it been started?'); + }); + + config.store = redisStore; +} else { + config.store = new session.MemoryStore(); +} + +module.exports = config; + diff --git a/server/config/site.js b/server/config/site.js new file mode 100644 index 0000000..abe6c39 --- /dev/null +++ b/server/config/site.js @@ -0,0 +1,51 @@ +'use strict'; + +const _ = require('lodash'); + +const domain = 'geolets.com'; + +const defaults = { + domain: domain, + title: 'Geolets', + name: 'geolets', + protocol: 'http', + get host() { + return this.port ? this.hostname + ':' + this.port : this.hostname; + }, + get url() { + return this.protocol + '://' + this.host + '/'; + }, + emails: { + robot: 'no-reply@thecodebureau.com', + info: 'info@thecodebureau.com', + webmaster: 'webmaster@thecodebureau.com', + order: 'info@thecodebureau.com' + } +}; + +module.exports = _.merge(defaults, { + development: { + hostname: 'localhost', + port: process.env.EXTERNAL_PORT || process.env.PORT || require('./port') + }, + + testing: { + hostname: 'localhost', + port: process.env.PORT || require('./port') + }, + + staging: { + hostname: 'staging.' + domain + }, + + production: { + hostname: domain, + //protocol: 'https', + emails: { + robot: 'no-reply@' + domain, + info: 'info@' + domain, + webmaster: 'webmaster@' + domain, + order: 'order@' + domain + } + } +}[ENV]); diff --git a/server/config/smtp.js b/server/config/smtp.js new file mode 100644 index 0000000..ae4a283 --- /dev/null +++ b/server/config/smtp.js @@ -0,0 +1,15 @@ +'use strict'; + +const _ = require('lodash'); + +const defaults = { + auth: { + user: 'SMTP_Injection', + // dev key + pass: '2eec390c5b3f5d593c9f152179bf51e90b073784' + }, + host: 'smtp.sparkpostmail.com', + port: 587 +}; + +module.exports = _.merge(defaults, {}[ENV]); diff --git a/server/routes.js b/server/routes.js new file mode 100644 index 0000000..8ab98ef --- /dev/null +++ b/server/routes.js @@ -0,0 +1,11 @@ +'use strict'; + +const master = require('./templates/master.marko'); + +module.exports = [ + [ '/', 'get', function (req, res, next) { + res.template = master; + + next(); + } ] +]; diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..1d8d4d4 --- /dev/null +++ b/server/server.js @@ -0,0 +1,123 @@ +'use strict'; + +require('babel-register'); + +/* + * The main file that sets up the Express instance and node + * + * @module server/server + * @type {Express instance} + */ + +// set up some globals (these are also set in Epiphany if not already set) +global.ENV = process.env.NODE_ENV || 'development'; +global.PWD = process.env.PWD || process.cwd(); + +// set default compiler options in marko +// this needs to be set like this to ensure the compiler +// uses this setting when generating custom tags from marko.json files +//require('marko/compiler').defaultOptions.writeToDisk = false; + +// make node understand `*.marko` files +require('marko/node-require').install(); + +// modules > native +const p = require('path'); + +// modules > 3rd party +const requireDir = require('require-dir'); +const _ = require('lodash'); +const express = require('express'); +const mongoose = require('mongoose'); + +// modules > express middlewares +const bodyParser = require('body-parser'); +const session = require('express-session'); +const cookieParser = require('cookie-parser'); + +const appModulePath = require('app-module-path'); + +// needed when symlinking warepot/mongopot/hats +appModulePath.addPath(p.join(process.env.PWD, 'node_modules')); +appModulePath.addPath(p.join(process.env.PWD, 'modules')); + +const chalk = require('chalk'); +const colorizeStack = require('warepot/util/colorize-stack'); + +// make error output stack pretty +process.on('uncaughtException', function (err) { + console.error(chalk.red('UNCAUGHT EXCEPTION')); + console.error(err); + console.log(err.fileName); + console.log(err.message); + console.log(err.name); + console.log(colorizeStack(err.stack)); + //logError(err, null, { console: true, database: false }); + process.exit(1); +}); + +const initRoutes = require('warepot/util/init-routes'); + +const config = requireDir('./config'); + +const prewares = [ + express.static(config.dir.static, ENV === 'production' ? { maxAge: '1 year' } : null), + express.static(config.dir.uploads, ENV === 'production' ? { maxAge: '1 year' } : null), + bodyParser.json(), + bodyParser.urlencoded({ extended: true }), + cookieParser(config.session.secret), + session(config.session), +]; + +if (ENV === 'development') { + // only log requests to console in development mode + prewares.unshift(require('morgan')('dev')); +} + +// set up default postwares +const postwares = [ + require('warepot/ensure-found'), + // transform and log error + require('warepot/error-handler'), + // respond + require('warepot/responder'), + // handle error rendering error + require('warepot/responder-error'), +]; + +const routes = [].concat( + prewares, + require('./routes'), + postwares +); + +const server = express(); + +// see http://expressjs.com/en/4x/api.html#app.settings.table +server.set('trust proxy', true); + +_.extend(server.locals, { + site: require('./config/site'), + lang: process.env.NODE_LANG || 'en', + js: require(p.join(PWD, 'public/js.json')), + css: require(p.join(PWD, 'public/css.json')) +}); + +initRoutes(server, routes); + +// override default res.render so we can use loaded marko templates, ie `res.render(markoTemplate)` +express.response.render = function (template) { + const locals = _.extend({}, server.locals, this.locals); + + template.render(locals, this); +}; + +// connect to mongodb +mongoose.connect(config.mongo.uri, _.omit(config.mongo, 'uri')); + +// start server and let them know it +server.listen(config.port, () => { + console.info('Express server started on port %s (%s)', config.port, ENV); +}); + +module.exports = server; diff --git a/server/templates/error.marko b/server/templates/error.marko new file mode 100644 index 0000000..e183c9c --- /dev/null +++ b/server/templates/error.marko @@ -0,0 +1,3 @@ +
+

Error!

+
diff --git a/server/templates/marko.json b/server/templates/marko.json new file mode 100644 index 0000000..80202b2 --- /dev/null +++ b/server/templates/marko.json @@ -0,0 +1,3 @@ +{ + "tags-dir": [] +} diff --git a/server/templates/master.marko b/server/templates/master.marko new file mode 100644 index 0000000..bb60709 --- /dev/null +++ b/server/templates/master.marko @@ -0,0 +1,23 @@ + + + + + Geolets + + + + + + + + + + +
+

Hello

+
+ + + diff --git a/src/.eslintrc b/src/.eslintrc new file mode 100644 index 0000000..a28ae37 --- /dev/null +++ b/src/.eslintrc @@ -0,0 +1,42 @@ +{ + "extends": "airbnb", + + "rules": { + "react/prop-types": 0, + "guard-for-in": 0, + "global-require": 0, + "no-underscore-dangle": 0, + "object-shorthand": 0, + "default-case": 0, + "one-var": 0, + "prefer-rest-params": 0, + "no-unused-vars": [ 2, { "args": "none" } ], + "no-alert": 0, + "quote-props": 0, + "no-nested-ternary": 0, + "no-use-before-define": [2, { "functions": false, "classes": true }], + "consistent-return": 0, + "no-eval": 0, + "prefer-arrow-callback": 0, + "array-bracket-spacing": 0, + "no-console": 0, + "indent": [ 2, 2, { "SwitchCase": 1 }], + "max-len": 0, + "comma-dangle": 0, + "no-param-reassign": 0, + "prefer-template": 0, + "curly": 0, + "func-names": 0, + "no-shadow": 0, + "spaced-comment": 0, + "strict": [ 2, "global" ] + }, + + "globals": { + "ENV": true, + "google": true, + "INITIAL_STATE": true, + "INITIAL_CONTEXT": true, + "PWD": true + } +} diff --git a/static/robots-production.txt b/static/robots-production.txt new file mode 100644 index 0000000..ea7d69c --- /dev/null +++ b/static/robots-production.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: /admin +Disallow: /api diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /