├── .gitignore ├── .jshintrc-client ├── .jshintrc-server ├── Gruntfile.js ├── LICENSE ├── README-original.md ├── README.md ├── app.js ├── config.example.js ├── layouts ├── account.jade ├── admin.jade └── default.jade ├── models.js ├── package.json ├── passport.js ├── public ├── favicon.ico ├── layouts │ ├── admin.js │ ├── admin.less │ ├── core.js │ ├── core.less │ └── ie-sucks.js ├── less │ ├── bootstrap-build.less │ ├── bootstrap-theme.less │ ├── font-awesome-build.less │ └── font-awesome-vars.less ├── media │ ├── ajax-pulse.gif │ ├── logo-symbol-32x32.png │ └── logo-symbol-64x64.png └── views │ ├── about │ └── index.less │ ├── account │ ├── index.js │ ├── index.less │ ├── settings │ │ └── index.js │ └── verification │ │ ├── index.js │ │ └── index.less │ ├── admin │ ├── accounts │ │ ├── details.js │ │ ├── details.less │ │ ├── index.js │ │ └── index.less │ ├── admin-groups │ │ ├── details.js │ │ ├── details.less │ │ ├── index.js │ │ └── index.less │ ├── administrators │ │ ├── details.js │ │ ├── details.less │ │ ├── index.js │ │ └── index.less │ ├── categories │ │ ├── details.js │ │ ├── index.js │ │ └── index.less │ ├── index.less │ ├── statuses │ │ ├── details.js │ │ ├── index.js │ │ └── index.less │ └── users │ │ ├── details.js │ │ ├── index.js │ │ └── index.less │ ├── contact │ ├── index.js │ └── index.less │ ├── index.less │ ├── login │ ├── forgot │ │ └── index.js │ ├── index.js │ └── reset │ │ └── index.js │ └── signup │ ├── index.js │ ├── index.less │ └── social.js ├── routes.js ├── schema ├── Account.js ├── Admin.js ├── AdminGroup.js ├── Category.js ├── LoginAttempt.js ├── Note.js ├── Status.js ├── StatusLog.js ├── User.js └── plugins │ └── pagedFind.js ├── util ├── sendmail │ └── index.js ├── slugify │ └── index.js └── workflow │ └── index.js └── views ├── about ├── index.jade └── index.js ├── account ├── index.jade ├── index.js ├── settings │ ├── index.jade │ └── index.js └── verification │ ├── email-html.jade │ ├── email-text.jade │ ├── index.jade │ └── index.js ├── admin ├── accounts │ ├── details.jade │ ├── index.jade │ └── index.js ├── admin-groups │ ├── details.jade │ ├── index.jade │ └── index.js ├── administrators │ ├── details.jade │ ├── index.jade │ └── index.js ├── categories │ ├── details.jade │ ├── index.jade │ └── index.js ├── index.jade ├── index.js ├── search │ └── index.js ├── statuses │ ├── details.jade │ ├── index.jade │ └── index.js └── users │ ├── details.jade │ ├── index.jade │ └── index.js ├── contact ├── email-html.jade ├── email-text.jade ├── index.jade └── index.js ├── http ├── 404.jade ├── 500.jade └── index.js ├── index.jade ├── index.js ├── login ├── forgot │ ├── email-html.jade │ ├── email-text.jade │ ├── index.jade │ └── index.js ├── index.jade ├── index.js └── reset │ ├── index.jade │ └── index.js ├── logout └── index.js └── signup ├── email-html.jade ├── email-text.jade ├── index.jade ├── index.js └── social.jade /.gitignore: -------------------------------------------------------------------------------- 1 | public/vendor/* 2 | node_modules/* 3 | *.min.js 4 | *.min.js.map 5 | *.min.css 6 | /config.js 7 | .nodemonignore 8 | -------------------------------------------------------------------------------- /.jshintrc-client: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, // {int} Maximum error before stopping 3 | 4 | // Enforcing 5 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 6 | "camelcase" : false, // true: Identifiers must be in camelCase 7 | "curly" : true, // true: Require {} for every new block or scope 8 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 9 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 10 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 11 | "indent" : 2, // {int} Number of spaces to use for indentation 12 | "latedef" : false, // true: Require variables/functions to be defined before being used 13 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 14 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 15 | "noempty" : true, // true: Prohibit use of empty blocks 16 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 17 | "plusplus" : false, // true: Prohibit use of `++` & `--` 18 | "quotmark" : false, // Quotation mark consistency: 19 | // false : do nothing (default) 20 | // true : ensure whatever is used is consistent 21 | // "single" : require single quotes 22 | // "double" : require double quotes 23 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 24 | "unused" : true, // true: Require all defined variables be used 25 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 26 | "trailing" : false, // true: Prohibit trailing whitespaces 27 | "maxparams" : false, // {int} Max number of formal params allowed per function 28 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 29 | "maxstatements" : false, // {int} Max number statements per function 30 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 31 | "maxlen" : false, // {int} Max number of characters per line 32 | 33 | // Relaxing 34 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 35 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 36 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 37 | "eqnull" : false, // true: Tolerate use of `== null` 38 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 39 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 40 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 41 | // (ex: `for each`, multiple try/catch, function expression…) 42 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 43 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 44 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 45 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 46 | "iterator" : false, // true: Tolerate using the `__iterator__` property 47 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 48 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 49 | "laxcomma" : false, // true: Tolerate comma-first style coding 50 | "loopfunc" : false, // true: Tolerate functions being defined in loops 51 | "multistr" : false, // true: Tolerate multi-line strings 52 | "proto" : false, // true: Tolerate using the `__proto__` property 53 | "scripturl" : false, // true: Tolerate script-targeted URLs 54 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 55 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 56 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 57 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 58 | "validthis" : false, // true: Tolerate using this in a non-constructor function 59 | 60 | // Environments 61 | "browser" : true, // Web Browser (window, document, etc) 62 | "couch" : false, // CouchDB 63 | "devel" : true, // Development/debugging (alert, confirm, etc) 64 | "dojo" : false, // Dojo Toolkit 65 | "jquery" : false, // jQuery 66 | "mootools" : false, // MooTools 67 | "node" : false, // Node.js 68 | "nonstandard" : true, // Widely adopted globals (escape, unescape, etc) 69 | "prototypejs" : false, // Prototype and Scriptaculous 70 | "rhino" : false, // Rhino 71 | "worker" : false, // Web Workers 72 | "wsh" : false, // Windows Scripting Host 73 | "yui" : false, // Yahoo User Interface 74 | 75 | // Legacy 76 | "nomen" : false, // true: Prohibit dangling `_` in variables 77 | "onevar" : false, // true: Allow only one `var` statement per function 78 | "passfail" : false, // true: Stop on first error 79 | "white" : false, // true: Check against strict whitespace and indentation rules 80 | 81 | // Custom Globals 82 | "predef" : ["$", "_", "Backbone", "moment", "app"] // additional predefined global variables 83 | } 84 | -------------------------------------------------------------------------------- /.jshintrc-server: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, // {int} Maximum error before stopping 3 | 4 | // Enforcing 5 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 6 | "camelcase" : false, // true: Identifiers must be in camelCase 7 | "curly" : true, // true: Require {} for every new block or scope 8 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 9 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 10 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 11 | "indent" : 2, // {int} Number of spaces to use for indentation 12 | "latedef" : false, // true: Require variables/functions to be defined before being used 13 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 14 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 15 | "noempty" : true, // true: Prohibit use of empty blocks 16 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 17 | "plusplus" : false, // true: Prohibit use of `++` & `--` 18 | "quotmark" : false, // Quotation mark consistency: 19 | // false : do nothing (default) 20 | // true : ensure whatever is used is consistent 21 | // "single" : require single quotes 22 | // "double" : require double quotes 23 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 24 | "unused" : "vars", // true: Require all defined variables be used 25 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 26 | "trailing" : false, // true: Prohibit trailing whitespaces 27 | "maxparams" : false, // {int} Max number of formal params allowed per function 28 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 29 | "maxstatements" : false, // {int} Max number statements per function 30 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 31 | "maxlen" : false, // {int} Max number of characters per line 32 | 33 | // Relaxing 34 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 35 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 36 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 37 | "eqnull" : false, // true: Tolerate use of `== null` 38 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 39 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 40 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 41 | // (ex: `for each`, multiple try/catch, function expression…) 42 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 43 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 44 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 45 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 46 | "iterator" : false, // true: Tolerate using the `__iterator__` property 47 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 48 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 49 | "laxcomma" : false, // true: Tolerate comma-first style coding 50 | "loopfunc" : false, // true: Tolerate functions being defined in loops 51 | "multistr" : false, // true: Tolerate multi-line strings 52 | "proto" : false, // true: Tolerate using the `__proto__` property 53 | "scripturl" : false, // true: Tolerate script-targeted URLs 54 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 55 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 56 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 57 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 58 | "validthis" : false, // true: Tolerate using this in a non-constructor function 59 | 60 | // Environments 61 | "browser" : false, // Web Browser (window, document, etc) 62 | "couch" : false, // CouchDB 63 | "devel" : true, // Development/debugging (alert, confirm, etc) 64 | "dojo" : false, // Dojo Toolkit 65 | "jquery" : false, // jQuery 66 | "mootools" : false, // MooTools 67 | "node" : true, // Node.js 68 | "nonstandard" : true, // Widely adopted globals (escape, unescape, etc) 69 | "prototypejs" : false, // Prototype and Scriptaculous 70 | "rhino" : false, // Rhino 71 | "worker" : false, // Web Workers 72 | "wsh" : false, // Windows Scripting Host 73 | "yui" : false, // Yahoo User Interface 74 | 75 | // Legacy 76 | "nomen" : false, // true: Prohibit dangling `_` in variables 77 | "onevar" : false, // true: Allow only one `var` statement per function 78 | "passfail" : false, // true: Stop on first error 79 | "white" : false, // true: Check against strict whitespace and indentation rules 80 | 81 | // Custom Globals 82 | "predef" : [ ] // additional predefined global variables 83 | } 84 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function(grunt) { 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | copy: { 7 | vendor: { 8 | files: [ 9 | { 10 | expand: true, cwd: 'node_modules/bootstrap/', 11 | src: ['js/**', 'less/**'], dest: 'public/vendor/bootstrap/' 12 | }, 13 | { 14 | expand: true, cwd: 'node_modules/backbone/', 15 | src: ['backbone.js'], dest: 'public/vendor/backbone/' 16 | }, 17 | { 18 | expand: true, cwd: 'node_modules/font-awesome/', 19 | src: ['fonts/**', 'less/**'], dest: 'public/vendor/font-awesome/' 20 | }, 21 | { 22 | expand: true, cwd: 'node_modules/html5shiv/dist/', 23 | src: ['html5shiv.js'], dest: 'public/vendor/html5shiv/' 24 | }, 25 | { 26 | expand: true, cwd: 'node_modules/jquery/dist/', 27 | src: ['jquery.js'], dest: 'public/vendor/jquery/' 28 | }, 29 | { 30 | expand: true, cwd: 'node_modules/jquery.cookie/', 31 | src: ['jquery.cookie.js'], dest: 'public/vendor/jquery.cookie/' 32 | }, 33 | { 34 | expand: true, cwd: 'node_modules/moment/', 35 | src: ['moment.js'], dest: 'public/vendor/momentjs/' 36 | }, 37 | { 38 | expand: true, cwd: 'node_modules/respond.js/src/', 39 | src: ['respond.js'], dest: 'public/vendor/respond/' 40 | }, 41 | { 42 | expand: true, cwd: 'node_modules/underscore/', 43 | src: ['underscore.js'], dest: 'public/vendor/underscore/' 44 | } 45 | ] 46 | } 47 | }, 48 | concurrent: { 49 | dev: { 50 | tasks: ['nodemon', 'watch'], 51 | options: { 52 | logConcurrentOutput: true 53 | } 54 | } 55 | }, 56 | nodemon: { 57 | dev: { 58 | script: 'app.js', 59 | options: { 60 | ignore: [ 61 | 'node_modules/**', 62 | 'public/**' 63 | ], 64 | ext: 'js' 65 | } 66 | } 67 | }, 68 | watch: { 69 | clientJS: { 70 | files: [ 71 | 'public/layouts/**/*.js', '!public/layouts/**/*.min.js', 72 | 'public/views/**/*.js', '!public/views/**/*.min.js' 73 | ], 74 | tasks: ['newer:uglify', 'newer:jshint:client'] 75 | }, 76 | serverJS: { 77 | files: ['views/**/*.js'], 78 | tasks: ['newer:jshint:server'] 79 | }, 80 | clientLess: { 81 | files: [ 82 | 'public/layouts/**/*.less', 83 | 'public/views/**/*.less', 84 | 'public/less/**/*.less' 85 | ], 86 | tasks: ['newer:less'] 87 | }, 88 | layoutLess: { 89 | files: [ 90 | 'public/layouts/**/*.less', 91 | 'public/less/**/*.less' 92 | ], 93 | tasks: ['less:layouts'] 94 | } 95 | }, 96 | uglify: { 97 | options: { 98 | sourceMap: true, 99 | sourceMapName: function(filePath) { 100 | return filePath + '.map'; 101 | } 102 | }, 103 | layouts: { 104 | files: { 105 | 'public/layouts/core.min.js': [ 106 | 'public/vendor/jquery/jquery.js', 107 | 'public/vendor/jquery.cookie/jquery.cookie.js', 108 | 'public/vendor/underscore/underscore.js', 109 | 'public/vendor/backbone/backbone.js', 110 | 'public/vendor/bootstrap/js/affix.js', 111 | 'public/vendor/bootstrap/js/alert.js', 112 | 'public/vendor/bootstrap/js/button.js', 113 | 'public/vendor/bootstrap/js/carousel.js', 114 | 'public/vendor/bootstrap/js/collapse.js', 115 | 'public/vendor/bootstrap/js/dropdown.js', 116 | 'public/vendor/bootstrap/js/modal.js', 117 | 'public/vendor/bootstrap/js/tooltip.js', 118 | 'public/vendor/bootstrap/js/popover.js', 119 | 'public/vendor/bootstrap/js/scrollspy.js', 120 | 'public/vendor/bootstrap/js/tab.js', 121 | 'public/vendor/bootstrap/js/transition.js', 122 | 'public/vendor/momentjs/moment.js', 123 | 'public/layouts/core.js' 124 | ], 125 | 'public/layouts/ie-sucks.min.js': [ 126 | 'public/vendor/html5shiv/html5shiv.js', 127 | 'public/vendor/respond/respond.js', 128 | 'public/layouts/ie-sucks.js' 129 | ], 130 | 'public/layouts/admin.min.js': ['public/layouts/admin.js'] 131 | } 132 | }, 133 | views: { 134 | files: [{ 135 | expand: true, 136 | cwd: 'public/views/', 137 | src: ['**/*.js', '!**/*.min.js'], 138 | dest: 'public/views/', 139 | ext: '.min.js' 140 | }] 141 | } 142 | }, 143 | jshint: { 144 | client: { 145 | options: { 146 | jshintrc: '.jshintrc-client', 147 | ignores: [ 148 | 'public/layouts/**/*.min.js', 149 | 'public/views/**/*.min.js' 150 | ] 151 | }, 152 | src: [ 153 | 'public/layouts/**/*.js', 154 | 'public/views/**/*.js' 155 | ] 156 | }, 157 | server: { 158 | options: { 159 | jshintrc: '.jshintrc-server' 160 | }, 161 | src: [ 162 | 'schema/**/*.js', 163 | 'views/**/*.js' 164 | ] 165 | } 166 | }, 167 | less: { 168 | options: { 169 | compress: true 170 | }, 171 | layouts: { 172 | files: { 173 | 'public/layouts/core.min.css': [ 174 | 'public/less/bootstrap-build.less', 175 | 'public/less/font-awesome-build.less', 176 | 'public/layouts/core.less' 177 | ], 178 | 'public/layouts/admin.min.css': ['public/layouts/admin.less'] 179 | } 180 | }, 181 | views: { 182 | files: [{ 183 | expand: true, 184 | cwd: 'public/views/', 185 | src: ['**/*.less'], 186 | dest: 'public/views/', 187 | ext: '.min.css' 188 | }] 189 | } 190 | }, 191 | clean: { 192 | js: { 193 | src: [ 194 | 'public/layouts/**/*.min.js', 195 | 'public/layouts/**/*.min.js.map', 196 | 'public/views/**/*.min.js', 197 | 'public/views/**/*.min.js.map' 198 | ] 199 | }, 200 | css: { 201 | src: [ 202 | 'public/layouts/**/*.min.css', 203 | 'public/views/**/*.min.css' 204 | ] 205 | }, 206 | vendor: { 207 | src: ['public/vendor/**'] 208 | } 209 | } 210 | }); 211 | 212 | grunt.loadNpmTasks('grunt-contrib-copy'); 213 | grunt.loadNpmTasks('grunt-contrib-uglify'); 214 | grunt.loadNpmTasks('grunt-contrib-jshint'); 215 | grunt.loadNpmTasks('grunt-contrib-less'); 216 | grunt.loadNpmTasks('grunt-contrib-clean'); 217 | grunt.loadNpmTasks('grunt-contrib-watch'); 218 | grunt.loadNpmTasks('grunt-concurrent'); 219 | grunt.loadNpmTasks('grunt-nodemon'); 220 | grunt.loadNpmTasks('grunt-newer'); 221 | 222 | grunt.registerTask('default', ['copy:vendor', 'newer:uglify', 'newer:less', 'concurrent']); 223 | grunt.registerTask('build', ['copy:vendor', 'uglify', 'less']); 224 | grunt.registerTask('lint', ['jshint']); 225 | }; 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Reza Akhavan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README-original.md: -------------------------------------------------------------------------------- 1 | # Drywall 2 | 3 | A website and user system starter. Implemented with Express and Backbone. 4 | 5 | [![Dependency Status](https://david-dm.org/jedireza/drywall.svg?theme=shields.io)](https://david-dm.org/jedireza/drywall) 6 | [![devDependency Status](https://david-dm.org/jedireza/drywall/dev-status.svg?theme=shields.io)](https://david-dm.org/jedireza/drywall#info=devDependencies) 7 | 8 | 9 | ## Technology 10 | 11 | Server side, Drywall is built with the [Express](http://expressjs.com/) 12 | framework. We're using [MongoDB](http://www.mongodb.org/) as a data store. 13 | 14 | The front-end is built with [Backbone](http://backbonejs.org/). 15 | We're using [Grunt](http://gruntjs.com/) for the asset pipeline. 16 | 17 | | On The Server | On The Client | Development | 18 | | ------------- | -------------- | ----------- | 19 | | Express | Bootstrap | Grunt | 20 | | Jade | Backbone.js | | 21 | | Mongoose | jQuery | | 22 | | Passport | Underscore.js | | 23 | | Async | Font-Awesome | | 24 | | EmailJS | Moment.js | | 25 | 26 | 27 | ## Live demo 28 | 29 | | Platform | Username | Password | 30 | | ------------------------------ | -------- | -------- | 31 | | https://drywall.herokuapp.com/ | root | h3r00t | 32 | 33 | __Note:__ The live demo has been modified so you cannot change the root user, 34 | the root user's linked admin role or the root admin group. This was done in 35 | order to keep the app ready to use at all times. 36 | 37 | 38 | ## Requirements 39 | 40 | You need [Node.js](http://nodejs.org/download/) and 41 | [MongoDB](http://www.mongodb.org/downloads) installed and running. 42 | 43 | We use [`bcrypt`](https://github.com/ncb000gt/node.bcrypt.js) for hashing 44 | secrets. If you have issues during installation related to `bcrypt` then [refer 45 | to this wiki 46 | page](https://github.com/jedireza/drywall/wiki/bcrypt-Installation-Trouble). 47 | 48 | We use [`emailjs`](https://github.com/eleith/emailjs) for email transport. If 49 | you have issues sending email [refer to this wiki 50 | page](https://github.com/jedireza/drywall/wiki/Trouble-sending-email). 51 | 52 | 53 | ## Installation 54 | 55 | ```bash 56 | $ git clone git@github.com:jedireza/drywall.git && cd ./drywall 57 | $ npm install 58 | ``` 59 | 60 | 61 | ## Setup 62 | 63 | First you need to setup your config file. 64 | 65 | ```bash 66 | $ mv ./config.example.js ./config.js #set mongodb and email credentials 67 | ``` 68 | 69 | Next, you need a few records in the database to start using the user system. 70 | 71 | Run these commands on mongo via the terminal. __Obviously you should use your 72 | email address.__ 73 | 74 | ```js 75 | use drywall; // or your mongo db name if different 76 | ``` 77 | 78 | ```js 79 | db.admingroups.insert({ _id: 'root', name: 'Root' }); 80 | db.admins.insert({ name: {first: 'Root', last: 'Admin', full: 'Root Admin'}, groups: ['root'] }); 81 | var rootAdmin = db.admins.findOne(); 82 | db.users.save({ username: 'root', isActive: 'yes', email: 'your@email.addy', roles: {admin: rootAdmin._id} }); 83 | var rootUser = db.users.findOne(); 84 | rootAdmin.user = { id: rootUser._id, name: rootUser.username }; 85 | db.admins.save(rootAdmin); 86 | ``` 87 | 88 | 89 | ## Running the app 90 | 91 | ```bash 92 | $ npm start 93 | 94 | # > Drywall@0.0.0 start /Users/jedireza/projects/jedireza/drywall 95 | # > grunt 96 | 97 | # Running "copy:vendor" (copy) task 98 | # ... 99 | 100 | # Running "concurrent:dev" (concurrent) task 101 | # Running "watch" task 102 | # Running "nodemon:dev" (nodemon) task 103 | # Waiting... 104 | # [nodemon] v1.3.7 105 | # [nodemon] to restart at any time, enter `rs` 106 | # [nodemon] watching: *.* 107 | # [nodemon] starting `node app.js` 108 | # Server is running on port 3000 109 | ``` 110 | 111 | Now just use the reset password feature to set a password. 112 | 113 | - Go to `http://localhost:3000/login/forgot/` 114 | - Submit your email address and wait a second. 115 | - Go check your email and get the reset link. 116 | - `http://localhost:3000/login/reset/:email/:token/` 117 | - Set a new password. 118 | 119 | Login. Customize. Enjoy. 120 | 121 | 122 | ## Philosophy 123 | 124 | - Create a website and user system. 125 | - Write code in a simple and consistent way. 126 | - Only create minor utilities or plugins to avoid repetitiveness. 127 | - Find and use good tools. 128 | - Use tools in their native/default behavior. 129 | 130 | 131 | ## Features 132 | 133 | - Basic front end web pages. 134 | - Contact page has form to email. 135 | - Login system with forgot password and reset password. 136 | - Signup and Login with Facebook, Twitter, GitHub, Google and Tumblr. 137 | - Optional email verification during signup flow. 138 | - User system with separate account and admin roles. 139 | - Admin groups with shared permission settings. 140 | - Administrator level permissions that override group permissions. 141 | - Global admin quick search component. 142 | 143 | 144 | ## Questions and contributing 145 | 146 | Any issues or questions (no matter how basic), open an issue. Please take the 147 | initiative to include basic debugging information like operating system 148 | and relevant version details such as: 149 | 150 | ```bash 151 | $ npm version 152 | 153 | # { drywall: '0.0.0', 154 | # npm: '2.5.1', 155 | # http_parser: '2.3', 156 | # modules: '14', 157 | # node: '0.12.0', 158 | # openssl: '1.0.1l', 159 | # uv: '1.0.2', 160 | # v8: '3.28.73', 161 | # zlib: '1.2.8' } 162 | ``` 163 | 164 | Contributions are welcome. Your code should: 165 | 166 | - pass `$ grunt lint` without error 167 | 168 | If you're changing something non-trivial, you may want to submit an issue 169 | first. 170 | 171 | 172 | ## License 173 | 174 | MIT 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # We moved! 2 | 3 | [Aqua](https://github.com/jedireza/aqua) is what you're looking for. 4 | 5 | Also take a look at [Frame](https://github.com/jedireza/frame), which is just 6 | the restful API parts of Aqua, bring your own front-end. 7 | 8 | 9 | ## Why the change? 10 | 11 | Drywall started as my first Node.js project. It was published on [Dec 20th, 12 | 2012](https://github.com/jedireza/drywall/tree/ac07c05c146ca52c9e26d4d60c63052364211087). 13 | I've learned a lot since then, especially about testing, which Drywall never 14 | got. And (at the time of writing) I'm a bigger fan of 15 | [hapi](http://hapijs.com/) than [express](http://expressjs.com/). 16 | 17 | 18 | ## Thank you 19 | 20 | Thank you so much for your interest in my projects. 21 | 22 | 23 | ## Where's the old README? 24 | 25 | [Right over here](README-original.md) 26 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var config = require('./config'), 5 | express = require('express'), 6 | cookieParser = require('cookie-parser'), 7 | bodyParser = require('body-parser'), 8 | session = require('express-session'), 9 | mongoStore = require('connect-mongo')(session), 10 | http = require('http'), 11 | path = require('path'), 12 | passport = require('passport'), 13 | mongoose = require('mongoose'), 14 | helmet = require('helmet'), 15 | csrf = require('csurf'); 16 | 17 | //create express app 18 | var app = express(); 19 | 20 | //keep reference to config 21 | app.config = config; 22 | 23 | //setup the web server 24 | app.server = http.createServer(app); 25 | 26 | //setup mongoose 27 | app.db = mongoose.createConnection(config.mongodb.uri); 28 | app.db.on('error', console.error.bind(console, 'mongoose connection error: ')); 29 | app.db.once('open', function () { 30 | //and... we have a data store 31 | }); 32 | 33 | //config data models 34 | require('./models')(app, mongoose); 35 | 36 | //settings 37 | app.disable('x-powered-by'); 38 | app.set('port', config.port); 39 | app.set('views', path.join(__dirname, 'views')); 40 | app.set('view engine', 'jade'); 41 | 42 | //middleware 43 | app.use(require('morgan')('dev')); 44 | app.use(require('compression')()); 45 | app.use(require('serve-static')(path.join(__dirname, 'public'))); 46 | app.use(require('method-override')()); 47 | app.use(bodyParser.json()); 48 | app.use(bodyParser.urlencoded({ extended: true })); 49 | app.use(cookieParser(config.cryptoKey)); 50 | app.use(session({ 51 | resave: true, 52 | saveUninitialized: true, 53 | secret: config.cryptoKey, 54 | store: new mongoStore({ url: config.mongodb.uri }) 55 | })); 56 | app.use(passport.initialize()); 57 | app.use(passport.session()); 58 | app.use(csrf({ cookie: { signed: true } })); 59 | helmet(app); 60 | 61 | //response locals 62 | app.use(function(req, res, next) { 63 | res.cookie('_csrfToken', req.csrfToken()); 64 | res.locals.user = {}; 65 | res.locals.user.defaultReturnUrl = req.user && req.user.defaultReturnUrl(); 66 | res.locals.user.username = req.user && req.user.username; 67 | next(); 68 | }); 69 | 70 | //global locals 71 | app.locals.projectName = app.config.projectName; 72 | app.locals.copyrightYear = new Date().getFullYear(); 73 | app.locals.copyrightName = app.config.companyName; 74 | app.locals.cacheBreaker = 'br34k-01'; 75 | 76 | //setup passport 77 | require('./passport')(app, passport); 78 | 79 | //setup routes 80 | require('./routes')(app, passport); 81 | 82 | //custom (friendly) error handler 83 | app.use(require('./views/http/index').http500); 84 | 85 | //setup utilities 86 | app.utility = {}; 87 | app.utility.sendmail = require('./util/sendmail'); 88 | app.utility.slugify = require('./util/slugify'); 89 | app.utility.workflow = require('./util/workflow'); 90 | 91 | //listen up 92 | app.server.listen(app.config.port, function(){ 93 | //and... we're live 94 | console.log('Server is running on port ' + config.port); 95 | }); 96 | -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.port = process.env.PORT || 3000; 4 | exports.mongodb = { 5 | uri: process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost:27017/drywall' 6 | }; 7 | exports.companyName = 'Acme, Inc.'; 8 | exports.projectName = 'Drywall'; 9 | exports.systemEmail = 'your@email.addy'; 10 | exports.cryptoKey = 'k3yb0ardc4t'; 11 | exports.loginAttempts = { 12 | forIp: 50, 13 | forIpAndUser: 7, 14 | logExpiration: '20m' 15 | }; 16 | exports.requireAccountVerification = false; 17 | exports.smtp = { 18 | from: { 19 | name: process.env.SMTP_FROM_NAME || exports.projectName +' Website', 20 | address: process.env.SMTP_FROM_ADDRESS || 'your@email.addy' 21 | }, 22 | credentials: { 23 | user: process.env.SMTP_USERNAME || 'your@email.addy', 24 | password: process.env.SMTP_PASSWORD || 'bl4rg!', 25 | host: process.env.SMTP_HOST || 'smtp.gmail.com', 26 | ssl: true 27 | } 28 | }; 29 | exports.oauth = { 30 | twitter: { 31 | key: process.env.TWITTER_OAUTH_KEY || '', 32 | secret: process.env.TWITTER_OAUTH_SECRET || '' 33 | }, 34 | facebook: { 35 | key: process.env.FACEBOOK_OAUTH_KEY || '', 36 | secret: process.env.FACEBOOK_OAUTH_SECRET || '' 37 | }, 38 | github: { 39 | key: process.env.GITHUB_OAUTH_KEY || '', 40 | secret: process.env.GITHUB_OAUTH_SECRET || '' 41 | }, 42 | google: { 43 | key: process.env.GOOGLE_OAUTH_KEY || '', 44 | secret: process.env.GOOGLE_OAUTH_SECRET || '' 45 | }, 46 | tumblr: { 47 | key: process.env.TUMBLR_OAUTH_KEY || '', 48 | secret: process.env.TUMBLR_OAUTH_SECRET || '' 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /layouts/account.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | title #{title} 6 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 7 | link(rel='stylesheet', href='/layouts/core.min.css?#{cacheBreaker}') 8 | block neck 9 | body 10 | div.navbar.navbar-default.navbar-fixed-top 11 | div.container 12 | div.navbar-header 13 | a.navbar-brand(href='/account/') 14 | img.navbar-logo(src='/media/logo-symbol-64x64.png', alt='Logo') 15 | span.navbar-brand-label #{projectName} 16 | button.navbar-toggle.collapsed(data-toggle='collapse', data-target='.my-navbar-collapse') 17 | span.icon-bar 18 | span.icon-bar 19 | span.icon-bar 20 | div.navbar-collapse.my-navbar-collapse.collapse 21 | ul.nav.navbar-nav 22 | li: a(href='/account/') My Account 23 | li: a(href='/account/settings/') Settings 24 | ul.nav.navbar-nav.pull-right 25 | li: a(href='/logout/') 26 | i.fa.fa-user 27 | | Sign Out 28 | 29 | div.page 30 | div.container 31 | block body 32 | 33 | div.footer 34 | div.container 35 | div.inner 36 | span.copyright.pull-right 37 | |© #{copyrightYear} #{copyrightName} 38 | ul.links 39 | li: a(href='/') Home 40 | li: a(href='/logout/') Sign Out 41 | div.clearfix 42 | 43 | div.ajax-spinner 44 | img(src='/media/ajax-pulse.gif', alt='Loading') 45 | 46 | //if lte IE 9 47 | script(src='/layouts/ie-sucks.min.js?#{cacheBreaker}') 48 | script(src='/layouts/core.min.js?#{cacheBreaker}') 49 | 50 | block feet 51 | -------------------------------------------------------------------------------- /layouts/admin.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | title #{title} 6 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 7 | link(rel='stylesheet', href='/layouts/core.min.css?#{cacheBreaker}') 8 | link(rel='stylesheet', href='/layouts/admin.min.css?#{cacheBreaker}') 9 | block neck 10 | body 11 | div.navbar.navbar-inverse.navbar-fixed-top 12 | div.container 13 | div.navbar-header 14 | a.navbar-brand(href='/admin/') 15 | img.navbar-logo(src='/media/logo-symbol-64x64.png', alt='Logo') 16 | span.navbar-brand-label #{projectName} 17 | button.navbar-toggle.collapsed(data-toggle='collapse', data-target='.my-navbar-collapse') 18 | span.icon-bar 19 | span.icon-bar 20 | span.icon-bar 21 | div.navbar-collapse.my-navbar-collapse.collapse 22 | ul.nav.navbar-nav 23 | li.dropdown 24 | a.dropdown-toggle(href='#', data-toggle='dropdown') 25 | | System  26 | span.caret 27 | ul.dropdown-menu 28 | li.dropdown-header Pivoted Settings 29 | li: a(href='/admin/statuses/') Statuses 30 | li: a(href='/admin/categories/') Categories 31 | li.divider 32 | li.dropdown-header User Admin 33 | li: a(href='/admin/users/') Users 34 | li: a(href='/admin/accounts/') Accounts 35 | li: a(href='/admin/administrators/') Administrators 36 | li: a(href='/admin/admin-groups/') Admin Groups 37 | form.navbar-form.pull-right#_search 38 | 39 | div.page 40 | div.container 41 | block body 42 | 43 | div.footer 44 | div.container 45 | div.inner 46 | span.copyright.pull-right 47 | |© #{copyrightYear} #{copyrightName} 48 | ul.links 49 | li: a(href='/') Home 50 | li: a(href='/logout/') Sign Out 51 | div.clearfix 52 | 53 | div.ajax-spinner 54 | img(src='/media/ajax-pulse.gif', alt='Loading') 55 | 56 | script(type='text/template', id='tmpl-_search') 57 | div.dropdown 58 | input.form-control(name='_search', type='text', placeholder='search', tab-index='1') 59 | ul#_search-results-rows.dropdown-menu 60 | script(type='text/template', id='tmpl-_search-results-row') 61 | |<% if (type && type == 'header') { %> 62 | |<%- name %> 63 | |<% } else { %> 64 | a(href!='<%= url %>') <%- name %> 65 | |<% } %> 66 | script(type='text/template', id='tmpl-_search-results-empty-row') 67 | li.dropdown-header no docs matched 68 | 69 | //if lte IE 9 70 | script(src='/layouts/ie-sucks.min.js?#{cacheBreaker}') 71 | script(src='/layouts/core.min.js?#{cacheBreaker}') 72 | script(src='/layouts/admin.min.js?#{cacheBreaker}') 73 | 74 | block feet 75 | -------------------------------------------------------------------------------- /layouts/default.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | title #{title} 6 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 7 | link(rel='stylesheet', href='/layouts/core.min.css?#{cacheBreaker}') 8 | block neck 9 | body 10 | div.navbar.navbar-default.navbar-fixed-top 11 | div.container 12 | div.navbar-header 13 | a.navbar-brand(href='/') 14 | img.navbar-logo(src='/media/logo-symbol-64x64.png', alt='Logo') 15 | span.navbar-brand-label #{projectName} 16 | button.navbar-toggle.collapsed(data-toggle='collapse', data-target='.my-navbar-collapse') 17 | span.icon-bar 18 | span.icon-bar 19 | span.icon-bar 20 | div.navbar-collapse.my-navbar-collapse.collapse 21 | ul.nav.navbar-nav 22 | li: a(href='/') Home 23 | li: a(href='/about/') About 24 | li: a(href='/signup/') Sign Up 25 | li: a(href='/contact/') Contact 26 | ul.nav.navbar-nav.navbar-right 27 | if user && user.username 28 | li: a(href='#{user.defaultReturnUrl}') 29 | i.fa.fa-user 30 | | #{user.username} 31 | else 32 | li: a(href='/login/') 33 | i.fa.fa-user 34 | | Sign In 35 | 36 | div.page 37 | div.container 38 | block body 39 | 40 | div.footer 41 | div.container 42 | span.copyright.pull-right 43 | |© #{copyrightYear} #{copyrightName} 44 | ul.links 45 | li: a(href='/') Home 46 | li: a(href='/contact/') Contact 47 | div.clearfix 48 | 49 | div.ajax-spinner 50 | img(src='/media/ajax-pulse.gif', alt='Loading') 51 | 52 | //if lte IE 9 53 | script(src='/layouts/ie-sucks.min.js?#{cacheBreaker}') 54 | script(src='/layouts/core.min.js?#{cacheBreaker}') 55 | 56 | block feet 57 | -------------------------------------------------------------------------------- /models.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | //embeddable docs first 5 | require('./schema/Note')(app, mongoose); 6 | require('./schema/Status')(app, mongoose); 7 | require('./schema/StatusLog')(app, mongoose); 8 | require('./schema/Category')(app, mongoose); 9 | 10 | //then regular docs 11 | require('./schema/User')(app, mongoose); 12 | require('./schema/Admin')(app, mongoose); 13 | require('./schema/AdminGroup')(app, mongoose); 14 | require('./schema/Account')(app, mongoose); 15 | require('./schema/LoginAttempt')(app, mongoose); 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Drywall", 3 | "version": "0.18.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "grunt" 7 | }, 8 | "author": "Reza Akhavan (http://reza.akhavan.me/)", 9 | "license": "MIT", 10 | "engines": { 11 | "node": ">=4.0.0" 12 | }, 13 | "dependencies": { 14 | "async": "1.x.x", 15 | "bcrypt": "0.8.x", 16 | "body-parser": "1.x.x", 17 | "compression": "1.x.x", 18 | "connect-mongo": "1.x.x", 19 | "cookie-parser": "1.x.x", 20 | "csurf": "1.x.x", 21 | "emailjs": "1.x.x", 22 | "express": "4.13.x", 23 | "express-session": "1.x.x", 24 | "helmet": "2.x.x", 25 | "jade": "1.x.x", 26 | "method-override": "2.x.x", 27 | "mongoose": "4.x.x", 28 | "morgan": "1.x.x", 29 | "passport": "0.3.x", 30 | "passport-facebook": "2.x.x", 31 | "passport-github": "1.x.x", 32 | "passport-google-oauth": "1.x.x", 33 | "passport-local": "1.x.x", 34 | "passport-oauth": "1.x.x", 35 | "passport-tumblr": "0.1.x", 36 | "passport-twitter": "1.x.x", 37 | "serve-static": "1.10.x", 38 | "underscore": "1.x.x" 39 | }, 40 | "devDependencies": { 41 | "backbone": "1.x.x", 42 | "bootstrap": "3.x.x", 43 | "font-awesome": "4.x.x", 44 | "grunt": "1.x.x", 45 | "grunt-cli": "1.x.x", 46 | "grunt-concurrent": "2.x.x", 47 | "grunt-contrib-clean": "1.x.x", 48 | "grunt-contrib-copy": "1.x.x", 49 | "grunt-contrib-jshint": "1.x.x", 50 | "grunt-contrib-less": "1.x.x", 51 | "grunt-contrib-uglify": "1.x.x", 52 | "grunt-contrib-watch": "1.x.x", 53 | "grunt-newer": "1.x.x", 54 | "grunt-nodemon": "0.4.x", 55 | "html5shiv": "3.x.x", 56 | "jquery": "2.x.x", 57 | "jquery.cookie": "1.x.x", 58 | "moment": "2.13.x", 59 | "respond.js": "1.x.x" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /passport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, passport) { 4 | var LocalStrategy = require('passport-local').Strategy, 5 | TwitterStrategy = require('passport-twitter').Strategy, 6 | GitHubStrategy = require('passport-github').Strategy, 7 | FacebookStrategy = require('passport-facebook').Strategy, 8 | GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, 9 | TumblrStrategy = require('passport-tumblr').Strategy; 10 | 11 | passport.use(new LocalStrategy( 12 | function(username, password, done) { 13 | var conditions = { isActive: 'yes' }; 14 | if (username.indexOf('@') === -1) { 15 | conditions.username = username; 16 | } 17 | else { 18 | conditions.email = username.toLowerCase(); 19 | } 20 | 21 | app.db.models.User.findOne(conditions, function(err, user) { 22 | if (err) { 23 | return done(err); 24 | } 25 | 26 | if (!user) { 27 | return done(null, false, { message: 'Unknown user' }); 28 | } 29 | 30 | app.db.models.User.validatePassword(password, user.password, function(err, isValid) { 31 | if (err) { 32 | return done(err); 33 | } 34 | 35 | if (!isValid) { 36 | return done(null, false, { message: 'Invalid password' }); 37 | } 38 | 39 | return done(null, user); 40 | }); 41 | }); 42 | } 43 | )); 44 | 45 | if (app.config.oauth.twitter.key) { 46 | passport.use(new TwitterStrategy({ 47 | consumerKey: app.config.oauth.twitter.key, 48 | consumerSecret: app.config.oauth.twitter.secret 49 | }, 50 | function(token, tokenSecret, profile, done) { 51 | done(null, false, { 52 | token: token, 53 | tokenSecret: tokenSecret, 54 | profile: profile 55 | }); 56 | } 57 | )); 58 | } 59 | 60 | if (app.config.oauth.github.key) { 61 | passport.use(new GitHubStrategy({ 62 | clientID: app.config.oauth.github.key, 63 | clientSecret: app.config.oauth.github.secret, 64 | customHeaders: { "User-Agent": app.config.projectName } 65 | }, 66 | function(accessToken, refreshToken, profile, done) { 67 | done(null, false, { 68 | accessToken: accessToken, 69 | refreshToken: refreshToken, 70 | profile: profile 71 | }); 72 | } 73 | )); 74 | } 75 | 76 | if (app.config.oauth.facebook.key) { 77 | passport.use(new FacebookStrategy({ 78 | clientID: app.config.oauth.facebook.key, 79 | clientSecret: app.config.oauth.facebook.secret 80 | }, 81 | function(accessToken, refreshToken, profile, done) { 82 | done(null, false, { 83 | accessToken: accessToken, 84 | refreshToken: refreshToken, 85 | profile: profile 86 | }); 87 | } 88 | )); 89 | } 90 | 91 | if (app.config.oauth.google.key) { 92 | passport.use(new GoogleStrategy({ 93 | clientID: app.config.oauth.google.key, 94 | clientSecret: app.config.oauth.google.secret 95 | }, 96 | function(accessToken, refreshToken, profile, done) { 97 | done(null, false, { 98 | accessToken: accessToken, 99 | refreshToken: refreshToken, 100 | profile: profile 101 | }); 102 | } 103 | )); 104 | } 105 | 106 | if (app.config.oauth.tumblr.key) { 107 | passport.use(new TumblrStrategy({ 108 | consumerKey: app.config.oauth.tumblr.key, 109 | consumerSecret: app.config.oauth.tumblr.secret 110 | }, 111 | function(token, tokenSecret, profile, done) { 112 | done(null, false, { 113 | token: token, 114 | tokenSecret: tokenSecret, 115 | profile: profile 116 | }); 117 | } 118 | )); 119 | } 120 | 121 | passport.serializeUser(function(user, done) { 122 | done(null, user._id); 123 | }); 124 | 125 | passport.deserializeUser(function(id, done) { 126 | app.db.models.User.findOne({ _id: id }).populate('roles.admin').populate('roles.account').exec(function(err, user) { 127 | if (user && user.roles && user.roles.admin) { 128 | user.roles.admin.populate("groups", function(err, admin) { 129 | done(err, user); 130 | }); 131 | } 132 | else { 133 | done(err, user); 134 | } 135 | }); 136 | }); 137 | }; 138 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedireza/drywall/523463e0ed0a3a47c31b44971eb0a9b75e280a87/public/favicon.ico -------------------------------------------------------------------------------- /public/layouts/admin.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app._SearchResult = Backbone.Model.extend({ 9 | defaults: { 10 | _id: undefined, 11 | name: '---', 12 | url: '---', 13 | type: 'result' 14 | } 15 | }); 16 | 17 | app._SearchCollection = Backbone.Collection.extend({ 18 | model: app._SearchResult, 19 | url: '/admin/search/', 20 | parse: function(response) { 21 | var outcome = []; 22 | 23 | if (response.users.length) { 24 | outcome.push({name: 'Users', type: 'header'}); 25 | } 26 | 27 | _.each(response.users, function(user) { 28 | outcome.push({name: user.username, url: '/admin/users/'+ user._id +'/'}); 29 | }); 30 | 31 | if (response.accounts.length) { 32 | outcome.push({name: 'Accounts', type: 'header'}); 33 | } 34 | 35 | _.each(response.accounts, function(account) { 36 | outcome.push({name: account.name.full, url: '/admin/accounts/'+ account._id +'/'}); 37 | }); 38 | 39 | if (response.administrators.length) { 40 | outcome.push({name: 'Administrators', type: 'header'}); 41 | } 42 | 43 | _.each(response.administrators, function(administrator) { 44 | outcome.push({name: administrator.name.full, url: '/admin/administrators/'+ administrator._id +'/'}); 45 | }); 46 | 47 | return outcome; 48 | } 49 | }); 50 | 51 | app._SearchView = Backbone.View.extend({ 52 | el: '#_search', 53 | template: _.template( $('#tmpl-_search').html() ), 54 | events: { 55 | 'keydown .form-control': 'startKeyBuffer' 56 | }, 57 | timeLastKeyPressed: undefined, 58 | lastTimeoutID: undefined, 59 | selectedResult: undefined, 60 | startKeyBuffer: function(event) { 61 | app._searchView.timeLastKeyPressed = (new Date()); 62 | 63 | //esc key 64 | if (parseInt(event.keyCode, null) === 27) { 65 | this.clearResults(); 66 | return; 67 | } 68 | 69 | //enter key 70 | if (parseInt(event.keyCode, null) === 13) { 71 | if (this.selectedResult !== undefined) { 72 | var url = this.$el.find('li.active a').attr('href'); 73 | if (url) { 74 | location.href = url; 75 | } 76 | } 77 | return false; 78 | } 79 | 80 | //up and down keys 81 | if (parseInt(event.keyCode, null) === 38 || parseInt(event.keyCode, null) === 40) { 82 | this.navigateResults(event); 83 | return false; 84 | } 85 | 86 | //ignore non-alphanumeric, except backspace 87 | if (!/[a-zA-Z0-9\-_ ]/.test(String.fromCharCode(parseInt(event.keyCode, null))) && parseInt(event.keyCode, null) !== 8) { 88 | return; 89 | } 90 | 91 | this.keyBuffer(); 92 | }, 93 | keyBuffer: function() { 94 | //only run search after 333 milliseconds have passed 95 | if (((new Date()) - app._searchView.timeLastKeyPressed) / 1000 >= 0.333) { 96 | app._searchView.runSearch(); 97 | } 98 | else { 99 | if (app._searchView.lastTimeoutID) { 100 | clearTimeout(app._searchView.lastTimeoutID); 101 | } 102 | 103 | app._searchView.lastTimeoutID = setTimeout(app._searchView.keyBuffer, 50); 104 | } 105 | }, 106 | runSearch: function() { 107 | var query = this.$el.find('.form-control').val(); 108 | if (!query) { 109 | this.clearResults(); 110 | return; 111 | } 112 | 113 | this.collection.fetch({ data: {q: query}, reset: true }); 114 | }, 115 | navigateResults: function(event) { 116 | var arrLinkResults = this.$el.find('li a').get(); 117 | 118 | var movingUp = (parseInt(event.keyCode, null) === 38); 119 | var movingDown = (parseInt(event.keyCode, null) === 40); 120 | 121 | if (this.selectedResult === undefined && this.$el.find('li a').get(0)) { 122 | this.selectedResult = -1; 123 | } 124 | 125 | if (movingUp && this.selectedResult === 0) { 126 | this.selectedResult = arrLinkResults.length - 1; 127 | } 128 | else if (movingUp) { 129 | this.selectedResult -= 1; 130 | } 131 | 132 | if (movingDown && this.selectedResult === (arrLinkResults.length - 1)) { 133 | this.selectedResult = 0; 134 | } 135 | else if (movingDown) { 136 | this.selectedResult += 1; 137 | } 138 | 139 | if (this.selectedResult > arrLinkResults.length) { 140 | this.selectedResult = 0; 141 | } 142 | 143 | if (arrLinkResults.length === 0) { 144 | this.selectedResult = undefined; 145 | } 146 | 147 | this.selectResult(); 148 | }, 149 | selectResult: function() { 150 | if (this.selectedResult !== undefined) { 151 | this.$el.find('li a').closest('li').attr('class', ''); 152 | this.$el.find('li a:eq('+ this.selectedResult +')').closest('li').attr('class', 'active'); 153 | } 154 | }, 155 | clearResults: function() { 156 | this.$el.find('.form-control').val(''); 157 | this.$el.find('.dropdown').removeClass('open'); 158 | this.$el.find('#_search-results-rows').html(''); 159 | this.selectedResult = undefined; 160 | }, 161 | initialize: function() { 162 | this.collection = new app._SearchCollection(); 163 | this.collection.on('reset', this.render, this); 164 | this.$el.html(this.template()); 165 | this.render(); 166 | }, 167 | render: function() { 168 | if (this.$el.find('.form-control').val() === '') { 169 | this.$el.find('.dropdown').removeClass('open'); 170 | } 171 | else { 172 | this.$el.find('.dropdown').addClass('open'); 173 | } 174 | 175 | $('#_search-results-rows').empty(); 176 | 177 | this.collection.each(function(record) { 178 | var view = new app._SearchResultView({ model: record }); 179 | $('#_search-results-rows').append( view.render().$el ); 180 | }, this); 181 | 182 | if (this.collection.length === 0) { 183 | $('#_search-results-rows').append( $('#tmpl-_search-results-empty-row').html() ); 184 | } 185 | } 186 | }); 187 | 188 | app._SearchResultView = Backbone.View.extend({ 189 | tagName: 'li', 190 | template: _.template( $('#tmpl-_search-results-row').html() ), 191 | events: { 192 | 'click .btn-details': 'goTo' 193 | }, 194 | goTo: function() { 195 | location.href = this.model.get('url'); 196 | }, 197 | render: function() { 198 | this.$el.html(this.template( this.model.attributes )); 199 | if (this.model.get('type') === 'header') { 200 | this.$el.addClass('dropdown-header'); 201 | } 202 | 203 | return this; 204 | } 205 | }); 206 | 207 | $(document).ready(function() { 208 | app._searchView = new app._SearchView(); 209 | }); 210 | }()); 211 | -------------------------------------------------------------------------------- /public/layouts/admin.less: -------------------------------------------------------------------------------- 1 | .navbar { 2 | .navbar-brand { 3 | color: #eee; 4 | } 5 | 6 | .navbar-form .dropdown { 7 | position: relative; 8 | } 9 | } 10 | 11 | .navbar .nav > li > a { 12 | text-transform: uppercase; 13 | font-weight: bold; 14 | } 15 | 16 | .filters { 17 | margin-bottom: 20px; 18 | } 19 | 20 | .stretch { 21 | width: 100%; 22 | } 23 | 24 | .nowrap { 25 | white-space: nowrap; 26 | } 27 | -------------------------------------------------------------------------------- /public/layouts/core.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | /* exported app */ 3 | 4 | var app; //the main declaration 5 | 6 | (function() { 7 | 'use strict'; 8 | 9 | $(document).ready(function() { 10 | //active (selected) navigation elements 11 | $('.nav [href="'+ window.location.pathname +'"]').closest('li').toggleClass('active'); 12 | 13 | //register global ajax handlers 14 | $(document).ajaxStart(function(){ $('.ajax-spinner').show(); }); 15 | $(document).ajaxStop(function(){ $('.ajax-spinner').hide(); }); 16 | $.ajaxSetup({ 17 | beforeSend: function (xhr) { 18 | xhr.setRequestHeader('x-csrf-token', $.cookie('_csrfToken')); 19 | } 20 | }); 21 | 22 | //ajax spinner follows mouse 23 | $(document).bind('mousemove', function(e) { 24 | $('.ajax-spinner').css({ 25 | left: e.pageX + 15, 26 | top: e.pageY 27 | }); 28 | }); 29 | }); 30 | }()); 31 | -------------------------------------------------------------------------------- /public/layouts/core.less: -------------------------------------------------------------------------------- 1 | @import "../vendor/bootstrap/less/mixins.less"; 2 | 3 | body { 4 | padding-top: 50px; 5 | } 6 | 7 | h1, h2 { 8 | letter-spacing: -1px; 9 | } 10 | 11 | fieldset { 12 | min-width: 0; 13 | margin-bottom: 20px; 14 | } 15 | 16 | table thead th { 17 | background-color: #ddd; 18 | } 19 | 20 | select, textarea, input, .form-control { 21 | font-size: 16px; //ios input zoom prevention hack 22 | } 23 | 24 | .input-group-btn .btn-group:first-of-type .btn { 25 | .border-left-radius(0); 26 | } 27 | 28 | .dropdown-header { 29 | font-weight: bold; 30 | text-transform: uppercase; 31 | padding-left: 12px; 32 | } 33 | 34 | .navbar { 35 | .box-shadow(0 2px 5px rgba(0,0,0,.125)); 36 | 37 | .navbar-brand { 38 | font-weight: bold; 39 | color: #003d5f; 40 | position: relative; 41 | display: inline-block; 42 | 43 | .navbar-logo { 44 | position: absolute; 45 | top: 10px; 46 | height: 32px; 47 | width: 32px; 48 | } 49 | } 50 | .navbar-brand-label { 51 | margin-left: 35px; 52 | } 53 | } 54 | 55 | .footer { 56 | margin-top: 50px; 57 | color: #999; 58 | 59 | .container { 60 | border-top: 1px solid #ccc; 61 | padding: 20px 5px 20px 5px; 62 | border-radius: 4px; 63 | } 64 | a, a:visited { 65 | color: #999; 66 | } 67 | .links { 68 | margin: 0; 69 | padding: 0; 70 | 71 | li { 72 | display: inline-block; 73 | padding-right: 10px; 74 | margin-right: 10px; 75 | list-style: none; 76 | border-right: 1px dotted #999; 77 | } 78 | li:last-child { 79 | padding-right: 0; 80 | margin-right: 0; 81 | border: none; 82 | } 83 | } 84 | } 85 | 86 | .ajax-spinner { 87 | position: absolute; 88 | z-index: 999; 89 | display: none; 90 | padding: 5px; 91 | font-size: inherit; 92 | line-height: 11px; 93 | background-color: #ccc; 94 | border-radius: 4px; 95 | 96 | img { 97 | width: inherit; 98 | height: inherit; 99 | margin: 0; 100 | } 101 | }​ 102 | 103 | .force-wrap { 104 | white-space: pre-wrap; /* css-3 */ 105 | white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ 106 | white-space: -pre-wrap; /* Opera 4-6 */ 107 | white-space: -o-pre-wrap; /* Opera 7 */ 108 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 109 | } 110 | -------------------------------------------------------------------------------- /public/layouts/ie-sucks.js: -------------------------------------------------------------------------------- 1 | //damn you ie! 2 | -------------------------------------------------------------------------------- /public/less/bootstrap-build.less: -------------------------------------------------------------------------------- 1 | // Core variables and mixins 2 | @import "../vendor/bootstrap/less/variables.less"; 3 | @import "../vendor/bootstrap/less/mixins.less"; 4 | 5 | // Reset 6 | @import "../vendor/bootstrap/less/normalize.less"; 7 | @import "../vendor/bootstrap/less/print.less"; 8 | 9 | // Core CSS 10 | @import "../vendor/bootstrap/less/scaffolding.less"; 11 | @import "../vendor/bootstrap/less/type.less"; 12 | @import "../vendor/bootstrap/less/code.less"; 13 | @import "../vendor/bootstrap/less/grid.less"; 14 | @import "../vendor/bootstrap/less/tables.less"; 15 | @import "../vendor/bootstrap/less/forms.less"; 16 | @import "../vendor/bootstrap/less/buttons.less"; 17 | 18 | // Components 19 | @import "../vendor/bootstrap/less/component-animations.less"; 20 | @import "../vendor/bootstrap/less/glyphicons.less"; 21 | @import "../vendor/bootstrap/less/dropdowns.less"; 22 | @import "../vendor/bootstrap/less/button-groups.less"; 23 | @import "../vendor/bootstrap/less/input-groups.less"; 24 | @import "../vendor/bootstrap/less/navs.less"; 25 | @import "../vendor/bootstrap/less/navbar.less"; 26 | @import "../vendor/bootstrap/less/breadcrumbs.less"; 27 | @import "../vendor/bootstrap/less/pagination.less"; 28 | @import "../vendor/bootstrap/less/pager.less"; 29 | @import "../vendor/bootstrap/less/labels.less"; 30 | @import "../vendor/bootstrap/less/badges.less"; 31 | @import "../vendor/bootstrap/less/jumbotron.less"; 32 | @import "../vendor/bootstrap/less/thumbnails.less"; 33 | @import "../vendor/bootstrap/less/alerts.less"; 34 | @import "../vendor/bootstrap/less/progress-bars.less"; 35 | @import "../vendor/bootstrap/less/media.less"; 36 | @import "../vendor/bootstrap/less/list-group.less"; 37 | @import "../vendor/bootstrap/less/panels.less"; 38 | @import "../vendor/bootstrap/less/wells.less"; 39 | @import "../vendor/bootstrap/less/close.less"; 40 | 41 | // Components w/ JavaScript 42 | @import "../vendor/bootstrap/less/modals.less"; 43 | @import "../vendor/bootstrap/less/tooltip.less"; 44 | @import "../vendor/bootstrap/less/popovers.less"; 45 | @import "../vendor/bootstrap/less/carousel.less"; 46 | 47 | // Utility classes 48 | @import "../vendor/bootstrap/less/utilities.less"; 49 | @import "../vendor/bootstrap/less/responsive-utilities.less"; 50 | 51 | // Theme 52 | @import "bootstrap-theme.less"; 53 | -------------------------------------------------------------------------------- /public/less/font-awesome-build.less: -------------------------------------------------------------------------------- 1 | @import "../vendor/font-awesome/less/variables.less"; 2 | @import "font-awesome-vars.less"; 3 | @import "../vendor/font-awesome/less/mixins.less"; 4 | @import "../vendor/font-awesome/less/path.less"; 5 | @import "../vendor/font-awesome/less/core.less"; 6 | @import "../vendor/font-awesome/less/larger.less"; 7 | @import "../vendor/font-awesome/less/fixed-width.less"; 8 | @import "../vendor/font-awesome/less/list.less"; 9 | @import "../vendor/font-awesome/less/bordered-pulled.less"; 10 | @import "../vendor/font-awesome/less/animated.less"; 11 | @import "../vendor/font-awesome/less/rotated-flipped.less"; 12 | @import "../vendor/font-awesome/less/stacked.less"; 13 | @import "../vendor/font-awesome/less/icons.less"; 14 | -------------------------------------------------------------------------------- /public/less/font-awesome-vars.less: -------------------------------------------------------------------------------- 1 | @fa-font-path: "/vendor/font-awesome/fonts"; 2 | -------------------------------------------------------------------------------- /public/media/ajax-pulse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedireza/drywall/523463e0ed0a3a47c31b44971eb0a9b75e280a87/public/media/ajax-pulse.gif -------------------------------------------------------------------------------- /public/media/logo-symbol-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedireza/drywall/523463e0ed0a3a47c31b44971eb0a9b75e280a87/public/media/logo-symbol-32x32.png -------------------------------------------------------------------------------- /public/media/logo-symbol-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedireza/drywall/523463e0ed0a3a47c31b44971eb0a9b75e280a87/public/media/logo-symbol-64x64.png -------------------------------------------------------------------------------- /public/views/about/index.less: -------------------------------------------------------------------------------- 1 | .special { 2 | text-align: center; 3 | } 4 | .super-awesome { 5 | display: block; 6 | margin-top: -15px; 7 | color: #7f7f7f; 8 | font-size: 20em; 9 | } 10 | -------------------------------------------------------------------------------- /public/views/account/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | $('.day-of-year').text(moment().format('DDD')); 5 | $('.day-of-month').text(moment().format('D')); 6 | $('.week-of-year').text(moment().format('w')); 7 | $('.day-of-week').text(moment().format('d')); 8 | $('.week-year').text(moment().format('gg')); 9 | $('.hour-of-day').text(moment().format('H')); 10 | }()); 11 | -------------------------------------------------------------------------------- /public/views/account/index.less: -------------------------------------------------------------------------------- 1 | .special { 2 | text-align: center; 3 | } 4 | .super-awesome { 5 | display: block; 6 | margin-top: -15px; 7 | color: #7f7f7f; 8 | font-size: 20em; 9 | } 10 | 11 | .stat { 12 | text-align: center; 13 | } 14 | .stat-value { 15 | color: #555; 16 | font-size: 2.2em; 17 | font-weight: bold; 18 | letter-spacing: -1px; 19 | } 20 | .stat-label { 21 | color: #999; 22 | font-weight: bold; 23 | } 24 | -------------------------------------------------------------------------------- /public/views/account/settings/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Account = Backbone.Model.extend({ 9 | idAttribute: '_id', 10 | url: '/account/settings/' 11 | }); 12 | 13 | app.User = Backbone.Model.extend({ 14 | idAttribute: '_id', 15 | url: '/account/settings/' 16 | }); 17 | 18 | app.Details = Backbone.Model.extend({ 19 | idAttribute: '_id', 20 | defaults: { 21 | success: false, 22 | errors: [], 23 | errfor: {}, 24 | first: '', 25 | middle: '', 26 | last: '', 27 | company: '', 28 | phone: '', 29 | zip: '' 30 | }, 31 | url: '/account/settings/', 32 | parse: function(response) { 33 | if (response.account) { 34 | app.mainView.account.set(response.account); 35 | delete response.account; 36 | } 37 | 38 | return response; 39 | } 40 | }); 41 | 42 | app.Identity = Backbone.Model.extend({ 43 | idAttribute: '_id', 44 | defaults: { 45 | success: false, 46 | errors: [], 47 | errfor: {}, 48 | username: '', 49 | email: '' 50 | }, 51 | url: '/account/settings/identity/', 52 | parse: function(response) { 53 | if (response.user) { 54 | app.mainView.user.set(response.user); 55 | delete response.user; 56 | } 57 | 58 | return response; 59 | } 60 | }); 61 | 62 | app.Password = Backbone.Model.extend({ 63 | idAttribute: '_id', 64 | defaults: { 65 | success: false, 66 | errors: [], 67 | errfor: {}, 68 | newPassword: '', 69 | confirm: '' 70 | }, 71 | url: '/account/settings/password/', 72 | parse: function(response) { 73 | if (response.user) { 74 | app.mainView.user.set(response.user); 75 | delete response.user; 76 | } 77 | 78 | return response; 79 | } 80 | }); 81 | 82 | app.DetailsView = Backbone.View.extend({ 83 | el: '#details', 84 | template: _.template( $('#tmpl-details').html() ), 85 | events: { 86 | 'click .btn-update': 'update' 87 | }, 88 | initialize: function() { 89 | this.model = new app.Details(); 90 | this.syncUp(); 91 | this.listenTo(app.mainView.account, 'change', this.syncUp); 92 | this.listenTo(this.model, 'sync', this.render); 93 | this.render(); 94 | }, 95 | syncUp: function() { 96 | this.model.set({ 97 | _id: app.mainView.account.id, 98 | first: app.mainView.account.get('name').first, 99 | middle: app.mainView.account.get('name').middle, 100 | last: app.mainView.account.get('name').last, 101 | company: app.mainView.account.get('company'), 102 | phone: app.mainView.account.get('phone'), 103 | zip: app.mainView.account.get('zip') 104 | }); 105 | }, 106 | render: function() { 107 | this.$el.html(this.template( this.model.attributes )); 108 | 109 | for (var key in this.model.attributes) { 110 | if (this.model.attributes.hasOwnProperty(key)) { 111 | this.$el.find('[name="'+ key +'"]').val(this.model.attributes[key]); 112 | } 113 | } 114 | }, 115 | update: function() { 116 | this.model.save({ 117 | first: this.$el.find('[name="first"]').val(), 118 | middle: this.$el.find('[name="middle"]').val(), 119 | last: this.$el.find('[name="last"]').val(), 120 | company: this.$el.find('[name="company"]').val(), 121 | phone: this.$el.find('[name="phone"]').val(), 122 | zip: this.$el.find('[name="zip"]').val() 123 | }); 124 | } 125 | }); 126 | 127 | app.IdentityView = Backbone.View.extend({ 128 | el: '#identity', 129 | template: _.template( $('#tmpl-identity').html() ), 130 | events: { 131 | 'click .btn-update': 'update' 132 | }, 133 | initialize: function() { 134 | this.model = new app.Identity(); 135 | this.syncUp(); 136 | this.listenTo(app.mainView.user, 'change', this.syncUp); 137 | this.listenTo(this.model, 'sync', this.render); 138 | this.render(); 139 | }, 140 | syncUp: function() { 141 | this.model.set({ 142 | _id: app.mainView.user.id, 143 | username: app.mainView.user.get('username'), 144 | email: app.mainView.user.get('email') 145 | }); 146 | }, 147 | render: function() { 148 | this.$el.html(this.template( this.model.attributes )); 149 | 150 | for (var key in this.model.attributes) { 151 | if (this.model.attributes.hasOwnProperty(key)) { 152 | this.$el.find('[name="'+ key +'"]').val(this.model.attributes[key]); 153 | } 154 | } 155 | }, 156 | update: function() { 157 | this.model.save({ 158 | username: this.$el.find('[name="username"]').val(), 159 | email: this.$el.find('[name="email"]').val() 160 | }); 161 | } 162 | }); 163 | 164 | app.PasswordView = Backbone.View.extend({ 165 | el: '#password', 166 | template: _.template( $('#tmpl-password').html() ), 167 | events: { 168 | 'click .btn-password': 'password' 169 | }, 170 | initialize: function() { 171 | this.model = new app.Password({ _id: app.mainView.user.id }); 172 | this.listenTo(this.model, 'sync', this.render); 173 | this.render(); 174 | }, 175 | render: function() { 176 | this.$el.html(this.template( this.model.attributes )); 177 | 178 | for (var key in this.model.attributes) { 179 | if (this.model.attributes.hasOwnProperty(key)) { 180 | this.$el.find('[name="'+ key +'"]').val(this.model.attributes[key]); 181 | } 182 | } 183 | }, 184 | password: function() { 185 | this.model.save({ 186 | newPassword: this.$el.find('[name="newPassword"]').val(), 187 | confirm: this.$el.find('[name="confirm"]').val() 188 | }); 189 | } 190 | }); 191 | 192 | app.MainView = Backbone.View.extend({ 193 | el: '.page .container', 194 | initialize: function() { 195 | app.mainView = this; 196 | this.account = new app.Account( JSON.parse( unescape($('#data-account').html()) ) ); 197 | this.user = new app.User( JSON.parse( unescape($('#data-user').html()) ) ); 198 | 199 | app.detailsView = new app.DetailsView(); 200 | app.identityView = new app.IdentityView(); 201 | app.passwordView = new app.PasswordView(); 202 | } 203 | }); 204 | 205 | $(document).ready(function() { 206 | app.mainView = new app.MainView(); 207 | }); 208 | }()); 209 | -------------------------------------------------------------------------------- /public/views/account/verification/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Verify = Backbone.Model.extend({ 9 | url: '/account/verification/', 10 | defaults: { 11 | success: false, 12 | errors: [], 13 | errfor: {}, 14 | keepFormOpen: false, 15 | email: '' 16 | } 17 | }); 18 | 19 | app.VerifyView = Backbone.View.extend({ 20 | el: '#verify', 21 | template: _.template( $('#tmpl-verify').html() ), 22 | events: { 23 | 'submit form': 'preventSubmit', 24 | 'click .btn-resend': 'resend', 25 | 'click .btn-verify': 'verify' 26 | }, 27 | initialize: function() { 28 | this.model = new app.Verify( JSON.parse($('#data-user').html()) ); 29 | this.listenTo(this.model, 'sync', this.render); 30 | this.render(); 31 | }, 32 | render: function() { 33 | this.$el.html(this.template( this.model.attributes )); 34 | }, 35 | preventSubmit: function(event) { 36 | event.preventDefault(); 37 | }, 38 | resend: function() { 39 | this.model.set({ 40 | keepFormOpen: true 41 | }); 42 | this.render(); 43 | }, 44 | verify: function() { 45 | this.$el.find('.btn-verify').attr('disabled', true); 46 | 47 | this.model.save({ 48 | email: this.$el.find('[name="email"]').val() 49 | }); 50 | } 51 | }); 52 | 53 | $(document).ready(function() { 54 | app.verifyView = new app.VerifyView(); 55 | }); 56 | }()); 57 | -------------------------------------------------------------------------------- /public/views/account/verification/index.less: -------------------------------------------------------------------------------- 1 | .special { 2 | text-align: center; 3 | } 4 | .super-awesome { 5 | display: block; 6 | margin-top: -15px; 7 | color: #7f7f7f; 8 | font-size: 20em; 9 | } 10 | .not-received-hidden { 11 | display: none; 12 | } 13 | .verify-form-hidden { 14 | display: none; 15 | } 16 | -------------------------------------------------------------------------------- /public/views/admin/accounts/details.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | 3 | .status-items { 4 | overflow: scroll; 5 | 6 | .status { 7 | padding: 10px 5px; 8 | font-size: 12px; 9 | border-bottom: 1px solid #ccc; 10 | 11 | .author { 12 | font-weight: normal; 13 | color: #999; 14 | text-shadow: none; 15 | background-color: transparent; 16 | border: 1px solid #ccc; 17 | } 18 | } 19 | .status:nth-child(even) { 20 | background-color: #f5f5f5; 21 | } 22 | } 23 | 24 | .notes-new { 25 | textarea { 26 | margin-bottom: 0; 27 | border-bottom: none; 28 | .border-bottom-radius(0); 29 | } 30 | .btn { 31 | .border-top-radius(0); 32 | } 33 | } 34 | 35 | .notes-items { 36 | .note { 37 | padding: 10px 5px; 38 | font-size: 12px; 39 | border-bottom: 1px solid #ccc; 40 | 41 | .author { 42 | font-weight: normal; 43 | color: #999; 44 | text-shadow: none; 45 | background-color: transparent; 46 | border: 1px solid #ccc; 47 | } 48 | } 49 | .note:nth-child(even) { 50 | background-color: #f5f5f5; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/views/admin/accounts/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | 3 | .input-group { 4 | margin-bottom: 20px; 5 | 6 | input { 7 | width: 200px !important; 8 | } 9 | 10 | .btn { 11 | .border-left-radius(0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/views/admin/admin-groups/details.less: -------------------------------------------------------------------------------- 1 | .permissions .input-group input[disabled] { 2 | border-radius: 0; 3 | border-bottom: none; 4 | } 5 | .permissions .input-group .btn { 6 | border-radius: 0; 7 | } 8 | .permissions .input-group:first-of-type input[disabled] { 9 | border-top-left-radius: 4px; 10 | } 11 | .permissions .input-group:first-of-type .btn-delete { 12 | border-top-right-radius: 4px; 13 | } 14 | .permissions .input-group:last-of-type input[disabled] { 15 | border-bottom-left-radius: 4px; 16 | border-bottom: 1px solid #ccc; 17 | } 18 | .permissions .input-group:last-of-type .btn-delete { 19 | border-bottom-right-radius: 4px; 20 | } 21 | -------------------------------------------------------------------------------- /public/views/admin/admin-groups/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | 3 | .input-group { 4 | margin-bottom: 20px; 5 | 6 | input { 7 | width: 200px !important; 8 | } 9 | 10 | .btn { 11 | .border-left-radius(0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/views/admin/administrators/details.less: -------------------------------------------------------------------------------- 1 | .groups .input-group input[disabled], 2 | .permissions .input-group input[disabled] { 3 | border-radius: 0; 4 | border-bottom: none; 5 | } 6 | .groups .input-group .btn, 7 | .permissions .input-group .btn { 8 | border-radius: 0; 9 | } 10 | .groups .input-group:first-of-type input[disabled], 11 | .permissions .input-group:first-of-type input[disabled] { 12 | border-top-left-radius: 4px; 13 | } 14 | .groups .input-group:first-of-type .btn-delete, 15 | .permissions .input-group:first-of-type .btn-delete { 16 | border-top-right-radius: 4px; 17 | } 18 | .groups .input-group:last-of-type input[disabled], 19 | .permissions .input-group:last-of-type input[disabled] { 20 | border-bottom-left-radius: 4px; 21 | border-bottom: 1px solid #ccc; 22 | } 23 | .groups .input-group:last-of-type .btn-delete, 24 | .permissions .input-group:last-of-type .btn-delete { 25 | border-bottom-right-radius: 4px; 26 | } 27 | -------------------------------------------------------------------------------- /public/views/admin/administrators/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | 3 | .input-group { 4 | margin-bottom: 20px; 5 | 6 | input { 7 | width: 200px !important; 8 | } 9 | 10 | .btn { 11 | .border-left-radius(0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/views/admin/categories/details.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Category = Backbone.Model.extend({ 9 | idAttribute: '_id', 10 | url: function() { 11 | return '/admin/categories/'+ this.id +'/'; 12 | } 13 | }); 14 | 15 | app.Delete = Backbone.Model.extend({ 16 | idAttribute: '_id', 17 | defaults: { 18 | success: false, 19 | errors: [], 20 | errfor: {} 21 | }, 22 | url: function() { 23 | return '/admin/categories/'+ app.mainView.model.id +'/'; 24 | } 25 | }); 26 | 27 | app.Details = Backbone.Model.extend({ 28 | idAttribute: '_id', 29 | defaults: { 30 | success: false, 31 | errors: [], 32 | errfor: {}, 33 | pivot: '', 34 | name: '' 35 | }, 36 | url: function() { 37 | return '/admin/categories/'+ app.mainView.model.id +'/'; 38 | }, 39 | parse: function(response) { 40 | if (response.category) { 41 | app.mainView.model.set(response.category); 42 | delete response.category; 43 | } 44 | 45 | return response; 46 | } 47 | }); 48 | 49 | app.HeaderView = Backbone.View.extend({ 50 | el: '#header', 51 | template: _.template( $('#tmpl-header').html() ), 52 | initialize: function() { 53 | this.model = app.mainView.model; 54 | this.listenTo(this.model, 'change', this.render); 55 | this.render(); 56 | }, 57 | render: function() { 58 | this.$el.html(this.template( this.model.attributes )); 59 | } 60 | }); 61 | 62 | app.DetailsView = Backbone.View.extend({ 63 | el: '#details', 64 | template: _.template( $('#tmpl-details').html() ), 65 | events: { 66 | 'click .btn-update': 'update' 67 | }, 68 | initialize: function() { 69 | this.model = new app.Details(); 70 | this.syncUp(); 71 | this.listenTo(app.mainView.model, 'change', this.syncUp); 72 | this.listenTo(this.model, 'sync', this.render); 73 | this.render(); 74 | }, 75 | syncUp: function() { 76 | this.model.set({ 77 | _id: app.mainView.model.id, 78 | pivot: app.mainView.model.get('pivot'), 79 | name: app.mainView.model.get('name') 80 | }); 81 | }, 82 | render: function() { 83 | this.$el.html(this.template( this.model.attributes )); 84 | 85 | for (var key in this.model.attributes) { 86 | if (this.model.attributes.hasOwnProperty(key)) { 87 | this.$el.find('[name="'+ key +'"]').val(this.model.attributes[key]); 88 | } 89 | } 90 | }, 91 | update: function() { 92 | this.model.save({ 93 | pivot: this.$el.find('[name="pivot"]').val(), 94 | name: this.$el.find('[name="name"]').val() 95 | }); 96 | } 97 | }); 98 | 99 | app.DeleteView = Backbone.View.extend({ 100 | el: '#delete', 101 | template: _.template( $('#tmpl-delete').html() ), 102 | events: { 103 | 'click .btn-delete': 'delete', 104 | }, 105 | initialize: function() { 106 | this.model = new app.Delete({ _id: app.mainView.model.id }); 107 | this.listenTo(this.model, 'sync', this.render); 108 | this.render(); 109 | }, 110 | render: function() { 111 | this.$el.html(this.template( this.model.attributes )); 112 | }, 113 | delete: function() { 114 | if (confirm('Are you sure?')) { 115 | this.model.destroy({ 116 | success: function(model, response) { 117 | if (response.success) { 118 | location.href = '/admin/categories/'; 119 | } 120 | else { 121 | app.deleteView.model.set(response); 122 | } 123 | } 124 | }); 125 | } 126 | } 127 | }); 128 | 129 | app.MainView = Backbone.View.extend({ 130 | el: '.page .container', 131 | initialize: function() { 132 | app.mainView = this; 133 | this.model = new app.Category( JSON.parse( unescape($('#data-record').html()) ) ); 134 | 135 | app.headerView = new app.HeaderView(); 136 | app.detailsView = new app.DetailsView(); 137 | app.deleteView = new app.DeleteView(); 138 | } 139 | }); 140 | 141 | $(document).ready(function() { 142 | app.mainView = new app.MainView(); 143 | }); 144 | }()); 145 | -------------------------------------------------------------------------------- /public/views/admin/categories/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | @import "../../../vendor/bootstrap/less/variables.less"; 3 | 4 | .input-group { 5 | margin-bottom: 20px; 6 | 7 | input { 8 | width: 100px !important; 9 | 10 | @media screen and (min-width: @grid-float-breakpoint) { 11 | width: 160px !important; 12 | } 13 | } 14 | 15 | input:nth-child(2) { 16 | border-left: none; 17 | } 18 | 19 | .btn { 20 | .border-left-radius(0) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/views/admin/index.less: -------------------------------------------------------------------------------- 1 | @import "../../vendor/bootstrap/less/mixins.less"; 2 | 3 | .special { 4 | text-align: center; 5 | } 6 | .super-awesome { 7 | display: block; 8 | margin-top: -15px; 9 | color: #7f7f7f; 10 | font-size: 20em; 11 | } 12 | 13 | .stat { 14 | text-align: center; 15 | #gradient > .vertical(@start-color: #555; @end-color: darken(#555, 12%)); 16 | background-color: #555; 17 | padding-left: 0; 18 | padding-right: 0; 19 | } 20 | .stat-value { 21 | color: #fff; 22 | font-size: 2.2em; 23 | font-weight: bold; 24 | letter-spacing: -1px; 25 | } 26 | .stat-label { 27 | color: #ccc; 28 | font-weight: bold; 29 | } 30 | -------------------------------------------------------------------------------- /public/views/admin/statuses/details.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Status = Backbone.Model.extend({ 9 | idAttribute: '_id', 10 | url: function() { 11 | return '/admin/statuses/'+ this.id +'/'; 12 | } 13 | }); 14 | 15 | app.Delete = Backbone.Model.extend({ 16 | idAttribute: '_id', 17 | defaults: { 18 | success: false, 19 | errors: [], 20 | errfor: {} 21 | }, 22 | url: function() { 23 | return '/admin/statuses/'+ app.mainView.model.id +'/'; 24 | } 25 | }); 26 | 27 | app.Details = Backbone.Model.extend({ 28 | idAttribute: '_id', 29 | defaults: { 30 | success: false, 31 | errors: [], 32 | errfor: {}, 33 | pivot: '', 34 | name: '' 35 | }, 36 | url: function() { 37 | return '/admin/statuses/'+ app.mainView.model.id +'/'; 38 | }, 39 | parse: function(response) { 40 | if (response.status) { 41 | app.mainView.model.set(response.status); 42 | delete response.status; 43 | } 44 | 45 | return response; 46 | } 47 | }); 48 | 49 | app.HeaderView = Backbone.View.extend({ 50 | el: '#header', 51 | template: _.template( $('#tmpl-header').html() ), 52 | initialize: function() { 53 | this.model = app.mainView.model; 54 | this.listenTo(this.model, 'change', this.render); 55 | this.render(); 56 | }, 57 | render: function() { 58 | this.$el.html(this.template( this.model.attributes )); 59 | } 60 | }); 61 | 62 | app.DetailsView = Backbone.View.extend({ 63 | el: '#details', 64 | template: _.template( $('#tmpl-details').html() ), 65 | events: { 66 | 'click .btn-update': 'update' 67 | }, 68 | initialize: function() { 69 | this.model = new app.Details(); 70 | this.syncUp(); 71 | this.listenTo(app.mainView.model, 'change', this.syncUp); 72 | this.listenTo(this.model, 'sync', this.render); 73 | this.render(); 74 | }, 75 | syncUp: function() { 76 | this.model.set({ 77 | _id: app.mainView.model.id, 78 | pivot: app.mainView.model.get('pivot'), 79 | name: app.mainView.model.get('name') 80 | }); 81 | }, 82 | render: function() { 83 | this.$el.html(this.template( this.model.attributes )); 84 | 85 | for (var key in this.model.attributes) { 86 | if (this.model.attributes.hasOwnProperty(key)) { 87 | this.$el.find('[name="'+ key +'"]').val(this.model.attributes[key]); 88 | } 89 | } 90 | }, 91 | update: function() { 92 | this.model.save({ 93 | pivot: this.$el.find('[name="pivot"]').val(), 94 | name: this.$el.find('[name="name"]').val() 95 | }); 96 | } 97 | }); 98 | 99 | app.DeleteView = Backbone.View.extend({ 100 | el: '#delete', 101 | template: _.template( $('#tmpl-delete').html() ), 102 | events: { 103 | 'click .btn-delete': 'delete', 104 | }, 105 | initialize: function() { 106 | this.model = new app.Delete({ _id: app.mainView.model.id }); 107 | this.listenTo(this.model, 'sync', this.render); 108 | this.render(); 109 | }, 110 | render: function() { 111 | this.$el.html(this.template( this.model.attributes )); 112 | }, 113 | delete: function() { 114 | if (confirm('Are you sure?')) { 115 | this.model.destroy({ 116 | success: function(model, response) { 117 | if (response.success) { 118 | location.href = '/admin/statuses/'; 119 | } 120 | else { 121 | app.deleteView.model.set(response); 122 | } 123 | } 124 | }); 125 | } 126 | } 127 | }); 128 | 129 | app.MainView = Backbone.View.extend({ 130 | el: '.page .container', 131 | initialize: function() { 132 | app.mainView = this; 133 | this.model = new app.Status( JSON.parse( unescape($('#data-record').html()) ) ); 134 | 135 | app.headerView = new app.HeaderView(); 136 | app.detailsView = new app.DetailsView(); 137 | app.deleteView = new app.DeleteView(); 138 | } 139 | }); 140 | 141 | $(document).ready(function() { 142 | app.mainView = new app.MainView(); 143 | }); 144 | }()); 145 | -------------------------------------------------------------------------------- /public/views/admin/statuses/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | @import "../../../vendor/bootstrap/less/variables.less"; 3 | 4 | .input-group { 5 | margin-bottom: 20px; 6 | 7 | input { 8 | width: 100px !important; 9 | 10 | @media screen and (min-width: @grid-float-breakpoint) { 11 | width: 160px !important; 12 | } 13 | } 14 | 15 | input:nth-child(2) { 16 | border-left: none; 17 | } 18 | 19 | .btn { 20 | .border-left-radius(0) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/views/admin/users/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vendor/bootstrap/less/mixins.less"; 2 | 3 | .input-group { 4 | margin-bottom: 20px; 5 | 6 | input { 7 | width: 200px !important; 8 | } 9 | 10 | .btn { 11 | .border-left-radius(0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/views/contact/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Contact = Backbone.Model.extend({ 9 | url: '/contact/', 10 | defaults: { 11 | success: false, 12 | errors: [], 13 | errfor: {}, 14 | name: '', 15 | email: '', 16 | message: '' 17 | } 18 | }); 19 | 20 | app.ContactView = Backbone.View.extend({ 21 | el: '#contact', 22 | template: _.template( $('#tmpl-contact').html() ), 23 | events: { 24 | 'submit form': 'preventSubmit', 25 | 'click .btn-contact': 'contact' 26 | }, 27 | initialize: function() { 28 | this.model = new app.Contact(); 29 | this.listenTo(this.model, 'sync', this.render); 30 | this.render(); 31 | }, 32 | render: function() { 33 | this.$el.html(this.template( this.model.attributes )); 34 | this.$el.find('[name="name"]').focus(); 35 | }, 36 | preventSubmit: function(event) { 37 | event.preventDefault(); 38 | }, 39 | contact: function() { 40 | this.$el.find('.btn-contact').attr('disabled', true); 41 | 42 | this.model.save({ 43 | name: this.$el.find('[name="name"]').val(), 44 | email: this.$el.find('[name="email"]').val(), 45 | message: this.$el.find('[name="message"]').val() 46 | }); 47 | } 48 | }); 49 | 50 | $(document).ready(function() { 51 | app.contactView = new app.ContactView(); 52 | }); 53 | }()); 54 | -------------------------------------------------------------------------------- /public/views/contact/index.less: -------------------------------------------------------------------------------- 1 | .special { 2 | text-align: center; 3 | } 4 | .super-awesome { 5 | display: block; 6 | margin-top: -15px; 7 | color: #7f7f7f; 8 | font-size: 20em; 9 | } 10 | -------------------------------------------------------------------------------- /public/views/index.less: -------------------------------------------------------------------------------- 1 | @import "../vendor/bootstrap/less/variables.less"; 2 | 3 | .jumbotron { 4 | background-color: transparent; 5 | text-align: center; 6 | padding: 30px 60px; 7 | 8 | h1 { 9 | font-weight: bold; 10 | } 11 | 12 | @media (max-width: @screen-tablet) { 13 | h1 { 14 | font-size: (@font-size-base * 3.5); 15 | } 16 | } 17 | @media (min-width: @screen-tablet) { 18 | h1 { 19 | font-size: (@font-size-base * 7.5); 20 | } 21 | } 22 | } 23 | 24 | .panel h3 { 25 | margin-top: 0; 26 | } 27 | -------------------------------------------------------------------------------- /public/views/login/forgot/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Forgot = Backbone.Model.extend({ 9 | url: '/login/forgot/', 10 | defaults: { 11 | success: false, 12 | errors: [], 13 | errfor: {}, 14 | email: '', 15 | } 16 | }); 17 | 18 | app.ForgotView = Backbone.View.extend({ 19 | el: '#forgot', 20 | template: _.template( $('#tmpl-forgot').html() ), 21 | events: { 22 | 'submit form': 'preventSubmit', 23 | 'keypress [name="email"]': 'forgotOnEnter', 24 | 'click .btn-forgot': 'forgot' 25 | }, 26 | initialize: function() { 27 | this.model = new app.Forgot(); 28 | this.listenTo(this.model, 'sync', this.render); 29 | this.render(); 30 | }, 31 | render: function() { 32 | this.$el.html(this.template( this.model.attributes )); 33 | this.$el.find('[name="email"]').focus(); 34 | return this; 35 | }, 36 | preventSubmit: function(event) { 37 | event.preventDefault(); 38 | }, 39 | forgotOnEnter: function(event) { 40 | if (event.keyCode !== 13) { return; } 41 | event.preventDefault(); 42 | this.forgot(); 43 | }, 44 | forgot: function() { 45 | this.$el.find('.btn-forgot').attr('disabled', true); 46 | 47 | this.model.save({ 48 | email: this.$el.find('[name="email"]').val() 49 | }); 50 | } 51 | }); 52 | 53 | $(document).ready(function() { 54 | app.forgotView = new app.ForgotView(); 55 | }); 56 | }()); 57 | -------------------------------------------------------------------------------- /public/views/login/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Login = Backbone.Model.extend({ 9 | url: '/login/', 10 | defaults: { 11 | errors: [], 12 | errfor: {}, 13 | username: '', 14 | password: '' 15 | } 16 | }); 17 | 18 | app.LoginView = Backbone.View.extend({ 19 | el: '#login', 20 | template: _.template( $('#tmpl-login').html() ), 21 | events: { 22 | 'submit form': 'preventSubmit', 23 | 'keypress [name="password"]': 'loginOnEnter', 24 | 'click .btn-login': 'login' 25 | }, 26 | initialize: function() { 27 | this.model = new app.Login(); 28 | this.listenTo(this.model, 'sync', this.render); 29 | this.render(); 30 | }, 31 | render: function() { 32 | this.$el.html(this.template( this.model.attributes )); 33 | this.$el.find('[name="username"]').focus(); 34 | }, 35 | preventSubmit: function(event) { 36 | event.preventDefault(); 37 | }, 38 | loginOnEnter: function(event) { 39 | if (event.keyCode !== 13) { return; } 40 | if ($(event.target).attr('name') !== 'password') { return; } 41 | event.preventDefault(); 42 | this.login(); 43 | }, 44 | login: function() { 45 | this.$el.find('.btn-login').attr('disabled', true); 46 | 47 | this.model.save({ 48 | username: this.$el.find('[name="username"]').val(), 49 | password: this.$el.find('[name="password"]').val() 50 | },{ 51 | success: function(model, response) { 52 | if (response.success) { 53 | location.href = '/login/'; 54 | } 55 | else { 56 | model.set(response); 57 | } 58 | } 59 | }); 60 | } 61 | }); 62 | 63 | $(document).ready(function() { 64 | app.loginView = new app.LoginView(); 65 | }); 66 | }()); 67 | -------------------------------------------------------------------------------- /public/views/login/reset/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Reset = Backbone.Model.extend({ 9 | defaults: { 10 | success: false, 11 | errors: [], 12 | errfor: {}, 13 | id: undefined, 14 | email: undefined, 15 | password: '', 16 | confirm: '' 17 | }, 18 | url: function() { 19 | return '/login/reset/'+ this.get('email') +'/'+ this.id +'/'; 20 | } 21 | }); 22 | 23 | app.ResetView = Backbone.View.extend({ 24 | el: '#reset', 25 | template: _.template( $('#tmpl-reset').html() ), 26 | events: { 27 | 'submit form': 'preventSubmit', 28 | 'keypress [name="confirm"]': 'resetOnEnter', 29 | 'click .btn-reset': 'reset' 30 | }, 31 | initialize: function() { 32 | this.listenTo(this.model, 'sync', this.render); 33 | this.render(); 34 | }, 35 | render: function() { 36 | this.$el.html(this.template( this.model.attributes )); 37 | this.$el.find('[name="password"]').focus(); 38 | return this; 39 | }, 40 | preventSubmit: function(event) { 41 | event.preventDefault(); 42 | }, 43 | resetOnEnter: function(event) { 44 | if (event.keyCode !== 13) { return; } 45 | event.preventDefault(); 46 | this.reset(); 47 | }, 48 | reset: function() { 49 | this.$el.find('.btn-reset').attr('disabled', true); 50 | 51 | this.model.save({ 52 | password: this.$el.find('[name="password"]').val(), 53 | confirm: this.$el.find('[name="confirm"]').val() 54 | }); 55 | } 56 | }); 57 | 58 | app.Router = Backbone.Router.extend({ 59 | routes: { 60 | 'login/reset/': 'start', 61 | 'login/reset/:email/:token/': 'start' 62 | }, 63 | start: function(email, token) { 64 | app.resetView = new app.ResetView({ model: new app.Reset({ id: token, email: email }) }); 65 | } 66 | }); 67 | 68 | $(document).ready(function() { 69 | app.router = new app.Router(); 70 | Backbone.history.start({ pushState: true }); 71 | }); 72 | }()); 73 | -------------------------------------------------------------------------------- /public/views/signup/index.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Signup = Backbone.Model.extend({ 9 | url: '/signup/', 10 | defaults: { 11 | errors: [], 12 | errfor: {}, 13 | username: '', 14 | email: '', 15 | password: '' 16 | } 17 | }); 18 | 19 | app.SignupView = Backbone.View.extend({ 20 | el: '#signup', 21 | template: _.template( $('#tmpl-signup').html() ), 22 | events: { 23 | 'submit form': 'preventSubmit', 24 | 'keypress [name="password"]': 'signupOnEnter', 25 | 'click .btn-signup': 'signup' 26 | }, 27 | initialize: function() { 28 | this.model = new app.Signup(); 29 | this.listenTo(this.model, 'sync', this.render); 30 | this.render(); 31 | }, 32 | render: function() { 33 | this.$el.html(this.template( this.model.attributes )); 34 | this.$el.find('[name="username"]').focus(); 35 | }, 36 | preventSubmit: function(event) { 37 | event.preventDefault(); 38 | }, 39 | signupOnEnter: function(event) { 40 | if (event.keyCode !== 13) { return; } 41 | if ($(event.target).attr('name') !== 'password') { return; } 42 | event.preventDefault(); 43 | this.signup(); 44 | }, 45 | signup: function() { 46 | this.$el.find('.btn-signup').attr('disabled', true); 47 | 48 | this.model.save({ 49 | username: this.$el.find('[name="username"]').val(), 50 | email: this.$el.find('[name="email"]').val(), 51 | password: this.$el.find('[name="password"]').val() 52 | },{ 53 | success: function(model, response) { 54 | if (response.success) { 55 | location.href = '/account/'; 56 | } 57 | else { 58 | model.set(response); 59 | } 60 | } 61 | }); 62 | } 63 | }); 64 | 65 | $(document).ready(function() { 66 | app.signupView = new app.SignupView(); 67 | }); 68 | }()); 69 | -------------------------------------------------------------------------------- /public/views/signup/index.less: -------------------------------------------------------------------------------- 1 | .marketing { 2 | text-align: center; 3 | } 4 | .super-awesome { 5 | display: block; 6 | margin-top: 50px; 7 | color: #7f7f7f; 8 | font-size: 20em; 9 | } 10 | -------------------------------------------------------------------------------- /public/views/signup/social.js: -------------------------------------------------------------------------------- 1 | /* global app:true */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | app = app || {}; 7 | 8 | app.Signup = Backbone.Model.extend({ 9 | url: '/signup/social/', 10 | defaults: { 11 | errors: [], 12 | errfor: {}, 13 | email: '' 14 | } 15 | }); 16 | 17 | app.SignupView = Backbone.View.extend({ 18 | el: '#signup', 19 | template: _.template( $('#tmpl-signup').html() ), 20 | events: { 21 | 'submit form': 'preventSubmit', 22 | 'keypress [name="password"]': 'signupOnEnter', 23 | 'click .btn-signup': 'signup' 24 | }, 25 | initialize: function() { 26 | this.model = new app.Signup(); 27 | this.model.set('email', $('#data-email').text()); 28 | this.listenTo(this.model, 'sync', this.render); 29 | this.render(); 30 | }, 31 | render: function() { 32 | this.$el.html(this.template( this.model.attributes )); 33 | this.$el.find('[name="email"]').focus(); 34 | }, 35 | preventSubmit: function(event) { 36 | event.preventDefault(); 37 | }, 38 | signupOnEnter: function(event) { 39 | if (event.keyCode !== 13) { return; } 40 | event.preventDefault(); 41 | this.signup(); 42 | }, 43 | signup: function() { 44 | this.$el.find('.btn-signup').attr('disabled', true); 45 | 46 | this.model.save({ 47 | email: this.$el.find('[name="email"]').val() 48 | },{ 49 | success: function(model, response) { 50 | if (response.success) { 51 | location.href = '/account/'; 52 | } 53 | else { 54 | model.set(response); 55 | } 56 | } 57 | }); 58 | } 59 | }); 60 | 61 | $(document).ready(function() { 62 | app.signupView = new app.SignupView(); 63 | }); 64 | }()); 65 | -------------------------------------------------------------------------------- /schema/Account.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var accountSchema = new mongoose.Schema({ 5 | user: { 6 | id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 7 | name: { type: String, default: '' } 8 | }, 9 | isVerified: { type: String, default: '' }, 10 | verificationToken: { type: String, default: '' }, 11 | name: { 12 | first: { type: String, default: '' }, 13 | middle: { type: String, default: '' }, 14 | last: { type: String, default: '' }, 15 | full: { type: String, default: '' } 16 | }, 17 | company: { type: String, default: '' }, 18 | phone: { type: String, default: '' }, 19 | zip: { type: String, default: '' }, 20 | status: { 21 | id: { type: String, ref: 'Status' }, 22 | name: { type: String, default: '' }, 23 | userCreated: { 24 | id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 25 | name: { type: String, default: '' }, 26 | time: { type: Date, default: Date.now } 27 | } 28 | }, 29 | statusLog: [mongoose.modelSchemas.StatusLog], 30 | notes: [mongoose.modelSchemas.Note], 31 | userCreated: { 32 | id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 33 | name: { type: String, default: '' }, 34 | time: { type: Date, default: Date.now } 35 | }, 36 | search: [String] 37 | }); 38 | accountSchema.plugin(require('./plugins/pagedFind')); 39 | accountSchema.index({ user: 1 }); 40 | accountSchema.index({ 'status.id': 1 }); 41 | accountSchema.index({ search: 1 }); 42 | accountSchema.set('autoIndex', (app.get('env') === 'development')); 43 | app.db.model('Account', accountSchema); 44 | }; 45 | -------------------------------------------------------------------------------- /schema/Admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var adminSchema = new mongoose.Schema({ 5 | user: { 6 | id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 7 | name: { type: String, default: '' } 8 | }, 9 | name: { 10 | full: { type: String, default: '' }, 11 | first: { type: String, default: '' }, 12 | middle: { type: String, default: '' }, 13 | last: { type: String, default: '' }, 14 | }, 15 | groups: [{ type: String, ref: 'AdminGroup' }], 16 | permissions: [{ 17 | name: String, 18 | permit: Boolean 19 | }], 20 | timeCreated: { type: Date, default: Date.now }, 21 | search: [String] 22 | }); 23 | adminSchema.methods.hasPermissionTo = function(something) { 24 | //check group permissions 25 | var groupHasPermission = false; 26 | for (var i = 0 ; i < this.groups.length ; i++) { 27 | for (var j = 0 ; j < this.groups[i].permissions.length ; j++) { 28 | if (this.groups[i].permissions[j].name === something) { 29 | if (this.groups[i].permissions[j].permit) { 30 | groupHasPermission = true; 31 | } 32 | } 33 | } 34 | } 35 | 36 | //check admin permissions 37 | for (var k = 0 ; k < this.permissions.length ; k++) { 38 | if (this.permissions[k].name === something) { 39 | if (this.permissions[k].permit) { 40 | return true; 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | 47 | return groupHasPermission; 48 | }; 49 | adminSchema.methods.isMemberOf = function(group) { 50 | for (var i = 0 ; i < this.groups.length ; i++) { 51 | if (this.groups[i]._id === group) { 52 | return true; 53 | } 54 | } 55 | 56 | return false; 57 | }; 58 | adminSchema.plugin(require('./plugins/pagedFind')); 59 | adminSchema.index({ 'user.id': 1 }); 60 | adminSchema.index({ search: 1 }); 61 | adminSchema.set('autoIndex', (app.get('env') === 'development')); 62 | app.db.model('Admin', adminSchema); 63 | }; 64 | -------------------------------------------------------------------------------- /schema/AdminGroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var adminGroupSchema = new mongoose.Schema({ 5 | _id: { type: String }, 6 | name: { type: String, default: '' }, 7 | permissions: [{ name: String, permit: Boolean }] 8 | }); 9 | adminGroupSchema.plugin(require('./plugins/pagedFind')); 10 | adminGroupSchema.index({ name: 1 }, { unique: true }); 11 | adminGroupSchema.set('autoIndex', (app.get('env') === 'development')); 12 | app.db.model('AdminGroup', adminGroupSchema); 13 | }; 14 | -------------------------------------------------------------------------------- /schema/Category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var categorySchema = new mongoose.Schema({ 5 | _id: { type: String }, 6 | pivot: { type: String, default: '' }, 7 | name: { type: String, default: '' } 8 | }); 9 | categorySchema.plugin(require('./plugins/pagedFind')); 10 | categorySchema.index({ pivot: 1 }); 11 | categorySchema.index({ name: 1 }); 12 | categorySchema.set('autoIndex', (app.get('env') === 'development')); 13 | app.db.model('Category', categorySchema); 14 | }; 15 | -------------------------------------------------------------------------------- /schema/LoginAttempt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var attemptSchema = new mongoose.Schema({ 5 | ip: { type: String, default: '' }, 6 | user: { type: String, default: '' }, 7 | time: { type: Date, default: Date.now, expires: app.config.loginAttempts.logExpiration } 8 | }); 9 | attemptSchema.index({ ip: 1 }); 10 | attemptSchema.index({ user: 1 }); 11 | attemptSchema.set('autoIndex', (app.get('env') === 'development')); 12 | app.db.model('LoginAttempt', attemptSchema); 13 | }; 14 | -------------------------------------------------------------------------------- /schema/Note.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var noteSchema = new mongoose.Schema({ 5 | data: { type: String, default: '' }, 6 | userCreated: { 7 | id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 8 | name: { type: String, default: '' }, 9 | time: { type: Date, default: Date.now } 10 | } 11 | }); 12 | app.db.model('Note', noteSchema); 13 | }; 14 | -------------------------------------------------------------------------------- /schema/Status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var statusSchema = new mongoose.Schema({ 5 | _id: { type: String }, 6 | pivot: { type: String, default: '' }, 7 | name: { type: String, default: '' } 8 | }); 9 | statusSchema.plugin(require('./plugins/pagedFind')); 10 | statusSchema.index({ pivot: 1 }); 11 | statusSchema.index({ name: 1 }); 12 | statusSchema.set('autoIndex', (app.get('env') === 'development')); 13 | app.db.model('Status', statusSchema); 14 | }; 15 | -------------------------------------------------------------------------------- /schema/StatusLog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var statusLogSchema = new mongoose.Schema({ 5 | id: { type: String, ref: 'Status' }, 6 | name: { type: String, default: '' }, 7 | userCreated: { 8 | id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 9 | name: { type: String, default: '' }, 10 | time: { type: Date, default: Date.now } 11 | } 12 | }); 13 | app.db.model('StatusLog', statusLogSchema); 14 | }; 15 | -------------------------------------------------------------------------------- /schema/User.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(app, mongoose) { 4 | var userSchema = new mongoose.Schema({ 5 | username: { type: String, unique: true }, 6 | password: String, 7 | email: { type: String, unique: true }, 8 | roles: { 9 | admin: { type: mongoose.Schema.Types.ObjectId, ref: 'Admin' }, 10 | account: { type: mongoose.Schema.Types.ObjectId, ref: 'Account' } 11 | }, 12 | isActive: String, 13 | timeCreated: { type: Date, default: Date.now }, 14 | resetPasswordToken: String, 15 | resetPasswordExpires: Date, 16 | twitter: {}, 17 | github: {}, 18 | facebook: {}, 19 | google: {}, 20 | tumblr: {}, 21 | search: [String] 22 | }); 23 | userSchema.methods.canPlayRoleOf = function(role) { 24 | if (role === "admin" && this.roles.admin) { 25 | return true; 26 | } 27 | 28 | if (role === "account" && this.roles.account) { 29 | return true; 30 | } 31 | 32 | return false; 33 | }; 34 | userSchema.methods.defaultReturnUrl = function() { 35 | var returnUrl = '/'; 36 | if (this.canPlayRoleOf('account')) { 37 | returnUrl = '/account/'; 38 | } 39 | 40 | if (this.canPlayRoleOf('admin')) { 41 | returnUrl = '/admin/'; 42 | } 43 | 44 | return returnUrl; 45 | }; 46 | userSchema.statics.encryptPassword = function(password, done) { 47 | var bcrypt = require('bcrypt'); 48 | bcrypt.genSalt(10, function(err, salt) { 49 | if (err) { 50 | return done(err); 51 | } 52 | 53 | bcrypt.hash(password, salt, function(err, hash) { 54 | done(err, hash); 55 | }); 56 | }); 57 | }; 58 | userSchema.statics.validatePassword = function(password, hash, done) { 59 | var bcrypt = require('bcrypt'); 60 | bcrypt.compare(password, hash, function(err, res) { 61 | done(err, res); 62 | }); 63 | }; 64 | userSchema.plugin(require('./plugins/pagedFind')); 65 | userSchema.index({ username: 1 }, { unique: true }); 66 | userSchema.index({ email: 1 }, { unique: true }); 67 | userSchema.index({ timeCreated: 1 }); 68 | userSchema.index({ 'twitter.id': 1 }); 69 | userSchema.index({ 'github.id': 1 }); 70 | userSchema.index({ 'facebook.id': 1 }); 71 | userSchema.index({ 'google.id': 1 }); 72 | userSchema.index({ search: 1 }); 73 | userSchema.set('autoIndex', (app.get('env') === 'development')); 74 | app.db.model('User', userSchema); 75 | }; 76 | -------------------------------------------------------------------------------- /schema/plugins/pagedFind.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = function pagedFindPlugin (schema) { 4 | schema.statics.pagedFind = function(options, cb) { 5 | var thisSchema = this; 6 | 7 | if (!options.filters) { 8 | options.filters = {}; 9 | } 10 | 11 | if (!options.keys) { 12 | options.keys = ''; 13 | } 14 | 15 | if (!options.limit) { 16 | options.limit = 20; 17 | } 18 | 19 | if (!options.page) { 20 | options.page = 1; 21 | } 22 | 23 | if (!options.sort) { 24 | options.sort = {}; 25 | } 26 | 27 | var output = { 28 | data: null, 29 | pages: { 30 | current: options.page, 31 | prev: 0, 32 | hasPrev: false, 33 | next: 0, 34 | hasNext: false, 35 | total: 0 36 | }, 37 | items: { 38 | begin: ((options.page * options.limit) - options.limit) + 1, 39 | end: options.page * options.limit, 40 | total: 0 41 | } 42 | }; 43 | 44 | var countResults = function(callback) { 45 | thisSchema.count(options.filters, function(err, count) { 46 | output.items.total = count; 47 | callback(null, 'done counting'); 48 | }); 49 | }; 50 | 51 | var getResults = function(callback) { 52 | var query = thisSchema.find(options.filters, options.keys); 53 | query.skip((options.page - 1) * options.limit); 54 | query.limit(options.limit); 55 | query.sort(options.sort); 56 | query.exec(function(err, results) { 57 | output.data = results; 58 | callback(null, 'done getting records'); 59 | }); 60 | }; 61 | 62 | require('async').parallel([ 63 | countResults, 64 | getResults 65 | ], 66 | function(err, results){ 67 | if (err) { 68 | cb(err, null); 69 | } 70 | 71 | //final paging math 72 | output.pages.total = Math.ceil(output.items.total / options.limit); 73 | output.pages.next = ((output.pages.current + 1) > output.pages.total ? 0 : output.pages.current + 1); 74 | output.pages.hasNext = (output.pages.next !== 0); 75 | output.pages.prev = output.pages.current - 1; 76 | output.pages.hasPrev = (output.pages.prev !== 0); 77 | if (output.items.end > output.items.total) { 78 | output.items.end = output.items.total; 79 | } 80 | 81 | cb(null, output); 82 | }); 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /util/sendmail/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(req, res, options) { 4 | /* options = { 5 | from: String, 6 | to: String, 7 | cc: String, 8 | bcc: String, 9 | text: String, 10 | textPath String, 11 | html: String, 12 | htmlPath: String, 13 | attachments: [String], 14 | success: Function, 15 | error: Function 16 | } */ 17 | 18 | var renderText = function(callback) { 19 | res.render(options.textPath, options.locals, function(err, text) { 20 | if (err) { 21 | callback(err, null); 22 | } 23 | else { 24 | options.text = text; 25 | return callback(null, 'done'); 26 | } 27 | }); 28 | }; 29 | 30 | var renderHtml = function(callback) { 31 | res.render(options.htmlPath, options.locals, function(err, html) { 32 | if (err) { 33 | callback(err, null); 34 | } 35 | else { 36 | options.html = html; 37 | return callback(null, 'done'); 38 | } 39 | }); 40 | }; 41 | 42 | var renderers = []; 43 | if (options.textPath) { 44 | renderers.push(renderText); 45 | } 46 | 47 | if (options.htmlPath) { 48 | renderers.push(renderHtml); 49 | } 50 | 51 | require('async').parallel( 52 | renderers, 53 | function(err, results){ 54 | if (err) { 55 | options.error('Email template render failed. '+ err); 56 | return; 57 | } 58 | 59 | var attachments = []; 60 | 61 | if (options.html) { 62 | attachments.push({ data: options.html, alternative: true }); 63 | } 64 | 65 | if (options.attachments) { 66 | for (var i = 0 ; i < options.attachments.length ; i++) { 67 | attachments.push(options.attachments[i]); 68 | } 69 | } 70 | 71 | var emailjs = require('emailjs/email'); 72 | var emailer = emailjs.server.connect( req.app.config.smtp.credentials ); 73 | emailer.send({ 74 | from: options.from, 75 | to: options.to, 76 | 'reply-to': options.replyTo || options.from, 77 | cc: options.cc, 78 | bcc: options.bcc, 79 | subject: options.subject, 80 | text: options.text, 81 | attachment: attachments 82 | }, function(err, message) { 83 | if (err) { 84 | options.error('Email failed to send. '+ err); 85 | return; 86 | } 87 | else { 88 | options.success(message); 89 | return; 90 | } 91 | }); 92 | } 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /util/slugify/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(text) { 4 | return text.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-'); 5 | }; 6 | -------------------------------------------------------------------------------- /util/workflow/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = function(req, res) { 4 | var workflow = new (require('events').EventEmitter)(); 5 | 6 | workflow.outcome = { 7 | success: false, 8 | errors: [], 9 | errfor: {} 10 | }; 11 | 12 | workflow.hasErrors = function() { 13 | return Object.keys(workflow.outcome.errfor).length !== 0 || workflow.outcome.errors.length !== 0; 14 | }; 15 | 16 | workflow.on('exception', function(err) { 17 | workflow.outcome.errors.push('Exception: '+ err); 18 | return workflow.emit('response'); 19 | }); 20 | 21 | workflow.on('response', function() { 22 | workflow.outcome.success = !workflow.hasErrors(); 23 | res.send(workflow.outcome); 24 | }); 25 | 26 | return workflow; 27 | }; 28 | -------------------------------------------------------------------------------- /views/about/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | res.render('about/index'); 5 | }; 6 | -------------------------------------------------------------------------------- /views/account/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/account 2 | 3 | block head 4 | title Account Area 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/account/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/account/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-sm-6 15 | div.page-header 16 | h1 My Account 17 | div.row 18 | div.col-sm-4 19 | div.well.stat 20 | div.stat-value.day-of-year -- 21 | div.stat-label Day of Year 22 | div.col-sm-4 23 | div.well.stat 24 | div.stat-value.day-of-month -- 25 | div.stat-label Day of Month 26 | div.col-sm-4 27 | div.well.stat 28 | div.stat-value.week-of-year -- 29 | div.stat-label Week of Year 30 | div.row 31 | div.col-sm-4 32 | div.well.stat 33 | div.stat-value.day-of-week -- 34 | div.stat-label Day of Week 35 | div.col-sm-4 36 | div.well.stat 37 | div.stat-value.week-year -- 38 | div.stat-label Week Year 39 | div.col-sm-4 40 | div.well.stat 41 | div.stat-value.hour-of-day -- 42 | div.stat-label Hour of Day 43 | div.col-sm-6.special 44 | div.page-header 45 | h1 Go Faster Everyday 46 | i.fa.fa-dashboard.super-awesome 47 | -------------------------------------------------------------------------------- /views/account/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | res.render('account/index'); 5 | }; 6 | -------------------------------------------------------------------------------- /views/account/settings/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/account 2 | 3 | block head 4 | title Account Settings 5 | 6 | block feet 7 | script(src='/views/account/settings/index.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-xs-12 12 | div.page-header 13 | h1 Account Settings 14 | div.row 15 | div.col-sm-9 16 | div#details 17 | div#identity 18 | div#password 19 | div.col-sm-3 20 | if oauthTwitter || oauthGitHub || oauthFacebook || oauthGoogle || oauthTumblr 21 | legend Social Connections 22 | if oauthMessage 23 | div.alerts 24 | div.alert.alert-info.alert-dismissable 25 | button.close(type='button', data-dismiss='alert') × 26 | |#{oauthMessage} 27 | if oauthTwitter 28 | if oauthTwitterActive 29 | a.btn.btn-block.btn-danger(href='/account/settings/twitter/disconnect/') 30 | i.fa.fa-twitter.fa-lg 31 | | Disconnect Twitter 32 | else 33 | a.btn.btn-block.btn-default(href='/account/settings/twitter/') 34 | i.fa.fa-twitter.fa-lg 35 | | Connect Twitter 36 | if oauthGitHub 37 | if oauthGitHubActive 38 | a.btn.btn-block.btn-danger(href='/account/settings/github/disconnect/') 39 | i.fa.fa-github.fa-lg 40 | | Disconnect GitHub 41 | else 42 | a.btn.btn-block.btn-default(href='/account/settings/github/') 43 | i.fa.fa-github.fa-lg 44 | | Connect GitHub 45 | if oauthFacebook 46 | if oauthFacebookActive 47 | a.btn.btn-block.btn-danger(href='/account/settings/facebook/disconnect/') 48 | i.fa.fa-facebook-square.fa-lg 49 | | Disconnect Facebook 50 | else 51 | a.btn.btn-block.btn-default(href='/account/settings/facebook/') 52 | i.fa.fa-facebook-square.fa-lg 53 | | Connect Facebook 54 | if oauthGoogle 55 | if oauthGoogleActive 56 | a.btn.btn-block.btn-danger(href='/account/settings/google/disconnect/') 57 | i.fa.fa-google-plus-square.fa-lg 58 | | Disconnect Google 59 | else 60 | a.btn.btn-block.btn-default(href='/account/settings/google/') 61 | i.fa.fa-google-plus-square.fa-lg 62 | | Connect Google 63 | if oauthTumblr 64 | if oauthTumblrActive 65 | a.btn.btn-block.btn-danger(href='/account/settings/tumblr/disconnect/') 66 | i.fa.fa-tumblr-square.fa-lg 67 | | Disconnect Tumblr 68 | else 69 | a.btn.btn-block.btn-default(href='/account/settings/tumblr/') 70 | i.fa.fa-tumblr-square.fa-lg 71 | | Connect Tumblr 72 | 73 | script(type='text/template', id='tmpl-details') 74 | fieldset 75 | legend Contact Info 76 | div.alerts 77 | |<% _.each(errors, function(err) { %> 78 | div.alert.alert-danger.alert-dismissable 79 | |<%- err %> 80 | |<% }); %> 81 | |<% if (success) { %> 82 | div.alert.alert-info.alert-dismissable 83 | button.close(type='button', data-dismiss='alert') × 84 | | Changes have been saved. 85 | |<% } %> 86 | div.form-group(class!='<%- errfor.first ? "has-error" : "" %>') 87 | label First Name: 88 | input.form-control(type='text', name='first', value!='<%- first %>') 89 | span.help-block <%- errfor.first %> 90 | div.form-group(class!='<%- errfor.middle ? "has-error" : "" %>') 91 | label Middle Name: 92 | input.form-control(type='text', name='middle', value!='<%- middle %>') 93 | span.help-block <%- errfor.middle %> 94 | div.form-group(class!='<%- errfor.last ? "has-error" : "" %>') 95 | label Last Name: 96 | input.form-control(type='text', name='last', value!='<%- last %>') 97 | span.help-block <%- errfor['last'] %> 98 | div.form-group(class!='<%- errfor.company ? "has-error" : "" %>') 99 | label Company Name: 100 | input.form-control(type='text', name='company', value!='<%- company %>') 101 | span.help-block <%- errfor.company %> 102 | div.form-group(class!='<%- errfor.phone ? "has-error" : "" %>') 103 | label Phone: 104 | input.form-control(type='text', name='phone', value!='<%- phone %>') 105 | span.help-block <%- errfor.phone %> 106 | div.form-group(class!='<%- errfor.zip ? "has-error" : "" %>') 107 | label Zip: 108 | input.form-control(type='text', name='zip', value!='<%- zip %>') 109 | span.help-block <%- errfor.zip %> 110 | div.form-group 111 | button.btn.btn-primary.btn-update(type='button') Update 112 | 113 | script(type='text/template', id='tmpl-identity') 114 | fieldset 115 | legend Identity 116 | div.alerts 117 | |<% _.each(errors, function(err) { %> 118 | div.alert.alert-danger.alert-dismissable 119 | button.close(type='button', data-dismiss='alert') × 120 | |<%- err %> 121 | |<% }); %> 122 | |<% if (success) { %> 123 | div.alert.alert-info.alert-dismissable 124 | button.close(type='button', data-dismiss='alert') × 125 | | Changes have been saved. 126 | |<% } %> 127 | div.form-group(class!='<%- errfor.username ? "has-error" : "" %>') 128 | label Username: 129 | input.form-control(type='text', name='username', value!='<%= username %>') 130 | span.help-block <%- errfor.username %> 131 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 132 | label Email: 133 | input.form-control(type='text', name='email', value!='<%= email %>') 134 | span.help-block <%- errfor.email %> 135 | div.form-group 136 | button.btn.btn-primary.btn-update(type='button') Update 137 | 138 | script(type='text/template', id='tmpl-password') 139 | fieldset 140 | legend Set Password 141 | div.alerts 142 | |<% _.each(errors, function(err) { %> 143 | div.alert.alert-danger.alert-dismissable 144 | button.close(type='button', data-dismiss='alert') × 145 | |<%- err %> 146 | |<% }); %> 147 | |<% if (success) { %> 148 | div.alert.alert-info.alert-dismissable 149 | button.close(type='button', data-dismiss='alert') × 150 | | A new password has been set. 151 | |<% } %> 152 | div.form-group(class!='<%- errfor.newPassword ? "has-error" : "" %>') 153 | label New Password: 154 | input.form-control(type='password', name='newPassword', value!='<%= newPassword %>') 155 | span.help-block <%- errfor.newPassword %> 156 | div.form-group(class!='<%- errfor.confirm ? "has-error" : "" %>') 157 | label Confirm Password: 158 | input.form-control(type='password', name='confirm', value!='<%= confirm %>') 159 | span.help-block <%- errfor.confirm %> 160 | div.form-group 161 | button.btn.btn-primary.btn-password(type='button') Set Password 162 | 163 | script(type='text/template', id='data-account') !{data.account} 164 | script(type='text/template', id='data-user') !{data.user} 165 | -------------------------------------------------------------------------------- /views/account/verification/email-html.jade: -------------------------------------------------------------------------------- 1 | h3 #{projectName} Email Verification 2 | p Your #{projectName} account is nearly ready. Please visit the link below to confirm your email address. 3 | a(href='#{verifyURL}') #{verifyURL} 4 | p Note: If you did not sign for a #{projectName} account, don't worry. You can ignore this email. 5 | p 6 | | Thanks, 7 | br 8 | | #{projectName} 9 | -------------------------------------------------------------------------------- /views/account/verification/email-text.jade: -------------------------------------------------------------------------------- 1 | | #{projectName} Email Verification 2 | = '\n' 3 | = '\n' 4 | | Your #{projectName} account is nearly ready. Please visit the link below to confirm your email address. 5 | = '\n' 6 | = '\n' 7 | | #{verifyURL} 8 | = '\n' 9 | = '\n' 10 | | Note: If you did not sign for a #{projectName} account, don't worry. You can ignore this email. 11 | = '\n' 12 | = '\n' 13 | | Thanks, 14 | | #{projectName} 15 | -------------------------------------------------------------------------------- /views/account/verification/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/account 2 | 3 | block head 4 | title Verification Required 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/account/verification/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/account/verification/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-sm-6 15 | div.page-header 16 | h1 Verification Required 17 | div.alert.alert-warning Your account is nearly ready. Check your inbox for next steps. 18 | div#verify 19 | div.col-sm-6.special 20 | div.page-header 21 | h1 You're Almost Done 22 | i.fa.fa-key.super-awesome 23 | 24 | script(type='text/template', id='tmpl-verify') 25 | form 26 | div.alerts 27 | |<% _.each(errors, function(err) { %> 28 | div.alert.alert-danger.alert-dismissable 29 | button.close(type='button', data-dismiss='alert') × 30 | |<%- err %> 31 | |<% }); %> 32 | |<% if (success) { %> 33 | div.alert.alert-info.alert-dismissable 34 | button.close(type='button', data-dismiss='alert') × 35 | | Verification email successfully re-sent. 36 | |<% } %> 37 | |<% if (!success) { %> 38 | div(class!='not-received<%= !keepFormOpen ? "" : " not-received-hidden" %>') 39 | a.btn.btn-link.btn-resend I checked my email and spam folder, nothing yet. 40 | div(class!='verify-form<%= keepFormOpen ? "" : " verify-form-hidden" %>') 41 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 42 | label Your Email: 43 | input.form-control(type='text', name='email', value!='<%= email %>') 44 | span.help-block <%- errfor.email %> 45 | div.form-group 46 | button.btn.btn-primary.btn-verify(type='button') Re-Send Verification 47 | |<% } %> 48 | 49 | script(type='text/template', id='data-user') !{data.user} 50 | -------------------------------------------------------------------------------- /views/account/verification/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sendVerificationEmail = function(req, res, options) { 4 | req.app.utility.sendmail(req, res, { 5 | from: req.app.config.smtp.from.name +' <'+ req.app.config.smtp.from.address +'>', 6 | to: options.email, 7 | subject: 'Verify Your '+ req.app.config.projectName +' Account', 8 | textPath: 'account/verification/email-text', 9 | htmlPath: 'account/verification/email-html', 10 | locals: { 11 | verifyURL: req.protocol +'://'+ req.headers.host +'/account/verification/' + options.verificationToken + '/', 12 | projectName: req.app.config.projectName 13 | }, 14 | success: function() { 15 | options.onSuccess(); 16 | }, 17 | error: function(err) { 18 | options.onError(err); 19 | } 20 | }); 21 | }; 22 | 23 | exports.init = function(req, res, next){ 24 | if (req.user.roles.account.isVerified === 'yes') { 25 | return res.redirect(req.user.defaultReturnUrl()); 26 | } 27 | 28 | var workflow = req.app.utility.workflow(req, res); 29 | 30 | workflow.on('renderPage', function() { 31 | req.app.db.models.User.findById(req.user.id, 'email').exec(function(err, user) { 32 | if (err) { 33 | return next(err); 34 | } 35 | 36 | res.render('account/verification/index', { 37 | data: { 38 | user: JSON.stringify(user) 39 | } 40 | }); 41 | }); 42 | }); 43 | 44 | workflow.on('generateTokenOrRender', function() { 45 | if (req.user.roles.account.verificationToken !== '') { 46 | return workflow.emit('renderPage'); 47 | } 48 | 49 | workflow.emit('generateToken'); 50 | }); 51 | 52 | workflow.on('generateToken', function() { 53 | var crypto = require('crypto'); 54 | crypto.randomBytes(21, function(err, buf) { 55 | if (err) { 56 | return next(err); 57 | } 58 | 59 | var token = buf.toString('hex'); 60 | req.app.db.models.User.encryptPassword(token, function(err, hash) { 61 | if (err) { 62 | return next(err); 63 | } 64 | 65 | workflow.emit('patchAccount', token, hash); 66 | }); 67 | }); 68 | }); 69 | 70 | workflow.on('patchAccount', function(token, hash) { 71 | var fieldsToSet = { verificationToken: hash }; 72 | var options = { new: true }; 73 | req.app.db.models.Account.findByIdAndUpdate(req.user.roles.account.id, fieldsToSet, options, function(err, account) { 74 | if (err) { 75 | return next(err); 76 | } 77 | 78 | sendVerificationEmail(req, res, { 79 | email: req.user.email, 80 | verificationToken: token, 81 | onSuccess: function() { 82 | return workflow.emit('renderPage'); 83 | }, 84 | onError: function(err) { 85 | return next(err); 86 | } 87 | }); 88 | }); 89 | }); 90 | 91 | workflow.emit('generateTokenOrRender'); 92 | }; 93 | 94 | exports.resendVerification = function(req, res, next){ 95 | if (req.user.roles.account.isVerified === 'yes') { 96 | return res.redirect(req.user.defaultReturnUrl()); 97 | } 98 | 99 | var workflow = req.app.utility.workflow(req, res); 100 | 101 | workflow.on('validate', function() { 102 | if (!req.body.email) { 103 | workflow.outcome.errfor.email = 'required'; 104 | } 105 | else if (!/^[a-zA-Z0-9\-\_\.\+]+@[a-zA-Z0-9\-\_\.]+\.[a-zA-Z0-9\-\_]+$/.test(req.body.email)) { 106 | workflow.outcome.errfor.email = 'invalid email format'; 107 | } 108 | 109 | if (workflow.hasErrors()) { 110 | return workflow.emit('response'); 111 | } 112 | 113 | workflow.emit('duplicateEmailCheck'); 114 | }); 115 | 116 | workflow.on('duplicateEmailCheck', function() { 117 | req.app.db.models.User.findOne({ email: req.body.email.toLowerCase(), _id: { $ne: req.user.id } }, function(err, user) { 118 | if (err) { 119 | return workflow.emit('exception', err); 120 | } 121 | 122 | if (user) { 123 | workflow.outcome.errfor.email = 'email already taken'; 124 | return workflow.emit('response'); 125 | } 126 | 127 | workflow.emit('patchUser'); 128 | }); 129 | }); 130 | 131 | workflow.on('patchUser', function() { 132 | var fieldsToSet = { email: req.body.email.toLowerCase() }; 133 | var options = { new: true }; 134 | req.app.db.models.User.findByIdAndUpdate(req.user.id, fieldsToSet, options, function(err, user) { 135 | if (err) { 136 | return workflow.emit('exception', err); 137 | } 138 | 139 | workflow.user = user; 140 | workflow.emit('generateToken'); 141 | }); 142 | }); 143 | 144 | workflow.on('generateToken', function() { 145 | var crypto = require('crypto'); 146 | crypto.randomBytes(21, function(err, buf) { 147 | if (err) { 148 | return next(err); 149 | } 150 | 151 | var token = buf.toString('hex'); 152 | req.app.db.models.User.encryptPassword(token, function(err, hash) { 153 | if (err) { 154 | return next(err); 155 | } 156 | 157 | workflow.emit('patchAccount', token, hash); 158 | }); 159 | }); 160 | }); 161 | 162 | workflow.on('patchAccount', function(token, hash) { 163 | var fieldsToSet = { verificationToken: hash }; 164 | var options = { new: true }; 165 | req.app.db.models.Account.findByIdAndUpdate(req.user.roles.account.id, fieldsToSet, options, function(err, account) { 166 | if (err) { 167 | return workflow.emit('exception', err); 168 | } 169 | 170 | sendVerificationEmail(req, res, { 171 | email: workflow.user.email, 172 | verificationToken: token, 173 | onSuccess: function() { 174 | workflow.emit('response'); 175 | }, 176 | onError: function(err) { 177 | workflow.outcome.errors.push('Error Sending: '+ err); 178 | workflow.emit('response'); 179 | } 180 | }); 181 | }); 182 | }); 183 | 184 | workflow.emit('validate'); 185 | }; 186 | 187 | exports.verify = function(req, res, next){ 188 | req.app.db.models.User.validatePassword(req.params.token, req.user.roles.account.verificationToken, function(err, isValid) { 189 | if (!isValid) { 190 | return res.redirect(req.user.defaultReturnUrl()); 191 | } 192 | 193 | var fieldsToSet = { isVerified: 'yes', verificationToken: '' }; 194 | var options = { new: true }; 195 | req.app.db.models.Account.findByIdAndUpdate(req.user.roles.account._id, fieldsToSet, options, function(err, account) { 196 | if (err) { 197 | return next(err); 198 | } 199 | 200 | return res.redirect(req.user.defaultReturnUrl()); 201 | }); 202 | }); 203 | }; 204 | -------------------------------------------------------------------------------- /views/admin/accounts/details.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Accounts / Details 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/accounts/details.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/accounts/details.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div.row 17 | div.col-sm-8 18 | div#details 19 | div#login 20 | div#delete 21 | div.col-sm-4 22 | fieldset 23 | div#status-new.status-new 24 | div#status-collection 25 | fieldset 26 | div#notes-new.notes-new 27 | div#notes-collection 28 | 29 | script(type='text/template', id='tmpl-header') 30 | div.page-header 31 | h1 32 | a(href='/admin/accounts/') Accounts 33 | | / <%- name.full %> 34 | 35 | script(type='text/template', id='tmpl-details') 36 | fieldset 37 | legend Contact Info 38 | div.alerts 39 | |<% _.each(errors, function(err) { %> 40 | div.alert.alert-danger.alert-dismissable 41 | |<%- err %> 42 | |<% }); %> 43 | |<% if (success) { %> 44 | div.alert.alert-info.alert-dismissable 45 | button.close(type='button', data-dismiss='alert') × 46 | | Changes have been saved. 47 | |<% } %> 48 | div.form-group(class!='<%- errfor.first ? "has-error" : "" %>') 49 | label First Name: 50 | input.form-control(type='text', name='first', value!='<%- first %>') 51 | span.help-block <%- errfor.first %> 52 | div.form-group(class!='<%- errfor.middle ? "has-error" : "" %>') 53 | label Middle Name: 54 | input.form-control(type='text', name='middle', value!='<%- middle %>') 55 | span.help-block <%- errfor.middle %> 56 | div.form-group(class!='<%- errfor.last ? "has-error" : "" %>') 57 | label Last Name: 58 | input.form-control(type='text', name='last', value!='<%- last %>') 59 | span.help-block <%- errfor['last'] %> 60 | div.form-group(class!='<%- errfor.company ? "has-error" : "" %>') 61 | label Company Name: 62 | input.form-control(type='text', name='company', value!='<%- company %>') 63 | span.help-block <%- errfor.company %> 64 | div.form-group(class!='<%- errfor.phone ? "has-error" : "" %>') 65 | label Phone: 66 | input.form-control(type='text', name='phone', value!='<%- phone %>') 67 | span.help-block <%- errfor.phone %> 68 | div.form-group(class!='<%- errfor.zip ? "has-error" : "" %>') 69 | label Zip: 70 | input.form-control(type='text', name='zip', value!='<%- zip %>') 71 | span.help-block <%- errfor.zip %> 72 | div.form-group 73 | button.btn.btn-primary.btn-update(type='button') Update 74 | 75 | script(type='text/template', id='tmpl-login') 76 | fieldset 77 | legend Login 78 | div.alerts 79 | |<% _.each(errors, function(err) { %> 80 | div.alert.alert-danger.alert-dismissable 81 | button.close(type='button', data-dismiss='alert') × 82 | |<%- err %> 83 | |<% }); %> 84 | div.form-group(class!='<%- errfor.newUsername ? "has-error" : "" %>') 85 | label Username: 86 | div.input-group 87 | |<% if (name) { %> 88 | input.form-control(disabled=true, value!='<%= name %>') 89 | div.input-group-btn 90 | button.btn.btn-warning.btn-user-unlink(type='button') Unlink 91 | button.btn.btn-default.btn-user-open(type='button') Open 92 | |<% } else { %> 93 | input.form-control(name='newUsername', type='text', placeholder='enter a username') 94 | div.input-group-btn 95 | button.btn.btn-success.btn-user-link(type='button') Link 96 | |<% } %> 97 | span.help-block <%- errfor.newUsername %> 98 | 99 | script(type='text/template', id='tmpl-status-new') 100 | legend Status 101 | div.alerts 102 | |<% _.each(errors, function(err) { %> 103 | div.alert.alert-danger.alert-dismissable 104 | button.close(type='button', data-dismiss='alert') × 105 | |<%- err %> 106 | |<% }); %> 107 | div.input-group 108 | select.form-control(name='status') 109 | option(value='') -- choose -- 110 | for status in data.statuses 111 | option(value='#{status._id}') #{status.name} 112 | div.input-group-btn 113 | button.btn.btn-default.btn-add Change 114 | 115 | script(type='text/template', id='tmpl-status-collection') 116 | div#status-items.status-items 117 | 118 | script(type='text/template', id='tmpl-status-item') 119 | div.pull-right.badge.author 120 | |<%= userCreated.name %> 121 | | -  122 | span.timeago <%= userCreated.time %> 123 | div <%- name %> 124 | div.clearfix 125 | 126 | script(type='text/template', id='tmpl-notes-new') 127 | legend Notes 128 | div.alerts 129 | |<% _.each(errors, function(err) { %> 130 | div.alert.alert-danger.alert-dismissable 131 | button.close(type='button', data-dismiss='alert') × 132 | |<%- err %> 133 | |<% }); %> 134 | textarea.form-control(rows='3', name='data', placeholder='enter notes') 135 | button.btn.btn-default.btn-block.btn-add Add New Note 136 | 137 | script(type='text/template', id='tmpl-notes-collection') 138 | div#notes-items.notes-items 139 | 140 | script(type='text/template', id='tmpl-notes-item') 141 | div.force-wrap <%- data %> 142 | div.pull-right.badge.author 143 | |<%= userCreated.name %> 144 | | -  145 | span.timeago <%= userCreated.time %> 146 | div.clearfix 147 | 148 | script(type='text/template', id='tmpl-notes-none') 149 | div.note.text-muted no notes found 150 | 151 | script(type='text/template', id='tmpl-delete') 152 | fieldset 153 | legend Danger Zone 154 | div.alerts 155 | |<% _.each(errors, function(err) { %> 156 | div.alert.alert-danger.alert-dismissable 157 | button.close(type='button', data-dismiss='alert') × 158 | |<%- err %> 159 | |<% }); %> 160 | div.form-group 161 | span.help-block 162 | span.label.label-danger If you do this, it cannot be undone. 163 | |  164 | span.text-muted You may also create orphaned document relationships too. 165 | div.form-group 166 | button.btn.btn-danger.btn-delete(type='button') Delete 167 | 168 | script(type='text/template', id='data-record') !{data.record} 169 | -------------------------------------------------------------------------------- /views/admin/accounts/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Manage Accounts 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/accounts/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/accounts/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div#filters 17 | div#results-table 18 | div#results-paging 19 | 20 | script(type='text/template', id='tmpl-header') 21 | div.page-header 22 | form.form-inline.pull-right 23 | div.input-group 24 | input.form-control(name='name', type='text', placeholder='enter a name', value!='<%- name.full %>') 25 | button.btn.btn-primary.btn-add(type='button') Add New 26 | h1 Accounts 27 | 28 | script(type='text/template', id='tmpl-filters') 29 | form.filters 30 | div.row 31 | div.col-sm-3 32 | label Search 33 | input.form-control(name='search', type='text') 34 | div.col-sm-3 35 | label Status 36 | select.form-control(name='status') 37 | option(value='') -- any -- 38 | for status in data.statuses 39 | option(value='#{status._id}') #{status.name} 40 | div.col-sm-3 41 | label Sort By 42 | select.form-control(name='sort') 43 | option(value='_id') id ▲ 44 | option(value='-_id') id ▼ 45 | option(value='name') name ▲ 46 | option(value='-name') name ▼ 47 | option(value='company') company ▲ 48 | option(value='-company') company ▼ 49 | div.col-sm-3 50 | label Limit 51 | select.form-control(name='limit') 52 | option(value='10') 10 items 53 | option(value='20', selected='selected') 20 items 54 | option(value='50') 50 items 55 | option(value='100') 100 items 56 | 57 | script(type='text/template', id='tmpl-results-table') 58 | table.table.table-striped 59 | thead 60 | tr 61 | th 62 | th 63 | | name 64 | span.pull-right.timeago.muted age 65 | th phone 66 | th status 67 | tbody#results-rows 68 | 69 | script(type='text/template', id='tmpl-results-row') 70 | td 71 | input.btn.btn-default.btn-sm.btn-details(type='button', value='Edit') 72 | td.stretch 73 | span.badge.badge-clear.timeago.pull-right(data-age='y') <%= userCreated.time %> 74 | |<%- name.full %> 75 | td.nowrap <%- phone %> 76 | td.nowrap 77 | div <%- status.name %> 78 | div.timeago.muted <%= status.userCreated.time %> 79 | 80 | script(type='text/template', id='tmpl-results-empty-row') 81 | tr 82 | td(colspan='4') no documents matched 83 | 84 | script(type='text/template', id='tmpl-results-paging') 85 | div.well 86 | div.btn-group.pull-left 87 | button.btn.btn-default(disabled=true) Page <%= pages.current %> of <%= pages.total %> 88 | button.btn.btn-default(disabled=true) Rows <%= items.begin %> - <%= items.end %> of <%= items.total %> 89 | div.btn-group.pull-right 90 | button.btn.btn-default.btn-page.btn-prev(data-page!='<%= pages.prev %>') Prev 91 | button.btn.btn-default.btn-page.btn-next(data-page!='<%= pages.next %>') Next 92 | div.clearfix 93 | 94 | script(type='text/template', id='data-results') !{data.results} 95 | -------------------------------------------------------------------------------- /views/admin/admin-groups/details.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Admin Groups / Details 5 | link(rel='stylesheet', href='/views/admin/admin-groups/details.min.css?#{cacheBreaker}') 6 | 7 | block feet 8 | script(src='/views/admin/admin-groups/details.min.js?#{cacheBreaker}') 9 | 10 | block body 11 | div.row 12 | div.col-xs-12 13 | div#header 14 | div#details 15 | div#permissions 16 | div#delete 17 | 18 | script(type='text/template', id='tmpl-header') 19 | div.page-header 20 | h1 21 | a(href='/admin/admin-groups/') Admin Groups 22 | | / <%- name %> 23 | 24 | script(type='text/template', id='tmpl-details') 25 | fieldset 26 | legend Details 27 | div.alerts 28 | |<% _.each(errors, function(err) { %> 29 | div.alert.alert-danger.alert-dismissable 30 | button.close(type='button', data-dismiss='alert') × 31 | |<%- err %> 32 | |<% }); %> 33 | |<% if (success) { %> 34 | div.alert.alert-info.alert-dismissable 35 | button.close(type='button', data-dismiss='alert') × 36 | | Changes have been saved. 37 | |<% } %> 38 | div.form-group(class!='<%- errfor.name ? "has-error" : "" %>') 39 | label Name: 40 | input.form-control(type='text', name='name', value!='<%= name %>') 41 | span.help-block <%- errfor.name %> 42 | div.form-group 43 | button.btn.btn-primary.btn-update(type='button') Update 44 | 45 | script(type='text/template', id='tmpl-permissions') 46 | fieldset 47 | legend Permissions 48 | div.alerts 49 | |<% _.each(errors, function(err) { %> 50 | div.alert.alert-danger.alert-dismissable 51 | button.close(type='button', data-dismiss='alert') × 52 | |<%- err %> 53 | |<% }); %> 54 | |<% if (success) { %> 55 | div.alert.alert-info.alert-dismissable 56 | button.close(type='button', data-dismiss='alert') × 57 | | Changes have been saved. 58 | |<% } %> 59 | div.form-group(class!='<%- errfor.newPermission ? "has-error" : "" %>') 60 | label New Setting: 61 | div.input-group 62 | input.form-control(name='newPermission', type='text', placeholder='enter a name') 63 | div.input-group-btn 64 | button.btn.btn-success.btn-add(type='button') Add 65 | span.help-block <%- errfor.newUsername %> 66 | div.form-group(class!='<%- errfor.newPermission ? "has-error" : "" %>') 67 | label Settings: 68 | div.permissions 69 | |<% _.each(permissions, function(permission) { %> 70 | div.input-group 71 | input.form-control(disabled=true, value!='<%= permission.name %>') 72 | div.input-group-btn 73 | |<% if (permission.permit) { %> 74 | button.btn.btn-default.btn-allow(type='button', disabled) Allow 75 | button.btn.btn-default.btn-deny(type='button') Deny 76 | |<% } else { %> 77 | button.btn.btn-default.btn-allow(type='button') Allow 78 | button.btn.btn-default.btn-deny(type='button', disabled) Deny 79 | |<% } %> 80 | button.btn.btn-danger.btn-delete(type='button') 81 | i.fa.fa-trash-o.fa-inverse 82 | |<% }); %> 83 | |<% if (permissions.length == 0) { %> 84 | span.badge 85 | | no permissions defined 86 | |<% } %> 87 | span.help-block <%- errfor.settings %> 88 | div.form-group 89 | button.btn.btn-primary.btn-set(type='button') Save Settings 90 | 91 | script(type='text/template', id='tmpl-delete') 92 | fieldset 93 | legend Danger Zone 94 | div.alerts 95 | |<% _.each(errors, function(err) { %> 96 | div.alert.alert-danger.alert-dismissable 97 | button.close(type='button', data-dismiss='alert') × 98 | |<%- err %> 99 | |<% }); %> 100 | div.form-group 101 | span.help-block 102 | span.label.label-danger If you do this, it cannot be undone. 103 | |  104 | span.text-muted You may also create orphaned document relationships too. 105 | div.form-group 106 | button.btn.btn-danger.btn-delete(type='button') Delete 107 | 108 | script(type='text/template', id='data-record') !{data.record} 109 | -------------------------------------------------------------------------------- /views/admin/admin-groups/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Manage Admin Groups 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/admin-groups/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/admin-groups/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div#filters 17 | div#results-table 18 | div#results-paging 19 | 20 | script(type='text/template', id='tmpl-header') 21 | div.page-header 22 | form.form-inline.pull-right 23 | div.input-group 24 | input.form-control(name='name', type='text', placeholder='enter a name', value!='<%= name %>') 25 | button.btn.btn-primary.btn-add(type='button') Add New 26 | h1 Admin Groups 27 | 28 | script(type='text/template', id='tmpl-filters') 29 | form.filters 30 | div.row 31 | div.col-sm-3 32 | label Name Search 33 | input.form-control(name='name', type='text') 34 | div.col-sm-3 35 | label Sort By 36 | select.form-control(name='sort') 37 | option(value='_id') id ▲ 38 | option(value='-_id') id ▼ 39 | option(value='name') name ▲ 40 | option(value='-name') name ▼ 41 | div.col-sm-3 42 | label Limit 43 | select.form-control(name='limit') 44 | option(value='10') 10 items 45 | option(value='20', selected='selected') 20 items 46 | option(value='50') 50 items 47 | option(value='100') 100 items 48 | 49 | script(type='text/template', id='tmpl-results-table') 50 | table.table.table-striped 51 | thead 52 | tr 53 | th 54 | th.stretch name 55 | th id 56 | tbody#results-rows 57 | 58 | script(type='text/template', id='tmpl-results-row') 59 | td 60 | input.btn.btn-default.btn-sm.btn-details(type='button', value='Edit') 61 | td <%- name %> 62 | td.nowrap <%= _id %> 63 | 64 | script(type='text/template', id='tmpl-results-empty-row') 65 | tr 66 | td(colspan='3') no documents matched 67 | 68 | script(type='text/template', id='tmpl-results-paging') 69 | div.well 70 | div.btn-group.pull-left 71 | button.btn.btn-default(disabled=true) Page <%= pages.current %> of <%= pages.total %> 72 | button.btn.btn-default(disabled=true) Rows <%= items.begin %> - <%= items.end %> of <%= items.total %> 73 | div.btn-group.pull-right 74 | button.btn.btn-default.btn-page.btn-prev(data-page!='<%= pages.prev %>') Prev 75 | button.btn.btn-default.btn-page.btn-next(data-page!='<%= pages.next %>') Next 76 | div.clearfix 77 | 78 | script(type='text/template', id='data-results') !{data.results} 79 | -------------------------------------------------------------------------------- /views/admin/admin-groups/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.find = function(req, res, next){ 4 | req.query.name = req.query.name ? req.query.name : ''; 5 | req.query.limit = req.query.limit ? parseInt(req.query.limit, null) : 20; 6 | req.query.page = req.query.page ? parseInt(req.query.page, null) : 1; 7 | req.query.sort = req.query.sort ? req.query.sort : '_id'; 8 | 9 | var filters = {}; 10 | if (req.query.name) { 11 | filters.name = new RegExp('^.*?'+ req.query.name +'.*$', 'i'); 12 | } 13 | 14 | req.app.db.models.AdminGroup.pagedFind({ 15 | filters: filters, 16 | keys: 'name', 17 | limit: req.query.limit, 18 | page: req.query.page, 19 | sort: req.query.sort 20 | }, function(err, results) { 21 | if (err) { 22 | return next(err); 23 | } 24 | 25 | if (req.xhr) { 26 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 27 | results.filters = req.query; 28 | res.send(results); 29 | } 30 | else { 31 | results.filters = req.query; 32 | res.render('admin/admin-groups/index', { data: { results: escape(JSON.stringify(results)) } }); 33 | } 34 | }); 35 | }; 36 | 37 | exports.read = function(req, res, next){ 38 | req.app.db.models.AdminGroup.findById(req.params.id).exec(function(err, adminGroup) { 39 | if (err) { 40 | return next(err); 41 | } 42 | 43 | if (req.xhr) { 44 | res.send(adminGroup); 45 | } 46 | else { 47 | res.render('admin/admin-groups/details', { data: { record: escape(JSON.stringify(adminGroup)) } }); 48 | } 49 | }); 50 | }; 51 | 52 | exports.create = function(req, res, next){ 53 | var workflow = req.app.utility.workflow(req, res); 54 | 55 | workflow.on('validate', function() { 56 | if (!req.user.roles.admin.isMemberOf('root')) { 57 | workflow.outcome.errors.push('You may not create admin groups.'); 58 | return workflow.emit('response'); 59 | } 60 | 61 | if (!req.body.name) { 62 | workflow.outcome.errors.push('Please enter a name.'); 63 | return workflow.emit('response'); 64 | } 65 | 66 | workflow.emit('duplicateAdminGroupCheck'); 67 | }); 68 | 69 | workflow.on('duplicateAdminGroupCheck', function() { 70 | req.app.db.models.AdminGroup.findById(req.app.utility.slugify(req.body.name)).exec(function(err, adminGroup) { 71 | if (err) { 72 | return workflow.emit('exception', err); 73 | } 74 | 75 | if (adminGroup) { 76 | workflow.outcome.errors.push('That group already exists.'); 77 | return workflow.emit('response'); 78 | } 79 | 80 | workflow.emit('createAdminGroup'); 81 | }); 82 | }); 83 | 84 | workflow.on('createAdminGroup', function() { 85 | var fieldsToSet = { 86 | _id: req.app.utility.slugify(req.body.name), 87 | name: req.body.name 88 | }; 89 | 90 | req.app.db.models.AdminGroup.create(fieldsToSet, function(err, adminGroup) { 91 | if (err) { 92 | return workflow.emit('exception', err); 93 | } 94 | 95 | workflow.outcome.record = adminGroup; 96 | return workflow.emit('response'); 97 | }); 98 | }); 99 | 100 | workflow.emit('validate'); 101 | }; 102 | 103 | exports.update = function(req, res, next){ 104 | var workflow = req.app.utility.workflow(req, res); 105 | 106 | workflow.on('validate', function() { 107 | if (!req.user.roles.admin.isMemberOf('root')) { 108 | workflow.outcome.errors.push('You may not update admin groups.'); 109 | return workflow.emit('response'); 110 | } 111 | 112 | if (!req.body.name) { 113 | workflow.outcome.errfor.name = 'required'; 114 | return workflow.emit('response'); 115 | } 116 | 117 | workflow.emit('patchAdminGroup'); 118 | }); 119 | 120 | workflow.on('patchAdminGroup', function() { 121 | var fieldsToSet = { 122 | name: req.body.name 123 | }; 124 | var options = { new: true }; 125 | req.app.db.models.AdminGroup.findByIdAndUpdate(req.params.id, fieldsToSet, options, function(err, adminGroup) { 126 | if (err) { 127 | return workflow.emit('exception', err); 128 | } 129 | 130 | workflow.outcome.adminGroup = adminGroup; 131 | return workflow.emit('response'); 132 | }); 133 | }); 134 | 135 | workflow.emit('validate'); 136 | }; 137 | 138 | exports.permissions = function(req, res, next){ 139 | var workflow = req.app.utility.workflow(req, res); 140 | 141 | workflow.on('validate', function() { 142 | if (!req.user.roles.admin.isMemberOf('root')) { 143 | workflow.outcome.errors.push('You may not change the permissions of admin groups.'); 144 | return workflow.emit('response'); 145 | } 146 | 147 | if (!req.body.permissions) { 148 | workflow.outcome.errfor.permissions = 'required'; 149 | return workflow.emit('response'); 150 | } 151 | 152 | workflow.emit('patchAdminGroup'); 153 | }); 154 | 155 | workflow.on('patchAdminGroup', function() { 156 | var fieldsToSet = { 157 | permissions: req.body.permissions 158 | }; 159 | var options = { new: true }; 160 | req.app.db.models.AdminGroup.findByIdAndUpdate(req.params.id, fieldsToSet, options, function(err, adminGroup) { 161 | if (err) { 162 | return workflow.emit('exception', err); 163 | } 164 | 165 | workflow.outcome.adminGroup = adminGroup; 166 | return workflow.emit('response'); 167 | }); 168 | }); 169 | 170 | workflow.emit('validate'); 171 | }; 172 | 173 | exports.delete = function(req, res, next){ 174 | var workflow = req.app.utility.workflow(req, res); 175 | 176 | workflow.on('validate', function() { 177 | if (!req.user.roles.admin.isMemberOf('root')) { 178 | workflow.outcome.errors.push('You may not delete admin groups.'); 179 | return workflow.emit('response'); 180 | } 181 | 182 | workflow.emit('deleteAdminGroup'); 183 | }); 184 | 185 | workflow.on('deleteAdminGroup', function(err) { 186 | req.app.db.models.AdminGroup.findByIdAndRemove(req.params.id, function(err, adminGroup) { 187 | if (err) { 188 | return workflow.emit('exception', err); 189 | } 190 | 191 | workflow.emit('response'); 192 | }); 193 | }); 194 | 195 | workflow.emit('validate'); 196 | }; 197 | -------------------------------------------------------------------------------- /views/admin/administrators/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Manage Administrators 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/administrators/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/administrators/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div#filters 17 | div#results-table 18 | div#results-paging 19 | 20 | script(type='text/template', id='tmpl-header') 21 | div.page-header 22 | form.form-inline.pull-right 23 | div.input-group 24 | input.form-control(name='name', type='text', placeholder='enter a name', value!='<%= name.full %>') 25 | button.btn.btn-primary.btn-add(type='button') Add New 26 | h1 Administrators 27 | 28 | script(type='text/template', id='tmpl-filters') 29 | form.filters 30 | div.row 31 | div.col-sm-3 32 | label Name Search 33 | input.form-control(name='search', type='text') 34 | div.col-sm-3 35 | label Sort By 36 | select.form-control(name='sort') 37 | option(value='_id') id ▲ 38 | option(value='-_id') id ▼ 39 | option(value='name') name ▲ 40 | option(value='-name') name ▼ 41 | div.col-sm-3 42 | label Limit 43 | select.form-control(name='limit') 44 | option(value='10') 10 items 45 | option(value='20', selected='selected') 20 items 46 | option(value='50') 50 items 47 | option(value='100') 100 items 48 | 49 | script(type='text/template', id='tmpl-results-table') 50 | table.table.table-striped 51 | thead 52 | tr 53 | th 54 | th.stretch name 55 | th id 56 | tbody#results-rows 57 | 58 | script(type='text/template', id='tmpl-results-row') 59 | td 60 | input.btn.btn-default.btn-sm.btn-details(type='button', value='Edit') 61 | td.nowrap <%- name.full %> 62 | td <%= _id %> 63 | 64 | script(type='text/template', id='tmpl-results-empty-row') 65 | tr 66 | td(colspan='3') no documents matched 67 | 68 | script(type='text/template', id='tmpl-results-paging') 69 | div.well 70 | div.btn-group.pull-left 71 | button.btn.btn-default(disabled=true) Page <%= pages.current %> of <%= pages.total %> 72 | button.btn.btn-default(disabled=true) Rows <%= items.begin %> - <%= items.end %> of <%= items.total %> 73 | div.btn-group.pull-right 74 | button.btn.btn-default.btn-page.btn-prev(data-page!='<%= pages.prev %>') Prev 75 | button.btn.btn-default.btn-page.btn-next(data-page!='<%= pages.next %>') Next 76 | div.clearfix 77 | 78 | script(type='text/template', id='data-results') !{data.results} 79 | -------------------------------------------------------------------------------- /views/admin/categories/details.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Categories / Details 5 | 6 | block feet 7 | script(src='/views/admin/categories/details.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-xs-12 12 | div#header 13 | div#details 14 | div#delete 15 | 16 | script(type='text/template', id='tmpl-header') 17 | div.page-header 18 | h1 19 | a(href='/admin/categories/') Categories 20 | | / <%- name %> 21 | 22 | script(type='text/template', id='tmpl-details') 23 | fieldset 24 | legend Details 25 | div.alerts 26 | |<% _.each(errors, function(err) { %> 27 | div.alert.alert-danger.alert-dismissable 28 | button.close(type='button', data-dismiss='alert') × 29 | |<%- err %> 30 | |<% }); %> 31 | |<% if (success) { %> 32 | div.alert.alert-info.alert-dismissable 33 | button.close(type='button', data-dismiss='alert') × 34 | | Changes have been saved. 35 | |<% } %> 36 | div.form-group(class!='<%- errfor.pivot ? "has-error" : "" %>') 37 | label Pivot: 38 | input.form-control(type='text', name='pivot', value!='<%- pivot %>') 39 | span.help-block <%- errfor.pivot %> 40 | div.form-group(class!='<%- errfor.name ? "has-error" : "" %>') 41 | label Name: 42 | input.form-control(type='text', name='name', value!='<%- name %>') 43 | span.help-block <%- errfor.name %> 44 | div.form-group 45 | button.btn.btn-primary.btn-update(type='button') Update 46 | 47 | script(type='text/template', id='tmpl-delete') 48 | fieldset 49 | legend Danger Zone 50 | div.alerts 51 | |<% _.each(errors, function(err) { %> 52 | div.alert.alert-danger.alert-dismissable 53 | button.close(type='button', data-dismiss='alert') × 54 | |<%- err %> 55 | |<% }); %> 56 | div.form-group 57 | span.help-block 58 | span.label.label-danger If you do this, it cannot be undone. 59 | |  60 | span.text-muted You may also create orphaned document relationships too. 61 | div.form-group 62 | button.btn.btn-danger.btn-delete(type='button') Delete 63 | 64 | script(type='text/template', id='data-record') !{data.record} 65 | -------------------------------------------------------------------------------- /views/admin/categories/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Manage Categories 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/categories/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/categories/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div#filters 17 | div#results-table 18 | div#results-paging 19 | 20 | script(type='text/template', id='tmpl-header') 21 | div.page-header 22 | form.form-inline.pull-right 23 | div.input-group 24 | input.form-control(name='pivot', type='text', placeholder='pivot', value!='<%= pivot %>') 25 | input.form-control(name='name', type='text', placeholder='name', value!='<%= name %>') 26 | button.btn.btn-primary.btn-add(type='button') Add New 27 | h1 Categories 28 | 29 | script(type='text/template', id='tmpl-filters') 30 | form.filters 31 | div.row 32 | div.col-sm-3 33 | label Pivot Search 34 | input.form-control(name='pivot', type='text') 35 | div.col-sm-3 36 | label Name Search 37 | input.form-control(name='name', type='text') 38 | div.col-sm-3 39 | label Sort By 40 | select.form-control(name='sort') 41 | option(value='_id') id ▲ 42 | option(value='-_id') id ▼ 43 | option(value='name') name ▲ 44 | option(value='-name') name ▼ 45 | div.col-sm-3 46 | label Limit 47 | select.form-control(name='limit') 48 | option(value='10') 10 items 49 | option(value='20', selected='selected') 20 items 50 | option(value='50') 50 items 51 | option(value='100') 100 items 52 | 53 | script(type='text/template', id='tmpl-results-table') 54 | table.table.table-striped 55 | thead 56 | tr 57 | th 58 | th pivot 59 | th.stretch name 60 | th id 61 | tbody#results-rows 62 | 63 | script(type='text/template', id='tmpl-results-row') 64 | td 65 | input.btn.btn-default.btn-sm.btn-details(type='button', value='Edit') 66 | td <%- pivot %> 67 | td <%- name %> 68 | td.nowrap <%= _id %> 69 | 70 | script(type='text/template', id='tmpl-results-empty-row') 71 | tr 72 | td(colspan='4') no documents matched 73 | 74 | script(type='text/template', id='tmpl-results-paging') 75 | div.well 76 | div.btn-group.pull-left 77 | button.btn.btn-default(disabled=true) Page <%= pages.current %> of <%= pages.total %> 78 | button.btn.btn-default(disabled=true) Rows <%= items.begin %> - <%= items.end %> of <%= items.total %> 79 | div.btn-group.pull-right 80 | button.btn.btn-default.btn-page.btn-prev(data-page!='<%= pages.prev %>') Prev 81 | button.btn.btn-default.btn-page.btn-next(data-page!='<%= pages.next %>') Next 82 | div.clearfix 83 | 84 | script(type='text/template', id='data-results') !{data.results} 85 | -------------------------------------------------------------------------------- /views/admin/categories/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.find = function(req, res, next){ 4 | req.query.pivot = req.query.pivot ? req.query.pivot : ''; 5 | req.query.name = req.query.name ? req.query.name : ''; 6 | req.query.limit = req.query.limit ? parseInt(req.query.limit, null) : 20; 7 | req.query.page = req.query.page ? parseInt(req.query.page, null) : 1; 8 | req.query.sort = req.query.sort ? req.query.sort : '_id'; 9 | 10 | var filters = {}; 11 | if (req.query.pivot) { 12 | filters.pivot = new RegExp('^.*?'+ req.query.pivot +'.*$', 'i'); 13 | } 14 | if (req.query.name) { 15 | filters.name = new RegExp('^.*?'+ req.query.name +'.*$', 'i'); 16 | } 17 | 18 | req.app.db.models.Category.pagedFind({ 19 | filters: filters, 20 | keys: 'pivot name', 21 | limit: req.query.limit, 22 | page: req.query.page, 23 | sort: req.query.sort 24 | }, function(err, results) { 25 | if (err) { 26 | return next(err); 27 | } 28 | 29 | if (req.xhr) { 30 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 31 | results.filters = req.query; 32 | res.send(results); 33 | } 34 | else { 35 | results.filters = req.query; 36 | res.render('admin/categories/index', { data: { results: escape(JSON.stringify(results)) } }); 37 | } 38 | }); 39 | }; 40 | 41 | exports.read = function(req, res, next){ 42 | req.app.db.models.Category.findById(req.params.id).exec(function(err, category) { 43 | if (err) { 44 | return next(err); 45 | } 46 | 47 | if (req.xhr) { 48 | res.send(category); 49 | } 50 | else { 51 | res.render('admin/categories/details', { data: { record: escape(JSON.stringify(category)) } }); 52 | } 53 | }); 54 | }; 55 | 56 | exports.create = function(req, res, next){ 57 | var workflow = req.app.utility.workflow(req, res); 58 | 59 | workflow.on('validate', function() { 60 | if (!req.user.roles.admin.isMemberOf('root')) { 61 | workflow.outcome.errors.push('You may not create categories.'); 62 | return workflow.emit('response'); 63 | } 64 | 65 | if (!req.body.pivot) { 66 | workflow.outcome.errors.push('A pivot is required.'); 67 | return workflow.emit('response'); 68 | } 69 | 70 | if (!req.body.name) { 71 | workflow.outcome.errors.push('A name is required.'); 72 | return workflow.emit('response'); 73 | } 74 | 75 | workflow.emit('duplicateCategoryCheck'); 76 | }); 77 | 78 | workflow.on('duplicateCategoryCheck', function() { 79 | req.app.db.models.Category.findById(req.app.utility.slugify(req.body.pivot +' '+ req.body.name)).exec(function(err, category) { 80 | if (err) { 81 | return workflow.emit('exception', err); 82 | } 83 | 84 | if (category) { 85 | workflow.outcome.errors.push('That category+pivot is already taken.'); 86 | return workflow.emit('response'); 87 | } 88 | 89 | workflow.emit('createCategory'); 90 | }); 91 | }); 92 | 93 | workflow.on('createCategory', function() { 94 | var fieldsToSet = { 95 | _id: req.app.utility.slugify(req.body.pivot +' '+ req.body.name), 96 | pivot: req.body.pivot, 97 | name: req.body.name 98 | }; 99 | 100 | req.app.db.models.Category.create(fieldsToSet, function(err, category) { 101 | if (err) { 102 | return workflow.emit('exception', err); 103 | } 104 | 105 | workflow.outcome.record = category; 106 | return workflow.emit('response'); 107 | }); 108 | }); 109 | 110 | workflow.emit('validate'); 111 | }; 112 | 113 | exports.update = function(req, res, next){ 114 | var workflow = req.app.utility.workflow(req, res); 115 | 116 | workflow.on('validate', function() { 117 | if (!req.user.roles.admin.isMemberOf('root')) { 118 | workflow.outcome.errors.push('You may not update categories.'); 119 | return workflow.emit('response'); 120 | } 121 | 122 | if (!req.body.pivot) { 123 | workflow.outcome.errfor.pivot = 'pivot'; 124 | return workflow.emit('response'); 125 | } 126 | 127 | if (!req.body.name) { 128 | workflow.outcome.errfor.name = 'required'; 129 | return workflow.emit('response'); 130 | } 131 | 132 | workflow.emit('patchCategory'); 133 | }); 134 | 135 | workflow.on('patchCategory', function() { 136 | var fieldsToSet = { 137 | pivot: req.body.pivot, 138 | name: req.body.name 139 | }; 140 | var options = { new: true }; 141 | req.app.db.models.Category.findByIdAndUpdate(req.params.id, fieldsToSet, options, function(err, category) { 142 | if (err) { 143 | return workflow.emit('exception', err); 144 | } 145 | 146 | workflow.outcome.category = category; 147 | return workflow.emit('response'); 148 | }); 149 | }); 150 | 151 | workflow.emit('validate'); 152 | }; 153 | 154 | exports.delete = function(req, res, next){ 155 | var workflow = req.app.utility.workflow(req, res); 156 | 157 | workflow.on('validate', function() { 158 | if (!req.user.roles.admin.isMemberOf('root')) { 159 | workflow.outcome.errors.push('You may not delete categories.'); 160 | return workflow.emit('response'); 161 | } 162 | 163 | workflow.emit('deleteCategory'); 164 | }); 165 | 166 | workflow.on('deleteCategory', function(err) { 167 | req.app.db.models.Category.findByIdAndRemove(req.params.id, function(err, category) { 168 | if (err) { 169 | return workflow.emit('exception', err); 170 | } 171 | workflow.emit('response'); 172 | }); 173 | }); 174 | 175 | workflow.emit('validate'); 176 | }; 177 | -------------------------------------------------------------------------------- /views/admin/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/admin 2 | 3 | block head 4 | title Admin Area 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/index.min.css?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-sm-6 12 | div.page-header 13 | h1 Admin Area 14 | div.row 15 | div.col-sm-4 16 | div.well.stat 17 | div.stat-value #{countUser} 18 | div.stat-label Users 19 | div.col-sm-4 20 | div.well.stat 21 | div.stat-value #{countAccount} 22 | div.stat-label Accounts 23 | div.col-sm-4 24 | div.well.stat 25 | div.stat-value #{countAdmin} 26 | div.stat-label Admins 27 | div.row 28 | div.col-sm-4 29 | div.well.stat 30 | div.stat-value #{countAdminGroup} 31 | div.stat-label Groups 32 | div.col-sm-4 33 | div.well.stat 34 | div.stat-value #{countCategory} 35 | div.stat-label Categories 36 | div.col-sm-4 37 | div.well.stat 38 | div.stat-value #{countStatus} 39 | div.stat-label Statuses 40 | div.col-sm-6.special 41 | div.page-header 42 | h1 Super Dashboard 43 | i.fa.fa-gears.super-awesome 44 | -------------------------------------------------------------------------------- /views/admin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res, next){ 4 | var sigma = {}; 5 | var collections = ['User', 'Account', 'Admin', 'AdminGroup', 'Category', 'Status']; 6 | var queries = []; 7 | 8 | collections.forEach(function(el, i, arr) { 9 | queries.push(function(done) { 10 | req.app.db.models[el].count({}, function(err, count) { 11 | if (err) { 12 | return done(err, null); 13 | } 14 | 15 | sigma['count'+ el] = count; 16 | done(null, el); 17 | }); 18 | }); 19 | }); 20 | 21 | var asyncFinally = function(err, results) { 22 | if (err) { 23 | return next(err); 24 | } 25 | 26 | res.render('admin/index', sigma); 27 | }; 28 | 29 | require('async').parallel(queries, asyncFinally); 30 | }; 31 | -------------------------------------------------------------------------------- /views/admin/search/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.find = function(req, res, next){ 4 | req.query.q = req.query.q ? req.query.q : ''; 5 | var regexQuery = new RegExp('^.*?'+ req.query.q +'.*$', 'i'); 6 | var outcome = {}; 7 | 8 | var searchUsers = function(done) { 9 | req.app.db.models.User.find({search: regexQuery}, 'username').sort('username').limit(10).lean().exec(function(err, results) { 10 | if (err) { 11 | return done(err, null); 12 | } 13 | 14 | outcome.users = results; 15 | done(null, 'searchUsers'); 16 | }); 17 | }; 18 | 19 | var searchAccounts = function(done) { 20 | req.app.db.models.Account.find({search: regexQuery}, 'name.full').sort('name.full').limit(10).lean().exec(function(err, results) { 21 | if (err) { 22 | return done(err, null); 23 | } 24 | 25 | outcome.accounts = results; 26 | return done(null, 'searchAccounts'); 27 | }); 28 | }; 29 | 30 | var searchAdministrators = function(done) { 31 | req.app.db.models.Admin.find({search: regexQuery}, 'name.full').sort('name.full').limit(10).lean().exec(function(err, results) { 32 | if (err) { 33 | return done(err, null); 34 | } 35 | 36 | outcome.administrators = results; 37 | return done(null, 'searchAdministrators'); 38 | }); 39 | }; 40 | 41 | var asyncFinally = function(err, results) { 42 | if (err) { 43 | return next(err, null); 44 | } 45 | 46 | res.send(outcome); 47 | }; 48 | 49 | require('async').parallel([searchUsers, searchAccounts, searchAdministrators], asyncFinally); 50 | }; 51 | -------------------------------------------------------------------------------- /views/admin/statuses/details.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Statuses / Details 5 | 6 | block feet 7 | script(src='/views/admin/statuses/details.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-xs-12 12 | div#header 13 | div#details 14 | div#delete 15 | 16 | script(type='text/template', id='tmpl-header') 17 | div.page-header 18 | h1 19 | a(href='/admin/statuses/') Statuses 20 | | / <%- name %> 21 | 22 | script(type='text/template', id='tmpl-details') 23 | fieldset 24 | legend Details 25 | div.alerts 26 | |<% _.each(errors, function(err) { %> 27 | div.alert.alert-danger.alert-dismissable 28 | button.close(type='button', data-dismiss='alert') × 29 | |<%- err %> 30 | |<% }); %> 31 | |<% if (success) { %> 32 | div.alert.alert-info.alert-dismissable 33 | button.close(type='button', data-dismiss='alert') × 34 | | Changes have been saved. 35 | |<% } %> 36 | div.form-group(class!='<%- errfor.pivot ? "has-error" : "" %>') 37 | label Pivot: 38 | input.form-control(type='text', name='pivot', value!='<%- pivot %>') 39 | span.help-block <%- errfor.pivot %> 40 | div.form-group(class!='<%- errfor.name ? "has-error" : "" %>') 41 | label Name: 42 | input.form-control(type='text', name='name', value!='<%- name %>') 43 | span.help-block <%- errfor.name %> 44 | div.form-group 45 | button.btn.btn-primary.btn-update(type='button') Update 46 | 47 | script(type='text/template', id='tmpl-delete') 48 | fieldset 49 | legend Danger Zone 50 | div.alerts 51 | |<% _.each(errors, function(err) { %> 52 | div.alert.alert-danger.alert-dismissable 53 | button.close(type='button', data-dismiss='alert') × 54 | |<%- err %> 55 | |<% }); %> 56 | div.form-group 57 | span.help-block 58 | span.label.label-danger If you do this, it cannot be undone. 59 | |  60 | span.text-muted You may also create orphaned document relationships too. 61 | div.form-group 62 | button.btn.btn-danger.btn-delete(type='button') Delete 63 | 64 | script(type='text/template', id='data-record') !{data.record} 65 | -------------------------------------------------------------------------------- /views/admin/statuses/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Manage Statuses 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/statuses/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/statuses/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div#filters 17 | div#results-table 18 | div#results-paging 19 | 20 | script(type='text/template', id='tmpl-header') 21 | div.page-header 22 | form.form-inline.pull-right 23 | div.input-group 24 | input.form-control(name='pivot', type='text', placeholder='pivot', value!='<%= pivot %>') 25 | input.form-control(name='name', type='text', placeholder='name', value!='<%= name %>') 26 | button.btn.btn-primary.btn-add(type='button') Add New 27 | h1 Statuses 28 | 29 | script(type='text/template', id='tmpl-filters') 30 | form.filters 31 | div.row 32 | div.col-sm-3 33 | label Pivot Search 34 | input.form-control(name='pivot', type='text') 35 | div.col-sm-3 36 | label Name Search 37 | input.form-control(name='name', type='text') 38 | div.col-sm-3 39 | label Sort By 40 | select.form-control(name='sort') 41 | option(value='_id') id ▲ 42 | option(value='-_id') id ▼ 43 | option(value='name') name ▲ 44 | option(value='-name') name ▼ 45 | div.col-sm-3 46 | label Limit 47 | select.form-control(name='limit') 48 | option(value='10') 10 items 49 | option(value='20', selected='selected') 20 items 50 | option(value='50') 50 items 51 | option(value='100') 100 items 52 | 53 | script(type='text/template', id='tmpl-results-table') 54 | table.table.table-striped 55 | thead 56 | tr 57 | th 58 | th pivot 59 | th.stretch name 60 | th id 61 | tbody#results-rows 62 | 63 | script(type='text/template', id='tmpl-results-row') 64 | td 65 | input.btn.btn-default.btn-sm.btn-details(type='button', value='Edit') 66 | td <%- pivot %> 67 | td <%- name %> 68 | td.nowrap <%= _id %> 69 | 70 | script(type='text/template', id='tmpl-results-empty-row') 71 | tr 72 | td(colspan='4') no documents matched 73 | 74 | script(type='text/template', id='tmpl-results-paging') 75 | div.well 76 | div.btn-group.pull-left 77 | button.btn.btn-default(disabled=true) Page <%= pages.current %> of <%= pages.total %> 78 | button.btn.btn-default(disabled=true) Rows <%= items.begin %> - <%= items.end %> of <%= items.total %> 79 | div.btn-group.pull-right 80 | button.btn.btn-default.btn-page.btn-prev(data-page!='<%= pages.prev %>') Prev 81 | button.btn.btn-default.btn-page.btn-next(data-page!='<%= pages.next %>') Next 82 | div.clearfix 83 | 84 | script(type='text/template', id='data-results') !{data.results} 85 | -------------------------------------------------------------------------------- /views/admin/statuses/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.find = function(req, res, next){ 4 | req.query.pivot = req.query.pivot ? req.query.pivot : ''; 5 | req.query.name = req.query.name ? req.query.name : ''; 6 | req.query.limit = req.query.limit ? parseInt(req.query.limit, null) : 20; 7 | req.query.page = req.query.page ? parseInt(req.query.page, null) : 1; 8 | req.query.sort = req.query.sort ? req.query.sort : '_id'; 9 | 10 | var filters = {}; 11 | if (req.query.pivot) { 12 | filters.pivot = new RegExp('^.*?'+ req.query.pivot +'.*$', 'i'); 13 | } 14 | 15 | if (req.query.name) { 16 | filters.name = new RegExp('^.*?'+ req.query.name +'.*$', 'i'); 17 | } 18 | 19 | req.app.db.models.Status.pagedFind({ 20 | filters: filters, 21 | keys: 'pivot name', 22 | limit: req.query.limit, 23 | page: req.query.page, 24 | sort: req.query.sort 25 | }, function(err, results) { 26 | if (err) { 27 | return next(err); 28 | } 29 | 30 | if (req.xhr) { 31 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 32 | results.filters = req.query; 33 | res.send(results); 34 | } 35 | else { 36 | results.filters = req.query; 37 | res.render('admin/statuses/index', { data: { results: escape(JSON.stringify(results)) } }); 38 | } 39 | }); 40 | }; 41 | 42 | exports.read = function(req, res, next){ 43 | req.app.db.models.Status.findById(req.params.id).exec(function(err, status) { 44 | if (err) { 45 | return next(err); 46 | } 47 | 48 | if (req.xhr) { 49 | res.send(status); 50 | } 51 | else { 52 | res.render('admin/statuses/details', { data: { record: escape(JSON.stringify(status)) } }); 53 | } 54 | }); 55 | }; 56 | 57 | exports.create = function(req, res, next){ 58 | var workflow = req.app.utility.workflow(req, res); 59 | 60 | workflow.on('validate', function() { 61 | if (!req.user.roles.admin.isMemberOf('root')) { 62 | workflow.outcome.errors.push('You may not create statuses.'); 63 | return workflow.emit('response'); 64 | } 65 | 66 | if (!req.body.pivot) { 67 | workflow.outcome.errors.push('A pivot is required.'); 68 | return workflow.emit('response'); 69 | } 70 | 71 | if (!req.body.name) { 72 | workflow.outcome.errors.push('A name is required.'); 73 | return workflow.emit('response'); 74 | } 75 | 76 | workflow.emit('duplicateStatusCheck'); 77 | }); 78 | 79 | workflow.on('duplicateStatusCheck', function() { 80 | req.app.db.models.Status.findById(req.app.utility.slugify(req.body.pivot +' '+ req.body.name)).exec(function(err, status) { 81 | if (err) { 82 | return workflow.emit('exception', err); 83 | } 84 | 85 | if (status) { 86 | workflow.outcome.errors.push('That status+pivot is already taken.'); 87 | return workflow.emit('response'); 88 | } 89 | 90 | workflow.emit('createStatus'); 91 | }); 92 | }); 93 | 94 | workflow.on('createStatus', function() { 95 | var fieldsToSet = { 96 | _id: req.app.utility.slugify(req.body.pivot +' '+ req.body.name), 97 | pivot: req.body.pivot, 98 | name: req.body.name 99 | }; 100 | 101 | req.app.db.models.Status.create(fieldsToSet, function(err, status) { 102 | if (err) { 103 | return workflow.emit('exception', err); 104 | } 105 | 106 | workflow.outcome.record = status; 107 | return workflow.emit('response'); 108 | }); 109 | }); 110 | 111 | workflow.emit('validate'); 112 | }; 113 | 114 | exports.update = function(req, res, next){ 115 | var workflow = req.app.utility.workflow(req, res); 116 | 117 | workflow.on('validate', function() { 118 | if (!req.user.roles.admin.isMemberOf('root')) { 119 | workflow.outcome.errors.push('You may not update statuses.'); 120 | return workflow.emit('response'); 121 | } 122 | 123 | if (!req.body.pivot) { 124 | workflow.outcome.errfor.pivot = 'pivot'; 125 | return workflow.emit('response'); 126 | } 127 | 128 | if (!req.body.name) { 129 | workflow.outcome.errfor.name = 'required'; 130 | return workflow.emit('response'); 131 | } 132 | 133 | workflow.emit('patchStatus'); 134 | }); 135 | 136 | workflow.on('patchStatus', function() { 137 | var fieldsToSet = { 138 | pivot: req.body.pivot, 139 | name: req.body.name 140 | }; 141 | var options = { new: true }; 142 | req.app.db.models.Status.findByIdAndUpdate(req.params.id, fieldsToSet, options, function(err, status) { 143 | if (err) { 144 | return workflow.emit('exception', err); 145 | } 146 | 147 | workflow.outcome.status = status; 148 | return workflow.emit('response'); 149 | }); 150 | }); 151 | 152 | workflow.emit('validate'); 153 | }; 154 | 155 | exports.delete = function(req, res, next){ 156 | var workflow = req.app.utility.workflow(req, res); 157 | 158 | workflow.on('validate', function() { 159 | if (!req.user.roles.admin.isMemberOf('root')) { 160 | workflow.outcome.errors.push('You may not delete statuses.'); 161 | return workflow.emit('response'); 162 | } 163 | 164 | workflow.emit('deleteStatus'); 165 | }); 166 | 167 | workflow.on('deleteStatus', function(err) { 168 | req.app.db.models.Status.findByIdAndRemove(req.params.id, function(err, status) { 169 | if (err) { 170 | return workflow.emit('exception', err); 171 | } 172 | 173 | workflow.emit('response'); 174 | }); 175 | }); 176 | 177 | workflow.emit('validate'); 178 | }; 179 | -------------------------------------------------------------------------------- /views/admin/users/details.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Users / Details 5 | 6 | block feet 7 | script(src='/views/admin/users/details.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-xs-12 12 | div#header 13 | div#identity 14 | div#roles 15 | div#password 16 | div#delete 17 | 18 | script(type='text/template', id='tmpl-header') 19 | div.page-header 20 | h1 21 | a(href='/admin/users/') Users 22 | | / <%= username %> 23 | 24 | script(type='text/template', id='tmpl-identity') 25 | fieldset 26 | legend Identity 27 | div.alerts 28 | |<% _.each(errors, function(err) { %> 29 | div.alert.alert-danger.alert-dismissable 30 | button.close(type='button', data-dismiss='alert') × 31 | |<%- err %> 32 | |<% }); %> 33 | |<% if (success) { %> 34 | div.alert.alert-info.alert-dismissable 35 | button.close(type='button', data-dismiss='alert') × 36 | | Changes have been saved. 37 | |<% } %> 38 | div.form-group(class!='<%- errfor.isActive ? "has-error" : "" %>') 39 | label Is Active: 40 | select.form-control(name='isActive') 41 | option(value='yes') yes 42 | option(value='no') no 43 | span.help-block <%- errfor.isActive %> 44 | div.form-group(class!='<%- errfor.username ? "has-error" : "" %>') 45 | label Username: 46 | input.form-control(type='text', name='username', value!='<%= username %>') 47 | span.help-block <%- errfor.username %> 48 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 49 | label Email: 50 | input.form-control(type='text', name='email', value!='<%= email %>') 51 | span.help-block <%- errfor.email %> 52 | div.form-group 53 | button.btn.btn-primary.btn-update(type='button') Update 54 | 55 | script(type='text/template', id='tmpl-roles') 56 | fieldset 57 | legend Roles 58 | div.alerts 59 | |<% _.each(errors, function(err) { %> 60 | div.alert.alert-danger.alert-dismissable 61 | button.close(type='button', data-dismiss='alert') × 62 | |<%- err %> 63 | |<% }); %> 64 | div.form-group(class!='<%- errfor.newAdminId ? "has-error" : "" %>') 65 | label Admin: 66 | div.controls 67 | div.input-group 68 | |<% if (roles && roles.admin) { %> 69 | input.form-control(disabled=true, value!='<%= roles.admin.name.full %>') 70 | div.input-group-btn 71 | button.btn.btn-warning.btn-admin-unlink(type='button') Unlink 72 | button.btn.btn-default.btn-admin-open(type='button') Open 73 | |<% } else { %> 74 | input.form-control(name='newAdminId', type='text', placeholder='enter admin id') 75 | div.input-group-btn 76 | button.btn.btn-success.btn-admin-link(type='button') Link 77 | |<% } %> 78 | span.help-block <%- errfor.newAdminId %> 79 | div.form-group(class!='<%- errfor.newAccountId ? "has-error" : "" %>') 80 | label Account: 81 | div.input-group 82 | |<% if (roles && roles.account) { %> 83 | input.form-control(disabled=true, value!='<%= roles.account.name.full %>') 84 | div.input-group-btn 85 | button.btn.btn-warning.btn-account-unlink(type='button') Unlink 86 | button.btn.btn-default.btn-account-open(type='button') Open 87 | |<% } else { %> 88 | input.form-control(name='newAccountId', type='text', placeholder='enter account id') 89 | div.input-group-btn 90 | button.btn.btn-success.btn-account-link(type='button') Link 91 | |<% } %> 92 | span.help-block <%- errfor.newAccountId %> 93 | 94 | script(type='text/template', id='tmpl-password') 95 | fieldset 96 | legend Set Password 97 | div.alerts 98 | |<% _.each(errors, function(err) { %> 99 | div.alert.alert-danger.alert-dismissable 100 | button.close(type='button', data-dismiss='alert') × 101 | |<%- err %> 102 | |<% }); %> 103 | |<% if (success) { %> 104 | div.alert.alert-info.alert-dismissable 105 | button.close(type='button', data-dismiss='alert') × 106 | | A new password has been set. 107 | |<% } %> 108 | div.form-group(class!='<%- errfor.newPassword ? "has-error" : "" %>') 109 | label New Password: 110 | input.form-control(type='password', name='newPassword', value!='<%= newPassword %>') 111 | span.help-block <%- errfor.newPassword %> 112 | div.form-group(class!='<%- errfor.confirm ? "has-error" : "" %>') 113 | label Confirm Password: 114 | input.form-control(type='password', name='confirm', value!='<%= confirm %>') 115 | span.help-block <%- errfor.confirm %> 116 | div.form-group 117 | button.btn.btn-primary.btn-password(type='button') Set Password 118 | 119 | script(type='text/template', id='tmpl-delete') 120 | fieldset 121 | legend Danger Zone 122 | div.alerts 123 | |<% _.each(errors, function(err) { %> 124 | div.alert.alert-danger.alert-dismissable 125 | button.close(type='button', data-dismiss='alert') × 126 | |<%- err %> 127 | |<% }); %> 128 | div.form-group 129 | span.help-block 130 | span.label.label-danger If you do this, it cannot be undone. 131 | |  132 | span.text-muted You may also create orphaned document relationships too. 133 | div.form-group 134 | button.btn.btn-danger.btn-delete(type='button') Delete 135 | 136 | script(type='text/template', id='data-record') !{data.record} 137 | -------------------------------------------------------------------------------- /views/admin/users/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/admin 2 | 3 | block head 4 | title Manage Users 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/admin/users/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/admin/users/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-xs-12 15 | div#header 16 | div#filters 17 | div#results-table 18 | div#results-paging 19 | 20 | script(type='text/template', id='tmpl-header') 21 | div.page-header 22 | form.form-inline.pull-right 23 | div.input-group 24 | input.form-control(name='username', type='text', placeholder='enter a username', value!='<%= username %>') 25 | button.btn.btn-primary.btn-add(type='button') Add New 26 | h1 Users 27 | 28 | script(type='text/template', id='tmpl-filters') 29 | form.filters 30 | div.row 31 | div.col-sm-3 32 | label Username Search 33 | input.form-control(name='username', type='text') 34 | div.col-sm-3 35 | label Can Play Role 36 | select.form-control(name='roles') 37 | option(value='') any 38 | option(value='admin') admin 39 | option(value='account') account 40 | div.col-sm-2 41 | label Is Active 42 | select.form-control(name='isActive') 43 | option(value='') either 44 | option(value='yes') yes 45 | option(value='no') no 46 | div.col-sm-2 47 | label Sort By 48 | select.form-control(name='sort') 49 | option(value='_id') id ▲ 50 | option(value='-_id') id ▼ 51 | option(value='username') username ▲ 52 | option(value='-username') username ▼ 53 | option(value='email') email ▲ 54 | option(value='-email') email ▼ 55 | div.col-sm-2 56 | label Limit 57 | select.form-control(name='limit') 58 | option(value='10') 10 items 59 | option(value='20', selected='selected') 20 items 60 | option(value='50') 50 items 61 | option(value='100') 100 items 62 | 63 | script(type='text/template', id='tmpl-results-table') 64 | table.table.table-striped 65 | thead 66 | tr 67 | th 68 | th username 69 | th.stretch email 70 | th active 71 | th id 72 | tbody#results-rows 73 | 74 | script(type='text/template', id='tmpl-results-row') 75 | td 76 | input.btn.btn-default.btn-sm.btn-details(type='button', value='Edit') 77 | td <%= username %> 78 | td <%= email %> 79 | td <%= isActive %> 80 | td <%= _id %> 81 | 82 | script(type='text/template', id='tmpl-results-empty-row') 83 | tr 84 | td(colspan='5') no documents matched 85 | 86 | script(type='text/template', id='tmpl-results-paging') 87 | div.well 88 | div.btn-group.pull-left 89 | button.btn.btn-default(disabled=true) Page <%= pages.current %> of <%= pages.total %> 90 | button.btn.btn-default(disabled=true) Rows <%= items.begin %> - <%= items.end %> of <%= items.total %> 91 | div.btn-group.pull-right 92 | button.btn.btn-default.btn-page.btn-prev(data-page!='<%= pages.prev %>') Prev 93 | button.btn.btn-default.btn-page.btn-next(data-page!='<%= pages.next %>') Next 94 | div.clearfix 95 | 96 | script(type='text/template', id='data-results') !{data.results} 97 | -------------------------------------------------------------------------------- /views/contact/email-html.jade: -------------------------------------------------------------------------------- 1 | h3 #{projectName} Contact Form 2 | table(border='1', cellpadding='5', cellspacing='0') 3 | tr 4 | td Name: 5 | td #{name} 6 | tr 7 | td Email: 8 | td #{email} 9 | tr 10 | td Message: 11 | td #{message} 12 | -------------------------------------------------------------------------------- /views/contact/email-text.jade: -------------------------------------------------------------------------------- 1 | | #{projectName} Contact Form 2 | = '\n' 3 | = '\n' 4 | | Name: #{name} 5 | | Email: #{email} 6 | | Message: 7 | | #{message} 8 | -------------------------------------------------------------------------------- /views/contact/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/default 2 | 3 | block head 4 | title Contact Us 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/contact/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/contact/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-sm-6 15 | div.page-header 16 | h1 Send A Message 17 | div#contact 18 | div.col-sm-6.special 19 | div.page-header 20 | h1 Contact Us 21 | p.lead Freddy can't wait to hear from you. 22 | i.fa.fa-reply-all.super-awesome 23 | address 1428 Elm Street • San Francisco, CA 94122 24 | 25 | script(type='text/template', id='tmpl-contact') 26 | form 27 | div.alerts 28 | |<% _.each(errors, function(err) { %> 29 | div.alert.alert-danger.alert-dismissable 30 | button.close(type='button', data-dismiss='alert') × 31 | |<%- err %> 32 | |<% }); %> 33 | |<% if (success) { %> 34 | div.alert.alert-info.alert-dismissable 35 | button.close(type='button', data-dismiss='alert') × 36 | | We have received your message. Thank you. 37 | |<% } %> 38 | |<% if (!success) { %> 39 | div.form-group(class!='<%- errfor.name ? "has-error" : "" %>') 40 | label Your Name: 41 | input.form-control(type='text', name='name', value!='<%= name %>') 42 | span.help-block <%- errfor.name %> 43 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 44 | label Your Email: 45 | input.form-control(type='text', name='email', value!='<%= email %>') 46 | span.help-block <%- errfor.email %> 47 | div.form-group(class!='<%- errfor.message ? "has-error" : "" %>') 48 | label Message: 49 | textarea.form-control(name='message', rows='5') <%= message %> 50 | span.help-block <%- errfor.message %> 51 | div.form-group 52 | button.btn.btn-primary.btn-contact(type='button') Send Message 53 | |<% } %> 54 | -------------------------------------------------------------------------------- /views/contact/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | res.render('contact/index'); 5 | }; 6 | 7 | exports.sendMessage = function(req, res){ 8 | var workflow = req.app.utility.workflow(req, res); 9 | 10 | workflow.on('validate', function() { 11 | if (!req.body.name) { 12 | workflow.outcome.errfor.name = 'required'; 13 | } 14 | 15 | if (!req.body.email) { 16 | workflow.outcome.errfor.email = 'required'; 17 | } 18 | 19 | if (!req.body.message) { 20 | workflow.outcome.errfor.message = 'required'; 21 | } 22 | 23 | if (workflow.hasErrors()) { 24 | return workflow.emit('response'); 25 | } 26 | 27 | workflow.emit('sendEmail'); 28 | }); 29 | 30 | workflow.on('sendEmail', function() { 31 | req.app.utility.sendmail(req, res, { 32 | from: req.app.config.smtp.from.name +' <'+ req.app.config.smtp.from.address +'>', 33 | replyTo: req.body.email, 34 | to: req.app.config.systemEmail, 35 | subject: req.app.config.projectName +' contact form', 36 | textPath: 'contact/email-text', 37 | htmlPath: 'contact/email-html', 38 | locals: { 39 | name: req.body.name, 40 | email: req.body.email, 41 | message: req.body.message, 42 | projectName: req.app.config.projectName 43 | }, 44 | success: function(message) { 45 | workflow.emit('response'); 46 | }, 47 | error: function(err) { 48 | workflow.outcome.errors.push('Error Sending: '+ err); 49 | workflow.emit('response'); 50 | } 51 | }); 52 | }); 53 | 54 | workflow.emit('validate'); 55 | }; 56 | -------------------------------------------------------------------------------- /views/http/404.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/default 2 | 3 | block head 4 | title Page Not Found 5 | 6 | block body 7 | h1 Page Not Found 8 | p.lead 9 | | The resource you requested doesn't exist. 10 | -------------------------------------------------------------------------------- /views/http/500.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/default 2 | 3 | block head 4 | title Server Error 5 | 6 | block body 7 | h1 Server Error 8 | p.lead 9 | | Sorry something went wrong. 10 | if err.stack 11 | pre #{err.stack} 12 | -------------------------------------------------------------------------------- /views/http/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.http404 = function(req, res){ 4 | res.status(404); 5 | if (req.xhr) { 6 | res.send({ error: 'Resource not found.' }); 7 | } 8 | else { 9 | res.render('http/404'); 10 | } 11 | }; 12 | 13 | exports.http500 = function(err, req, res, next){ 14 | res.status(500); 15 | 16 | var data = { err: {} }; 17 | if (req.app.get('env') === 'development') { 18 | data.err = err; 19 | console.log(err.stack); 20 | } 21 | 22 | if (req.xhr) { 23 | res.send({ error: 'Something went wrong.', details: data }); 24 | } 25 | else { 26 | res.render('http/500', data); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block head 4 | title Drywall is Running 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/index.min.css?#{cacheBreaker}') 8 | 9 | block body 10 | div.jumbotron 11 | h1 Success 12 | p.lead 13 | | Your Node.js website and user system is running. May the force be with you. 14 | div 15 | a.btn.btn-primary.btn-lg(href='/signup/') Create an Account 16 | |  or  17 | a.btn.btn-warning.btn-lg(href='/login/forgot/') Reset Your Password 18 | div.clearfix 19 | div.row 20 | div.col-sm-4 21 | div.panel.panel-default 22 | div.panel-body 23 | h3 About Us 24 | p At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti. 25 | a.btn.btn-default.btn-block(href='/about/') Learn More 26 | div.col-sm-4 27 | div.panel.panel-default 28 | div.panel-body 29 | h3 Sign Up 30 | p At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti. 31 | a.btn.btn-default.btn-block(href='/signup/') Learn More 32 | div.col-sm-4 33 | div.panel.panel-default 34 | div.panel-body 35 | h3 Contact Us 36 | p At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti. 37 | a.btn.btn-default.btn-block(href='/contact/') Learn More 38 | -------------------------------------------------------------------------------- /views/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | res.render('index'); 5 | }; 6 | -------------------------------------------------------------------------------- /views/login/forgot/email-html.jade: -------------------------------------------------------------------------------- 1 | h3 Forgot your password? 2 | p 3 | | We received a request to reset the password for your account (#{username}). 4 | p 5 | | To reset your password, click on the link below (or copy and paste the URL into your browser): 6 | p 7 | a(href='#{resetLink}') #{resetLink} 8 | p 9 | | Thanks, 10 | br 11 | | #{projectName} 12 | -------------------------------------------------------------------------------- /views/login/forgot/email-text.jade: -------------------------------------------------------------------------------- 1 | | Forgot your password? 2 | = '\n' 3 | = '\n' 4 | | We received a request to reset the password for your account (#{username}). 5 | = '\n' 6 | = '\n' 7 | | To reset your password, click on the link below (or copy and paste the URL into your browser): 8 | | #{resetLink} 9 | = '\n' 10 | = '\n' 11 | | Thanks, 12 | | #{projectName} 13 | -------------------------------------------------------------------------------- /views/login/forgot/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/default 2 | 3 | block head 4 | title Forgot Your Password? 5 | 6 | block feet 7 | script(src='/views/login/forgot/index.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-sm-6 12 | div.page-header 13 | h1 Forgot Your Password? 14 | div#forgot 15 | 16 | script(type='text/template', id='tmpl-forgot') 17 | form 18 | div.alerts 19 | |<% _.each(errors, function(err) { %> 20 | div.alert.alert-danger 21 | button.close(type='button', data-dismiss='alert') × 22 | |<%- err %> 23 | |<% }); %> 24 | |<% if (success) { %> 25 | div.alert.alert-info 26 | button.close(type='button', data-dismiss='alert') × 27 | | If an account matched that address, an email will be sent with instructions. 28 | |<% } %> 29 | |<% if (!success) { %> 30 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 31 | label Enter Your Email: 32 | input.form-control(type='text', name='email', value!='<%= email %>') 33 | span.help-block <%- errfor.email %> 34 | |<% } %> 35 | div.form-group 36 | |<% if (!success) { %> 37 | button.btn.btn-primary.btn-forgot(type='button') Send Reset 38 | |<% } %> 39 | |  40 | a.btn.btn-link(href='/login/') Back to Login 41 | -------------------------------------------------------------------------------- /views/login/forgot/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | if (req.isAuthenticated()) { 5 | res.redirect(req.user.defaultReturnUrl()); 6 | } 7 | else { 8 | res.render('login/forgot/index'); 9 | } 10 | }; 11 | 12 | exports.send = function(req, res, next){ 13 | var workflow = req.app.utility.workflow(req, res); 14 | 15 | workflow.on('validate', function() { 16 | if (!req.body.email) { 17 | workflow.outcome.errfor.email = 'required'; 18 | return workflow.emit('response'); 19 | } 20 | 21 | workflow.emit('generateToken'); 22 | }); 23 | 24 | workflow.on('generateToken', function() { 25 | var crypto = require('crypto'); 26 | crypto.randomBytes(21, function(err, buf) { 27 | if (err) { 28 | return next(err); 29 | } 30 | 31 | var token = buf.toString('hex'); 32 | req.app.db.models.User.encryptPassword(token, function(err, hash) { 33 | if (err) { 34 | return next(err); 35 | } 36 | 37 | workflow.emit('patchUser', token, hash); 38 | }); 39 | }); 40 | }); 41 | 42 | workflow.on('patchUser', function(token, hash) { 43 | var conditions = { email: req.body.email.toLowerCase() }; 44 | var fieldsToSet = { 45 | resetPasswordToken: hash, 46 | resetPasswordExpires: Date.now() + 10000000 47 | }; 48 | req.app.db.models.User.findOneAndUpdate(conditions, fieldsToSet, function(err, user) { 49 | if (err) { 50 | return workflow.emit('exception', err); 51 | } 52 | 53 | if (!user) { 54 | return workflow.emit('response'); 55 | } 56 | 57 | workflow.emit('sendEmail', token, user); 58 | }); 59 | }); 60 | 61 | workflow.on('sendEmail', function(token, user) { 62 | req.app.utility.sendmail(req, res, { 63 | from: req.app.config.smtp.from.name +' <'+ req.app.config.smtp.from.address +'>', 64 | to: user.email, 65 | subject: 'Reset your '+ req.app.config.projectName +' password', 66 | textPath: 'login/forgot/email-text', 67 | htmlPath: 'login/forgot/email-html', 68 | locals: { 69 | username: user.username, 70 | resetLink: req.protocol +'://'+ req.headers.host +'/login/reset/'+ user.email +'/'+ token +'/', 71 | projectName: req.app.config.projectName 72 | }, 73 | success: function(message) { 74 | workflow.emit('response'); 75 | }, 76 | error: function(err) { 77 | workflow.outcome.errors.push('Error Sending: '+ err); 78 | workflow.emit('response'); 79 | } 80 | }); 81 | }); 82 | 83 | workflow.emit('validate'); 84 | }; 85 | -------------------------------------------------------------------------------- /views/login/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/default 2 | 3 | block head 4 | title Login 5 | 6 | block feet 7 | script(src='/views/login/index.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-sm-6 12 | div.page-header 13 | h1 Sign In 14 | div#login 15 | if oauthTwitter || oauthGitHub || oauthFacebook || oauthGoogle || oauthTumblr 16 | hr 17 | p Or sign in using... 18 | if oauthMessage 19 | div.alerts 20 | div.alert.alert-info.alert-dismissable 21 | button.close(type='button', data-dismiss='alert') × 22 | |#{oauthMessage}  23 | b 24 | a(href='/signup/') Sign Up Here 25 | div.form-actions 26 | div.btn-group.btn-group-justified 27 | if oauthTwitter 28 | a.btn.btn-info(href='/login/twitter/') 29 | i.fa.fa-twitter.fa-lg 30 | | Twitter 31 | if oauthGitHub 32 | a.btn.btn-info(href='/login/github/') 33 | i.fa.fa-github.fa-lg 34 | | GitHub 35 | if oauthFacebook 36 | a.btn.btn-info(href='/login/facebook/') 37 | i.fa.fa-facebook-square.fa-lg 38 | | Facebook 39 | if oauthGoogle 40 | a.btn.btn-info(href='/login/google/') 41 | i.fa.fa-google-plus-square.fa-lg 42 | | Google 43 | if oauthTumblr 44 | a.btn.btn-info(href='/login/tumblr/') 45 | i.fa.fa-tumblr-square.fa-lg 46 | | Tumblr 47 | 48 | script(type='text/template', id='tmpl-login') 49 | form 50 | div.alerts 51 | |<% _.each(errors, function(err) { %> 52 | div.alert.alert-danger.alert-dismissable 53 | button.close(type='button', data-dismiss='alert') × 54 | |<%- err %> 55 | |<% }); %> 56 | div.form-group(class!='<%- errfor.username ? "has-error" : "" %>') 57 | label Username or Email: 58 | input.form-control(type='text', name='username', value!='<%= username %>') 59 | span.help-block <%- errfor.username %> 60 | div.form-group(class!='<%- errfor.password ? "has-error" : "" %>') 61 | label Password: 62 | input.form-control(type='password', name='password', value!='<%= password %>') 63 | span.help-block <%- errfor.password %> 64 | div.form-actions 65 | button.btn.btn-primary.btn-login(type='button') Sign In 66 | |  67 | a.btn.btn-link(href='/login/forgot/') Forgot your password? 68 | -------------------------------------------------------------------------------- /views/login/reset/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../../layouts/default 2 | 3 | block head 4 | title Reset Your Password 5 | 6 | block feet 7 | script(src='/views/login/reset/index.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-sm-6 12 | div.page-header 13 | h1 Reset Your Password 14 | div#reset 15 | 16 | script(type='text/template', id='tmpl-reset') 17 | form 18 | div.alerts 19 | |<% _.each(errors, function(err) { %> 20 | div.alert.alert-danger.alert-dismissable 21 | button.close(type='button', data-dismiss='alert') × 22 | |<%- err %> 23 | |<% }); %> 24 | |<% if (success) { %> 25 | div.alert.alert-info.alert-dismissable 26 | button.close(type='button', data-dismiss='alert') × 27 | | Your password has been reset. Please login to confirm. 28 | |<% } %> 29 | |<% if (id == undefined) { %> 30 | div.alert.alert-warning.alert-dismissable 31 | button.close(type='button', data-dismiss='alert') × 32 | | You do not have a valid reset request. 33 | |<% } %> 34 | |<% if (!success && id != undefined) { %> 35 | div.form-group(class!='<%- errfor.password ? "has-error" : "" %>') 36 | label New Password: 37 | input.form-control(type='password', name='password', value!='<%= password %>') 38 | span.help-block <%- errfor.password %> 39 | div.form-group(class!='<%- errfor.confirm ? "has-error" : "" %>') 40 | label Confirm Password: 41 | input.form-control(type='password', name='confirm', value!='<%= confirm %>') 42 | span.help-block <%- errfor.confirm %> 43 | |<% } %> 44 | div.form-group 45 | |<% if (!success && id != undefined) { %> 46 | button.btn.btn-primary.btn-reset(type='button') Set Password 47 | |<% } %> 48 | |  49 | a.btn.btn-link(href='/login/') Back to Login 50 | -------------------------------------------------------------------------------- /views/login/reset/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | if (req.isAuthenticated()) { 5 | res.redirect(req.user.defaultReturnUrl()); 6 | } 7 | else { 8 | res.render('login/reset/index'); 9 | } 10 | }; 11 | 12 | exports.set = function(req, res){ 13 | var workflow = req.app.utility.workflow(req, res); 14 | 15 | workflow.on('validate', function() { 16 | if (!req.body.password) { 17 | workflow.outcome.errfor.password = 'required'; 18 | } 19 | 20 | if (!req.body.confirm) { 21 | workflow.outcome.errfor.confirm = 'required'; 22 | } 23 | 24 | if (req.body.password !== req.body.confirm) { 25 | workflow.outcome.errors.push('Passwords do not match.'); 26 | } 27 | 28 | if (workflow.hasErrors()) { 29 | return workflow.emit('response'); 30 | } 31 | 32 | workflow.emit('findUser'); 33 | }); 34 | 35 | workflow.on('findUser', function() { 36 | var conditions = { 37 | email: req.params.email, 38 | resetPasswordExpires: { $gt: Date.now() } 39 | }; 40 | req.app.db.models.User.findOne(conditions, function(err, user) { 41 | if (err) { 42 | return workflow.emit('exception', err); 43 | } 44 | 45 | if (!user) { 46 | workflow.outcome.errors.push('Invalid request.'); 47 | return workflow.emit('response'); 48 | } 49 | 50 | req.app.db.models.User.validatePassword(req.params.token, user.resetPasswordToken, function(err, isValid) { 51 | if (err) { 52 | return workflow.emit('exception', err); 53 | } 54 | 55 | if (!isValid) { 56 | workflow.outcome.errors.push('Invalid request.'); 57 | return workflow.emit('response'); 58 | } 59 | 60 | workflow.emit('patchUser', user); 61 | }); 62 | }); 63 | }); 64 | 65 | workflow.on('patchUser', function(user) { 66 | req.app.db.models.User.encryptPassword(req.body.password, function(err, hash) { 67 | if (err) { 68 | return workflow.emit('exception', err); 69 | } 70 | 71 | var fieldsToSet = { password: hash, resetPasswordToken: '' }; 72 | var options = { new: true }; 73 | req.app.db.models.User.findByIdAndUpdate(user._id, fieldsToSet, options, function(err, user) { 74 | if (err) { 75 | return workflow.emit('exception', err); 76 | } 77 | 78 | workflow.emit('response'); 79 | }); 80 | }); 81 | }); 82 | 83 | workflow.emit('validate'); 84 | }; 85 | -------------------------------------------------------------------------------- /views/logout/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = function(req, res){ 4 | req.logout(); 5 | res.redirect('/'); 6 | }; 7 | -------------------------------------------------------------------------------- /views/signup/email-html.jade: -------------------------------------------------------------------------------- 1 | h3 Welcome to #{projectName} 2 | p Thanks for signing up. As requested, your account has been created. Here are your login credentials: 3 | table(border='1', cellpadding='5', cellspacing='0') 4 | tr 5 | td Username: 6 | td #{username} 7 | tr 8 | td Email: 9 | td #{email} 10 | tr 11 | td Login Here: 12 | td #{loginURL} 13 | p 14 | | Thanks, 15 | br 16 | | #{projectName} 17 | -------------------------------------------------------------------------------- /views/signup/email-text.jade: -------------------------------------------------------------------------------- 1 | | Welcome to #{projectName} 2 | = '\n' 3 | = '\n' 4 | | Thanks for signing up. As requested, your account has been created. Here are your login credentials: 5 | = '\n' 6 | = '\n' 7 | | Username: #{username} 8 | | Email: #{email} 9 | = '\n' 10 | = '\n' 11 | | Login Here: #{loginURL} 12 | = '\n' 13 | = '\n' 14 | | Thanks, 15 | | #{projectName} 16 | -------------------------------------------------------------------------------- /views/signup/index.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/default 2 | 3 | block head 4 | title Sign Up 5 | 6 | block neck 7 | link(rel='stylesheet', href='/views/signup/index.min.css?#{cacheBreaker}') 8 | 9 | block feet 10 | script(src='/views/signup/index.min.js?#{cacheBreaker}') 11 | 12 | block body 13 | div.row 14 | div.col-sm-6 15 | div.page-header 16 | h1 Sign Up 17 | div#signup 18 | if oauthTwitter || oauthGitHub || oauthFacebook || oauthGoogle || oauthTumblr 19 | hr 20 | p Or sign up using... 21 | if oauthMessage 22 | div.alerts 23 | div.alert.alert-info.alert-dismissable 24 | button.close(type='button', data-dismiss='alert') × 25 | |#{oauthMessage}  26 | b 27 | a(href='/login/') Login Here 28 | div.btn-group.btn-group-justified 29 | if oauthTwitter 30 | a.btn.btn-info(href='/signup/twitter/') 31 | i.fa.fa-twitter.fa-lg 32 | | Twitter 33 | if oauthGitHub 34 | a.btn.btn-info(href='/signup/github/') 35 | i.fa.fa-github.fa-lg 36 | | GitHub 37 | if oauthFacebook 38 | a.btn.btn-info(href='/signup/facebook/') 39 | i.fa.fa-facebook-square.fa-lg 40 | | Facebook 41 | if oauthGoogle 42 | a.btn.btn-info(href='/signup/google/') 43 | i.fa.fa-google-plus-square.fa-lg 44 | | Google 45 | if oauthTumblr 46 | a.btn.btn-info(href='/signup/tumblr/') 47 | i.fa.fa-tumblr-square.fa-lg 48 | | Tumblr 49 | div.col-sm-6.marketing 50 | div.page-header 51 | h1 Campy Benefits 52 | p.lead Really, you will love it inside. It's super great! 53 | i.fa.fa-thumbs-o-up.super-awesome 54 | 55 | script(type='text/template', id='tmpl-signup') 56 | form 57 | div.alerts 58 | |<% _.each(errors, function(err) { %> 59 | div.alert.alert-danger.alert-dismissable 60 | button.close(type='button', data-dismiss='alert') × 61 | |<%- err %> 62 | |<% }); %> 63 | div.form-group(class!='<%- errfor.username ? "has-error" : "" %>') 64 | label Pick a Username: 65 | input.form-control(type='text', name='username', value!='<%= username %>') 66 | span.help-block <%- errfor.username %> 67 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 68 | label Enter Your Email: 69 | input.form-control(type='text', name='email', value!='<%= email %>') 70 | span.help-block <%- errfor.email %> 71 | div.form-group(class!='<%- errfor.password ? "has-error" : "" %>') 72 | label Create a Password: 73 | input.form-control(type='password', name='password', value!='<%= password %>') 74 | span.help-block <%- errfor.password %> 75 | div.form-group 76 | button.btn.btn-primary.btn-signup(type='button') Create My Account 77 | -------------------------------------------------------------------------------- /views/signup/social.jade: -------------------------------------------------------------------------------- 1 | extends ../../layouts/default 2 | 3 | block head 4 | title Sign Up 5 | 6 | block feet 7 | script(src='/views/signup/social.min.js?#{cacheBreaker}') 8 | 9 | block body 10 | div.row 11 | div.col-sm-6 12 | div.page-header 13 | h1 Complete Sign Up 14 | div#signup 15 | 16 | script(type='text/template', id='tmpl-signup') 17 | form 18 | div.alerts 19 | |<% _.each(errors, function(err) { %> 20 | div.alert.alert-danger.alert-dismissable 21 | button.close(type='button', data-dismiss='alert') × 22 | |<%- err %> 23 | |<% }); %> 24 | div.form-group(class!='<%- errfor.email ? "has-error" : "" %>') 25 | label Enter Your Email: 26 | input.form-control(type='text', name='email', value!='<%= email %>') 27 | span.help-block <%- errfor.email %> 28 | div.form-group 29 | button.btn.btn-primary.btn-signup(type='button') Create My Account 30 | 31 | script(type='text/template', id='data-email') !{email} 32 | --------------------------------------------------------------------------------