initial check-in with working wdiff server-side api
This commit is contained in:
commit
e84561898f
1
.buildignore
Normal file
1
.buildignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.coffee
|
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
# Change these settings to your own preference
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# We recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
public
|
||||||
|
.tmp
|
||||||
|
.sass-cache
|
||||||
|
.idea
|
||||||
|
client/bower_components
|
||||||
|
dist
|
||||||
|
/server/config/local.env.js
|
||||||
|
wdiff-1.2.2
|
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- '0.10'
|
||||||
|
- '0.11'
|
||||||
|
before_script:
|
||||||
|
- npm install -g bower grunt-cli
|
||||||
|
- gem install sass
|
||||||
|
- bower install
|
||||||
|
services: mongodb
|
51
.yo-rc.json
Normal file
51
.yo-rc.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"generator-angular-fullstack": {
|
||||||
|
"insertRoutes": true,
|
||||||
|
"registerRoutesFile": "server/routes.js",
|
||||||
|
"routesNeedle": "// Insert routes below",
|
||||||
|
"routesBase": "/api/",
|
||||||
|
"pluralizeRoutes": true,
|
||||||
|
"insertSockets": true,
|
||||||
|
"registerSocketsFile": "server/config/socketio.js",
|
||||||
|
"socketsNeedle": "// Insert sockets below",
|
||||||
|
"filters": {
|
||||||
|
"js": true,
|
||||||
|
"jade": true,
|
||||||
|
"sass": true,
|
||||||
|
"ngroute": true,
|
||||||
|
"bootstrap": true,
|
||||||
|
"uibootstrap": true,
|
||||||
|
"mongoose": true,
|
||||||
|
"auth": true,
|
||||||
|
"oauth": true,
|
||||||
|
"googleAuth": true,
|
||||||
|
"facebookAuth": true,
|
||||||
|
"twitterAuth": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generator-ng-component": {
|
||||||
|
"routeDirectory": "client/app/",
|
||||||
|
"directiveDirectory": "client/app/",
|
||||||
|
"filterDirectory": "client/app/",
|
||||||
|
"serviceDirectory": "client/app/",
|
||||||
|
"basePath": "client",
|
||||||
|
"moduleName": "",
|
||||||
|
"filters": [
|
||||||
|
"ngroute"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
"js",
|
||||||
|
"jade",
|
||||||
|
"scss"
|
||||||
|
],
|
||||||
|
"directiveSimpleTemplates": "",
|
||||||
|
"directiveComplexTemplates": "",
|
||||||
|
"filterTemplates": "",
|
||||||
|
"serviceTemplates": "",
|
||||||
|
"factoryTemplates": "",
|
||||||
|
"controllerTemplates": "",
|
||||||
|
"decoratorTemplates": "",
|
||||||
|
"providerTemplates": "",
|
||||||
|
"routeTemplates": ""
|
||||||
|
}
|
||||||
|
}
|
696
Gruntfile.js
Normal file
696
Gruntfile.js
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
// Generated on 2015-02-05 using generator-angular-fullstack 2.0.13
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (grunt) {
|
||||||
|
var localConfig;
|
||||||
|
try {
|
||||||
|
localConfig = require('./server/config/local.env');
|
||||||
|
} catch(e) {
|
||||||
|
localConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load grunt tasks automatically, when needed
|
||||||
|
require('jit-grunt')(grunt, {
|
||||||
|
express: 'grunt-express-server',
|
||||||
|
useminPrepare: 'grunt-usemin',
|
||||||
|
ngtemplates: 'grunt-angular-templates',
|
||||||
|
cdnify: 'grunt-google-cdn',
|
||||||
|
protractor: 'grunt-protractor-runner',
|
||||||
|
injector: 'grunt-asset-injector',
|
||||||
|
buildcontrol: 'grunt-build-control'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Time how long tasks take. Can help when optimizing build times
|
||||||
|
require('time-grunt')(grunt);
|
||||||
|
|
||||||
|
// Define the configuration for all the tasks
|
||||||
|
grunt.initConfig({
|
||||||
|
|
||||||
|
// Project settings
|
||||||
|
pkg: grunt.file.readJSON('package.json'),
|
||||||
|
yeoman: {
|
||||||
|
// configurable paths
|
||||||
|
client: require('./bower.json').appPath || 'client',
|
||||||
|
dist: 'dist'
|
||||||
|
},
|
||||||
|
express: {
|
||||||
|
options: {
|
||||||
|
port: process.env.PORT || 9000
|
||||||
|
},
|
||||||
|
dev: {
|
||||||
|
options: {
|
||||||
|
script: 'server/app.js',
|
||||||
|
debug: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prod: {
|
||||||
|
options: {
|
||||||
|
script: 'dist/server/app.js'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
server: {
|
||||||
|
url: 'http://localhost:<%= express.options.port %>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
injectJS: {
|
||||||
|
files: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.js',
|
||||||
|
'!<%= yeoman.client %>/{app,components}/**/*.spec.js',
|
||||||
|
'!<%= yeoman.client %>/{app,components}/**/*.mock.js',
|
||||||
|
'!<%= yeoman.client %>/app/app.js'],
|
||||||
|
tasks: ['injector:scripts']
|
||||||
|
},
|
||||||
|
injectCss: {
|
||||||
|
files: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.css'
|
||||||
|
],
|
||||||
|
tasks: ['injector:css']
|
||||||
|
},
|
||||||
|
mochaTest: {
|
||||||
|
files: ['server/**/*.spec.js'],
|
||||||
|
tasks: ['env:test', 'mochaTest']
|
||||||
|
},
|
||||||
|
jsTest: {
|
||||||
|
files: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.spec.js',
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.mock.js'
|
||||||
|
],
|
||||||
|
tasks: ['newer:jshint:all', 'karma']
|
||||||
|
},
|
||||||
|
injectSass: {
|
||||||
|
files: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'],
|
||||||
|
tasks: ['injector:sass']
|
||||||
|
},
|
||||||
|
sass: {
|
||||||
|
files: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'],
|
||||||
|
tasks: ['sass', 'autoprefixer']
|
||||||
|
},
|
||||||
|
jade: {
|
||||||
|
files: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/*',
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.jade'],
|
||||||
|
tasks: ['jade']
|
||||||
|
},
|
||||||
|
gruntfile: {
|
||||||
|
files: ['Gruntfile.js']
|
||||||
|
},
|
||||||
|
livereload: {
|
||||||
|
files: [
|
||||||
|
'{.tmp,<%= yeoman.client %>}/{app,components}/**/*.css',
|
||||||
|
'{.tmp,<%= yeoman.client %>}/{app,components}/**/*.html',
|
||||||
|
'{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',
|
||||||
|
'!{.tmp,<%= yeoman.client %>}{app,components}/**/*.spec.js',
|
||||||
|
'!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js',
|
||||||
|
'<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}'
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
livereload: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
express: {
|
||||||
|
files: [
|
||||||
|
'server/**/*.{js,json}'
|
||||||
|
],
|
||||||
|
tasks: ['express:dev', 'wait'],
|
||||||
|
options: {
|
||||||
|
livereload: true,
|
||||||
|
nospawn: true //Without this option specified express won't be reloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Make sure code styles are up to par and there are no obvious mistakes
|
||||||
|
jshint: {
|
||||||
|
options: {
|
||||||
|
jshintrc: '<%= yeoman.client %>/.jshintrc',
|
||||||
|
reporter: require('jshint-stylish')
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
options: {
|
||||||
|
jshintrc: 'server/.jshintrc'
|
||||||
|
},
|
||||||
|
src: [
|
||||||
|
'server/**/*.js',
|
||||||
|
'!server/**/*.spec.js'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
serverTest: {
|
||||||
|
options: {
|
||||||
|
jshintrc: 'server/.jshintrc-spec'
|
||||||
|
},
|
||||||
|
src: ['server/**/*.spec.js']
|
||||||
|
},
|
||||||
|
all: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.js',
|
||||||
|
'!<%= yeoman.client %>/{app,components}/**/*.spec.js',
|
||||||
|
'!<%= yeoman.client %>/{app,components}/**/*.mock.js'
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
src: [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.spec.js',
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.mock.js'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Empties folders to start fresh
|
||||||
|
clean: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
dot: true,
|
||||||
|
src: [
|
||||||
|
'.tmp',
|
||||||
|
'<%= yeoman.dist %>/*',
|
||||||
|
'!<%= yeoman.dist %>/.git*',
|
||||||
|
'!<%= yeoman.dist %>/.openshift',
|
||||||
|
'!<%= yeoman.dist %>/Procfile'
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
server: '.tmp'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add vendor prefixed styles
|
||||||
|
autoprefixer: {
|
||||||
|
options: {
|
||||||
|
browsers: ['last 1 version']
|
||||||
|
},
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
cwd: '.tmp/',
|
||||||
|
src: '{,*/}*.css',
|
||||||
|
dest: '.tmp/'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Debugging with node inspector
|
||||||
|
'node-inspector': {
|
||||||
|
custom: {
|
||||||
|
options: {
|
||||||
|
'web-host': 'localhost'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use nodemon to run server in debug mode with an initial breakpoint
|
||||||
|
nodemon: {
|
||||||
|
debug: {
|
||||||
|
script: 'server/app.js',
|
||||||
|
options: {
|
||||||
|
nodeArgs: ['--debug-brk'],
|
||||||
|
env: {
|
||||||
|
PORT: process.env.PORT || 9000
|
||||||
|
},
|
||||||
|
callback: function (nodemon) {
|
||||||
|
nodemon.on('log', function (event) {
|
||||||
|
console.log(event.colour);
|
||||||
|
});
|
||||||
|
|
||||||
|
// opens browser on initial server start
|
||||||
|
nodemon.on('config:update', function () {
|
||||||
|
setTimeout(function () {
|
||||||
|
require('open')('http://localhost:8080/debug?port=5858');
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Automatically inject Bower components into the app
|
||||||
|
wiredep: {
|
||||||
|
target: {
|
||||||
|
src: '<%= yeoman.client %>/index.html',
|
||||||
|
ignorePath: '<%= yeoman.client %>/',
|
||||||
|
exclude: [/bootstrap-sass-official/, /bootstrap.js/, '/json3/', '/es5-shim/', /bootstrap.css/, /font-awesome.css/ ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Renames files for browser caching purposes
|
||||||
|
rev: {
|
||||||
|
dist: {
|
||||||
|
files: {
|
||||||
|
src: [
|
||||||
|
'<%= yeoman.dist %>/public/{,*/}*.js',
|
||||||
|
'<%= yeoman.dist %>/public/{,*/}*.css',
|
||||||
|
'<%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
|
||||||
|
'<%= yeoman.dist %>/public/assets/fonts/*'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reads HTML for usemin blocks to enable smart builds that automatically
|
||||||
|
// concat, minify and revision files. Creates configurations in memory so
|
||||||
|
// additional tasks can operate on them
|
||||||
|
useminPrepare: {
|
||||||
|
html: ['<%= yeoman.client %>/index.html'],
|
||||||
|
options: {
|
||||||
|
dest: '<%= yeoman.dist %>/public'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Performs rewrites based on rev and the useminPrepare configuration
|
||||||
|
usemin: {
|
||||||
|
html: ['<%= yeoman.dist %>/public/{,*/}*.html'],
|
||||||
|
css: ['<%= yeoman.dist %>/public/{,*/}*.css'],
|
||||||
|
js: ['<%= yeoman.dist %>/public/{,*/}*.js'],
|
||||||
|
options: {
|
||||||
|
assetsDirs: [
|
||||||
|
'<%= yeoman.dist %>/public',
|
||||||
|
'<%= yeoman.dist %>/public/assets/images'
|
||||||
|
],
|
||||||
|
// This is so we update image references in our ng-templates
|
||||||
|
patterns: {
|
||||||
|
js: [
|
||||||
|
[/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// The following *-min tasks produce minified files in the dist folder
|
||||||
|
imagemin: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
cwd: '<%= yeoman.client %>/assets/images',
|
||||||
|
src: '{,*/}*.{png,jpg,jpeg,gif}',
|
||||||
|
dest: '<%= yeoman.dist %>/public/assets/images'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
svgmin: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
cwd: '<%= yeoman.client %>/assets/images',
|
||||||
|
src: '{,*/}*.svg',
|
||||||
|
dest: '<%= yeoman.dist %>/public/assets/images'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Allow the use of non-minsafe AngularJS files. Automatically makes it
|
||||||
|
// minsafe compatible so Uglify does not destroy the ng references
|
||||||
|
ngAnnotate: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
cwd: '.tmp/concat',
|
||||||
|
src: '*/**.js',
|
||||||
|
dest: '.tmp/concat'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Package all the html partials into a single javascript payload
|
||||||
|
ngtemplates: {
|
||||||
|
options: {
|
||||||
|
// This should be the name of your apps angular module
|
||||||
|
module: 'markdownFormatWdiffApp',
|
||||||
|
htmlmin: {
|
||||||
|
collapseBooleanAttributes: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true
|
||||||
|
},
|
||||||
|
usemin: 'app/app.js'
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
cwd: '<%= yeoman.client %>',
|
||||||
|
src: ['{app,components}/**/*.html'],
|
||||||
|
dest: '.tmp/templates.js'
|
||||||
|
},
|
||||||
|
tmp: {
|
||||||
|
cwd: '.tmp',
|
||||||
|
src: ['{app,components}/**/*.html'],
|
||||||
|
dest: '.tmp/tmp-templates.js'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Replace Google CDN references
|
||||||
|
cdnify: {
|
||||||
|
dist: {
|
||||||
|
html: ['<%= yeoman.dist %>/public/*.html']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Copies remaining files to places other tasks can use
|
||||||
|
copy: {
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
dot: true,
|
||||||
|
cwd: '<%= yeoman.client %>',
|
||||||
|
dest: '<%= yeoman.dist %>/public',
|
||||||
|
src: [
|
||||||
|
'*.{ico,png,txt}',
|
||||||
|
'.htaccess',
|
||||||
|
'bower_components/**/*',
|
||||||
|
'assets/images/{,*/}*.{webp}',
|
||||||
|
'assets/fonts/**/*',
|
||||||
|
'index.html'
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
expand: true,
|
||||||
|
cwd: '.tmp/images',
|
||||||
|
dest: '<%= yeoman.dist %>/public/assets/images',
|
||||||
|
src: ['generated/*']
|
||||||
|
}, {
|
||||||
|
expand: true,
|
||||||
|
dest: '<%= yeoman.dist %>',
|
||||||
|
src: [
|
||||||
|
'package.json',
|
||||||
|
'server/**/*'
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
expand: true,
|
||||||
|
cwd: '<%= yeoman.client %>',
|
||||||
|
dest: '.tmp/',
|
||||||
|
src: ['{app,components}/**/*.css']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildcontrol: {
|
||||||
|
options: {
|
||||||
|
dir: 'dist',
|
||||||
|
commit: true,
|
||||||
|
push: true,
|
||||||
|
connectCommits: false,
|
||||||
|
message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
|
||||||
|
},
|
||||||
|
heroku: {
|
||||||
|
options: {
|
||||||
|
remote: 'heroku',
|
||||||
|
branch: 'master'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openshift: {
|
||||||
|
options: {
|
||||||
|
remote: 'openshift',
|
||||||
|
branch: 'master'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Run some tasks in parallel to speed up the build process
|
||||||
|
concurrent: {
|
||||||
|
server: [
|
||||||
|
'jade',
|
||||||
|
'sass',
|
||||||
|
],
|
||||||
|
test: [
|
||||||
|
'jade',
|
||||||
|
'sass',
|
||||||
|
],
|
||||||
|
debug: {
|
||||||
|
tasks: [
|
||||||
|
'nodemon',
|
||||||
|
'node-inspector'
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
logConcurrentOutput: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dist: [
|
||||||
|
'jade',
|
||||||
|
'sass',
|
||||||
|
'imagemin',
|
||||||
|
'svgmin'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test settings
|
||||||
|
karma: {
|
||||||
|
unit: {
|
||||||
|
configFile: 'karma.conf.js',
|
||||||
|
singleRun: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mochaTest: {
|
||||||
|
options: {
|
||||||
|
reporter: 'spec'
|
||||||
|
},
|
||||||
|
src: ['server/**/*.spec.js']
|
||||||
|
},
|
||||||
|
|
||||||
|
protractor: {
|
||||||
|
options: {
|
||||||
|
configFile: 'protractor.conf.js'
|
||||||
|
},
|
||||||
|
chrome: {
|
||||||
|
options: {
|
||||||
|
args: {
|
||||||
|
browser: 'chrome'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
NODE_ENV: 'test'
|
||||||
|
},
|
||||||
|
prod: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
},
|
||||||
|
all: localConfig
|
||||||
|
},
|
||||||
|
|
||||||
|
// Compiles Jade to html
|
||||||
|
jade: {
|
||||||
|
compile: {
|
||||||
|
options: {
|
||||||
|
data: {
|
||||||
|
debug: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
cwd: '<%= yeoman.client %>',
|
||||||
|
src: [
|
||||||
|
'{app,components}/**/*.jade'
|
||||||
|
],
|
||||||
|
dest: '.tmp',
|
||||||
|
ext: '.html'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Compiles Sass to CSS
|
||||||
|
sass: {
|
||||||
|
server: {
|
||||||
|
options: {
|
||||||
|
loadPath: [
|
||||||
|
'<%= yeoman.client %>/bower_components',
|
||||||
|
'<%= yeoman.client %>/app',
|
||||||
|
'<%= yeoman.client %>/components'
|
||||||
|
],
|
||||||
|
compass: false
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
'.tmp/app/app.css' : '<%= yeoman.client %>/app/app.scss'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
injector: {
|
||||||
|
options: {
|
||||||
|
|
||||||
|
},
|
||||||
|
// Inject application script files into index.html (doesn't include bower)
|
||||||
|
scripts: {
|
||||||
|
options: {
|
||||||
|
transform: function(filePath) {
|
||||||
|
filePath = filePath.replace('/client/', '');
|
||||||
|
filePath = filePath.replace('/.tmp/', '');
|
||||||
|
return '<script src="' + filePath + '"></script>';
|
||||||
|
},
|
||||||
|
starttag: '<!-- injector:js -->',
|
||||||
|
endtag: '<!-- endinjector -->'
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
'<%= yeoman.client %>/index.html': [
|
||||||
|
['{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',
|
||||||
|
'!{.tmp,<%= yeoman.client %>}/app/app.js',
|
||||||
|
'!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js',
|
||||||
|
'!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inject component scss into app.scss
|
||||||
|
sass: {
|
||||||
|
options: {
|
||||||
|
transform: function(filePath) {
|
||||||
|
filePath = filePath.replace('/client/app/', '');
|
||||||
|
filePath = filePath.replace('/client/components/', '');
|
||||||
|
return '@import \'' + filePath + '\';';
|
||||||
|
},
|
||||||
|
starttag: '// injector',
|
||||||
|
endtag: '// endinjector'
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
'<%= yeoman.client %>/app/app.scss': [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.{scss,sass}',
|
||||||
|
'!<%= yeoman.client %>/app/app.{scss,sass}'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inject component css into index.html
|
||||||
|
css: {
|
||||||
|
options: {
|
||||||
|
transform: function(filePath) {
|
||||||
|
filePath = filePath.replace('/client/', '');
|
||||||
|
filePath = filePath.replace('/.tmp/', '');
|
||||||
|
return '<link rel="stylesheet" href="' + filePath + '">';
|
||||||
|
},
|
||||||
|
starttag: '<!-- injector:css -->',
|
||||||
|
endtag: '<!-- endinjector -->'
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
'<%= yeoman.client %>/index.html': [
|
||||||
|
'<%= yeoman.client %>/{app,components}/**/*.css'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Used for delaying livereload until after server has restarted
|
||||||
|
grunt.registerTask('wait', function () {
|
||||||
|
grunt.log.ok('Waiting for server reload...');
|
||||||
|
|
||||||
|
var done = this.async();
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
grunt.log.writeln('Done waiting!');
|
||||||
|
done();
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('express-keepalive', 'Keep grunt running', function() {
|
||||||
|
this.async();
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('serve', function (target) {
|
||||||
|
if (target === 'dist') {
|
||||||
|
return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'open', 'express-keepalive']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === 'debug') {
|
||||||
|
return grunt.task.run([
|
||||||
|
'clean:server',
|
||||||
|
'env:all',
|
||||||
|
'injector:sass',
|
||||||
|
'concurrent:server',
|
||||||
|
'injector',
|
||||||
|
'wiredep',
|
||||||
|
'autoprefixer',
|
||||||
|
'concurrent:debug'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
grunt.task.run([
|
||||||
|
'clean:server',
|
||||||
|
'env:all',
|
||||||
|
'injector:sass',
|
||||||
|
'concurrent:server',
|
||||||
|
'injector',
|
||||||
|
'wiredep',
|
||||||
|
'autoprefixer',
|
||||||
|
'express:dev',
|
||||||
|
'wait',
|
||||||
|
'open',
|
||||||
|
'watch'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('server', function () {
|
||||||
|
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
|
||||||
|
grunt.task.run(['serve']);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('test', function(target) {
|
||||||
|
if (target === 'server') {
|
||||||
|
return grunt.task.run([
|
||||||
|
'env:all',
|
||||||
|
'env:test',
|
||||||
|
'mochaTest'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (target === 'client') {
|
||||||
|
return grunt.task.run([
|
||||||
|
'clean:server',
|
||||||
|
'env:all',
|
||||||
|
'injector:sass',
|
||||||
|
'concurrent:test',
|
||||||
|
'injector',
|
||||||
|
'autoprefixer',
|
||||||
|
'karma'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (target === 'e2e') {
|
||||||
|
return grunt.task.run([
|
||||||
|
'clean:server',
|
||||||
|
'env:all',
|
||||||
|
'env:test',
|
||||||
|
'injector:sass',
|
||||||
|
'concurrent:test',
|
||||||
|
'injector',
|
||||||
|
'wiredep',
|
||||||
|
'autoprefixer',
|
||||||
|
'express:dev',
|
||||||
|
'protractor'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
else grunt.task.run([
|
||||||
|
'test:server',
|
||||||
|
'test:client'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('build', [
|
||||||
|
'clean:dist',
|
||||||
|
'injector:sass',
|
||||||
|
'concurrent:dist',
|
||||||
|
'injector',
|
||||||
|
'wiredep',
|
||||||
|
'useminPrepare',
|
||||||
|
'autoprefixer',
|
||||||
|
'ngtemplates',
|
||||||
|
'concat',
|
||||||
|
'ngAnnotate',
|
||||||
|
'copy:dist',
|
||||||
|
'cdnify',
|
||||||
|
'cssmin',
|
||||||
|
'uglify',
|
||||||
|
'rev',
|
||||||
|
'usemin'
|
||||||
|
]);
|
||||||
|
|
||||||
|
grunt.registerTask('default', [
|
||||||
|
'newer:jshint',
|
||||||
|
'test',
|
||||||
|
'build'
|
||||||
|
]);
|
||||||
|
};
|
25
bin/markdown-format-wdiff
Normal file
25
bin/markdown-format-wdiff
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Markdown-wdiff -- format diff of Markdown files with decoration
|
||||||
|
# Usage:
|
||||||
|
# wdiff old.md new.md | markdown-format-wdiff
|
||||||
|
# git diff --word-diff origin/master -- README.md docs/tutorial/README.md | markdown-format-wdiff
|
||||||
|
#
|
||||||
|
# Author: Jaeho Shin <netj@cs.stanford.edu>
|
||||||
|
# Created: 2013-11-18
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# word diff the given unified diff as input, and format it by hunks
|
||||||
|
sed '
|
||||||
|
# format ins/del of words
|
||||||
|
s|\[-|<del class="del">|g; s|-]|</del>|g
|
||||||
|
s|{+|<ins class="ins">|g; s|+}|</ins>|g
|
||||||
|
'
|
||||||
|
|
||||||
|
# attach a small stylesheet
|
||||||
|
#echo '
|
||||||
|
#<style>
|
||||||
|
# .del,.ins{ display: inline-block; margin-left: 0.5ex; }
|
||||||
|
# .del { background-color: #fcc; }
|
||||||
|
# .ins{ background-color: #cfc; }
|
||||||
|
#</style>
|
||||||
|
#'
|
108
bin/markdown-git-changes
Normal file
108
bin/markdown-git-changes
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Summarize commit history and diff of Markdown files in the current Git repo
|
||||||
|
# Usage:
|
||||||
|
# markdown-git-changes [COMMIT [MARKDOWN_FILE]...]
|
||||||
|
#
|
||||||
|
# You can specify COMMIT as `--' to automatically see changes of given files
|
||||||
|
# against the upstream branch, HEAD^, or HEAD.
|
||||||
|
#
|
||||||
|
# Author: Jaeho Shin <netj@cs.stanford.edu>
|
||||||
|
# Created: 2013-11-18
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
Here=$(dirname "$0")
|
||||||
|
PATH="$Here:$PATH" # add this directory to PATH for markdown-format-wdiff
|
||||||
|
|
||||||
|
# TODO passthru any diff options
|
||||||
|
|
||||||
|
# how to find markdown files in the current git repo
|
||||||
|
files=()
|
||||||
|
find_markdown_files() {
|
||||||
|
local IFS=$'\n'
|
||||||
|
set -- $(git ls-files | grep '\.\(md\|mkd\|markdn\|markdown\)$')
|
||||||
|
files=("$@")
|
||||||
|
}
|
||||||
|
|
||||||
|
# diff against given commit, or upstream branch, or HEAD^ if no local changes, or HEAD
|
||||||
|
since=--; [ $# -eq 0 ] || { since=$1; shift; }
|
||||||
|
if [ x"$since" = x"--" ]; then
|
||||||
|
find_markdown_files
|
||||||
|
since=`
|
||||||
|
git rev-parse --abbrev-ref HEAD@{upstream} 2>/dev/null ||
|
||||||
|
case $(git status --porcelain --untracked-files=no "${files[@]}" | wc -l) in
|
||||||
|
0) echo 'HEAD^' ;;
|
||||||
|
*) echo 'HEAD'
|
||||||
|
esac
|
||||||
|
`
|
||||||
|
fi
|
||||||
|
# unless list of files are given, find Markdown documents in the Git repo
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
files=("$@")
|
||||||
|
elif [ ${#files[@]} -eq 0 ]; then
|
||||||
|
find_markdown_files
|
||||||
|
fi
|
||||||
|
|
||||||
|
# make sure ${files[@]} isn't empty
|
||||||
|
[ ${#files[@]} -gt 0 ] || files+=(.)
|
||||||
|
|
||||||
|
|
||||||
|
# summarize commit history
|
||||||
|
{
|
||||||
|
echo "\$ git log --oneline "$since"..HEAD $*"
|
||||||
|
git log --oneline "$since"..HEAD "${files[@]}"
|
||||||
|
} |
|
||||||
|
sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
# and embed the overall word diff
|
||||||
|
git diff ${GIT_DIFF_OPTS:-} --word-diff --patch-with-stat \
|
||||||
|
--minimal --patience \
|
||||||
|
"$since" -- "${files[@]}" |
|
||||||
|
sed '
|
||||||
|
# format prologue
|
||||||
|
1,/^diff /{
|
||||||
|
/^diff/! s/^/ /
|
||||||
|
}
|
||||||
|
|
||||||
|
# format file headers
|
||||||
|
/^diff /,/^+++ /{
|
||||||
|
/^diff /{
|
||||||
|
s|^diff .* \([^/]/\)\(.*\)|<div class="file-start"><code>\2</code></div>|
|
||||||
|
a\
|
||||||
|
\
|
||||||
|
|
||||||
|
}
|
||||||
|
/^<div class="file-start">/! s/^/ /
|
||||||
|
}
|
||||||
|
|
||||||
|
# format hunks
|
||||||
|
/^@@ -.* +.* @@/{
|
||||||
|
s| @@.*| @@|
|
||||||
|
s|^|<div class="hunk-start"><code>|
|
||||||
|
s|$|</code></div>|
|
||||||
|
}
|
||||||
|
' |
|
||||||
|
markdown-format-wdiff
|
||||||
|
echo '
|
||||||
|
<style>
|
||||||
|
pre:first-of-type { width: 78%; margin-left: auto; margin-right: auto; }
|
||||||
|
.file-start + p + pre,
|
||||||
|
.file-start + pre { margin-left: 61.8%; }
|
||||||
|
.file-start,
|
||||||
|
.hunk-start{ text-align: right; }
|
||||||
|
|
||||||
|
.file-start code{ font-size: inherit; }
|
||||||
|
|
||||||
|
.file-start/*:not(:first-of-type)*/{
|
||||||
|
font-size: 150%;
|
||||||
|
margin-top: 23.6%;
|
||||||
|
border-bottom: 1ex solid #ccc;
|
||||||
|
padding-bottom: 1ex;
|
||||||
|
}
|
||||||
|
.hunk-start{
|
||||||
|
margin-top: 2ex;
|
||||||
|
border-bottom: 1ex dashed #ccc;
|
||||||
|
padding-bottom: 1ex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
'
|
24
bower.json
Normal file
24
bower.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "markdown-format-wdiff",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"angular": ">=1.2.*",
|
||||||
|
"json3": "~3.3.1",
|
||||||
|
"es5-shim": "~3.0.1",
|
||||||
|
"jquery": "~1.11.0",
|
||||||
|
"bootstrap-sass-official": "~3.1.1",
|
||||||
|
"bootstrap": "~3.1.1",
|
||||||
|
"angular-resource": ">=1.2.*",
|
||||||
|
"angular-cookies": ">=1.2.*",
|
||||||
|
"angular-sanitize": ">=1.2.*",
|
||||||
|
"angular-route": ">=1.2.*",
|
||||||
|
"angular-bootstrap": "~0.11.0",
|
||||||
|
"font-awesome": ">=4.1.0",
|
||||||
|
"lodash": "~2.4.1",
|
||||||
|
"angular-markdown-directive": "~0.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"angular-mocks": ">=1.2.*",
|
||||||
|
"angular-scenario": ">=1.2.*"
|
||||||
|
}
|
||||||
|
}
|
543
client/.htaccess
Normal file
543
client/.htaccess
Normal file
@ -0,0 +1,543 @@
|
|||||||
|
# Apache Configuration File
|
||||||
|
|
||||||
|
# (!) Using `.htaccess` files slows down Apache, therefore, if you have access
|
||||||
|
# to the main server config file (usually called `httpd.conf`), you should add
|
||||||
|
# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html.
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # CROSS-ORIGIN RESOURCE SHARING (CORS) #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Cross-domain AJAX requests |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Enable cross-origin AJAX requests.
|
||||||
|
# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
|
||||||
|
# http://enable-cors.org/
|
||||||
|
|
||||||
|
# <IfModule mod_headers.c>
|
||||||
|
# Header set Access-Control-Allow-Origin "*"
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | CORS-enabled images |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Send the CORS header for images when browsers request it.
|
||||||
|
# https://developer.mozilla.org/en/CORS_Enabled_Image
|
||||||
|
# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
|
||||||
|
# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
|
||||||
|
|
||||||
|
<IfModule mod_setenvif.c>
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
<FilesMatch "\.(gif|ico|jpe?g|png|svg|svgz|webp)$">
|
||||||
|
SetEnvIf Origin ":" IS_CORS
|
||||||
|
Header set Access-Control-Allow-Origin "*" env=IS_CORS
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Web fonts access |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Allow access from all domains for web fonts
|
||||||
|
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
<FilesMatch "\.(eot|font.css|otf|ttc|ttf|woff)$">
|
||||||
|
Header set Access-Control-Allow-Origin "*"
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # ERRORS #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | 404 error prevention for non-existing redirected folders |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Prevent Apache from returning a 404 error for a rewrite if a directory
|
||||||
|
# with the same name does not exist.
|
||||||
|
# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews
|
||||||
|
# http://www.webmasterworld.com/apache/3808792.htm
|
||||||
|
|
||||||
|
Options -MultiViews
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Custom error messages / pages |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# You can customize what Apache returns to the client in case of an error (see
|
||||||
|
# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.:
|
||||||
|
|
||||||
|
ErrorDocument 404 /404.html
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # INTERNET EXPLORER #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Better website experience |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Force IE to render pages in the highest available mode in the various
|
||||||
|
# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf.
|
||||||
|
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set X-UA-Compatible "IE=edge"
|
||||||
|
# `mod_headers` can't match based on the content-type, however, we only
|
||||||
|
# want to send this header for HTML pages and not for the other resources
|
||||||
|
<FilesMatch "\.(appcache|crx|css|eot|gif|htc|ico|jpe?g|js|m4a|m4v|manifest|mp4|oex|oga|ogg|ogv|otf|pdf|png|safariextz|svg|svgz|ttf|vcf|webapp|webm|webp|woff|xml|xpi)$">
|
||||||
|
Header unset X-UA-Compatible
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Cookie setting from iframes |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Allow cookies to be set from iframes in IE.
|
||||||
|
|
||||||
|
# <IfModule mod_headers.c>
|
||||||
|
# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Screen flicker |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Stop screen flicker in IE on CSS rollovers (this only works in
|
||||||
|
# combination with the `ExpiresByType` directives for images from below).
|
||||||
|
|
||||||
|
# BrowserMatch "MSIE" brokenvary=1
|
||||||
|
# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1
|
||||||
|
# BrowserMatch "Opera" !brokenvary
|
||||||
|
# SetEnvIf brokenvary 1 force-no-vary
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # MIME TYPES AND ENCODING #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Proper MIME types for all files |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
<IfModule mod_mime.c>
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
AddType audio/mp4 m4a f4a f4b
|
||||||
|
AddType audio/ogg oga ogg
|
||||||
|
|
||||||
|
# JavaScript
|
||||||
|
# Normalize to standard type (it's sniffed in IE anyways):
|
||||||
|
# http://tools.ietf.org/html/rfc4329#section-7.2
|
||||||
|
AddType application/javascript js jsonp
|
||||||
|
AddType application/json json
|
||||||
|
|
||||||
|
# Video
|
||||||
|
AddType video/mp4 mp4 m4v f4v f4p
|
||||||
|
AddType video/ogg ogv
|
||||||
|
AddType video/webm webm
|
||||||
|
AddType video/x-flv flv
|
||||||
|
|
||||||
|
# Web fonts
|
||||||
|
AddType application/font-woff woff
|
||||||
|
AddType application/vnd.ms-fontobject eot
|
||||||
|
|
||||||
|
# Browsers usually ignore the font MIME types and sniff the content,
|
||||||
|
# however, Chrome shows a warning if other MIME types are used for the
|
||||||
|
# following fonts.
|
||||||
|
AddType application/x-font-ttf ttc ttf
|
||||||
|
AddType font/opentype otf
|
||||||
|
|
||||||
|
# Make SVGZ fonts work on iPad:
|
||||||
|
# https://twitter.com/FontSquirrel/status/14855840545
|
||||||
|
AddType image/svg+xml svg svgz
|
||||||
|
AddEncoding gzip svgz
|
||||||
|
|
||||||
|
# Other
|
||||||
|
AddType application/octet-stream safariextz
|
||||||
|
AddType application/x-chrome-extension crx
|
||||||
|
AddType application/x-opera-extension oex
|
||||||
|
AddType application/x-shockwave-flash swf
|
||||||
|
AddType application/x-web-app-manifest+json webapp
|
||||||
|
AddType application/x-xpinstall xpi
|
||||||
|
AddType application/xml atom rdf rss xml
|
||||||
|
AddType image/webp webp
|
||||||
|
AddType image/x-icon ico
|
||||||
|
AddType text/cache-manifest appcache manifest
|
||||||
|
AddType text/vtt vtt
|
||||||
|
AddType text/x-component htc
|
||||||
|
AddType text/x-vcard vcf
|
||||||
|
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | UTF-8 encoding |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Use UTF-8 encoding for anything served as `text/html` or `text/plain`.
|
||||||
|
AddDefaultCharset utf-8
|
||||||
|
|
||||||
|
# Force UTF-8 for certain file formats.
|
||||||
|
<IfModule mod_mime.c>
|
||||||
|
AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # URL REWRITES #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Rewrite engine |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Turning on the rewrite engine and enabling the `FollowSymLinks` option is
|
||||||
|
# necessary for the following directives to work.
|
||||||
|
|
||||||
|
# If your web host doesn't allow the `FollowSymlinks` option, you may need to
|
||||||
|
# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the
|
||||||
|
# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
|
||||||
|
|
||||||
|
# Also, some cloud hosting services require `RewriteBase` to be set:
|
||||||
|
# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
Options +FollowSymlinks
|
||||||
|
# Options +SymLinksIfOwnerMatch
|
||||||
|
RewriteEngine On
|
||||||
|
# RewriteBase /
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Suppressing / Forcing the "www." at the beginning of URLs |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The same content should never be available under two different URLs especially
|
||||||
|
# not with and without "www." at the beginning. This can cause SEO problems
|
||||||
|
# (duplicate content), therefore, you should choose one of the alternatives and
|
||||||
|
# redirect the other one.
|
||||||
|
|
||||||
|
# By default option 1 (no "www.") is activated:
|
||||||
|
# http://no-www.org/faq.php?q=class_b
|
||||||
|
|
||||||
|
# If you'd prefer to use option 2, just comment out all the lines from option 1
|
||||||
|
# and uncomment the ones from option 2.
|
||||||
|
|
||||||
|
# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME!
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
# Option 1: rewrite www.example.com → example.com
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteCond %{HTTPS} !=on
|
||||||
|
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||||
|
RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
# Option 2: rewrite example.com → www.example.com
|
||||||
|
|
||||||
|
# Be aware that the following might not be a good idea if you use "real"
|
||||||
|
# subdomains for certain parts of your website.
|
||||||
|
|
||||||
|
# <IfModule mod_rewrite.c>
|
||||||
|
# RewriteCond %{HTTPS} !=on
|
||||||
|
# RewriteCond %{HTTP_HOST} !^www\..+$ [NC]
|
||||||
|
# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # SECURITY #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Content Security Policy (CSP) |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# You can mitigate the risk of cross-site scripting and other content-injection
|
||||||
|
# attacks by setting a Content Security Policy which whitelists trusted sources
|
||||||
|
# of content for your site.
|
||||||
|
|
||||||
|
# The example header below allows ONLY scripts that are loaded from the current
|
||||||
|
# site's origin (no inline scripts, no CDN, etc). This almost certainly won't
|
||||||
|
# work as-is for your site!
|
||||||
|
|
||||||
|
# To get all the details you'll need to craft a reasonable policy for your site,
|
||||||
|
# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or
|
||||||
|
# see the specification: http://w3.org/TR/CSP).
|
||||||
|
|
||||||
|
# <IfModule mod_headers.c>
|
||||||
|
# Header set Content-Security-Policy "script-src 'self'; object-src 'self'"
|
||||||
|
# <FilesMatch "\.(appcache|crx|css|eot|gif|htc|ico|jpe?g|js|m4a|m4v|manifest|mp4|oex|oga|ogg|ogv|otf|pdf|png|safariextz|svg|svgz|ttf|vcf|webapp|webm|webp|woff|xml|xpi)$">
|
||||||
|
# Header unset Content-Security-Policy
|
||||||
|
# </FilesMatch>
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | File access |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Block access to directories without a default document.
|
||||||
|
# Usually you should leave this uncommented because you shouldn't allow anyone
|
||||||
|
# to surf through every directory on your server (which may includes rather
|
||||||
|
# private places like the CMS's directories).
|
||||||
|
|
||||||
|
<IfModule mod_autoindex.c>
|
||||||
|
Options -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
# Block access to hidden files and directories.
|
||||||
|
# This includes directories used by version control systems such as Git and SVN.
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteCond %{SCRIPT_FILENAME} -d [OR]
|
||||||
|
RewriteCond %{SCRIPT_FILENAME} -f
|
||||||
|
RewriteRule "(^|/)\." - [F]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
# Block access to backup and source files.
|
||||||
|
# These files may be left by some text editors and can pose a great security
|
||||||
|
# danger when anyone has access to them.
|
||||||
|
|
||||||
|
<FilesMatch "(^#.*#|\.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$">
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
Satisfy All
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Secure Sockets Layer (SSL) |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.:
|
||||||
|
# prevent `https://www.example.com` when your certificate only allows
|
||||||
|
# `https://secure.example.com`.
|
||||||
|
|
||||||
|
# <IfModule mod_rewrite.c>
|
||||||
|
# RewriteCond %{SERVER_PORT} !^443
|
||||||
|
# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L]
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
# Force client-side SSL redirection.
|
||||||
|
|
||||||
|
# If a user types "example.com" in his browser, the above rule will redirect him
|
||||||
|
# to the secure version of the site. That still leaves a window of opportunity
|
||||||
|
# (the initial HTTP connection) for an attacker to downgrade or redirect the
|
||||||
|
# request. The following header ensures that browser will ONLY connect to your
|
||||||
|
# server via HTTPS, regardless of what the users type in the address bar.
|
||||||
|
# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/
|
||||||
|
|
||||||
|
# <IfModule mod_headers.c>
|
||||||
|
# Header set Strict-Transport-Security max-age=16070400;
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Server software information |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Avoid displaying the exact Apache version number, the description of the
|
||||||
|
# generic OS-type and the information about Apache's compiled-in modules.
|
||||||
|
|
||||||
|
# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`!
|
||||||
|
|
||||||
|
# ServerTokens Prod
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # WEB PERFORMANCE #
|
||||||
|
# ##############################################################################
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Compression |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
|
||||||
|
# Force compression for mangled headers.
|
||||||
|
# http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
|
||||||
|
<IfModule mod_setenvif.c>
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
|
||||||
|
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
|
||||||
|
</IfModule>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Compress all output labeled with one of the following MIME-types
|
||||||
|
# (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
|
||||||
|
# and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines
|
||||||
|
# as `AddOutputFilterByType` is still in the core directives).
|
||||||
|
<IfModule mod_filter.c>
|
||||||
|
AddOutputFilterByType DEFLATE application/atom+xml \
|
||||||
|
application/javascript \
|
||||||
|
application/json \
|
||||||
|
application/rss+xml \
|
||||||
|
application/vnd.ms-fontobject \
|
||||||
|
application/x-font-ttf \
|
||||||
|
application/x-web-app-manifest+json \
|
||||||
|
application/xhtml+xml \
|
||||||
|
application/xml \
|
||||||
|
font/opentype \
|
||||||
|
image/svg+xml \
|
||||||
|
image/x-icon \
|
||||||
|
text/css \
|
||||||
|
text/html \
|
||||||
|
text/plain \
|
||||||
|
text/x-component \
|
||||||
|
text/xml
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Content transformations |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Prevent some of the mobile network providers from modifying the content of
|
||||||
|
# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5.
|
||||||
|
|
||||||
|
# <IfModule mod_headers.c>
|
||||||
|
# Header set Cache-Control "no-transform"
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | ETag removal |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Since we're sending far-future expires headers (see below), ETags can
|
||||||
|
# be removed: http://developer.yahoo.com/performance/rules.html#etags.
|
||||||
|
|
||||||
|
# `FileETag None` is not enough for every server.
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header unset ETag
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
FileETag None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Expires headers (for better cache control) |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The following expires headers are set pretty far in the future. If you don't
|
||||||
|
# control versioning with filename-based cache busting, consider lowering the
|
||||||
|
# cache time for resources like CSS and JS to something like 1 week.
|
||||||
|
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
|
||||||
|
ExpiresActive on
|
||||||
|
ExpiresDefault "access plus 1 month"
|
||||||
|
|
||||||
|
# CSS
|
||||||
|
ExpiresByType text/css "access plus 1 year"
|
||||||
|
|
||||||
|
# Data interchange
|
||||||
|
ExpiresByType application/json "access plus 0 seconds"
|
||||||
|
ExpiresByType application/xml "access plus 0 seconds"
|
||||||
|
ExpiresByType text/xml "access plus 0 seconds"
|
||||||
|
|
||||||
|
# Favicon (cannot be renamed!)
|
||||||
|
ExpiresByType image/x-icon "access plus 1 week"
|
||||||
|
|
||||||
|
# HTML components (HTCs)
|
||||||
|
ExpiresByType text/x-component "access plus 1 month"
|
||||||
|
|
||||||
|
# HTML
|
||||||
|
ExpiresByType text/html "access plus 0 seconds"
|
||||||
|
|
||||||
|
# JavaScript
|
||||||
|
ExpiresByType application/javascript "access plus 1 year"
|
||||||
|
|
||||||
|
# Manifest files
|
||||||
|
ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
|
||||||
|
ExpiresByType text/cache-manifest "access plus 0 seconds"
|
||||||
|
|
||||||
|
# Media
|
||||||
|
ExpiresByType audio/ogg "access plus 1 month"
|
||||||
|
ExpiresByType image/gif "access plus 1 month"
|
||||||
|
ExpiresByType image/jpeg "access plus 1 month"
|
||||||
|
ExpiresByType image/png "access plus 1 month"
|
||||||
|
ExpiresByType video/mp4 "access plus 1 month"
|
||||||
|
ExpiresByType video/ogg "access plus 1 month"
|
||||||
|
ExpiresByType video/webm "access plus 1 month"
|
||||||
|
|
||||||
|
# Web feeds
|
||||||
|
ExpiresByType application/atom+xml "access plus 1 hour"
|
||||||
|
ExpiresByType application/rss+xml "access plus 1 hour"
|
||||||
|
|
||||||
|
# Web fonts
|
||||||
|
ExpiresByType application/font-woff "access plus 1 month"
|
||||||
|
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
|
||||||
|
ExpiresByType application/x-font-ttf "access plus 1 month"
|
||||||
|
ExpiresByType font/opentype "access plus 1 month"
|
||||||
|
ExpiresByType image/svg+xml "access plus 1 month"
|
||||||
|
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Filename-based cache busting |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# If you're not using a build process to manage your filename version revving,
|
||||||
|
# you might want to consider enabling the following directives to route all
|
||||||
|
# requests such as `/css/style.12345.css` to `/css/style.css`.
|
||||||
|
|
||||||
|
# To understand why this is important and a better idea than `*.css?v231`, read:
|
||||||
|
# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring
|
||||||
|
|
||||||
|
# <IfModule mod_rewrite.c>
|
||||||
|
# RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
# RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | File concatenation |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Allow concatenation from within specific CSS and JS files, e.g.:
|
||||||
|
# Inside of `script.combined.js` you could have
|
||||||
|
# <!--#include file="libs/jquery.js" -->
|
||||||
|
# <!--#include file="plugins/jquery.idletimer.js" -->
|
||||||
|
# and they would be included into this single file.
|
||||||
|
|
||||||
|
# <IfModule mod_include.c>
|
||||||
|
# <FilesMatch "\.combined\.js$">
|
||||||
|
# Options +Includes
|
||||||
|
# AddOutputFilterByType INCLUDES application/javascript application/json
|
||||||
|
# SetOutputFilter INCLUDES
|
||||||
|
# </FilesMatch>
|
||||||
|
# <FilesMatch "\.combined\.css$">
|
||||||
|
# Options +Includes
|
||||||
|
# AddOutputFilterByType INCLUDES text/css
|
||||||
|
# SetOutputFilter INCLUDES
|
||||||
|
# </FilesMatch>
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# | Persistent connections |
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Allow multiple requests to be sent over the same TCP connection:
|
||||||
|
# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive.
|
||||||
|
|
||||||
|
# Enable if you serve a lot of static content but, be aware of the
|
||||||
|
# possible disadvantages!
|
||||||
|
|
||||||
|
# <IfModule mod_headers.c>
|
||||||
|
# Header set Connection Keep-Alive
|
||||||
|
# </IfModule>
|
38
client/.jshintrc
Normal file
38
client/.jshintrc
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"esnext": true,
|
||||||
|
"bitwise": true,
|
||||||
|
"camelcase": true,
|
||||||
|
"curly": true,
|
||||||
|
"eqeqeq": true,
|
||||||
|
"immed": true,
|
||||||
|
"indent": 2,
|
||||||
|
"latedef": true,
|
||||||
|
"newcap": true,
|
||||||
|
"noarg": true,
|
||||||
|
"quotmark": "single",
|
||||||
|
"regexp": true,
|
||||||
|
"undef": true,
|
||||||
|
"unused": true,
|
||||||
|
"strict": true,
|
||||||
|
"trailing": true,
|
||||||
|
"smarttabs": true,
|
||||||
|
"globals": {
|
||||||
|
"jQuery": true,
|
||||||
|
"angular": true,
|
||||||
|
"console": true,
|
||||||
|
"$": true,
|
||||||
|
"_": true,
|
||||||
|
"moment": true,
|
||||||
|
"describe": true,
|
||||||
|
"beforeEach": true,
|
||||||
|
"module": true,
|
||||||
|
"inject": true,
|
||||||
|
"it": true,
|
||||||
|
"expect": true,
|
||||||
|
"browser": true,
|
||||||
|
"element": true,
|
||||||
|
"by": true
|
||||||
|
}
|
||||||
|
}
|
19
client/app/account/account.js
Normal file
19
client/app/account/account.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.config(function ($routeProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.when('/login', {
|
||||||
|
templateUrl: 'app/account/login/login.html',
|
||||||
|
controller: 'LoginCtrl'
|
||||||
|
})
|
||||||
|
.when('/signup', {
|
||||||
|
templateUrl: 'app/account/signup/signup.html',
|
||||||
|
controller: 'SignupCtrl'
|
||||||
|
})
|
||||||
|
.when('/settings', {
|
||||||
|
templateUrl: 'app/account/settings/settings.html',
|
||||||
|
controller: 'SettingsCtrl',
|
||||||
|
authenticate: true
|
||||||
|
});
|
||||||
|
});
|
29
client/app/account/login/login.controller.js
Normal file
29
client/app/account/login/login.controller.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.controller('LoginCtrl', function ($scope, Auth, $location, $window) {
|
||||||
|
$scope.user = {};
|
||||||
|
$scope.errors = {};
|
||||||
|
|
||||||
|
$scope.login = function(form) {
|
||||||
|
$scope.submitted = true;
|
||||||
|
|
||||||
|
if(form.$valid) {
|
||||||
|
Auth.login({
|
||||||
|
email: $scope.user.email,
|
||||||
|
password: $scope.user.password
|
||||||
|
})
|
||||||
|
.then( function() {
|
||||||
|
// Logged in, redirect to home
|
||||||
|
$location.path('/');
|
||||||
|
})
|
||||||
|
.catch( function(err) {
|
||||||
|
$scope.errors.other = err.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.loginOauth = function(provider) {
|
||||||
|
$window.location.href = '/auth/' + provider;
|
||||||
|
};
|
||||||
|
});
|
42
client/app/account/login/login.jade
Normal file
42
client/app/account/login/login.jade
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
div(ng-include='"components/navbar/navbar.html"')
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-sm-12
|
||||||
|
h1 Login
|
||||||
|
|
||||||
|
.col-sm-12
|
||||||
|
form.form(name='form', ng-submit='login(form)', novalidate='')
|
||||||
|
.form-group
|
||||||
|
label Email
|
||||||
|
input.form-control(type='text', name='email', ng-model='user.email')
|
||||||
|
.form-group
|
||||||
|
label Password
|
||||||
|
input.form-control(type='password', name='password', ng-model='user.password')
|
||||||
|
|
||||||
|
.form-group.has-error
|
||||||
|
p.help-block(ng-show='form.email.$error.required && form.password.$error.required && submitted')
|
||||||
|
| Please enter your email and password.
|
||||||
|
p.help-block {{ errors.other }}
|
||||||
|
|
||||||
|
div
|
||||||
|
button.btn.btn-inverse.btn-lg.btn-login(type='submit')
|
||||||
|
| Login
|
||||||
|
= ' '
|
||||||
|
a.btn.btn-default.btn-lg.btn-register(href='/signup')
|
||||||
|
| Register
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
div
|
||||||
|
a.btn.btn-facebook(href='', ng-click='loginOauth("facebook")')
|
||||||
|
i.fa.fa-facebook
|
||||||
|
| Connect with Facebook
|
||||||
|
= ' '
|
||||||
|
a.btn.btn-google-plus(href='', ng-click='loginOauth("google")')
|
||||||
|
i.fa.fa-google-plus
|
||||||
|
| Connect with Google+
|
||||||
|
= ' '
|
||||||
|
a.btn.btn-twitter(href='', ng-click='loginOauth("twitter")')
|
||||||
|
i.fa.fa-twitter
|
||||||
|
| Connect with Twitter
|
||||||
|
hr
|
30
client/app/account/login/login.scss
Normal file
30
client/app/account/login/login.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Colors
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$btnText: #fff;
|
||||||
|
$btnTextAlt: #000;
|
||||||
|
|
||||||
|
$btnFacebookBackground: #3B5998;
|
||||||
|
$btnFacebookBackgroundHighlight: #133783;
|
||||||
|
$btnTwitterBackground: #2daddc;
|
||||||
|
$btnTwitterBackgroundHighlight: #0271bf;
|
||||||
|
$btnGooglePlusBackground: #dd4b39;
|
||||||
|
$btnGooglePlusBackgroundHighlight: #c53727;
|
||||||
|
$btnGithubBackground: #fafafa;
|
||||||
|
$btnGithubBackgroundHighlight: #ccc;
|
||||||
|
|
||||||
|
// Social buttons
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.btn-facebook {
|
||||||
|
@include button-variant($btnText, $btnFacebookBackgroundHighlight, $btnFacebookBackgroundHighlight);
|
||||||
|
}
|
||||||
|
.btn-twitter {
|
||||||
|
@include button-variant($btnText, $btnTwitterBackground, $btnTwitterBackgroundHighlight);
|
||||||
|
}
|
||||||
|
.btn-google-plus {
|
||||||
|
@include button-variant($btnText, $btnGooglePlusBackground, $btnGooglePlusBackgroundHighlight);
|
||||||
|
}
|
||||||
|
.btn-github {
|
||||||
|
@include button-variant($btnTextAlt, $btnGithubBackground, $btnGithubBackgroundHighlight);
|
||||||
|
}
|
21
client/app/account/settings/settings.controller.js
Normal file
21
client/app/account/settings/settings.controller.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.controller('SettingsCtrl', function ($scope, User, Auth) {
|
||||||
|
$scope.errors = {};
|
||||||
|
|
||||||
|
$scope.changePassword = function(form) {
|
||||||
|
$scope.submitted = true;
|
||||||
|
if(form.$valid) {
|
||||||
|
Auth.changePassword( $scope.user.oldPassword, $scope.user.newPassword )
|
||||||
|
.then( function() {
|
||||||
|
$scope.message = 'Password successfully changed.';
|
||||||
|
})
|
||||||
|
.catch( function() {
|
||||||
|
form.password.$setValidity('mongoose', false);
|
||||||
|
$scope.errors.other = 'Incorrect password';
|
||||||
|
$scope.message = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
21
client/app/account/settings/settings.jade
Normal file
21
client/app/account/settings/settings.jade
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
div(ng-include='"components/navbar/navbar.html"')
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-sm-12
|
||||||
|
h1 Change Password
|
||||||
|
.col-sm-12
|
||||||
|
form.form(name='form', ng-submit='changePassword(form)', novalidate='')
|
||||||
|
.form-group
|
||||||
|
label Current Password
|
||||||
|
input.form-control(type='password', name='password', ng-model='user.oldPassword', mongoose-error='')
|
||||||
|
p.help-block(ng-show='form.password.$error.mongoose')
|
||||||
|
| {{ errors.other }}
|
||||||
|
.form-group
|
||||||
|
label New Password
|
||||||
|
input.form-control(type='password', name='newPassword', ng-model='user.newPassword', ng-minlength='3', required='')
|
||||||
|
p.help-block(ng-show='(form.newPassword.$error.minlength || form.newPassword.$error.required) && (form.newPassword.$dirty || submitted)')
|
||||||
|
| Password must be at least 3 characters.
|
||||||
|
|
||||||
|
p.help-block {{ message }}
|
||||||
|
|
||||||
|
button.btn.btn-lg.btn-primary(type='submit') Save changes
|
37
client/app/account/signup/signup.controller.js
Normal file
37
client/app/account/signup/signup.controller.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.controller('SignupCtrl', function ($scope, Auth, $location, $window) {
|
||||||
|
$scope.user = {};
|
||||||
|
$scope.errors = {};
|
||||||
|
|
||||||
|
$scope.register = function(form) {
|
||||||
|
$scope.submitted = true;
|
||||||
|
|
||||||
|
if(form.$valid) {
|
||||||
|
Auth.createUser({
|
||||||
|
name: $scope.user.name,
|
||||||
|
email: $scope.user.email,
|
||||||
|
password: $scope.user.password
|
||||||
|
})
|
||||||
|
.then( function() {
|
||||||
|
// Account created, redirect to home
|
||||||
|
$location.path('/');
|
||||||
|
})
|
||||||
|
.catch( function(err) {
|
||||||
|
err = err.data;
|
||||||
|
$scope.errors = {};
|
||||||
|
|
||||||
|
// Update validity of form fields that match the mongoose errors
|
||||||
|
angular.forEach(err.errors, function(error, field) {
|
||||||
|
form[field].$setValidity('mongoose', false);
|
||||||
|
$scope.errors[field] = error.message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.loginOauth = function(provider) {
|
||||||
|
$window.location.href = '/auth/' + provider;
|
||||||
|
};
|
||||||
|
});
|
57
client/app/account/signup/signup.jade
Normal file
57
client/app/account/signup/signup.jade
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
div(ng-include='"components/navbar/navbar.html"')
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-sm-12
|
||||||
|
h1 Sign up
|
||||||
|
.col-sm-12
|
||||||
|
form.form(name='form', ng-submit='register(form)', novalidate='')
|
||||||
|
.form-group(ng-class='{ "has-success": form.name.$valid && submitted,\
|
||||||
|
"has-error": form.name.$invalid && submitted }')
|
||||||
|
label Name
|
||||||
|
input.form-control(type='text', name='name', ng-model='user.name', required='')
|
||||||
|
p.help-block(ng-show='form.name.$error.required && submitted')
|
||||||
|
| A name is required
|
||||||
|
|
||||||
|
.form-group(ng-class='{ "has-success": form.email.$valid && submitted,\
|
||||||
|
"has-error": form.email.$invalid && submitted }')
|
||||||
|
label Email
|
||||||
|
input.form-control(type='email', name='email', ng-model='user.email', required='', mongoose-error='')
|
||||||
|
p.help-block(ng-show='form.email.$error.email && submitted')
|
||||||
|
| Doesn't look like a valid email.
|
||||||
|
p.help-block(ng-show='form.email.$error.required && submitted')
|
||||||
|
| What's your email address?
|
||||||
|
p.help-block(ng-show='form.email.$error.mongoose')
|
||||||
|
| {{ errors.email }}
|
||||||
|
|
||||||
|
.form-group(ng-class='{ "has-success": form.password.$valid && submitted,\
|
||||||
|
"has-error": form.password.$invalid && submitted }')
|
||||||
|
label Password
|
||||||
|
input.form-control(type='password', name='password', ng-model='user.password', ng-minlength='3', required='', mongoose-error='')
|
||||||
|
p.help-block(ng-show='(form.password.$error.minlength || form.password.$error.required) && submitted')
|
||||||
|
| Password must be at least 3 characters.
|
||||||
|
p.help-block(ng-show='form.password.$error.mongoose')
|
||||||
|
| {{ errors.password }}
|
||||||
|
|
||||||
|
div
|
||||||
|
button.btn.btn-inverse.btn-lg.btn-login(type='submit')
|
||||||
|
| Sign up
|
||||||
|
= ' '
|
||||||
|
a.btn.btn-default.btn-lg.btn-register(href='/login')
|
||||||
|
| Login
|
||||||
|
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
div
|
||||||
|
a.btn.btn-facebook(href='', ng-click='loginOauth("facebook")')
|
||||||
|
i.fa.fa-facebook
|
||||||
|
| Connect with Facebook
|
||||||
|
= ' '
|
||||||
|
a.btn.btn-google-plus(href='', ng-click='loginOauth("google")')
|
||||||
|
i.fa.fa-google-plus
|
||||||
|
| Connect with Google+
|
||||||
|
= ' '
|
||||||
|
a.btn.btn-twitter(href='', ng-click='loginOauth("twitter")')
|
||||||
|
i.fa.fa-twitter
|
||||||
|
| Connect with Twitter
|
||||||
|
hr
|
17
client/app/admin/admin.controller.js
Normal file
17
client/app/admin/admin.controller.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.controller('AdminCtrl', function ($scope, $http, Auth, User) {
|
||||||
|
|
||||||
|
// Use the User $resource to fetch all users
|
||||||
|
$scope.users = User.query();
|
||||||
|
|
||||||
|
$scope.delete = function(user) {
|
||||||
|
User.remove({ id: user._id });
|
||||||
|
angular.forEach($scope.users, function(u, i) {
|
||||||
|
if (u === user) {
|
||||||
|
$scope.users.splice(i, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
11
client/app/admin/admin.jade
Normal file
11
client/app/admin/admin.jade
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
div(ng-include='"components/navbar/navbar.html"')
|
||||||
|
.container
|
||||||
|
p
|
||||||
|
| The delete user and user index api routes are restricted to users with the 'admin' role.
|
||||||
|
ul.list-group
|
||||||
|
li.list-group-item(ng-repeat='user in users')
|
||||||
|
strong {{user.name}}
|
||||||
|
br
|
||||||
|
span.text-muted {{user.email}}
|
||||||
|
a.trash(ng-click='delete(user)')
|
||||||
|
span.glyphicon.glyphicon-trash.pull-right
|
10
client/app/admin/admin.js
Normal file
10
client/app/admin/admin.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.config(function ($routeProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.when('/admin', {
|
||||||
|
templateUrl: 'app/admin/admin.html',
|
||||||
|
controller: 'AdminCtrl'
|
||||||
|
});
|
||||||
|
});
|
1
client/app/admin/admin.scss
Normal file
1
client/app/admin/admin.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
.trash { color:rgb(209, 91, 71); }
|
56
client/app/app.js
Normal file
56
client/app/app.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp', [
|
||||||
|
'ngCookies',
|
||||||
|
'ngResource',
|
||||||
|
'ngSanitize',
|
||||||
|
'ngRoute',
|
||||||
|
'ui.bootstrap',
|
||||||
|
'btford.markdown'
|
||||||
|
])
|
||||||
|
.config(function ($routeProvider, $locationProvider, $httpProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.otherwise({
|
||||||
|
redirectTo: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
$locationProvider.html5Mode(true);
|
||||||
|
$httpProvider.interceptors.push('authInterceptor');
|
||||||
|
})
|
||||||
|
|
||||||
|
.factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) {
|
||||||
|
return {
|
||||||
|
// Add authorization token to headers
|
||||||
|
request: function (config) {
|
||||||
|
config.headers = config.headers || {};
|
||||||
|
if ($cookieStore.get('token')) {
|
||||||
|
config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Intercept 401s and redirect you to login
|
||||||
|
responseError: function(response) {
|
||||||
|
if(response.status === 401) {
|
||||||
|
$location.path('/login');
|
||||||
|
// remove any stale tokens
|
||||||
|
$cookieStore.remove('token');
|
||||||
|
return $q.reject(response);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $q.reject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
.run(function ($rootScope, $location, Auth) {
|
||||||
|
// Redirect to login if route requires auth and you're not logged in
|
||||||
|
$rootScope.$on('$routeChangeStart', function (event, next) {
|
||||||
|
Auth.isLoggedInAsync(function(loggedIn) {
|
||||||
|
if (next.authenticate && !loggedIn) {
|
||||||
|
$location.path('/login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
24
client/app/app.scss
Normal file
24
client/app/app.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
$icon-font-path: "/bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/";
|
||||||
|
$fa-font-path: "/bower_components/font-awesome/fonts";
|
||||||
|
|
||||||
|
@import 'bootstrap-sass-official/vendor/assets/stylesheets/bootstrap';
|
||||||
|
@import 'font-awesome/scss/font-awesome';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-wide Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
.browsehappy {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component styles are injected through grunt
|
||||||
|
// injector
|
||||||
|
@import 'account/login/login.scss';
|
||||||
|
@import 'admin/admin.scss';
|
||||||
|
@import 'wdiff/wdiff.scss';
|
||||||
|
@import 'modal/modal.scss';
|
||||||
|
// endinjector
|
49
client/app/wdiff/wdiff.controller.js
Normal file
49
client/app/wdiff/wdiff.controller.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.controller('WdiffCtrl', function ($scope, $http) {
|
||||||
|
$scope.docA = "";
|
||||||
|
$scope.docB = "";
|
||||||
|
$scope.wdiff = "";
|
||||||
|
$scope.wdiffMarkdown = "";
|
||||||
|
$scope.displayAsMarkdown = true;
|
||||||
|
|
||||||
|
$scope.compare = function() {
|
||||||
|
$http.post('/api/wdiff', //+($scope.displayAsMarkdown ? '/markdown': ''),
|
||||||
|
{ a: $scope.docA, b: $scope.docB },
|
||||||
|
{headers:{"Content-Type":"application/json"}})
|
||||||
|
.success(function (data) {
|
||||||
|
if ($scope.displayAsMarkdown) {
|
||||||
|
var markdown = data.wdiff;
|
||||||
|
markdown = markdown.replace(/\[-/g, '<del>');
|
||||||
|
markdown = markdown.replace(/-\]/g, '</del>');
|
||||||
|
markdown = markdown.replace(/{\+/g, '<ins>');
|
||||||
|
markdown = markdown.replace(/\+}/g, '</ins>');
|
||||||
|
|
||||||
|
$scope.wdiffMarkdown = markdown; //data.markdown;
|
||||||
|
$scope.wdiff = '';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
$scope.wdiff = data.wdiff;
|
||||||
|
$scope.wdiffMarkdown = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* courtesy some rando (doesn't work):
|
||||||
|
function expandTextarea(id) {
|
||||||
|
var element = document.getElementById(id);
|
||||||
|
|
||||||
|
element.addEventListener('keyup', function() {
|
||||||
|
this.style.overflow = 'hidden';
|
||||||
|
this.style.height = 0;
|
||||||
|
this.style.height = this.scrollHeight + 'px';
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
expandTextarea('docA');
|
||||||
|
expandTextarea('docB');
|
||||||
|
*/
|
||||||
|
|
||||||
|
})
|
52
client/app/wdiff/wdiff.jade
Normal file
52
client/app/wdiff/wdiff.jade
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
div(ng-include='"components/navbar/navbar.html"')
|
||||||
|
|
||||||
|
header#banner.hero-unit
|
||||||
|
.container
|
||||||
|
h1 wdiff
|
||||||
|
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-lg-3.col-sm-12
|
||||||
|
.col-lg-6.col-sm-12
|
||||||
|
div.diff-container
|
||||||
|
div(ng-bind='wdiff')
|
||||||
|
.col-lg-3.col-sm-12
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-lg-3.col-sm-12
|
||||||
|
.col-lg-6.col-sm-12
|
||||||
|
div.diff-container
|
||||||
|
div(btf-markdown='wdiffMarkdown')
|
||||||
|
.col-lg-3.col-sm-12
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
form.row
|
||||||
|
.col-lg-12.form-group
|
||||||
|
div.checkbox
|
||||||
|
label
|
||||||
|
input(type='checkbox', ng-model='displayAsMarkdown')
|
||||||
|
| Display as Markdown
|
||||||
|
|
||||||
|
.col-lg-12.form-group
|
||||||
|
div.btn-group.btn-group-justified(role='group')
|
||||||
|
a.btn.btn-primary(type='button', ng-click='compare()') compare
|
||||||
|
|
||||||
|
.col-lg-6.col-sm-12.form-group
|
||||||
|
label(for='docA')
|
||||||
|
| Document A
|
||||||
|
textarea.form-control(id='docA', ng-model='docA')
|
||||||
|
|
||||||
|
.col-lg-6.col-sm-12.form-group
|
||||||
|
label(for='docB')
|
||||||
|
| Document B
|
||||||
|
textarea.form-control(id='docB', ng-model='docB')
|
||||||
|
|
||||||
|
|
||||||
|
footer.footer
|
||||||
|
.container
|
||||||
|
p
|
||||||
|
| Wdiff online
|
||||||
|
= ' | '
|
||||||
|
a(href='https://madanworb.com') Adam Brown
|
||||||
|
|
10
client/app/wdiff/wdiff.js
Normal file
10
client/app/wdiff/wdiff.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.config(function ($routeProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.when('/', {
|
||||||
|
templateUrl: 'app/wdiff/wdiff.html',
|
||||||
|
controller: 'WdiffCtrl'
|
||||||
|
});
|
||||||
|
});
|
37
client/app/wdiff/wdiff.scss
Normal file
37
client/app/wdiff/wdiff.scss
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
.wdiff-container .ins {
|
||||||
|
}
|
||||||
|
|
||||||
|
.wdiff-container .del {
|
||||||
|
}
|
||||||
|
|
||||||
|
#docA, #docB {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner h1 {
|
||||||
|
font-size: 60px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-unit {
|
||||||
|
position: relative;
|
||||||
|
padding: 30px 15px;
|
||||||
|
color: #F5F5F5;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
background: #4393B9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
margin-top: 70px;
|
||||||
|
border-top: 1px solid #E5E5E5;
|
||||||
|
}
|
BIN
client/assets/images/yeoman.png
Normal file
BIN
client/assets/images/yeoman.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
146
client/components/auth/auth.service.js
Normal file
146
client/components/auth/auth.service.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.factory('Auth', function Auth($location, $rootScope, $http, User, $cookieStore, $q) {
|
||||||
|
var currentUser = {};
|
||||||
|
if($cookieStore.get('token')) {
|
||||||
|
currentUser = User.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate user and save token
|
||||||
|
*
|
||||||
|
* @param {Object} user - login info
|
||||||
|
* @param {Function} callback - optional
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
login: function(user, callback) {
|
||||||
|
var cb = callback || angular.noop;
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
$http.post('/auth/local', {
|
||||||
|
email: user.email,
|
||||||
|
password: user.password
|
||||||
|
}).
|
||||||
|
success(function(data) {
|
||||||
|
$cookieStore.put('token', data.token);
|
||||||
|
currentUser = User.get();
|
||||||
|
deferred.resolve(data);
|
||||||
|
return cb();
|
||||||
|
}).
|
||||||
|
error(function(err) {
|
||||||
|
this.logout();
|
||||||
|
deferred.reject(err);
|
||||||
|
return cb(err);
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete access token and user info
|
||||||
|
*
|
||||||
|
* @param {Function}
|
||||||
|
*/
|
||||||
|
logout: function() {
|
||||||
|
$cookieStore.remove('token');
|
||||||
|
currentUser = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
*
|
||||||
|
* @param {Object} user - user info
|
||||||
|
* @param {Function} callback - optional
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
createUser: function(user, callback) {
|
||||||
|
var cb = callback || angular.noop;
|
||||||
|
|
||||||
|
return User.save(user,
|
||||||
|
function(data) {
|
||||||
|
$cookieStore.put('token', data.token);
|
||||||
|
currentUser = User.get();
|
||||||
|
return cb(user);
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
this.logout();
|
||||||
|
return cb(err);
|
||||||
|
}.bind(this)).$promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change password
|
||||||
|
*
|
||||||
|
* @param {String} oldPassword
|
||||||
|
* @param {String} newPassword
|
||||||
|
* @param {Function} callback - optional
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
changePassword: function(oldPassword, newPassword, callback) {
|
||||||
|
var cb = callback || angular.noop;
|
||||||
|
|
||||||
|
return User.changePassword({ id: currentUser._id }, {
|
||||||
|
oldPassword: oldPassword,
|
||||||
|
newPassword: newPassword
|
||||||
|
}, function(user) {
|
||||||
|
return cb(user);
|
||||||
|
}, function(err) {
|
||||||
|
return cb(err);
|
||||||
|
}).$promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all available info on authenticated user
|
||||||
|
*
|
||||||
|
* @return {Object} user
|
||||||
|
*/
|
||||||
|
getCurrentUser: function() {
|
||||||
|
return currentUser;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is logged in
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
isLoggedIn: function() {
|
||||||
|
return currentUser.hasOwnProperty('role');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for currentUser to resolve before checking if user is logged in
|
||||||
|
*/
|
||||||
|
isLoggedInAsync: function(cb) {
|
||||||
|
if(currentUser.hasOwnProperty('$promise')) {
|
||||||
|
currentUser.$promise.then(function() {
|
||||||
|
cb(true);
|
||||||
|
}).catch(function() {
|
||||||
|
cb(false);
|
||||||
|
});
|
||||||
|
} else if(currentUser.hasOwnProperty('role')) {
|
||||||
|
cb(true);
|
||||||
|
} else {
|
||||||
|
cb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is an admin
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
isAdmin: function() {
|
||||||
|
return currentUser.role === 'admin';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auth token
|
||||||
|
*/
|
||||||
|
getToken: function() {
|
||||||
|
return $cookieStore.get('token');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
22
client/components/auth/user.service.js
Normal file
22
client/components/auth/user.service.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.factory('User', function ($resource) {
|
||||||
|
return $resource('/api/users/:id/:controller', {
|
||||||
|
id: '@_id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
changePassword: {
|
||||||
|
method: 'PUT',
|
||||||
|
params: {
|
||||||
|
controller:'password'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get: {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
id:'me'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
8
client/components/modal/modal.jade
Normal file
8
client/components/modal/modal.jade
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.modal-header
|
||||||
|
button.close(ng-if='modal.dismissable', type='button', ng-click='$dismiss()') ×
|
||||||
|
h4.modal-title(ng-if='modal.title', ng-bind='modal.title')
|
||||||
|
.modal-body
|
||||||
|
p(ng-if='modal.text', ng-bind='modal.text')
|
||||||
|
div(ng-if='modal.html', ng-bind-html='modal.html')
|
||||||
|
.modal-footer
|
||||||
|
button.btn(ng-repeat='button in modal.buttons', ng-class='button.classes', ng-click='button.click($event)', ng-bind='button.text')
|
25
client/components/modal/modal.scss
Normal file
25
client/components/modal/modal.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.modal-primary,
|
||||||
|
.modal-info,
|
||||||
|
.modal-success,
|
||||||
|
.modal-warning,
|
||||||
|
.modal-danger {
|
||||||
|
.modal-header {
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modal-primary .modal-header {
|
||||||
|
background: $brand-primary;
|
||||||
|
}
|
||||||
|
.modal-info .modal-header {
|
||||||
|
background: $brand-info;
|
||||||
|
}
|
||||||
|
.modal-success .modal-header {
|
||||||
|
background: $brand-success;
|
||||||
|
}
|
||||||
|
.modal-warning .modal-header {
|
||||||
|
background: $brand-warning;
|
||||||
|
}
|
||||||
|
.modal-danger .modal-header {
|
||||||
|
background: $brand-danger;
|
||||||
|
}
|
77
client/components/modal/modal.service.js
Normal file
77
client/components/modal/modal.service.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.factory('Modal', function ($rootScope, $modal) {
|
||||||
|
/**
|
||||||
|
* Opens a modal
|
||||||
|
* @param {Object} scope - an object to be merged with modal's scope
|
||||||
|
* @param {String} modalClass - (optional) class(es) to be applied to the modal
|
||||||
|
* @return {Object} - the instance $modal.open() returns
|
||||||
|
*/
|
||||||
|
function openModal(scope, modalClass) {
|
||||||
|
var modalScope = $rootScope.$new();
|
||||||
|
scope = scope || {};
|
||||||
|
modalClass = modalClass || 'modal-default';
|
||||||
|
|
||||||
|
angular.extend(modalScope, scope);
|
||||||
|
|
||||||
|
return $modal.open({
|
||||||
|
templateUrl: 'components/modal/modal.html',
|
||||||
|
windowClass: modalClass,
|
||||||
|
scope: modalScope
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API here
|
||||||
|
return {
|
||||||
|
|
||||||
|
/* Confirmation modals */
|
||||||
|
confirm: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a function to open a delete confirmation modal (ex. ng-click='myModalFn(name, arg1, arg2...)')
|
||||||
|
* @param {Function} del - callback, ran when delete is confirmed
|
||||||
|
* @return {Function} - the function to open the modal (ex. myModalFn)
|
||||||
|
*/
|
||||||
|
delete: function(del) {
|
||||||
|
del = del || angular.noop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a delete confirmation modal
|
||||||
|
* @param {String} name - name or info to show on modal
|
||||||
|
* @param {All} - any additional args are passed staight to del callback
|
||||||
|
*/
|
||||||
|
return function() {
|
||||||
|
var args = Array.prototype.slice.call(arguments),
|
||||||
|
name = args.shift(),
|
||||||
|
deleteModal;
|
||||||
|
|
||||||
|
deleteModal = openModal({
|
||||||
|
modal: {
|
||||||
|
dismissable: true,
|
||||||
|
title: 'Confirm Delete',
|
||||||
|
html: '<p>Are you sure you want to delete <strong>' + name + '</strong> ?</p>',
|
||||||
|
buttons: [{
|
||||||
|
classes: 'btn-danger',
|
||||||
|
text: 'Delete',
|
||||||
|
click: function(e) {
|
||||||
|
deleteModal.close(e);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
classes: 'btn-default',
|
||||||
|
text: 'Cancel',
|
||||||
|
click: function(e) {
|
||||||
|
deleteModal.dismiss(e);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}, 'modal-danger');
|
||||||
|
|
||||||
|
deleteModal.result.then(function(event) {
|
||||||
|
del.apply(event, args);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
17
client/components/mongoose-error/mongoose-error.directive.js
Normal file
17
client/components/mongoose-error/mongoose-error.directive.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes server error when user updates input
|
||||||
|
*/
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.directive('mongooseError', function () {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
require: 'ngModel',
|
||||||
|
link: function(scope, element, attrs, ngModel) {
|
||||||
|
element.on('keydown', function() {
|
||||||
|
return ngModel.$setValidity('mongoose', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
23
client/components/navbar/navbar.controller.js
Normal file
23
client/components/navbar/navbar.controller.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('markdownFormatWdiffApp')
|
||||||
|
.controller('NavbarCtrl', function ($scope, $location, Auth) {
|
||||||
|
$scope.menu = [{
|
||||||
|
'title': 'Home',
|
||||||
|
'link': '/'
|
||||||
|
}];
|
||||||
|
|
||||||
|
$scope.isCollapsed = true;
|
||||||
|
$scope.isLoggedIn = Auth.isLoggedIn;
|
||||||
|
$scope.isAdmin = Auth.isAdmin;
|
||||||
|
$scope.getCurrentUser = Auth.getCurrentUser;
|
||||||
|
|
||||||
|
$scope.logout = function() {
|
||||||
|
Auth.logout();
|
||||||
|
$location.path('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.isActive = function(route) {
|
||||||
|
return route === $location.path();
|
||||||
|
};
|
||||||
|
});
|
34
client/components/navbar/navbar.jade
Normal file
34
client/components/navbar/navbar.jade
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
div.navbar.navbar-default.navbar-static-top(ng-controller='NavbarCtrl')
|
||||||
|
div.container
|
||||||
|
div.navbar-header
|
||||||
|
button.navbar-toggle(type='button', ng-click='isCollapsed = !isCollapsed')
|
||||||
|
span.sr-only Toggle navigation
|
||||||
|
span.icon-bar
|
||||||
|
span.icon-bar
|
||||||
|
span.icon-bar
|
||||||
|
a.navbar-brand(href='/') markdown-format-wdiff
|
||||||
|
|
||||||
|
div#navbar-main.navbar-collapse.collapse(collapse='isCollapsed')
|
||||||
|
ul.nav.navbar-nav
|
||||||
|
li(ng-repeat='item in menu', ng-class='{active: isActive(item.link)}')
|
||||||
|
a(ng-href='{{item.link}}') {{item.title}}
|
||||||
|
|
||||||
|
li(ng-show='isAdmin()', ng-class='{active: isActive("/admin")}')
|
||||||
|
a(href='/admin') Admin
|
||||||
|
|
||||||
|
ul.nav.navbar-nav.navbar-right
|
||||||
|
li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/signup")}')
|
||||||
|
a(href='/signup') Sign up
|
||||||
|
|
||||||
|
li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/login")}')
|
||||||
|
a(href='/login') Login
|
||||||
|
|
||||||
|
li(ng-show='isLoggedIn()')
|
||||||
|
p.navbar-text Hello {{ getCurrentUser().name }}
|
||||||
|
|
||||||
|
li(ng-show='isLoggedIn()', ng-class='{active: isActive("/settings")}')
|
||||||
|
a(href='/settings')
|
||||||
|
span.glyphicon.glyphicon-cog
|
||||||
|
|
||||||
|
li(ng-show='isLoggedIn()', ng-class='{active: isActive("/logout")}')
|
||||||
|
a(href='', ng-click='logout()') Logout
|
BIN
client/favicon.ico
Normal file
BIN
client/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
81
client/index.html
Normal file
81
client/index.html
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<base href="/">
|
||||||
|
<title></title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
|
||||||
|
<!-- build:css(client) app/vendor.css -->
|
||||||
|
<!-- bower:css -->
|
||||||
|
<!-- endbower -->
|
||||||
|
<!-- endbuild -->
|
||||||
|
<!-- build:css({.tmp,client}) app/app.css -->
|
||||||
|
<link rel="stylesheet" href="app/app.css">
|
||||||
|
<!-- injector:css -->
|
||||||
|
<!-- endinjector -->
|
||||||
|
<!-- endbuild -->
|
||||||
|
</head>
|
||||||
|
<body ng-app="markdownFormatWdiffApp">
|
||||||
|
<!--[if lt IE 7]>
|
||||||
|
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
<!-- Add your site or application content here -->
|
||||||
|
<div ng-view=""></div>
|
||||||
|
|
||||||
|
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID -->
|
||||||
|
<script>
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-XXXXX-X');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="bower_components/es5-shim/es5-shim.js"></script>
|
||||||
|
<script src="bower_components/json3/lib/json3.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
<!-- build:js({client,node_modules}) app/vendor.js -->
|
||||||
|
<!-- bower:js -->
|
||||||
|
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||||
|
<script src="bower_components/angular/angular.js"></script>
|
||||||
|
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||||
|
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
|
||||||
|
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
|
||||||
|
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||||
|
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
|
||||||
|
<script src="bower_components/lodash/dist/lodash.compat.js"></script>
|
||||||
|
<script src="bower_components/showdown/src/showdown.js"></script>
|
||||||
|
<script src="bower_components/angular-markdown-directive/markdown.js"></script>
|
||||||
|
<!-- endbower -->
|
||||||
|
<!-- endbuild -->
|
||||||
|
|
||||||
|
<!-- build:js({.tmp,client}) app/app.js -->
|
||||||
|
<script src="app/app.js"></script>
|
||||||
|
<!-- injector:js -->
|
||||||
|
<script src="app/account/account.js"></script>
|
||||||
|
<script src="app/account/login/login.controller.js"></script>
|
||||||
|
<script src="app/account/settings/settings.controller.js"></script>
|
||||||
|
<script src="app/account/signup/signup.controller.js"></script>
|
||||||
|
<script src="app/admin/admin.controller.js"></script>
|
||||||
|
<script src="app/admin/admin.js"></script>
|
||||||
|
<script src="app/wdiff/wdiff.controller.js"></script>
|
||||||
|
<script src="app/wdiff/wdiff.js"></script>
|
||||||
|
<script src="components/auth/auth.service.js"></script>
|
||||||
|
<script src="components/auth/user.service.js"></script>
|
||||||
|
<script src="components/modal/modal.service.js"></script>
|
||||||
|
<script src="components/mongoose-error/mongoose-error.directive.js"></script>
|
||||||
|
<script src="components/navbar/navbar.controller.js"></script>
|
||||||
|
<!-- endinjector -->
|
||||||
|
<!-- endbuild -->
|
||||||
|
</body>
|
||||||
|
</html>
|
3
client/robots.txt
Normal file
3
client/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# robotstxt.org
|
||||||
|
|
||||||
|
User-agent: *
|
15
e2e/main/main.po.js
Normal file
15
e2e/main/main.po.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* This file uses the Page Object pattern to define the main page for tests
|
||||||
|
* https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MainPage = function() {
|
||||||
|
this.heroEl = element(by.css('.hero-unit'));
|
||||||
|
this.h1El = this.heroEl.element(by.css('h1'));
|
||||||
|
this.imgEl = this.heroEl.element(by.css('img'));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = new MainPage();
|
||||||
|
|
16
e2e/main/main.spec.js
Normal file
16
e2e/main/main.spec.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('Main View', function() {
|
||||||
|
var page;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
browser.get('/');
|
||||||
|
page = require('./main.po');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include jumbotron with correct data', function() {
|
||||||
|
expect(page.h1El.getText()).toBe('\'Allo, \'Allo!');
|
||||||
|
expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/);
|
||||||
|
expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman');
|
||||||
|
});
|
||||||
|
});
|
79
karma.conf.js
Normal file
79
karma.conf.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Karma configuration
|
||||||
|
// http://karma-runner.github.io/0.10/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function(config) {
|
||||||
|
config.set({
|
||||||
|
// base path, that will be used to resolve files and exclude
|
||||||
|
basePath: '',
|
||||||
|
|
||||||
|
// testing framework to use (jasmine/mocha/qunit/...)
|
||||||
|
frameworks: ['jasmine'],
|
||||||
|
|
||||||
|
// list of files / patterns to load in the browser
|
||||||
|
files: [
|
||||||
|
'client/bower_components/jquery/dist/jquery.js',
|
||||||
|
'client/bower_components/angular/angular.js',
|
||||||
|
'client/bower_components/angular-mocks/angular-mocks.js',
|
||||||
|
'client/bower_components/angular-resource/angular-resource.js',
|
||||||
|
'client/bower_components/angular-cookies/angular-cookies.js',
|
||||||
|
'client/bower_components/angular-sanitize/angular-sanitize.js',
|
||||||
|
'client/bower_components/angular-route/angular-route.js',
|
||||||
|
'client/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
|
||||||
|
'client/bower_components/lodash/dist/lodash.compat.js',
|
||||||
|
'client/app/app.js',
|
||||||
|
'client/app/app.coffee',
|
||||||
|
'client/app/**/*.js',
|
||||||
|
'client/app/**/*.coffee',
|
||||||
|
'client/components/**/*.js',
|
||||||
|
'client/components/**/*.coffee',
|
||||||
|
'client/app/**/*.jade',
|
||||||
|
'client/components/**/*.jade',
|
||||||
|
'client/app/**/*.html',
|
||||||
|
'client/components/**/*.html'
|
||||||
|
],
|
||||||
|
|
||||||
|
preprocessors: {
|
||||||
|
'**/*.jade': 'ng-jade2js',
|
||||||
|
'**/*.html': 'html2js',
|
||||||
|
'**/*.coffee': 'coffee',
|
||||||
|
},
|
||||||
|
|
||||||
|
ngHtml2JsPreprocessor: {
|
||||||
|
stripPrefix: 'client/'
|
||||||
|
},
|
||||||
|
|
||||||
|
ngJade2JsPreprocessor: {
|
||||||
|
stripPrefix: 'client/'
|
||||||
|
},
|
||||||
|
|
||||||
|
// list of files / patterns to exclude
|
||||||
|
exclude: [],
|
||||||
|
|
||||||
|
// web server port
|
||||||
|
port: 8080,
|
||||||
|
|
||||||
|
// level of logging
|
||||||
|
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
|
||||||
|
|
||||||
|
// enable / disable watching file and executing tests whenever any file changes
|
||||||
|
autoWatch: false,
|
||||||
|
|
||||||
|
|
||||||
|
// Start these browsers, currently available:
|
||||||
|
// - Chrome
|
||||||
|
// - ChromeCanary
|
||||||
|
// - Firefox
|
||||||
|
// - Opera
|
||||||
|
// - Safari (only Mac)
|
||||||
|
// - PhantomJS
|
||||||
|
// - IE (only Windows)
|
||||||
|
browsers: ['PhantomJS'],
|
||||||
|
|
||||||
|
|
||||||
|
// Continuous Integration mode
|
||||||
|
// if true, it capture browsers, run tests and exit
|
||||||
|
singleRun: false
|
||||||
|
});
|
||||||
|
};
|
98
npm-debug.log
Normal file
98
npm-debug.log
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
0 info it worked if it ends with ok
|
||||||
|
1 verbose cli [ '/usr/bin/node',
|
||||||
|
1 verbose cli '/usr/bin/npm',
|
||||||
|
1 verbose cli 'install',
|
||||||
|
1 verbose cli '--save',
|
||||||
|
1 verbose cli 'child_process' ]
|
||||||
|
2 info using npm@1.4.28
|
||||||
|
3 info using node@v0.10.36
|
||||||
|
4 verbose node symlink /usr/bin/node
|
||||||
|
5 warn package.json fs@0.0.2 fs is also the name of a node core module.
|
||||||
|
6 warn package.json fs@0.0.2 No description
|
||||||
|
7 warn package.json fs@0.0.2 No repository field.
|
||||||
|
8 warn package.json fs@0.0.2 No README data
|
||||||
|
9 warn package.json karma-chrome-launcher@0.1.7 No README data
|
||||||
|
10 warn package.json karma-coffee-preprocessor@0.2.1 No README data
|
||||||
|
11 warn package.json karma-firefox-launcher@0.1.4 No README data
|
||||||
|
12 warn package.json karma-phantomjs-launcher@0.1.4 No README data
|
||||||
|
13 warn package.json passport-local@0.1.6 No README data
|
||||||
|
14 verbose readDependencies using package.json deps
|
||||||
|
15 verbose cache add [ 'child_process', null ]
|
||||||
|
16 verbose cache add name=undefined spec="child_process" args=["child_process",null]
|
||||||
|
17 verbose parsed url { protocol: null,
|
||||||
|
17 verbose parsed url slashes: null,
|
||||||
|
17 verbose parsed url auth: null,
|
||||||
|
17 verbose parsed url host: null,
|
||||||
|
17 verbose parsed url port: null,
|
||||||
|
17 verbose parsed url hostname: null,
|
||||||
|
17 verbose parsed url hash: null,
|
||||||
|
17 verbose parsed url search: null,
|
||||||
|
17 verbose parsed url query: null,
|
||||||
|
17 verbose parsed url pathname: 'child_process',
|
||||||
|
17 verbose parsed url path: 'child_process',
|
||||||
|
17 verbose parsed url href: 'child_process' }
|
||||||
|
18 silly lockFile 4845fa97-child-process child_process
|
||||||
|
19 verbose lock child_process /home/docker/.npm/4845fa97-child-process.lock
|
||||||
|
20 silly lockFile 4845fa97-child-process child_process
|
||||||
|
21 silly lockFile 4845fa97-child-process child_process
|
||||||
|
22 verbose addNamed [ 'child_process', '' ]
|
||||||
|
23 verbose addNamed [ null, '*' ]
|
||||||
|
24 silly lockFile 512a7ef6-child-process child_process@
|
||||||
|
25 verbose lock child_process@ /home/docker/.npm/512a7ef6-child-process.lock
|
||||||
|
26 silly addNameRange { name: 'child_process', range: '*', hasData: false }
|
||||||
|
27 verbose request where is /child_process
|
||||||
|
28 verbose request registry https://registry.npmjs.org/
|
||||||
|
29 verbose request id 64c0f71547b88d8b
|
||||||
|
30 verbose url raw /child_process
|
||||||
|
31 verbose url resolving [ 'https://registry.npmjs.org/', './child_process' ]
|
||||||
|
32 verbose url resolved https://registry.npmjs.org/child_process
|
||||||
|
33 verbose request where is https://registry.npmjs.org/child_process
|
||||||
|
34 info trying registry request attempt 1 at 00:16:07
|
||||||
|
35 http GET https://registry.npmjs.org/child_process
|
||||||
|
36 http 404 https://registry.npmjs.org/child_process
|
||||||
|
37 verbose headers { date: 'Thu, 05 Feb 2015 00:15:58 GMT',
|
||||||
|
37 verbose headers server: 'CouchDB/1.6.1 (Erlang OTP/R14B04)',
|
||||||
|
37 verbose headers 'content-type': 'application/json',
|
||||||
|
37 verbose headers 'cache-control': 'max-age=0',
|
||||||
|
37 verbose headers 'content-length': '52',
|
||||||
|
37 verbose headers 'accept-ranges': 'bytes',
|
||||||
|
37 verbose headers via: '1.1 varnish',
|
||||||
|
37 verbose headers age: '0',
|
||||||
|
37 verbose headers 'x-served-by': 'cache-iad2144-IAD',
|
||||||
|
37 verbose headers 'x-cache': 'MISS',
|
||||||
|
37 verbose headers 'x-cache-hits': '0',
|
||||||
|
37 verbose headers 'x-timer': 'S1423095358.512957,VS0,VE62',
|
||||||
|
37 verbose headers 'keep-alive': 'timeout=10, max=50',
|
||||||
|
37 verbose headers connection: 'Keep-Alive' }
|
||||||
|
38 silly registry.get cb [ 404,
|
||||||
|
38 silly registry.get { date: 'Thu, 05 Feb 2015 00:15:58 GMT',
|
||||||
|
38 silly registry.get server: 'CouchDB/1.6.1 (Erlang OTP/R14B04)',
|
||||||
|
38 silly registry.get 'content-type': 'application/json',
|
||||||
|
38 silly registry.get 'cache-control': 'max-age=0',
|
||||||
|
38 silly registry.get 'content-length': '52',
|
||||||
|
38 silly registry.get 'accept-ranges': 'bytes',
|
||||||
|
38 silly registry.get via: '1.1 varnish',
|
||||||
|
38 silly registry.get age: '0',
|
||||||
|
38 silly registry.get 'x-served-by': 'cache-iad2144-IAD',
|
||||||
|
38 silly registry.get 'x-cache': 'MISS',
|
||||||
|
38 silly registry.get 'x-cache-hits': '0',
|
||||||
|
38 silly registry.get 'x-timer': 'S1423095358.512957,VS0,VE62',
|
||||||
|
38 silly registry.get 'keep-alive': 'timeout=10, max=50',
|
||||||
|
38 silly registry.get connection: 'Keep-Alive' } ]
|
||||||
|
39 silly lockFile 512a7ef6-child-process child_process@
|
||||||
|
40 silly lockFile 512a7ef6-child-process child_process@
|
||||||
|
41 error 404 404 Not Found: child_process
|
||||||
|
41 error 404
|
||||||
|
41 error 404 'child_process' is not in the npm registry.
|
||||||
|
41 error 404 You should bug the author to publish it
|
||||||
|
41 error 404 It was specified as a dependency of 'markdown-format-wdiff'
|
||||||
|
41 error 404
|
||||||
|
41 error 404 Note that you can also install from a
|
||||||
|
41 error 404 tarball, folder, or http url, or git url.
|
||||||
|
42 error System Linux 3.13.0-32-generic
|
||||||
|
43 error command "/usr/bin/node" "/usr/bin/npm" "install" "--save" "child_process"
|
||||||
|
44 error cwd /data/markdown-format-wdiff
|
||||||
|
45 error node -v v0.10.36
|
||||||
|
46 error npm -v 1.4.28
|
||||||
|
47 error code E404
|
||||||
|
48 verbose exit [ 1, true ]
|
94
package.json
Normal file
94
package.json
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"name": "markdown-format-wdiff",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "server/app.js",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "~1.5.0",
|
||||||
|
"composable-middleware": "^0.3.0",
|
||||||
|
"compression": "~1.0.1",
|
||||||
|
"connect-mongo": "^0.4.1",
|
||||||
|
"cookie-parser": "~1.0.1",
|
||||||
|
"errorhandler": "~1.0.0",
|
||||||
|
"express": "~4.0.0",
|
||||||
|
"express-jwt": "^0.1.3",
|
||||||
|
"express-session": "~1.0.2",
|
||||||
|
"jade": "~1.2.0",
|
||||||
|
"jsonwebtoken": "^0.3.0",
|
||||||
|
"lodash": "~2.4.1",
|
||||||
|
"method-override": "~1.0.0",
|
||||||
|
"mongoose": "~3.8.8",
|
||||||
|
"morgan": "~1.0.0",
|
||||||
|
"passport": "~0.2.0",
|
||||||
|
"passport-facebook": "latest",
|
||||||
|
"passport-google-oauth": "latest",
|
||||||
|
"passport-local": "~0.1.6",
|
||||||
|
"passport-twitter": "latest",
|
||||||
|
"serve-favicon": "~2.0.1",
|
||||||
|
"temp": "^0.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"grunt": "~0.4.4",
|
||||||
|
"grunt-autoprefixer": "~0.7.2",
|
||||||
|
"grunt-wiredep": "~1.8.0",
|
||||||
|
"grunt-concurrent": "~0.5.0",
|
||||||
|
"grunt-contrib-clean": "~0.5.0",
|
||||||
|
"grunt-contrib-concat": "~0.4.0",
|
||||||
|
"grunt-contrib-copy": "~0.5.0",
|
||||||
|
"grunt-contrib-cssmin": "~0.9.0",
|
||||||
|
"grunt-contrib-htmlmin": "~0.2.0",
|
||||||
|
"grunt-contrib-imagemin": "~0.7.1",
|
||||||
|
"grunt-contrib-jshint": "~0.10.0",
|
||||||
|
"grunt-contrib-uglify": "~0.4.0",
|
||||||
|
"grunt-contrib-watch": "~0.6.1",
|
||||||
|
"grunt-contrib-jade": "^0.11.0",
|
||||||
|
"grunt-google-cdn": "~0.4.0",
|
||||||
|
"grunt-newer": "~0.7.0",
|
||||||
|
"grunt-ng-annotate": "^0.2.3",
|
||||||
|
"grunt-rev": "~0.1.0",
|
||||||
|
"grunt-svgmin": "~0.4.0",
|
||||||
|
"grunt-usemin": "~2.1.1",
|
||||||
|
"grunt-env": "~0.4.1",
|
||||||
|
"grunt-node-inspector": "~0.1.5",
|
||||||
|
"grunt-nodemon": "~0.2.0",
|
||||||
|
"grunt-angular-templates": "^0.5.4",
|
||||||
|
"grunt-dom-munger": "^3.4.0",
|
||||||
|
"grunt-protractor-runner": "^1.1.0",
|
||||||
|
"grunt-asset-injector": "^0.1.0",
|
||||||
|
"grunt-karma": "~0.8.2",
|
||||||
|
"grunt-build-control": "DaftMonk/grunt-build-control",
|
||||||
|
"grunt-mocha-test": "~0.10.2",
|
||||||
|
"grunt-contrib-sass": "^0.7.3",
|
||||||
|
"jit-grunt": "^0.5.0",
|
||||||
|
"time-grunt": "~0.3.1",
|
||||||
|
"grunt-express-server": "~0.4.17",
|
||||||
|
"grunt-open": "~0.2.3",
|
||||||
|
"open": "~0.0.4",
|
||||||
|
"jshint-stylish": "~0.1.5",
|
||||||
|
"connect-livereload": "~0.4.0",
|
||||||
|
"karma-ng-scenario": "~0.1.0",
|
||||||
|
"karma-firefox-launcher": "~0.1.3",
|
||||||
|
"karma-script-launcher": "~0.1.0",
|
||||||
|
"karma-html2js-preprocessor": "~0.1.0",
|
||||||
|
"karma-ng-jade2js-preprocessor": "^0.1.2",
|
||||||
|
"karma-jasmine": "~0.1.5",
|
||||||
|
"karma-chrome-launcher": "~0.1.3",
|
||||||
|
"requirejs": "~2.1.11",
|
||||||
|
"karma-requirejs": "~0.2.1",
|
||||||
|
"karma-coffee-preprocessor": "~0.2.1",
|
||||||
|
"karma-jade-preprocessor": "0.0.11",
|
||||||
|
"karma-phantomjs-launcher": "~0.1.4",
|
||||||
|
"karma": "~0.12.9",
|
||||||
|
"karma-ng-html2js-preprocessor": "~0.1.0",
|
||||||
|
"supertest": "~0.11.0",
|
||||||
|
"should": "~3.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server/app.js",
|
||||||
|
"test": "grunt test",
|
||||||
|
"update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
50
protractor.conf.js
Normal file
50
protractor.conf.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Protractor configuration
|
||||||
|
// https://github.com/angular/protractor/blob/master/referenceConf.js
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.config = {
|
||||||
|
// The timeout for each script run on the browser. This should be longer
|
||||||
|
// than the maximum time your application needs to stabilize between tasks.
|
||||||
|
allScriptsTimeout: 110000,
|
||||||
|
|
||||||
|
// A base URL for your application under test. Calls to protractor.get()
|
||||||
|
// with relative paths will be prepended with this.
|
||||||
|
baseUrl: 'http://localhost:' + (process.env.PORT || '9000'),
|
||||||
|
|
||||||
|
// If true, only chromedriver will be started, not a standalone selenium.
|
||||||
|
// Tests for browsers other than chrome will not run.
|
||||||
|
chromeOnly: true,
|
||||||
|
|
||||||
|
// list of files / patterns to load in the browser
|
||||||
|
specs: [
|
||||||
|
'e2e/**/*.spec.js'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Patterns to exclude.
|
||||||
|
exclude: [],
|
||||||
|
|
||||||
|
// ----- Capabilities to be passed to the webdriver instance ----
|
||||||
|
//
|
||||||
|
// For a full list of available capabilities, see
|
||||||
|
// https://code.google.com/p/selenium/wiki/DesiredCapabilities
|
||||||
|
// and
|
||||||
|
// https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js
|
||||||
|
capabilities: {
|
||||||
|
'browserName': 'chrome'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----- The test framework -----
|
||||||
|
//
|
||||||
|
// Jasmine and Cucumber are fully supported as a test and assertion framework.
|
||||||
|
// Mocha has limited beta support. You will need to include your own
|
||||||
|
// assertion framework if working with mocha.
|
||||||
|
framework: 'jasmine',
|
||||||
|
|
||||||
|
// ----- Options to be passed to minijasminenode -----
|
||||||
|
//
|
||||||
|
// See the full list at https://github.com/juliemr/minijasminenode
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
defaultTimeoutInterval: 30000
|
||||||
|
}
|
||||||
|
};
|
15
server/.jshintrc
Normal file
15
server/.jshintrc
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"node": true,
|
||||||
|
"esnext": true,
|
||||||
|
"bitwise": true,
|
||||||
|
"eqeqeq": true,
|
||||||
|
"immed": true,
|
||||||
|
"latedef": "nofunc",
|
||||||
|
"newcap": true,
|
||||||
|
"noarg": true,
|
||||||
|
"regexp": true,
|
||||||
|
"undef": true,
|
||||||
|
"smarttabs": true,
|
||||||
|
"asi": true,
|
||||||
|
"debug": true
|
||||||
|
}
|
11
server/.jshintrc-spec
Normal file
11
server/.jshintrc-spec
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": ".jshintrc",
|
||||||
|
"globals": {
|
||||||
|
"describe": true,
|
||||||
|
"it": true,
|
||||||
|
"before": true,
|
||||||
|
"beforeEach": true,
|
||||||
|
"after": true,
|
||||||
|
"afterEach": true
|
||||||
|
}
|
||||||
|
}
|
15
server/api/thing/index.js
Normal file
15
server/api/thing/index.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var controller = require('./thing.controller');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', controller.index);
|
||||||
|
router.get('/:id', controller.show);
|
||||||
|
router.post('/', controller.create);
|
||||||
|
router.put('/:id', controller.update);
|
||||||
|
router.patch('/:id', controller.update);
|
||||||
|
router.delete('/:id', controller.destroy);
|
||||||
|
|
||||||
|
module.exports = router;
|
68
server/api/thing/thing.controller.js
Normal file
68
server/api/thing/thing.controller.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Using Rails-like standard naming convention for endpoints.
|
||||||
|
* GET /things -> index
|
||||||
|
* POST /things -> create
|
||||||
|
* GET /things/:id -> show
|
||||||
|
* PUT /things/:id -> update
|
||||||
|
* DELETE /things/:id -> destroy
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var _ = require('lodash');
|
||||||
|
var Thing = require('./thing.model');
|
||||||
|
|
||||||
|
// Get list of things
|
||||||
|
exports.index = function(req, res) {
|
||||||
|
Thing.find(function (err, things) {
|
||||||
|
if(err) { return handleError(res, err); }
|
||||||
|
return res.json(200, things);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get a single thing
|
||||||
|
exports.show = function(req, res) {
|
||||||
|
Thing.findById(req.params.id, function (err, thing) {
|
||||||
|
if(err) { return handleError(res, err); }
|
||||||
|
if(!thing) { return res.send(404); }
|
||||||
|
return res.json(thing);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a new thing in the DB.
|
||||||
|
exports.create = function(req, res) {
|
||||||
|
Thing.create(req.body, function(err, thing) {
|
||||||
|
if(err) { return handleError(res, err); }
|
||||||
|
return res.json(201, thing);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updates an existing thing in the DB.
|
||||||
|
exports.update = function(req, res) {
|
||||||
|
if(req.body._id) { delete req.body._id; }
|
||||||
|
Thing.findById(req.params.id, function (err, thing) {
|
||||||
|
if (err) { return handleError(res, err); }
|
||||||
|
if(!thing) { return res.send(404); }
|
||||||
|
var updated = _.merge(thing, req.body);
|
||||||
|
updated.save(function (err) {
|
||||||
|
if (err) { return handleError(res, err); }
|
||||||
|
return res.json(200, thing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deletes a thing from the DB.
|
||||||
|
exports.destroy = function(req, res) {
|
||||||
|
Thing.findById(req.params.id, function (err, thing) {
|
||||||
|
if(err) { return handleError(res, err); }
|
||||||
|
if(!thing) { return res.send(404); }
|
||||||
|
thing.remove(function(err) {
|
||||||
|
if(err) { return handleError(res, err); }
|
||||||
|
return res.send(204);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleError(res, err) {
|
||||||
|
return res.send(500, err);
|
||||||
|
}
|
12
server/api/thing/thing.model.js
Normal file
12
server/api/thing/thing.model.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var mongoose = require('mongoose'),
|
||||||
|
Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
var ThingSchema = new Schema({
|
||||||
|
name: String,
|
||||||
|
info: String,
|
||||||
|
active: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Thing', ThingSchema);
|
20
server/api/thing/thing.spec.js
Normal file
20
server/api/thing/thing.spec.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var should = require('should');
|
||||||
|
var app = require('../../app');
|
||||||
|
var request = require('supertest');
|
||||||
|
|
||||||
|
describe('GET /api/things', function() {
|
||||||
|
|
||||||
|
it('should respond with JSON array', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/api/things')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
res.body.should.be.instanceof(Array);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
17
server/api/user/index.js
Normal file
17
server/api/user/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var controller = require('./user.controller');
|
||||||
|
var config = require('../../config/environment');
|
||||||
|
var auth = require('../../auth/auth.service');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', auth.hasRole('admin'), controller.index);
|
||||||
|
router.delete('/:id', auth.hasRole('admin'), controller.destroy);
|
||||||
|
router.get('/me', auth.isAuthenticated(), controller.me);
|
||||||
|
router.put('/:id/password', auth.isAuthenticated(), controller.changePassword);
|
||||||
|
router.get('/:id', auth.isAuthenticated(), controller.show);
|
||||||
|
router.post('/', controller.create);
|
||||||
|
|
||||||
|
module.exports = router;
|
101
server/api/user/user.controller.js
Normal file
101
server/api/user/user.controller.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var User = require('./user.model');
|
||||||
|
var passport = require('passport');
|
||||||
|
var config = require('../../config/environment');
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
var validationError = function(res, err) {
|
||||||
|
return res.json(422, err);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of users
|
||||||
|
* restriction: 'admin'
|
||||||
|
*/
|
||||||
|
exports.index = function(req, res) {
|
||||||
|
User.find({}, '-salt -hashedPassword', function (err, users) {
|
||||||
|
if(err) return res.send(500, err);
|
||||||
|
res.json(200, users);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user
|
||||||
|
*/
|
||||||
|
exports.create = function (req, res, next) {
|
||||||
|
var newUser = new User(req.body);
|
||||||
|
newUser.provider = 'local';
|
||||||
|
newUser.role = 'user';
|
||||||
|
newUser.save(function(err, user) {
|
||||||
|
if (err) return validationError(res, err);
|
||||||
|
var token = jwt.sign({_id: user._id }, config.secrets.session, { expiresInMinutes: 60*5 });
|
||||||
|
res.json({ token: token });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single user
|
||||||
|
*/
|
||||||
|
exports.show = function (req, res, next) {
|
||||||
|
var userId = req.params.id;
|
||||||
|
|
||||||
|
User.findById(userId, function (err, user) {
|
||||||
|
if (err) return next(err);
|
||||||
|
if (!user) return res.send(401);
|
||||||
|
res.json(user.profile);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a user
|
||||||
|
* restriction: 'admin'
|
||||||
|
*/
|
||||||
|
exports.destroy = function(req, res) {
|
||||||
|
User.findByIdAndRemove(req.params.id, function(err, user) {
|
||||||
|
if(err) return res.send(500, err);
|
||||||
|
return res.send(204);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a users password
|
||||||
|
*/
|
||||||
|
exports.changePassword = function(req, res, next) {
|
||||||
|
var userId = req.user._id;
|
||||||
|
var oldPass = String(req.body.oldPassword);
|
||||||
|
var newPass = String(req.body.newPassword);
|
||||||
|
|
||||||
|
User.findById(userId, function (err, user) {
|
||||||
|
if(user.authenticate(oldPass)) {
|
||||||
|
user.password = newPass;
|
||||||
|
user.save(function(err) {
|
||||||
|
if (err) return validationError(res, err);
|
||||||
|
res.send(200);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.send(403);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get my info
|
||||||
|
*/
|
||||||
|
exports.me = function(req, res, next) {
|
||||||
|
var userId = req.user._id;
|
||||||
|
User.findOne({
|
||||||
|
_id: userId
|
||||||
|
}, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt
|
||||||
|
if (err) return next(err);
|
||||||
|
if (!user) return res.json(401);
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication callback
|
||||||
|
*/
|
||||||
|
exports.authCallback = function(req, res, next) {
|
||||||
|
res.redirect('/');
|
||||||
|
};
|
149
server/api/user/user.model.js
Normal file
149
server/api/user/user.model.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var mongoose = require('mongoose');
|
||||||
|
var Schema = mongoose.Schema;
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var authTypes = ['github', 'twitter', 'facebook', 'google'];
|
||||||
|
|
||||||
|
var UserSchema = new Schema({
|
||||||
|
name: String,
|
||||||
|
email: { type: String, lowercase: true },
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
default: 'user'
|
||||||
|
},
|
||||||
|
hashedPassword: String,
|
||||||
|
provider: String,
|
||||||
|
salt: String,
|
||||||
|
facebook: {},
|
||||||
|
twitter: {},
|
||||||
|
google: {},
|
||||||
|
github: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtuals
|
||||||
|
*/
|
||||||
|
UserSchema
|
||||||
|
.virtual('password')
|
||||||
|
.set(function(password) {
|
||||||
|
this._password = password;
|
||||||
|
this.salt = this.makeSalt();
|
||||||
|
this.hashedPassword = this.encryptPassword(password);
|
||||||
|
})
|
||||||
|
.get(function() {
|
||||||
|
return this._password;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public profile information
|
||||||
|
UserSchema
|
||||||
|
.virtual('profile')
|
||||||
|
.get(function() {
|
||||||
|
return {
|
||||||
|
'name': this.name,
|
||||||
|
'role': this.role
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Non-sensitive info we'll be putting in the token
|
||||||
|
UserSchema
|
||||||
|
.virtual('token')
|
||||||
|
.get(function() {
|
||||||
|
return {
|
||||||
|
'_id': this._id,
|
||||||
|
'role': this.role
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Validate empty email
|
||||||
|
UserSchema
|
||||||
|
.path('email')
|
||||||
|
.validate(function(email) {
|
||||||
|
if (authTypes.indexOf(this.provider) !== -1) return true;
|
||||||
|
return email.length;
|
||||||
|
}, 'Email cannot be blank');
|
||||||
|
|
||||||
|
// Validate empty password
|
||||||
|
UserSchema
|
||||||
|
.path('hashedPassword')
|
||||||
|
.validate(function(hashedPassword) {
|
||||||
|
if (authTypes.indexOf(this.provider) !== -1) return true;
|
||||||
|
return hashedPassword.length;
|
||||||
|
}, 'Password cannot be blank');
|
||||||
|
|
||||||
|
// Validate email is not taken
|
||||||
|
UserSchema
|
||||||
|
.path('email')
|
||||||
|
.validate(function(value, respond) {
|
||||||
|
var self = this;
|
||||||
|
this.constructor.findOne({email: value}, function(err, user) {
|
||||||
|
if(err) throw err;
|
||||||
|
if(user) {
|
||||||
|
if(self.id === user.id) return respond(true);
|
||||||
|
return respond(false);
|
||||||
|
}
|
||||||
|
respond(true);
|
||||||
|
});
|
||||||
|
}, 'The specified email address is already in use.');
|
||||||
|
|
||||||
|
var validatePresenceOf = function(value) {
|
||||||
|
return value && value.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-save hook
|
||||||
|
*/
|
||||||
|
UserSchema
|
||||||
|
.pre('save', function(next) {
|
||||||
|
if (!this.isNew) return next();
|
||||||
|
|
||||||
|
if (!validatePresenceOf(this.hashedPassword) && authTypes.indexOf(this.provider) === -1)
|
||||||
|
next(new Error('Invalid password'));
|
||||||
|
else
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods
|
||||||
|
*/
|
||||||
|
UserSchema.methods = {
|
||||||
|
/**
|
||||||
|
* Authenticate - check if the passwords are the same
|
||||||
|
*
|
||||||
|
* @param {String} plainText
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
authenticate: function(plainText) {
|
||||||
|
return this.encryptPassword(plainText) === this.hashedPassword;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make salt
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
makeSalt: function() {
|
||||||
|
return crypto.randomBytes(16).toString('base64');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt password
|
||||||
|
*
|
||||||
|
* @param {String} password
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
encryptPassword: function(password) {
|
||||||
|
if (!password || !this.salt) return '';
|
||||||
|
var salt = new Buffer(this.salt, 'base64');
|
||||||
|
return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = mongoose.model('User', UserSchema);
|
60
server/api/user/user.model.spec.js
Normal file
60
server/api/user/user.model.spec.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var should = require('should');
|
||||||
|
var app = require('../../app');
|
||||||
|
var User = require('./user.model');
|
||||||
|
|
||||||
|
var user = new User({
|
||||||
|
provider: 'local',
|
||||||
|
name: 'Fake User',
|
||||||
|
email: 'test@test.com',
|
||||||
|
password: 'password'
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User Model', function() {
|
||||||
|
before(function(done) {
|
||||||
|
// Clear users before testing
|
||||||
|
User.remove().exec().then(function() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function(done) {
|
||||||
|
User.remove().exec().then(function() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should begin with no users', function(done) {
|
||||||
|
User.find({}, function(err, users) {
|
||||||
|
users.should.have.length(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when saving a duplicate user', function(done) {
|
||||||
|
user.save(function() {
|
||||||
|
var userDup = new User(user);
|
||||||
|
userDup.save(function(err) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when saving without an email', function(done) {
|
||||||
|
user.email = '';
|
||||||
|
user.save(function(err) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should authenticate user if password is valid", function() {
|
||||||
|
return user.authenticate('password').should.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not authenticate user if password is invalid", function() {
|
||||||
|
return user.authenticate('blah').should.not.be.true;
|
||||||
|
});
|
||||||
|
});
|
12
server/api/wdiff/index.js
Normal file
12
server/api/wdiff/index.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var controller = require('./wdiff.controller');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.post('/', controller.compare);
|
||||||
|
router.post('/markdown', controller.compareMarkdown);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
94
server/api/wdiff/wdiff.controller.js
Normal file
94
server/api/wdiff/wdiff.controller.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var _ = require('lodash'),
|
||||||
|
temp = require('temp'),
|
||||||
|
fs = require('fs'),
|
||||||
|
exec = require('child_process').exec;
|
||||||
|
|
||||||
|
// Automatically track and cleanup files at exit
|
||||||
|
temp.track();
|
||||||
|
|
||||||
|
exports.compare = function(req, res) {
|
||||||
|
doCompare(req, res, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.compareMarkdown = function(req, res) {
|
||||||
|
doCompare(req, res, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform a comparison
|
||||||
|
// The request should be a json object with two string fields: 'a' and 'b'
|
||||||
|
function doCompare(req, res, isMarkdown) {
|
||||||
|
|
||||||
|
//check for properly formatted request
|
||||||
|
if (req.headers["content-type"].toLowerCase() != "application/json")
|
||||||
|
return handleError(res, {error: "Content-type must be 'application/json'"});
|
||||||
|
|
||||||
|
if (!req.body.a || !req.body.b)
|
||||||
|
return handleError(res, {error: "Request data should be of the form {a:'text a', b:'text b'}"});
|
||||||
|
|
||||||
|
var a = req.body.a;
|
||||||
|
var b = req.body.b;
|
||||||
|
|
||||||
|
//!!! this nested file-open is not a good pattern
|
||||||
|
// better would be to use promises and write the two files asynchronously
|
||||||
|
|
||||||
|
// open the first file
|
||||||
|
temp.open('wdiffa-', function(err, filea) {
|
||||||
|
//handle errors
|
||||||
|
if (err)
|
||||||
|
return handleError(res, err);
|
||||||
|
|
||||||
|
//write the string to the file
|
||||||
|
fs.write(filea.fd, a);
|
||||||
|
|
||||||
|
//close the file
|
||||||
|
fs.close(filea.fd, function(err) {
|
||||||
|
if (err)
|
||||||
|
return handleError(res, err);
|
||||||
|
|
||||||
|
//open the second file
|
||||||
|
temp.open('wdiffa-', function(err, fileb) {
|
||||||
|
if (err)
|
||||||
|
return handleError(res, err);
|
||||||
|
|
||||||
|
//write the string to the file
|
||||||
|
fs.write(fileb.fd, b);
|
||||||
|
|
||||||
|
//close the file
|
||||||
|
fs.close(fileb.fd, function(err) {
|
||||||
|
if (err)
|
||||||
|
return handleError(res, err);
|
||||||
|
|
||||||
|
var cmd = "./bin/wdiff " + filea.path + " " +fileb.path;
|
||||||
|
exec(cmd, function(err, stdout) {
|
||||||
|
|
||||||
|
if (err && err.code!=1 && err.code!=0) {
|
||||||
|
return handleError(res,err);
|
||||||
|
}
|
||||||
|
|
||||||
|
//if no difference was found by wdiff, err.code will be 0
|
||||||
|
var wdiffSame;
|
||||||
|
wdiffSame = (err && err.code == 0) ? true:false;
|
||||||
|
|
||||||
|
//sub del and ins
|
||||||
|
var markdown = stdout;
|
||||||
|
markdown = markdown.replace(/\[-/g, '<del>');
|
||||||
|
markdown = markdown.replace(/-\]/g, '</del>');
|
||||||
|
markdown = markdown.replace(/{\+/g, '<ins>');
|
||||||
|
markdown = markdown.replace(/\+}/g, '</ins>');
|
||||||
|
|
||||||
|
var resData = {wdiff:stdout, same: wdiffSame, markdown: markdown};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleError(res, err) {
|
||||||
|
return res.send(500, err);
|
||||||
|
}
|
32
server/app.js
Normal file
32
server/app.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Main application file
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Set default node environment to development
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var mongoose = require('mongoose');
|
||||||
|
var config = require('./config/environment');
|
||||||
|
|
||||||
|
// Connect to database
|
||||||
|
mongoose.connect(config.mongo.uri, config.mongo.options);
|
||||||
|
|
||||||
|
// Populate DB with sample data
|
||||||
|
if(config.seedDB) { require('./config/seed'); }
|
||||||
|
|
||||||
|
// Setup server
|
||||||
|
var app = express();
|
||||||
|
var server = require('http').createServer(app);
|
||||||
|
require('./config/express')(app);
|
||||||
|
require('./routes')(app);
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
server.listen(config.port, config.ip, function () {
|
||||||
|
console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose app
|
||||||
|
exports = module.exports = app;
|
76
server/auth/auth.service.js
Normal file
76
server/auth/auth.service.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var mongoose = require('mongoose');
|
||||||
|
var passport = require('passport');
|
||||||
|
var config = require('../config/environment');
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
|
var expressJwt = require('express-jwt');
|
||||||
|
var compose = require('composable-middleware');
|
||||||
|
var User = require('../api/user/user.model');
|
||||||
|
var validateJwt = expressJwt({ secret: config.secrets.session });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the user object to the request if authenticated
|
||||||
|
* Otherwise returns 403
|
||||||
|
*/
|
||||||
|
function isAuthenticated() {
|
||||||
|
return compose()
|
||||||
|
// Validate jwt
|
||||||
|
.use(function(req, res, next) {
|
||||||
|
// allow access_token to be passed through query parameter as well
|
||||||
|
if(req.query && req.query.hasOwnProperty('access_token')) {
|
||||||
|
req.headers.authorization = 'Bearer ' + req.query.access_token;
|
||||||
|
}
|
||||||
|
validateJwt(req, res, next);
|
||||||
|
})
|
||||||
|
// Attach user to request
|
||||||
|
.use(function(req, res, next) {
|
||||||
|
User.findById(req.user._id, function (err, user) {
|
||||||
|
if (err) return next(err);
|
||||||
|
if (!user) return res.send(401);
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user role meets the minimum requirements of the route
|
||||||
|
*/
|
||||||
|
function hasRole(roleRequired) {
|
||||||
|
if (!roleRequired) throw new Error('Required role needs to be set');
|
||||||
|
|
||||||
|
return compose()
|
||||||
|
.use(isAuthenticated())
|
||||||
|
.use(function meetsRequirements(req, res, next) {
|
||||||
|
if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.send(403);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a jwt token signed by the app secret
|
||||||
|
*/
|
||||||
|
function signToken(id) {
|
||||||
|
return jwt.sign({ _id: id }, config.secrets.session, { expiresInMinutes: 60*5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set token cookie directly for oAuth strategies
|
||||||
|
*/
|
||||||
|
function setTokenCookie(req, res) {
|
||||||
|
if (!req.user) return res.json(404, { message: 'Something went wrong, please try again.'});
|
||||||
|
var token = signToken(req.user._id, req.user.role);
|
||||||
|
res.cookie('token', JSON.stringify(token));
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isAuthenticated = isAuthenticated;
|
||||||
|
exports.hasRole = hasRole;
|
||||||
|
exports.signToken = signToken;
|
||||||
|
exports.setTokenCookie = setTokenCookie;
|
21
server/auth/facebook/index.js
Normal file
21
server/auth/facebook/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var passport = require('passport');
|
||||||
|
var auth = require('../auth.service');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router
|
||||||
|
.get('/', passport.authenticate('facebook', {
|
||||||
|
scope: ['email', 'user_about_me'],
|
||||||
|
failureRedirect: '/signup',
|
||||||
|
session: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
.get('/callback', passport.authenticate('facebook', {
|
||||||
|
failureRedirect: '/signup',
|
||||||
|
session: false
|
||||||
|
}), auth.setTokenCookie);
|
||||||
|
|
||||||
|
module.exports = router;
|
37
server/auth/facebook/passport.js
Normal file
37
server/auth/facebook/passport.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
var passport = require('passport');
|
||||||
|
var FacebookStrategy = require('passport-facebook').Strategy;
|
||||||
|
|
||||||
|
exports.setup = function (User, config) {
|
||||||
|
passport.use(new FacebookStrategy({
|
||||||
|
clientID: config.facebook.clientID,
|
||||||
|
clientSecret: config.facebook.clientSecret,
|
||||||
|
callbackURL: config.facebook.callbackURL
|
||||||
|
},
|
||||||
|
function(accessToken, refreshToken, profile, done) {
|
||||||
|
User.findOne({
|
||||||
|
'facebook.id': profile.id
|
||||||
|
},
|
||||||
|
function(err, user) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
user = new User({
|
||||||
|
name: profile.displayName,
|
||||||
|
email: profile.emails[0].value,
|
||||||
|
role: 'user',
|
||||||
|
username: profile.username,
|
||||||
|
provider: 'facebook',
|
||||||
|
facebook: profile._json
|
||||||
|
});
|
||||||
|
user.save(function(err) {
|
||||||
|
if (err) done(err);
|
||||||
|
return done(err, user);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return done(err, user);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
24
server/auth/google/index.js
Normal file
24
server/auth/google/index.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var passport = require('passport');
|
||||||
|
var auth = require('../auth.service');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router
|
||||||
|
.get('/', passport.authenticate('google', {
|
||||||
|
failureRedirect: '/signup',
|
||||||
|
scope: [
|
||||||
|
'https://www.googleapis.com/auth/userinfo.profile',
|
||||||
|
'https://www.googleapis.com/auth/userinfo.email'
|
||||||
|
],
|
||||||
|
session: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
.get('/callback', passport.authenticate('google', {
|
||||||
|
failureRedirect: '/signup',
|
||||||
|
session: false
|
||||||
|
}), auth.setTokenCookie);
|
||||||
|
|
||||||
|
module.exports = router;
|
33
server/auth/google/passport.js
Normal file
33
server/auth/google/passport.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
var passport = require('passport');
|
||||||
|
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
|
||||||
|
|
||||||
|
exports.setup = function (User, config) {
|
||||||
|
passport.use(new GoogleStrategy({
|
||||||
|
clientID: config.google.clientID,
|
||||||
|
clientSecret: config.google.clientSecret,
|
||||||
|
callbackURL: config.google.callbackURL
|
||||||
|
},
|
||||||
|
function(accessToken, refreshToken, profile, done) {
|
||||||
|
User.findOne({
|
||||||
|
'google.id': profile.id
|
||||||
|
}, function(err, user) {
|
||||||
|
if (!user) {
|
||||||
|
user = new User({
|
||||||
|
name: profile.displayName,
|
||||||
|
email: profile.emails[0].value,
|
||||||
|
role: 'user',
|
||||||
|
username: profile.username,
|
||||||
|
provider: 'google',
|
||||||
|
google: profile._json
|
||||||
|
});
|
||||||
|
user.save(function(err) {
|
||||||
|
if (err) done(err);
|
||||||
|
return done(err, user);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return done(err, user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
21
server/auth/index.js
Normal file
21
server/auth/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var passport = require('passport');
|
||||||
|
var config = require('../config/environment');
|
||||||
|
var User = require('../api/user/user.model');
|
||||||
|
|
||||||
|
// Passport Configuration
|
||||||
|
require('./local/passport').setup(User, config);
|
||||||
|
require('./facebook/passport').setup(User, config);
|
||||||
|
require('./google/passport').setup(User, config);
|
||||||
|
require('./twitter/passport').setup(User, config);
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.use('/local', require('./local'));
|
||||||
|
router.use('/facebook', require('./facebook'));
|
||||||
|
router.use('/twitter', require('./twitter'));
|
||||||
|
router.use('/google', require('./google'));
|
||||||
|
|
||||||
|
module.exports = router;
|
20
server/auth/local/index.js
Normal file
20
server/auth/local/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var passport = require('passport');
|
||||||
|
var auth = require('../auth.service');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router.post('/', function(req, res, next) {
|
||||||
|
passport.authenticate('local', function (err, user, info) {
|
||||||
|
var error = err || info;
|
||||||
|
if (error) return res.json(401, error);
|
||||||
|
if (!user) return res.json(404, {message: 'Something went wrong, please try again.'});
|
||||||
|
|
||||||
|
var token = auth.signToken(user._id, user.role);
|
||||||
|
res.json({token: token});
|
||||||
|
})(req, res, next)
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
25
server/auth/local/passport.js
Normal file
25
server/auth/local/passport.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
var passport = require('passport');
|
||||||
|
var LocalStrategy = require('passport-local').Strategy;
|
||||||
|
|
||||||
|
exports.setup = function (User, config) {
|
||||||
|
passport.use(new LocalStrategy({
|
||||||
|
usernameField: 'email',
|
||||||
|
passwordField: 'password' // this is the virtual field on the model
|
||||||
|
},
|
||||||
|
function(email, password, done) {
|
||||||
|
User.findOne({
|
||||||
|
email: email.toLowerCase()
|
||||||
|
}, function(err, user) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return done(null, false, { message: 'This email is not registered.' });
|
||||||
|
}
|
||||||
|
if (!user.authenticate(password)) {
|
||||||
|
return done(null, false, { message: 'This password is not correct.' });
|
||||||
|
}
|
||||||
|
return done(null, user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
20
server/auth/twitter/index.js
Normal file
20
server/auth/twitter/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var passport = require('passport');
|
||||||
|
var auth = require('../auth.service');
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
router
|
||||||
|
.get('/', passport.authenticate('twitter', {
|
||||||
|
failureRedirect: '/signup',
|
||||||
|
session: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
.get('/callback', passport.authenticate('twitter', {
|
||||||
|
failureRedirect: '/signup',
|
||||||
|
session: false
|
||||||
|
}), auth.setTokenCookie);
|
||||||
|
|
||||||
|
module.exports = router;
|
35
server/auth/twitter/passport.js
Normal file
35
server/auth/twitter/passport.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
exports.setup = function (User, config) {
|
||||||
|
var passport = require('passport');
|
||||||
|
var TwitterStrategy = require('passport-twitter').Strategy;
|
||||||
|
|
||||||
|
passport.use(new TwitterStrategy({
|
||||||
|
consumerKey: config.twitter.clientID,
|
||||||
|
consumerSecret: config.twitter.clientSecret,
|
||||||
|
callbackURL: config.twitter.callbackURL
|
||||||
|
},
|
||||||
|
function(token, tokenSecret, profile, done) {
|
||||||
|
User.findOne({
|
||||||
|
'twitter.id_str': profile.id
|
||||||
|
}, function(err, user) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
user = new User({
|
||||||
|
name: profile.displayName,
|
||||||
|
username: profile.username,
|
||||||
|
role: 'user',
|
||||||
|
provider: 'twitter',
|
||||||
|
twitter: profile._json
|
||||||
|
});
|
||||||
|
user.save(function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
return done(err, user);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return done(err, user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
20
server/components/errors/index.js
Normal file
20
server/components/errors/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Error responses
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports[404] = function pageNotFound(req, res) {
|
||||||
|
var viewFilePath = '404';
|
||||||
|
var statusCode = 404;
|
||||||
|
var result = {
|
||||||
|
status: statusCode
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(result.status);
|
||||||
|
res.render(viewFilePath, function (err) {
|
||||||
|
if (err) { return res.json(result, result.status); }
|
||||||
|
|
||||||
|
res.render(viewFilePath);
|
||||||
|
});
|
||||||
|
};
|
12
server/config/environment/development.js
Normal file
12
server/config/environment/development.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Development specific configuration
|
||||||
|
// ==================================
|
||||||
|
module.exports = {
|
||||||
|
// MongoDB connection options
|
||||||
|
mongo: {
|
||||||
|
uri: 'mongodb://mongodb/markdownformatwdiff-dev'
|
||||||
|
},
|
||||||
|
|
||||||
|
seedDB: false
|
||||||
|
};
|
67
server/config/environment/index.js
Normal file
67
server/config/environment/index.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
function requiredProcessEnv(name) {
|
||||||
|
if(!process.env[name]) {
|
||||||
|
throw new Error('You must set the ' + name + ' environment variable');
|
||||||
|
}
|
||||||
|
return process.env[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
// All configurations will extend these options
|
||||||
|
// ============================================
|
||||||
|
var all = {
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
|
||||||
|
// Root path of server
|
||||||
|
root: path.normalize(__dirname + '/../../..'),
|
||||||
|
|
||||||
|
// Server port
|
||||||
|
port: process.env.PORT || 9000,
|
||||||
|
|
||||||
|
// Should we populate the DB with sample data?
|
||||||
|
seedDB: false,
|
||||||
|
|
||||||
|
// Secret for session, you will want to change this and make it an environment variable
|
||||||
|
secrets: {
|
||||||
|
session: 'markdown-format-wdiff-secret'
|
||||||
|
},
|
||||||
|
|
||||||
|
// List of user roles
|
||||||
|
userRoles: ['guest', 'user', 'admin'],
|
||||||
|
|
||||||
|
// MongoDB connection options
|
||||||
|
mongo: {
|
||||||
|
options: {
|
||||||
|
db: {
|
||||||
|
safe: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
facebook: {
|
||||||
|
clientID: process.env.FACEBOOK_ID || 'id',
|
||||||
|
clientSecret: process.env.FACEBOOK_SECRET || 'secret',
|
||||||
|
callbackURL: (process.env.DOMAIN || '') + '/auth/facebook/callback'
|
||||||
|
},
|
||||||
|
|
||||||
|
twitter: {
|
||||||
|
clientID: process.env.TWITTER_ID || 'id',
|
||||||
|
clientSecret: process.env.TWITTER_SECRET || 'secret',
|
||||||
|
callbackURL: (process.env.DOMAIN || '') + '/auth/twitter/callback'
|
||||||
|
},
|
||||||
|
|
||||||
|
google: {
|
||||||
|
clientID: process.env.GOOGLE_ID || 'id',
|
||||||
|
clientSecret: process.env.GOOGLE_SECRET || 'secret',
|
||||||
|
callbackURL: (process.env.DOMAIN || '') + '/auth/google/callback'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the config object based on the NODE_ENV
|
||||||
|
// ==============================================
|
||||||
|
module.exports = _.merge(
|
||||||
|
all,
|
||||||
|
require('./' + process.env.NODE_ENV + '.js') || {});
|
23
server/config/environment/production.js
Normal file
23
server/config/environment/production.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Production specific configuration
|
||||||
|
// =================================
|
||||||
|
module.exports = {
|
||||||
|
// Server IP
|
||||||
|
ip: process.env.OPENSHIFT_NODEJS_IP ||
|
||||||
|
process.env.IP ||
|
||||||
|
undefined,
|
||||||
|
|
||||||
|
// Server port
|
||||||
|
port: process.env.OPENSHIFT_NODEJS_PORT ||
|
||||||
|
process.env.PORT ||
|
||||||
|
8080,
|
||||||
|
|
||||||
|
// MongoDB connection options
|
||||||
|
mongo: {
|
||||||
|
uri: process.env.MONGOLAB_URI ||
|
||||||
|
process.env.MONGOHQ_URL ||
|
||||||
|
process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME ||
|
||||||
|
'mongodb://localhost/markdownformatwdiff'
|
||||||
|
}
|
||||||
|
};
|
10
server/config/environment/test.js
Normal file
10
server/config/environment/test.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Test specific configuration
|
||||||
|
// ===========================
|
||||||
|
module.exports = {
|
||||||
|
// MongoDB connection options
|
||||||
|
mongo: {
|
||||||
|
uri: 'mongodb://localhost/markdownformatwdiff-test'
|
||||||
|
}
|
||||||
|
};
|
58
server/config/express.js
Normal file
58
server/config/express.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Express configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var favicon = require('serve-favicon');
|
||||||
|
var morgan = require('morgan');
|
||||||
|
var compression = require('compression');
|
||||||
|
var bodyParser = require('body-parser');
|
||||||
|
var methodOverride = require('method-override');
|
||||||
|
var cookieParser = require('cookie-parser');
|
||||||
|
var errorHandler = require('errorhandler');
|
||||||
|
var path = require('path');
|
||||||
|
var config = require('./environment');
|
||||||
|
var passport = require('passport');
|
||||||
|
var session = require('express-session');
|
||||||
|
var mongoStore = require('connect-mongo')(session);
|
||||||
|
var mongoose = require('mongoose');
|
||||||
|
|
||||||
|
module.exports = function(app) {
|
||||||
|
var env = app.get('env');
|
||||||
|
|
||||||
|
app.set('views', config.root + '/server/views');
|
||||||
|
app.set('view engine', 'jade');
|
||||||
|
app.use(compression());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(methodOverride());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(passport.initialize());
|
||||||
|
|
||||||
|
// Persist sessions with mongoStore
|
||||||
|
// We need to enable sessions for passport twitter because its an oauth 1.0 strategy
|
||||||
|
app.use(session({
|
||||||
|
secret: config.secrets.session,
|
||||||
|
resave: true,
|
||||||
|
saveUninitialized: true,
|
||||||
|
store: new mongoStore({ mongoose_connection: mongoose.connection })
|
||||||
|
}));
|
||||||
|
|
||||||
|
if ('production' === env) {
|
||||||
|
app.use(favicon(path.join(config.root, 'public', 'favicon.ico')));
|
||||||
|
app.use(express.static(path.join(config.root, 'public')));
|
||||||
|
app.set('appPath', config.root + '/public');
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('development' === env || 'test' === env) {
|
||||||
|
app.use(require('connect-livereload')());
|
||||||
|
app.use(express.static(path.join(config.root, '.tmp')));
|
||||||
|
app.use(express.static(path.join(config.root, 'client')));
|
||||||
|
app.set('appPath', 'client');
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
app.use(errorHandler()); // Error handler - has to be last
|
||||||
|
}
|
||||||
|
};
|
23
server/config/local.env.sample.js
Normal file
23
server/config/local.env.sample.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Use local.env.js for environment variables that grunt will set when the server starts locally.
|
||||||
|
// Use for your api keys, secrets, etc. This file should not be tracked by git.
|
||||||
|
//
|
||||||
|
// You will need to set these on the server you deploy to.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DOMAIN: 'http://localhost:9000',
|
||||||
|
SESSION_SECRET: 'markdownformatwdiff-secret',
|
||||||
|
|
||||||
|
FACEBOOK_ID: 'app-id',
|
||||||
|
FACEBOOK_SECRET: 'secret',
|
||||||
|
|
||||||
|
TWITTER_ID: 'app-id',
|
||||||
|
TWITTER_SECRET: 'secret',
|
||||||
|
|
||||||
|
GOOGLE_ID: 'app-id',
|
||||||
|
GOOGLE_SECRET: 'secret',
|
||||||
|
|
||||||
|
// Control debug level for modules using visionmedia/debug
|
||||||
|
DEBUG: ''
|
||||||
|
};
|
49
server/config/seed.js
Normal file
49
server/config/seed.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Populate DB with sample data on server start
|
||||||
|
* to disable, edit config/environment/index.js, and set `seedDB: false`
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Thing = require('../api/thing/thing.model');
|
||||||
|
var User = require('../api/user/user.model');
|
||||||
|
|
||||||
|
Thing.find({}).remove(function() {
|
||||||
|
Thing.create({
|
||||||
|
name : 'Development Tools',
|
||||||
|
info : 'Integration with popular tools such as Bower, Grunt, Karma, Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, Stylus, Sass, CoffeeScript, and Less.'
|
||||||
|
}, {
|
||||||
|
name : 'Server and Client integration',
|
||||||
|
info : 'Built with a powerful and fun stack: MongoDB, Express, AngularJS, and Node.'
|
||||||
|
}, {
|
||||||
|
name : 'Smart Build System',
|
||||||
|
info : 'Build system ignores `spec` files, allowing you to keep tests alongside code. Automatic injection of scripts and styles into your index.html'
|
||||||
|
}, {
|
||||||
|
name : 'Modular Structure',
|
||||||
|
info : 'Best practice client and server structures allow for more code reusability and maximum scalability'
|
||||||
|
}, {
|
||||||
|
name : 'Optimized Build',
|
||||||
|
info : 'Build process packs up your templates as a single JavaScript payload, minifies your scripts/css/images, and rewrites asset names for caching.'
|
||||||
|
},{
|
||||||
|
name : 'Deployment Ready',
|
||||||
|
info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift subgenerators'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
User.find({}).remove(function() {
|
||||||
|
User.create({
|
||||||
|
provider: 'local',
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@test.com',
|
||||||
|
password: 'test'
|
||||||
|
}, {
|
||||||
|
provider: 'local',
|
||||||
|
role: 'admin',
|
||||||
|
name: 'Admin',
|
||||||
|
email: 'admin@admin.com',
|
||||||
|
password: 'admin'
|
||||||
|
}, function() {
|
||||||
|
console.log('finished populating users');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
27
server/routes.js
Normal file
27
server/routes.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Main application routes
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var errors = require('./components/errors');
|
||||||
|
|
||||||
|
module.exports = function(app) {
|
||||||
|
|
||||||
|
// Insert routes below
|
||||||
|
app.use('/api/things', require('./api/thing'));
|
||||||
|
app.use('/api/wdiff', require('./api/wdiff'));
|
||||||
|
app.use('/api/users', require('./api/user'));
|
||||||
|
|
||||||
|
app.use('/auth', require('./auth'));
|
||||||
|
|
||||||
|
// All undefined asset or api routes should return a 404
|
||||||
|
app.route('/:url(api|auth|components|app|bower_components|assets)/*')
|
||||||
|
.get(errors[404]);
|
||||||
|
|
||||||
|
// All other routes should redirect to the index.html
|
||||||
|
app.route('/*')
|
||||||
|
.get(function(req, res) {
|
||||||
|
res.sendfile(app.get('appPath') + '/index.html');
|
||||||
|
});
|
||||||
|
};
|
133
server/views/404.jade
Normal file
133
server/views/404.jade
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang='en')
|
||||||
|
head
|
||||||
|
meta(charset='utf-8')
|
||||||
|
title Page Not Found :(
|
||||||
|
style.
|
||||||
|
::-moz-selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
padding: 30px 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #737373;
|
||||||
|
background: #f0f0f0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
input {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
max-width: 500px;
|
||||||
|
_width: 500px;
|
||||||
|
padding: 30px 20px 50px;
|
||||||
|
border: 1px solid #b3b3b3;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
|
||||||
|
background: #fcfcfc;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0 10px;
|
||||||
|
font-size: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1 span {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 1.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding: 0 0 0 40px;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 380px;
|
||||||
|
_width: 380px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
/* google search */
|
||||||
|
#goog-fixurl ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#goog-fixurl form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#goog-wm-qt,
|
||||||
|
#goog-wm-sb {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: normal;
|
||||||
|
vertical-align: top;
|
||||||
|
color: #444;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
#goog-wm-qt {
|
||||||
|
width: 220px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px 10px 0 0;
|
||||||
|
box-shadow: inset 0 1px 1px #ccc;
|
||||||
|
}
|
||||||
|
#goog-wm-sb {
|
||||||
|
display: inline-block;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
margin: 5px 0 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||||
|
background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||||
|
background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||||
|
background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
*overflow: visible;
|
||||||
|
*display: inline;
|
||||||
|
*zoom: 1;
|
||||||
|
}
|
||||||
|
#goog-wm-sb:hover,
|
||||||
|
#goog-wm-sb:focus {
|
||||||
|
border-color: #aaa;
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
#goog-wm-qt:hover,
|
||||||
|
#goog-wm-qt:focus {
|
||||||
|
border-color: #105cb6;
|
||||||
|
outline: 0;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
body
|
||||||
|
.container
|
||||||
|
h1
|
||||||
|
| Not found
|
||||||
|
span :(
|
||||||
|
p Sorry, but the page you were trying to view does not exist.
|
||||||
|
p It looks like this was the result of either:
|
||||||
|
ul
|
||||||
|
li a mistyped address
|
||||||
|
li an out-of-date link
|
||||||
|
script.
|
||||||
|
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
|
||||||
|
script(src='//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js')
|
1
test/a.txt
Normal file
1
test/a.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
lorem ipsum
|
1
test/b.txt
Normal file
1
test/b.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
lorum ipsum
|
Loading…
Reference in New Issue
Block a user