├── .babelrc ├── .editorconfig ├── .env-sample ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── client ├── actions │ └── api.js ├── components │ ├── alert.jsx │ ├── form │ │ ├── button.jsx │ │ ├── control-group.jsx │ │ ├── select-control.jsx │ │ ├── spinner.jsx │ │ ├── text-control.jsx │ │ └── textarea-control.jsx │ ├── modal.jsx │ ├── paging.jsx │ ├── route-redirect.jsx │ └── route-status.jsx ├── core │ ├── bootstrap.less │ └── font-awesome.less ├── helpers │ ├── json-fetch.js │ ├── link-state.js │ └── parse-validation.js ├── media │ ├── favicon.ico │ ├── logo-square-inverse.png │ └── logo-square.png └── pages │ ├── account │ ├── app.jsx │ ├── footer.jsx │ ├── home │ │ ├── index.jsx │ │ └── index.less │ ├── index.jsx │ ├── index.less │ ├── navbar.jsx │ ├── not-found.jsx │ └── settings │ │ ├── actions.js │ │ ├── constants.js │ │ ├── details-form.jsx │ │ ├── index.jsx │ │ ├── password-form.jsx │ │ ├── reducers │ │ ├── details.js │ │ ├── password.js │ │ └── user.js │ │ ├── store.js │ │ └── user-form.jsx │ ├── admin │ ├── accounts │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── details-form.jsx │ │ │ ├── index.jsx │ │ │ ├── index.less │ │ │ ├── reducers │ │ │ │ ├── delete.js │ │ │ │ ├── details.js │ │ │ │ ├── note.js │ │ │ │ ├── status.js │ │ │ │ └── user.js │ │ │ ├── store.js │ │ │ └── user-form.jsx │ │ └── search │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── create-new-form.jsx │ │ │ ├── filter-form.jsx │ │ │ ├── index.jsx │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ ├── results.jsx │ │ │ └── store.js │ ├── admin-groups │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── details-form.jsx │ │ │ ├── index.jsx │ │ │ ├── permissions-form.jsx │ │ │ ├── reducers │ │ │ │ ├── delete.js │ │ │ │ ├── details.js │ │ │ │ └── permissions.js │ │ │ └── store.js │ │ └── search │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── create-new-form.jsx │ │ │ ├── filter-form.jsx │ │ │ ├── index.jsx │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ ├── results.jsx │ │ │ └── store.js │ ├── admins │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── details-form.jsx │ │ │ ├── groups-form.jsx │ │ │ ├── index.jsx │ │ │ ├── permissions-form.jsx │ │ │ ├── reducers │ │ │ │ ├── delete.js │ │ │ │ ├── details.js │ │ │ │ ├── groups.js │ │ │ │ ├── permissions.js │ │ │ │ └── user.js │ │ │ ├── store.js │ │ │ └── user-form.jsx │ │ └── search │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── create-new-form.jsx │ │ │ ├── filter-form.jsx │ │ │ ├── index.jsx │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ ├── results.jsx │ │ │ └── store.js │ ├── app.jsx │ ├── components │ │ ├── delete-form.jsx │ │ ├── filter-form-hoc.jsx │ │ ├── note-form.jsx │ │ └── status-form.jsx │ ├── footer.jsx │ ├── home │ │ ├── index.jsx │ │ └── index.less │ ├── index.jsx │ ├── index.less │ ├── navbar.jsx │ ├── not-found.jsx │ ├── statuses │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── details-form.jsx │ │ │ ├── index.jsx │ │ │ ├── reducers │ │ │ │ ├── delete.js │ │ │ │ └── details.js │ │ │ └── store.js │ │ └── search │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── create-new-form.jsx │ │ │ ├── filter-form.jsx │ │ │ ├── index.jsx │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ ├── results.jsx │ │ │ └── store.js │ └── users │ │ ├── details │ │ ├── actions.js │ │ ├── constants.js │ │ ├── details-form.jsx │ │ ├── index.jsx │ │ ├── password-form.jsx │ │ ├── reducers │ │ │ ├── delete.js │ │ │ ├── details.js │ │ │ └── password.js │ │ ├── roles-form.jsx │ │ └── store.js │ │ └── search │ │ ├── actions.js │ │ ├── constants.js │ │ ├── create-new-form.jsx │ │ ├── filter-form.jsx │ │ ├── index.jsx │ │ ├── reducers │ │ ├── create-new.js │ │ └── results.js │ │ ├── results.jsx │ │ └── store.js │ └── main │ ├── about │ └── index.jsx │ ├── app-universal.jsx │ ├── app.jsx │ ├── contact │ ├── actions.js │ ├── constants.js │ ├── form.jsx │ ├── index.jsx │ └── store.js │ ├── footer.jsx │ ├── home │ ├── index.jsx │ └── index.less │ ├── index.jsx │ ├── index.less │ ├── login │ ├── actions.js │ ├── constants.js │ ├── forgot │ │ ├── index.jsx │ │ └── store.js │ ├── home │ │ ├── index.jsx │ │ └── store.js │ ├── logout │ │ ├── index.jsx │ │ └── store.js │ └── reset │ │ ├── index.jsx │ │ └── store.js │ ├── navbar.jsx │ ├── not-found.jsx │ └── signup │ ├── actions.js │ ├── constants.js │ ├── form.jsx │ ├── index.jsx │ └── store.js ├── config.js ├── first-time-setup.js ├── gulp ├── build.js ├── clean.js ├── default.js ├── less.js ├── media.js ├── nodemon.js ├── watch.js └── webpack.js ├── gulpfile.js ├── index.js ├── manifest.js ├── package-lock.json ├── package.json ├── server.js ├── server ├── api │ ├── accounts.js │ ├── admin-groups.js │ ├── admins.js │ ├── auth-attempts.js │ ├── contact.js │ ├── index.js │ ├── login.js │ ├── logout.js │ ├── sessions.js │ ├── signup.js │ ├── statuses.js │ └── users.js ├── auth.js ├── emails │ ├── contact.hbs.md │ ├── forgot-password.hbs.md │ └── welcome.hbs.md ├── mailer.js ├── models │ ├── account.js │ ├── admin-group.js │ ├── admin.js │ ├── auth-attempt.js │ ├── note-entry.js │ ├── session.js │ ├── status-entry.js │ ├── status.js │ └── user.js └── web │ ├── account │ ├── index.js │ └── index.jsx │ ├── admin │ ├── index.js │ └── index.jsx │ ├── main │ ├── index.js │ └── index.jsx │ └── public.js └── test ├── client ├── actions │ └── api.js ├── components │ ├── alert.js │ ├── form │ │ ├── button.js │ │ ├── control-group.js │ │ ├── select-control.js │ │ ├── spinner.js │ │ ├── text-control.js │ │ └── textarea-control.js │ ├── modal.js │ └── paging.js ├── helpers │ ├── json-fetch.js │ ├── link-state.js │ └── parse-validation.js └── pages │ ├── account │ ├── app.js │ ├── footer.js │ ├── home │ │ └── index.js │ ├── index.js │ ├── navbar.js │ ├── not-found.js │ └── settings │ │ ├── actions.js │ │ ├── details-form.js │ │ ├── index.js │ │ ├── password-form.js │ │ ├── reducers │ │ ├── details.js │ │ ├── password.js │ │ └── user.js │ │ └── user-form.js │ ├── admin │ ├── accounts │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── details-form.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ │ ├── delete.js │ │ │ │ ├── details.js │ │ │ │ ├── note.js │ │ │ │ ├── status.js │ │ │ │ └── user.js │ │ │ └── user-form.js │ │ └── search │ │ │ ├── actions.js │ │ │ ├── create-new-form.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ └── results.js │ ├── admin-groups │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── details-form.js │ │ │ ├── index.js │ │ │ ├── permissions-form.js │ │ │ └── reducers │ │ │ │ ├── delete.js │ │ │ │ ├── details.js │ │ │ │ └── permissions.js │ │ └── search │ │ │ ├── actions.js │ │ │ ├── create-new-form.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ └── results.js │ ├── admins │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── details-form.js │ │ │ ├── groups-form.js │ │ │ ├── index.js │ │ │ ├── permissions-form.js │ │ │ ├── reducers │ │ │ │ ├── delete.js │ │ │ │ ├── details.js │ │ │ │ ├── groups.js │ │ │ │ ├── permissions.js │ │ │ │ └── user.js │ │ │ └── user-form.js │ │ └── search │ │ │ ├── actions.js │ │ │ ├── create-new-form.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ └── results.js │ ├── app.js │ ├── components │ │ ├── delete-form.js │ │ ├── filter-form-fixture.jsx │ │ ├── filter-form-hoc.js │ │ ├── note-form.js │ │ └── status-form.js │ ├── footer.js │ ├── home │ │ └── index.js │ ├── index.js │ ├── navbar.js │ ├── not-found.js │ ├── statuses │ │ ├── details │ │ │ ├── actions.js │ │ │ ├── details-form.js │ │ │ ├── index.js │ │ │ └── reducers │ │ │ │ ├── delete.js │ │ │ │ └── details.js │ │ └── search │ │ │ ├── actions.js │ │ │ ├── create-new-form.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ ├── create-new.js │ │ │ └── results.js │ │ │ └── results.js │ └── users │ │ ├── details │ │ ├── actions.js │ │ ├── details-form.js │ │ ├── index.js │ │ ├── password-form.js │ │ ├── reducers │ │ │ ├── delete.js │ │ │ ├── details.js │ │ │ └── password.js │ │ └── roles-form.js │ │ └── search │ │ ├── actions.js │ │ ├── create-new-form.js │ │ ├── index.js │ │ ├── reducers │ │ ├── create-new.js │ │ └── results.js │ │ └── results.js │ └── main │ ├── about │ └── index.js │ ├── app.js │ ├── contact │ ├── actions.js │ ├── constants.js │ ├── form.js │ ├── index.js │ └── store.js │ ├── home │ └── index.js │ ├── index.js │ ├── login │ ├── actions.js │ ├── constants.js │ ├── forgot │ │ ├── index.js │ │ └── store.js │ ├── home │ │ ├── index.js │ │ └── store.js │ ├── logout │ │ ├── index.js │ │ └── store.js │ └── reset │ │ ├── index.js │ │ └── store.js │ ├── not-found.js │ └── signup │ ├── actions.js │ ├── constants.js │ ├── form.js │ ├── index.js │ └── store.js ├── lab ├── client-after.js ├── client-before.js ├── server-after.js └── transform.js ├── misc ├── config.js ├── index.js └── manifest.js └── server ├── api ├── accounts.js ├── admin-groups.js ├── admins.js ├── auth-attempts.js ├── contact.js ├── index.js ├── login.js ├── logout.js ├── sessions.js ├── signup.js ├── statuses.js └── users.js ├── auth.js ├── fixtures ├── cookie-account.js ├── cookie-admin.js ├── credentials-account.js ├── credentials-admin.js └── make-mock-model.js ├── mailer.js ├── models ├── account.js ├── admin-group.js ├── admin.js ├── auth-attempt.js ├── note-entry.js ├── session.js ├── status-entry.js ├── status.js └── user.js └── web ├── account.js ├── admin.js └── main.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "ignore": [ 4 | "server/models/*.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 4 space indentation 2 | [*.js] 3 | indent_style = space 4 | indent_size = 4 5 | [*.jsx] 6 | indent_style = space 7 | indent_size = 4 8 | [*.less] 9 | indent_style = space 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | SMTP_PASSWORD=secret 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | public/* 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-hapi", 4 | "plugin:react/recommended" 5 | ], 6 | "plugins": [ 7 | "react" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | public/* 3 | test/artifacts/* 4 | .env 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: node_js 3 | node_js: 4 | - "8" 5 | services: 6 | - mongodb 7 | env: 8 | - NODE_ENV=test CXX=g++-4.8 9 | before_install: 10 | - sudo apt-get install unicode-data 11 | addons: 12 | apt: 13 | sources: 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - g++-4.8 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /client/actions/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const JsonFetch = require('../helpers/json-fetch'); 3 | 4 | 5 | class Actions { 6 | static get(url, query, store, typeReq, typeRes, callback) { 7 | 8 | const request = { method: 'GET', url, query }; 9 | this.makeRequest(request, store, typeReq, typeRes, callback); 10 | } 11 | 12 | static put(url, data, store, typeReq, typeRes, callback) { 13 | 14 | const request = { method: 'PUT', url, data }; 15 | this.makeRequest(request, store, typeReq, typeRes, callback); 16 | } 17 | 18 | static post(url, data, store, typeReq, typeRes, callback) { 19 | 20 | const request = { method: 'POST', url, data }; 21 | this.makeRequest(request, store, typeReq, typeRes, callback); 22 | } 23 | 24 | static delete(url, query, store, typeReq, typeRes, callback) { 25 | 26 | const request = { method: 'DELETE', url, query }; 27 | this.makeRequest(request, store, typeReq, typeRes, callback); 28 | } 29 | 30 | static makeRequest(request, store, typeReq, typeRes, callback) { 31 | 32 | store.dispatch({ 33 | type: typeReq, 34 | request 35 | }); 36 | 37 | JsonFetch(request, (err, response) => { 38 | 39 | store.dispatch({ 40 | type: typeRes, 41 | err, 42 | response 43 | }); 44 | 45 | if (callback) { 46 | callback(err, response); 47 | } 48 | }); 49 | } 50 | } 51 | 52 | 53 | module.exports = Actions; 54 | -------------------------------------------------------------------------------- /client/components/alert.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const PropTypes = require('prop-types'); 3 | const React = require('react'); 4 | 5 | 6 | const propTypes = { 7 | message: PropTypes.string, 8 | onClose: PropTypes.func, 9 | type: PropTypes.oneOf(['success', 'info', 'warning', 'danger']) 10 | }; 11 | 12 | 13 | class Alert extends React.Component { 14 | render() { 15 | 16 | let close; 17 | 18 | if (this.props.onClose) { 19 | close = ; 26 | } 27 | 28 | return ( 29 |
30 | {close} 31 | {this.props.message} 32 |
33 | ); 34 | } 35 | } 36 | 37 | Alert.propTypes = propTypes; 38 | 39 | 40 | module.exports = Alert; 41 | -------------------------------------------------------------------------------- /client/components/form/button.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ClassNames = require('classnames'); 3 | const ObjectAssign = require('object-assign'); 4 | const PropTypes = require('prop-types'); 5 | const React = require('react'); 6 | 7 | 8 | const propTypes = { 9 | children: PropTypes.node, 10 | disabled: PropTypes.bool, 11 | inputClasses: PropTypes.object, 12 | name: PropTypes.string, 13 | onClick: PropTypes.func, 14 | type: PropTypes.string, 15 | value: PropTypes.oneOfType([ 16 | PropTypes.string, 17 | PropTypes.number 18 | ]) 19 | }; 20 | const defaultProps = { 21 | type: 'button' 22 | }; 23 | 24 | 25 | class Button extends React.Component { 26 | render() { 27 | 28 | const inputClasses = ClassNames(ObjectAssign({ 29 | 'btn': true 30 | }, this.props.inputClasses)); 31 | 32 | return ( 33 | 43 | ); 44 | } 45 | } 46 | 47 | Button.propTypes = propTypes; 48 | Button.defaultProps = defaultProps; 49 | 50 | 51 | module.exports = Button; 52 | -------------------------------------------------------------------------------- /client/components/form/control-group.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ClassNames = require('classnames'); 3 | const ObjectAssign = require('object-assign'); 4 | const PropTypes = require('prop-types'); 5 | const React = require('react'); 6 | 7 | 8 | const propTypes = { 9 | children: PropTypes.node, 10 | groupClasses: PropTypes.object, 11 | hasError: PropTypes.bool, 12 | help: PropTypes.string, 13 | helpClasses: PropTypes.object, 14 | hideHelp: PropTypes.bool, 15 | hideLabel: PropTypes.bool, 16 | label: PropTypes.string, 17 | labelClasses: PropTypes.object 18 | }; 19 | 20 | 21 | class ControlGroup extends React.Component { 22 | render() { 23 | 24 | const groupClasses = ClassNames(ObjectAssign({ 25 | 'form-group': true, 26 | 'has-error': this.props.hasError 27 | }, this.props.groupClasses)); 28 | 29 | const labelClasses = ClassNames(ObjectAssign({ 30 | 'control-label': true 31 | }, this.props.labelClasses)); 32 | 33 | const helpClasses = ClassNames(ObjectAssign({ 34 | 'help-block': true 35 | }, this.props.helpClasses)); 36 | 37 | let label; 38 | 39 | if (!this.props.hideLabel) { 40 | label = ; 43 | } 44 | 45 | let help; 46 | 47 | if (!this.props.hideHelp) { 48 | help = 49 | {this.props.help} 50 | ; 51 | } 52 | 53 | return ( 54 |
55 | {label} 56 | {this.props.children} 57 | {help} 58 |
59 | ); 60 | } 61 | } 62 | 63 | ControlGroup.propTypes = propTypes; 64 | 65 | 66 | module.exports = ControlGroup; 67 | -------------------------------------------------------------------------------- /client/components/form/spinner.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ClassNames = require('classnames'); 3 | const PropTypes = require('prop-types'); 4 | const React = require('react'); 5 | 6 | 7 | const propTypes = { 8 | show: PropTypes.bool, 9 | space: PropTypes.string 10 | }; 11 | 12 | 13 | class Spinner extends React.Component { 14 | render() { 15 | 16 | let spaceLeft; 17 | 18 | if (this.props.space === 'left') { 19 | spaceLeft = '\u00A0\u00A0'; 20 | } 21 | 22 | let spaceRight; 23 | 24 | if (this.props.space === 'right') { 25 | spaceRight = '\u00A0\u00A0'; 26 | } 27 | 28 | const spinnerClasses = ClassNames({ 29 | hidden: !this.props.show 30 | }); 31 | 32 | return ( 33 | 34 | {spaceLeft} 35 | 36 | {spaceRight} 37 | 38 | ); 39 | } 40 | } 41 | 42 | Spinner.propTypes = propTypes; 43 | 44 | 45 | module.exports = Spinner; 46 | -------------------------------------------------------------------------------- /client/components/form/textarea-control.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ClassNames = require('classnames'); 3 | const ControlGroup = require('./control-group.jsx'); 4 | const ObjectAssign = require('object-assign'); 5 | const PropTypes = require('prop-types'); 6 | const React = require('react'); 7 | 8 | 9 | const propTypes = { 10 | disabled: PropTypes.bool, 11 | hasError: PropTypes.bool, 12 | help: PropTypes.string, 13 | inputClasses: PropTypes.object, 14 | label: PropTypes.string, 15 | name: PropTypes.string, 16 | onChange: PropTypes.func, 17 | placeholder: PropTypes.string, 18 | rows: PropTypes.string, 19 | value: PropTypes.oneOfType([ 20 | PropTypes.string, 21 | PropTypes.number 22 | ]) 23 | }; 24 | 25 | 26 | class TextareaControl extends React.Component { 27 | focus() { 28 | 29 | return this.input.focus(); 30 | } 31 | 32 | value() { 33 | 34 | return this.input.value; 35 | } 36 | 37 | render() { 38 | 39 | const inputClasses = ClassNames(ObjectAssign({ 40 | 'form-control': true 41 | }, this.props.inputClasses)); 42 | 43 | return ( 44 | 48 | 49 |