Initial commit.

This commit is contained in:
Linus Miller 2016-07-14 17:15:55 +02:00
commit 2abfa90765
43 changed files with 1597 additions and 0 deletions

16
.babelrc Normal file
View File

@ -0,0 +1,16 @@
{
"env": {
"server": {
"presets": [ "es2015-node6" ],
"plugins": [
"add-module-exports"
]
},
"client": {
"presets": [ "es2015", "react" ],
"plugins": [
"add-module-exports"
]
}
}
}

43
.eslintrc Normal file
View File

@ -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
}
}

40
.gitignore vendored Normal file
View File

@ -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

63
README.md Normal file
View File

@ -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).

44
bin/create-organization.js Executable file
View File

@ -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();

47
bin/create-roles.js Executable file
View File

@ -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();

98
bin/create-user.js Executable file
View File

@ -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));

22
gulp/LICENSE Normal file
View File

@ -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.

23
gulp/README.md Normal file
View File

@ -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

156
gulp/config.js Normal file
View File

@ -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') ]
},
};

30
gulp/index.js Normal file
View File

@ -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));

View File

@ -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));

113
gulp/tasks/browserify.js Normal file
View File

@ -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);
});

47
gulp/tasks/less.js Normal file
View File

@ -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));
});

33
gulp/tasks/nodemon.js Normal file
View File

@ -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();
}
});
});
});

27
gulp/tasks/raster.js Normal file
View File

@ -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');
});
});

39
gulp/tasks/static.js Normal file
View File

@ -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');
});
});

27
gulp/tasks/svg.js Normal file
View File

@ -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');
});
});

16
gulp/tasks/watch.js Normal file
View File

@ -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));
});
});

44
gulp/tasks/wipe.js Normal file
View File

@ -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();
}
}
});
});
});

View File

@ -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');
}
};

7
gulpfile.js Normal file
View File

@ -0,0 +1,7 @@
'use strict';
require('./gulp');
process.on('SIGINT', () => {
process.exit(0);
});

51
img/svg/form-invalid.svg Executable file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="20"
height="20"
viewBox="0 0 20 20"
enable-background="new 0 0 512 512"
xml:space="preserve"
id="svg3651"
inkscape:version="0.91 r13725"
sodipodi:docname="form-invalid.svg"><metadata
id="metadata3658"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs3656" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3654"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="5.2149125"
inkscape:cx="-70.540527"
inkscape:cy="6.8496999"
inkscape:current-layer="svg3651" /><path
id="warning-4-icon"
d="m 8.667476,4.11709 2.665048,0 0,8.08252 -2.665048,0 0,-8.08252 z M 10,16.27126 c -0.794272,0 -1.438155,-0.64388 -1.438155,-1.4381 0,-0.79428 0.643883,-1.43821 1.438155,-1.43821 0.794223,0 1.438107,0.64393 1.438107,1.43821 0,0.79422 -0.643884,1.4381 -1.438107,1.4381 z M 10,1.94175 c 4.453495,0 8.058252,3.60412 8.058252,8.05825 0,4.45345 -3.604126,8.05825 -8.058252,8.05825 C 5.546505,18.05825 1.941748,14.45413 1.941748,10 1.941748,5.54655 5.545874,1.94175 10,1.94175 Z M 10,0 C 4.477136,0 0,4.47714 0,10 0,15.52286 4.477136,20 10,20 15.522864,20 20,15.52286 20,10 20,4.47714 15.522864,0 10,0 Z"
style="fill:#ffae00;fill-opacity:1"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

51
img/svg/form-valid.svg Executable file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="20"
height="20"
viewBox="0 0 20 20"
enable-background="new 0 0 512 512"
xml:space="preserve"
id="svg3383"
inkscape:version="0.91 r13725"
sodipodi:docname="form-valid.svg"><metadata
id="metadata3390"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs3388" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3386"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.1230469"
inkscape:cx="-261.47826"
inkscape:cy="206"
inkscape:current-layer="svg3383" /><path
id="check-mark-4-icon"
d="M 20,10 C 20,15.52287 15.522864,20 10,20 4.477136,20 0,15.52287 0,10 0,4.47713 4.477136,0 10,0 15.522864,0 20,4.47713 20,10 Z m -1.941747,0 C 18.058253,5.54587 14.453495,1.94175 10,1.94175 5.545874,1.94175 1.941747,5.54655 1.941747,10 c 0,4.45413 3.604758,8.05825 8.058253,8.05825 4.454126,0 8.058253,-3.6048 8.058253,-8.05825 z M 13.991456,5.71053 8.354757,11.34723 5.913204,8.905 4.102719,10.71583 8.354806,14.96908 15.802767,7.52063 13.991456,5.71053 Z"
style="fill:#0066ff;fill-opacity:1"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

0
less/main.less Normal file
View File

74
package.json Normal file
View File

@ -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"
}
}

10
server/config/dir.js Normal file
View File

@ -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')
};

View File

@ -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]);

7
server/config/globals.js Normal file
View File

@ -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';

View File

@ -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' ],
}
};

14
server/config/mongo.js Normal file
View File

@ -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]);

10
server/config/port.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
const basePort = 3050;
module.exports = {
development: basePort,
testing: basePort + 1,
staging: basePort + 2,
production: basePort + 3
}[ENV];

37
server/config/session.js Normal file
View File

@ -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;

51
server/config/site.js Normal file
View File

@ -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]);

15
server/config/smtp.js Normal file
View File

@ -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]);

11
server/routes.js Normal file
View File

@ -0,0 +1,11 @@
'use strict';
const master = require('./templates/master.marko');
module.exports = [
[ '/', 'get', function (req, res, next) {
res.template = master;
next();
} ]
];

123
server/server.js Normal file
View File

@ -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;

View File

@ -0,0 +1,3 @@
<section>
<h1>Error!</h1>
</section>

View File

@ -0,0 +1,3 @@
{
"tags-dir": []
}

View File

@ -0,0 +1,23 @@
<script marko-init>
var error = require('./error.marko');
</script>
<!doctype html>
<html xml:lang="sv" lang="sv">
<head>
<title>Geolets</title>
<!-- Meta Tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- CSS -->
<link rel="stylesheet" href="/css/main${data.css.suffix}.css">
</head>
<body>
<main>
<h1>Hello</h1>
</main>
</body>
</html>

42
src/.eslintrc Normal file
View File

@ -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
}
}

View File

@ -0,0 +1,3 @@
User-agent: *
Disallow: /admin
Disallow: /api

2
static/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /