├── src ├── client │ ├── styles │ │ ├── mixins.less │ │ ├── variables.less │ │ └── styles.less │ ├── images │ │ ├── busy.gif │ │ ├── icon.png │ │ ├── logo.png │ │ └── default-image.jpg │ ├── app │ │ ├── widgets │ │ │ ├── widgets.module.js │ │ │ ├── widget-header.html │ │ │ └── ht-widget-header.directive.js │ │ ├── blocks │ │ │ ├── logger │ │ │ │ ├── logger.module.js │ │ │ │ └── logger.js │ │ │ ├── exception │ │ │ │ ├── exception.module.js │ │ │ │ ├── exception.js │ │ │ │ ├── exception-handler.provider.spec.js │ │ │ │ └── exception-handler.provider.js │ │ │ └── router │ │ │ │ ├── router.module.js │ │ │ │ └── router-helper.provider.js │ │ ├── login │ │ │ ├── login.module.js │ │ │ ├── logout.controller.js │ │ │ ├── login.controller.js │ │ │ ├── login.route.js │ │ │ └── login.html │ │ ├── signup │ │ │ ├── signup.module.js │ │ │ ├── signup_activate.controller.js │ │ │ ├── signup.controller.js │ │ │ ├── signup_validation_process.controller.js │ │ │ ├── signup_validation.controller.js │ │ │ ├── templates │ │ │ │ ├── signup_validation_process.html │ │ │ │ ├── signup_activate.html │ │ │ │ ├── signup_validation.html │ │ │ │ ├── signup.html │ │ │ │ └── signup_profile.html │ │ │ ├── signup_profile.controller.js │ │ │ └── signup.route.js │ │ ├── dashboard │ │ │ ├── dashboard.module.js │ │ │ ├── dashboard.route.js │ │ │ ├── dashboard.controller.js │ │ │ ├── dashboard.route.spec.js │ │ │ ├── dashboard.controller.spec.js │ │ │ └── dashboard.html │ │ ├── layout │ │ │ ├── layout.module.js │ │ │ ├── shell.html │ │ │ ├── shell.controller.js │ │ │ ├── ht-top-nav.directive.js │ │ │ ├── shell.controller.spec.js │ │ │ ├── sidebar.controller.spec.js │ │ │ ├── ht-top-nav.html │ │ │ └── sidebar.controller.js │ │ ├── home │ │ │ ├── home.module.js │ │ │ ├── home.route.js │ │ │ ├── createTaskModalController.js │ │ │ ├── editTaskModalController.js │ │ │ ├── createTaskModal.html │ │ │ ├── editTaskModal.html │ │ │ ├── home.html │ │ │ └── homeController.js │ │ ├── users │ │ │ ├── users.module.js │ │ │ ├── controllers │ │ │ │ ├── editUserModalController.js │ │ │ │ ├── addUserModalController.js │ │ │ │ ├── password_recoveryController.js │ │ │ │ ├── password_resetController.js │ │ │ │ └── usersListController.js │ │ │ ├── templates │ │ │ │ ├── editUserModal.html │ │ │ │ ├── addUserModal.html │ │ │ │ ├── password_recovery.html │ │ │ │ ├── password_reset.html │ │ │ │ └── usersList.html │ │ │ └── users.route.js │ │ ├── core │ │ │ ├── constants.js │ │ │ ├── core.module.js │ │ │ ├── templates │ │ │ │ ├── alert.html │ │ │ │ └── 404.html │ │ │ ├── directives │ │ │ │ ├── ng-enter.js │ │ │ │ └── compare-to.js │ │ │ ├── filters │ │ │ │ └── boolFilter.js │ │ │ ├── core.route.js │ │ │ ├── services │ │ │ │ ├── alert.js │ │ │ │ ├── users.js │ │ │ │ └── tasks.js │ │ │ ├── controllers │ │ │ │ └── alert.js │ │ │ ├── core.route.spec.js │ │ │ ├── config.js │ │ │ └── interceptors │ │ │ │ └── request.js │ │ └── app.module.js │ └── test-helpers │ │ └── mock-data.js └── server │ ├── start.js │ ├── templates │ ├── recovery-password-email │ │ ├── style.scss │ │ ├── text.hbs │ │ └── html.hbs │ ├── validate-email │ │ ├── style.scss │ │ ├── text.hbs │ │ └── html.hbs │ ├── welcome-email │ │ ├── style.scss │ │ ├── text.hbs │ │ └── html.hbs │ └── _common.scss │ ├── index.js │ ├── start_clusters.js │ ├── tests │ ├── mocha.opts │ ├── helpers.js │ └── routes │ │ ├── index.js │ │ ├── users.js │ │ ├── token.js │ │ └── signup.js │ ├── config │ ├── config.js │ ├── logger.js │ ├── env │ │ ├── test.js │ │ ├── production.js │ │ └── development.js │ ├── db.js │ ├── acl.js │ ├── auth.js │ ├── certs │ │ ├── mean.cert │ │ └── mean.key │ ├── boot.js │ └── express.js │ ├── services │ ├── core.js │ ├── tokens.js │ ├── tasks.js │ ├── sessions.js │ ├── email.js │ ├── signup.js │ └── users.js │ ├── error.js │ ├── fixtures │ ├── user.js │ └── admins.js │ ├── routes │ ├── index.js │ ├── core.js │ ├── token.js │ ├── signup.js │ ├── social.js │ └── tasks.js │ ├── clusters.js │ └── models │ ├── tasks.js │ ├── sessions.js │ └── users.js ├── .babelrc ├── Procfile ├── .eslintignore ├── .bowerrc ├── .editorconfig ├── .htmlhintrc ├── .gitignore ├── .travis.yml ├── .eslintrc ├── bower.json ├── karma.conf.js ├── README.md ├── .snyk ├── gulp.config.js └── package.json /src/client/styles/mixins.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node node_modules/gulp/bin/gulp serve-build 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | bower_components/** 3 | *.spec.js 4 | -------------------------------------------------------------------------------- /src/server/start.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./index.js'); 3 | -------------------------------------------------------------------------------- /src/server/templates/recovery-password-email/style.scss: -------------------------------------------------------------------------------- 1 | @import '../common'; -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import app from './config/boot'; 2 | 3 | module.exports = app(); 4 | -------------------------------------------------------------------------------- /src/server/start_clusters.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./clusters.js'); 3 | -------------------------------------------------------------------------------- /src/client/images/busy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toptive/express-angular-starter/HEAD/src/client/images/busy.gif -------------------------------------------------------------------------------- /src/client/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toptive/express-angular-starter/HEAD/src/client/images/icon.png -------------------------------------------------------------------------------- /src/client/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toptive/express-angular-starter/HEAD/src/client/images/logo.png -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "scripts": { 4 | "postinstall": "gulp wiredep" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/server/tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --require tests/helpers 2 | --reporter spec 3 | --compilers js:babel-register 4 | --slow 5000 5 | -------------------------------------------------------------------------------- /src/client/app/widgets/widgets.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.widgets', []); 5 | }()); 6 | -------------------------------------------------------------------------------- /src/client/images/default-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toptive/express-angular-starter/HEAD/src/client/images/default-image.jpg -------------------------------------------------------------------------------- /src/client/app/blocks/logger/logger.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('blocks.logger', []); 5 | }()); 6 | -------------------------------------------------------------------------------- /src/client/app/login/login.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.login', [ 5 | 'app.core', 6 | ]); 7 | }()); 8 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.signup', [ 5 | 'app.core', 6 | ]); 7 | }()); 8 | -------------------------------------------------------------------------------- /src/client/app/blocks/exception/exception.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('blocks.exception', ['blocks.logger']); 5 | }()); 6 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.dashboard', [ 5 | 'app.core', 6 | ]); 7 | }()); 8 | -------------------------------------------------------------------------------- /src/client/app/layout/layout.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.layout', ['app.core', 'ui.bootstrap.collapse']); 5 | }()); 6 | -------------------------------------------------------------------------------- /src/client/app/home/home.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.home', [ 5 | 'app.core', 6 | 'ui.bootstrap', 7 | ]); 8 | }()); 9 | -------------------------------------------------------------------------------- /src/client/app/users/users.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app.users', [ 5 | 'app.core', 6 | 'ui.bootstrap', 7 | ]); 8 | }()); 9 | -------------------------------------------------------------------------------- /src/server/config/config.js: -------------------------------------------------------------------------------- 1 | const env = (process.env.NODE_ENV) ? process.env.NODE_ENV : 'development'; 2 | const config = require('./env/' + env + '.js'); 3 | 4 | module.exports = config; 5 | -------------------------------------------------------------------------------- /src/client/app/blocks/router/router.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('blocks.router', [ 5 | 'ui.router', 6 | 'blocks.logger', 7 | ]); 8 | }()); 9 | -------------------------------------------------------------------------------- /src/server/templates/validate-email/style.scss: -------------------------------------------------------------------------------- 1 | @import '../common'; 2 | 3 | body { 4 | background-color: #ddd; 5 | color: white; 6 | } 7 | 8 | h1 { 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/server/templates/welcome-email/style.scss: -------------------------------------------------------------------------------- 1 | @import '../common'; 2 | 3 | body { 4 | background-color: #ddd; 5 | color: white; 6 | } 7 | 8 | h1 { 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/server/templates/welcome-email/text.hbs: -------------------------------------------------------------------------------- 1 | Hey {{firstName}} {{lastName}} ({{username}}), welcome to MEAN. 2 | 3 | To start working with us please enter to {{url}} 4 | 5 | Regards, MEAN team. 6 | -------------------------------------------------------------------------------- /src/server/templates/validate-email/text.hbs: -------------------------------------------------------------------------------- 1 | Hey {{username}}, welcome to MEAN. 2 | 3 | Please follow the link below to complete the validation of the password 4 | 5 | {{url}} 6 | 7 | Regards, MEAN team. 8 | -------------------------------------------------------------------------------- /src/server/services/core.js: -------------------------------------------------------------------------------- 1 | const service = {}; 2 | 3 | service.getIndexMessage = function () { 4 | return Promise.resolve({ 5 | status: 'mean API', 6 | }); 7 | }; 8 | 9 | module.exports = service; 10 | -------------------------------------------------------------------------------- /src/server/templates/recovery-password-email/text.hbs: -------------------------------------------------------------------------------- 1 | Hey {{username}}, welcome to MEAN. 2 | 3 | Please follow the link below to complete to recover your password 4 | 5 | {{url}} 6 | 7 | 8 | Regards, MEAN team. 9 | -------------------------------------------------------------------------------- /src/client/app/core/constants.js: -------------------------------------------------------------------------------- 1 | /* global toastr:false, moment:false */ 2 | (function () { 3 | 'use strict'; 4 | 5 | angular 6 | .module('app.core') 7 | .constant('toastr', toastr) 8 | .constant('moment', moment); 9 | }()); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true, 3 | "attr-lowercase": true, 4 | "attr-value-double-quotes": false, 5 | "tag-pair": true, 6 | "id-unique": true, 7 | "src-not-empty": true, 8 | "attr-no-duplication": true, 9 | "spec-char-escape": true 10 | } 11 | -------------------------------------------------------------------------------- /src/server/tests/helpers.js: -------------------------------------------------------------------------------- 1 | import supertest from 'supertest'; 2 | import chai from 'chai'; 3 | import dirtyChai from 'dirty-chai'; 4 | import app from '../index.js'; 5 | 6 | chai.use(dirtyChai); 7 | 8 | global.app = app; 9 | global.request = supertest(app); 10 | global.expect = chai.expect; 11 | -------------------------------------------------------------------------------- /src/client/app/app.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('app', [ 5 | 'app.core', 6 | 'app.widgets', 7 | 'app.login', 8 | 'app.signup', 9 | 'app.layout', 10 | 'app.users', 11 | 'app.home', 12 | 'app.dashboard', 13 | ]); 14 | }()); 15 | -------------------------------------------------------------------------------- /src/client/app/layout/shell.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 |
-------------------------------------------------------------------------------- /src/server/error.js: -------------------------------------------------------------------------------- 1 | const errors = {}; 2 | 3 | errors.get = (error) => { 4 | if (error.errors && error.errors[0] && error.errors[0].message) { 5 | return { msg: error.errors[0].message }; 6 | } 7 | return { msg: (error.message) ? error.message : 'Something went wrong' }; 8 | }; 9 | 10 | module.exports = errors; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 3 | node_modules/ 4 | bower_components/ 5 | build/ 6 | report/ 7 | logs/ 8 | .tmp/ 9 | 10 | *.sqlite 11 | .coverrun 12 | mean_relational_test.sqlite 13 | .coverdata 14 | coverage 15 | 16 | server-coverage/ 17 | yarn.lock 18 | 19 | -------------------------------------------------------------------------------- /src/client/app/core/core.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core', [ 6 | 'ngAnimate', 'ngSanitize', 7 | 'ngStorage', 8 | 'blocks.exception', 'blocks.logger', 'blocks.router', 9 | 'ui.router', 10 | 'chart.js', 11 | 'ui.router', 'angular-loading-bar', 'wysiwyg.module', 12 | 'angular-filepicker', 13 | ]); 14 | }()); 15 | -------------------------------------------------------------------------------- /src/server/tests/routes/index.js: -------------------------------------------------------------------------------- 1 | describe('Routes: Index', () => { 2 | describe('GET /', () => { 3 | it('returns the API status', done => { 4 | request.get('/api/v1') 5 | .expect(200) 6 | .end((err, res) => { 7 | const expected = { status: 'mean API' }; 8 | expect(res.body).to.eql(expected); 9 | done(err); 10 | }); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | env: 3 | - NODE_ENV=travis CXX=g++-4.8 4 | node_js: 5 | - "5.10" 6 | - "6.3" 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | 14 | sudo: false 15 | cache: 16 | directories: 17 | - node_modules 18 | notifications: 19 | email: false 20 | script: 21 | - npm run coverage 22 | after_script: 23 | - npm run coveralls 24 | -------------------------------------------------------------------------------- /src/server/config/logger.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import winston from 'winston'; 3 | 4 | if (!fs.existsSync('logs')) { 5 | fs.mkdirSync('logs'); 6 | } 7 | 8 | module.exports = new winston.Logger({ 9 | transports: [ 10 | new winston.transports.File({ 11 | level: 'info', 12 | filename: 'logs/app.log', 13 | maxsize: 1048576, 14 | maxFiles: 10, 15 | colorize: false, 16 | }), 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/client/app/login/logout.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.login') 6 | .controller('LogoutController', LogoutController); 7 | 8 | LogoutController.$inject = ['logger', 'authentication']; 9 | /* @ngInject */ 10 | function LogoutController(logger, authentication) { 11 | activate(); 12 | 13 | function activate() { 14 | authentication.logout(); 15 | } 16 | } 17 | }()); 18 | -------------------------------------------------------------------------------- /src/client/app/widgets/widget-header.html: -------------------------------------------------------------------------------- 1 |
2 |
{{title}}
3 | ({{subtitle}}) 4 |
5 | {{rightText}} 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/client/app/core/templates/alert.html: -------------------------------------------------------------------------------- 1 | 4 | 8 | -------------------------------------------------------------------------------- /src/server/fixtures/user.js: -------------------------------------------------------------------------------- 1 | import Users from './../models/users'; 2 | 3 | module.exports = () => { 4 | return Users.findOrCreate({ 5 | where: { 6 | role: 'user', 7 | }, 8 | defaults: { 9 | firstName: 'Jhon', 10 | lastName: 'Doe', 11 | password: 'user', 12 | username: 'user', 13 | email: 'user@user.com', 14 | role: 'user', 15 | emailValidate: 1, 16 | status: 'active', 17 | }, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/server/fixtures/admins.js: -------------------------------------------------------------------------------- 1 | import Users from './../models/users'; 2 | 3 | module.exports = () => { 4 | return Users.findOrCreate({ 5 | where: { 6 | role: 'admin', 7 | }, 8 | defaults: { 9 | firstName: 'Admin', 10 | lastName: 'Admin', 11 | password: 'admin', 12 | username: 'admin', 13 | email: 'admin@admin.com', 14 | role: 'admin', 15 | emailValidate: 1, 16 | status: 'active', 17 | }, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/server/templates/welcome-email/html.hbs: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 |
5 |
6 | Hey {{firstName}} {{lastName}} ({{username}}), welcome to MEAN. 7 |
8 |

9 | To start working with us please click 10 |

11 | Here 12 |
13 | 14 |
15 | 18 | -------------------------------------------------------------------------------- /src/client/styles/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | @gray-darker: lighten(#000, 13.5%); 4 | @gray-dark: lighten(#000, 20%); 5 | @gray: lighten(#000, 33.5%); 6 | @gray-light: lighten(#000, 60%); 7 | @gray-lighter: lighten(#000, 93.5%); 8 | @gray-lightest: lighten(#000, 97.25%); 9 | @brand-primary: #428bca; 10 | @brand-success: #5cb85c; 11 | @brand-info: #5bc0de; 12 | @brand-warning: #f0ad4e; 13 | @brand-danger: #d9534f; 14 | 15 | @color_blue: #1171a3; 16 | @color_important: #fa3031; 17 | -------------------------------------------------------------------------------- /src/server/templates/validate-email/html.hbs: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 |
5 |
6 | Hey {{username}}, welcome to MEAN. 7 |
8 |

9 | Please follow the link below to complete the validation of the password 10 |

11 | Here 12 |
13 | 14 |
15 | 18 | -------------------------------------------------------------------------------- /src/server/templates/recovery-password-email/html.hbs: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 |
5 |
6 | Hey {{username}}, welcome to MEAN. 7 |
8 |

9 | Please follow the link below to complete to recover your password 10 |

11 | Here 12 |
13 | 14 |
15 | 18 | -------------------------------------------------------------------------------- /src/server/config/env/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | database: 'mean_relational_test', 3 | username: '', 4 | password: '', 5 | params: { 6 | dialect: 'sqlite', 7 | storage: 'mean_relational_test.sqlite', 8 | logging: false, 9 | define: { 10 | underscored: true, 11 | }, 12 | }, 13 | jwtSecret: 'Mean-relational-test', 14 | jwtSession: { session: false }, 15 | emailService: 'Gmail', 16 | auth: { 17 | user: '', 18 | pass: '', 19 | }, 20 | verifyEmail: true, 21 | urlBaseClient: 'https://localhost:9000', 22 | urlBaseApi: 'https://localhost:3000', 23 | }; 24 | -------------------------------------------------------------------------------- /src/client/app/core/directives/ng-enter.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | /* 4 | This directive allows us to pass a function in on an enter key to do what we want. 5 | */ 6 | angular 7 | .module('app.core') 8 | .directive('ngEnter', ngEnter); 9 | 10 | function ngEnter() { 11 | return function (scope, element, attrs) { 12 | element.bind('keydown keypress', (event) => { 13 | if (event.which === 13) { 14 | scope.$apply(() => { 15 | scope.$eval(attrs.ngEnter); 16 | }); 17 | event.preventDefault(); 18 | } 19 | }); 20 | }; 21 | } 22 | }()); 23 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import coreRoutes from './core'; 3 | import signupRoutes from './signup'; 4 | import socialRoutes from './social'; 5 | import tasksRoutes from './tasks'; 6 | import tokenRoutes from './token'; 7 | import usersRoutes from './users'; 8 | 9 | const router = express.Router(); // eslint-disable-line new-cap 10 | 11 | // mount all routes at / 12 | router.use('/', coreRoutes); 13 | router.use('/', signupRoutes); 14 | router.use('/', socialRoutes); 15 | router.use('/', tasksRoutes); 16 | router.use('/', tokenRoutes); 17 | router.use('/', usersRoutes); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /src/client/app/layout/shell.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.layout') 6 | .controller('ShellController', ShellController); 7 | 8 | ShellController.$inject = ['$rootScope', '$timeout', 'config', 'logger']; 9 | /* @ngInject */ 10 | function ShellController($rootScope, $timeout, config, logger) { 11 | const vm = this; 12 | vm.busyMessage = 'Please wait ...'; 13 | vm.isBusy = true; 14 | $rootScope.showSplash = false; 15 | vm.navline = { 16 | title: config.appTitle, 17 | }; 18 | 19 | activate(); 20 | 21 | function activate() { 22 | } 23 | } 24 | }()); 25 | -------------------------------------------------------------------------------- /src/server/clusters.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster'; 2 | import os from 'os'; 3 | 4 | const CPUS = os.cpus(); 5 | if (cluster.isMaster) { 6 | CPUS.forEach(() => cluster.fork()); 7 | cluster.on('listening', worker => { 8 | console.log('Cluster %d connected', worker.process.pid); 9 | }); 10 | cluster.on('disconnect', worker => { 11 | console.log('Cluster %d disconnected', worker.process.pid); 12 | }); 13 | cluster.on('exit', worker => { 14 | console.log('Cluster %d is dead', worker.process.pid); 15 | cluster.fork(); 16 | // Ensure to starts a new cluster if an old one dies 17 | }); 18 | } else { 19 | require('./index.js'); 20 | } 21 | -------------------------------------------------------------------------------- /src/client/app/core/directives/compare-to.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .directive('compareTo', compareTo); 7 | 8 | function compareTo() { 9 | return { 10 | require: 'ngModel', 11 | scope: { 12 | otherModelValue: '=compareTo', 13 | }, 14 | link: (scope, element, attributes, ngModel) => { 15 | ngModel.$validators.compareTo = (modelValue) => { 16 | return modelValue === scope.otherModelValue; 17 | }; 18 | 19 | scope.$watch('otherModelValue', () => { 20 | ngModel.$validate(); 21 | }); 22 | }, 23 | }; 24 | } 25 | }()); 26 | -------------------------------------------------------------------------------- /src/client/app/core/filters/boolFilter.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .filter('boolFilter', boolFilter); 7 | 8 | boolFilter.$inject = ['$sce']; 9 | /* @ngInject */ 10 | function boolFilter($sce) { 11 | return function (input) { 12 | let output = $sce.trustAsHtml(''); 14 | if (input === false) { 15 | output = $sce.trustAsHtml(''); 17 | } 18 | return output; 19 | }; 20 | } 21 | }()); 22 | -------------------------------------------------------------------------------- /src/client/app/layout/ht-top-nav.directive.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.layout') 6 | .directive('htTopNav', htTopNav); 7 | 8 | /* @ngInject */ 9 | function htTopNav() { 10 | const directive = { 11 | bindToController: true, 12 | controller: TopNavController, 13 | controllerAs: 'vm', 14 | restrict: 'EA', 15 | scope: { 16 | 'navline': '=', 17 | }, 18 | templateUrl: 'app/layout/ht-top-nav.html', 19 | }; 20 | 21 | TopNavController.$inject = []; 22 | 23 | /* @ngInject */ 24 | function TopNavController() { 25 | } 26 | 27 | return directive; 28 | } 29 | }()); 30 | -------------------------------------------------------------------------------- /src/server/models/tasks.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import db from './../config/db'; 3 | 4 | const Tasks = db.sequelize.define('Tasks', { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | title: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | validate: { 14 | notEmpty: true, 15 | }, 16 | }, 17 | done: { 18 | type: Sequelize.BOOLEAN, 19 | allowNull: false, 20 | defaultValue: false, 21 | }, 22 | }, { 23 | classMethods: { 24 | associate: (models) => { 25 | Tasks.belongsTo(models.Users); 26 | }, 27 | }, 28 | }); 29 | 30 | export default Tasks; 31 | -------------------------------------------------------------------------------- /src/client/app/core/core.route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .run(appRun); 7 | 8 | /* @ngInject */ 9 | function appRun(routerHelper) { 10 | const otherwise = '/404'; 11 | routerHelper.configureStates(getStates(), otherwise); 12 | } 13 | 14 | function getStates() { 15 | return [ 16 | { 17 | state: '404', 18 | config: { 19 | url: '/404', 20 | templateUrl: 'app/core/templates/404.html', 21 | title: '404', 22 | roles: ['guest', 'user', 'admin'], 23 | }, 24 | settings: { 25 | roles: ['guest', 'user', 'admin'], 26 | }, 27 | }, 28 | ]; 29 | } 30 | }()); 31 | -------------------------------------------------------------------------------- /src/client/app/blocks/exception/exception.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('blocks.exception') 6 | .factory('exception', exception); 7 | 8 | /* @ngInject */ 9 | function exception($q, logger) { 10 | const service = { 11 | catcher, 12 | }; 13 | return service; 14 | 15 | function catcher(message) { 16 | return function (e) { 17 | let thrownDescription; 18 | let newMessage; 19 | if (e.data && e.data.msg) { 20 | thrownDescription = '\n' + e.data.msg; 21 | newMessage = message + thrownDescription; 22 | } 23 | e.data.msg = newMessage; 24 | logger.error(newMessage); 25 | return $q.reject(e); 26 | }; 27 | } 28 | } 29 | }()); 30 | -------------------------------------------------------------------------------- /src/client/app/signup/signup_activate.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.signup') 6 | .controller('SignupActivateController', SignupActivateController); 7 | 8 | SignupActivateController.$inject = ['$state', 'logger', 'authentication']; 9 | /* @ngInject */ 10 | function SignupActivateController($state, logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Signup activate'; 13 | vm.user = authentication.getUser(); 14 | vm.activateProfile = activateProfile; 15 | 16 | function activateProfile() { 17 | authentication.activateAccount().then(res => { 18 | logger.success(res.msg); 19 | $state.go(authentication.continueFrom()); 20 | }); 21 | } 22 | } 23 | }()); 24 | -------------------------------------------------------------------------------- /src/client/app/core/services/alert.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .factory('alert', alert); 7 | 8 | alert.$inject = ['$uibModal']; 9 | /* @ngInject */ 10 | function alert($uibModal) { 11 | const service = { 12 | show, 13 | }; 14 | 15 | return service; 16 | 17 | function show(options) { 18 | const modalInstance = $uibModal.open({ 19 | templateUrl: 'app/core/templates/alert.html', 20 | controller: 'AlertController', 21 | controllerAs: 'ac', 22 | animation: true, 23 | resolve: { 24 | options: () => { 25 | return options; 26 | }, 27 | }, 28 | }); 29 | 30 | return modalInstance.result; 31 | } 32 | } 33 | }()); 34 | -------------------------------------------------------------------------------- /src/client/app/home/home.route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.home') 6 | .run(appRun); 7 | 8 | appRun.$inject = ['routerHelper']; 9 | /* @ngInject */ 10 | function appRun(routerHelper) { 11 | routerHelper.configureStates(getStates()); 12 | } 13 | 14 | function getStates() { 15 | return [ 16 | { 17 | state: 'home', 18 | config: { 19 | url: '/', 20 | templateUrl: 'app/home/home.html', 21 | controller: 'HomeController', 22 | controllerAs: 'hc', 23 | title: 'Home', 24 | settings: { 25 | nav: 0, 26 | content: 'Home', 27 | roles: ['user', 'admin'], 28 | }, 29 | }, 30 | }, 31 | ]; 32 | } 33 | }()); 34 | -------------------------------------------------------------------------------- /src/client/app/users/controllers/editUserModalController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.users') 6 | .controller('EditUserModalController', EditUserModalController); 7 | 8 | EditUserModalController.$inject = ['$uibModalInstance', 'usersservice', 'user']; 9 | /* @ngInject */ 10 | function EditUserModalController($uibModalInstance, usersservice, user) { 11 | const vm = this; 12 | vm.roles = ['admin', 'user']; 13 | vm.user = user; 14 | 15 | vm.save = save; 16 | vm.cancel = cancel; 17 | 18 | function save() { 19 | usersservice.editUser(vm.user) 20 | .then(() => { $uibModalInstance.close(); }); 21 | } 22 | 23 | function cancel() { 24 | $uibModalInstance.dismiss('cancel'); 25 | } 26 | } 27 | }()); 28 | -------------------------------------------------------------------------------- /src/client/app/login/login.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.login') 6 | .controller('LoginController', LoginController); 7 | 8 | LoginController.$inject = ['logger', 'authentication']; 9 | /* @ngInject */ 10 | function LoginController(logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Login'; 13 | vm.credentials = { 14 | identification: '', 15 | password: '', 16 | }; 17 | vm.login = login; 18 | 19 | activate(); 20 | 21 | function activate() {} 22 | 23 | function login(form) { 24 | if (form.$valid) { 25 | authentication.login(vm.credentials).then(data => { 26 | logger.success(`Welcome ${data.firstName} ${data.lastName}!`); 27 | }); 28 | } 29 | } 30 | } 31 | }()); 32 | -------------------------------------------------------------------------------- /src/client/app/login/login.route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.login') 6 | .run(appRun); 7 | 8 | appRun.$inject = ['routerHelper']; 9 | /* @ngInject */ 10 | function appRun(routerHelper) { 11 | routerHelper.configureStates(getStates()); 12 | } 13 | 14 | function getStates() { 15 | return [ 16 | { 17 | state: 'login', 18 | config: { 19 | url: '/login', 20 | templateUrl: 'app/login/login.html', 21 | controller: 'LoginController', 22 | controllerAs: 'vm', 23 | title: 'Login', 24 | settings: { 25 | nav: 0, 26 | content: ' Login', 27 | roles: ['guest'], 28 | }, 29 | }, 30 | }, 31 | ]; 32 | } 33 | }()); 34 | -------------------------------------------------------------------------------- /src/client/app/users/controllers/addUserModalController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.users') 6 | .controller('AddUserModalController', AddUserModalController); 7 | 8 | AddUserModalController.$inject = ['$uibModalInstance', 'usersservice']; 9 | /* @ngInject */ 10 | function AddUserModalController($uibModalInstance, usersservice) { 11 | const vm = this; 12 | vm.roles = ['admin', 'user']; 13 | vm.user = { 14 | name: '', 15 | role: 'admin', 16 | }; 17 | vm.save = save; 18 | vm.cancel = cancel; 19 | 20 | function save() { 21 | usersservice.createUser(vm.user) 22 | .then(() => { $uibModalInstance.close(); }); 23 | } 24 | 25 | function cancel() { 26 | $uibModalInstance.dismiss('cancel'); 27 | } 28 | } 29 | }()); 30 | -------------------------------------------------------------------------------- /src/client/app/users/controllers/password_recoveryController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.users') 6 | .controller('PasswordRecoveryController', PasswordRecoveryController); 7 | 8 | PasswordRecoveryController.$inject = ['$state', 'logger', 'authentication']; 9 | /* @ngInject */ 10 | function PasswordRecoveryController($state, logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Password Recovery'; 13 | vm.credentials = { 14 | identification: '', 15 | }; 16 | vm.recover = recover; 17 | 18 | function recover(form) { 19 | if (form.$valid) { 20 | authentication.forgot(vm.credentials).then(data => { 21 | logger.success(data.msg); 22 | $state.go('login'); 23 | }); 24 | } 25 | } 26 | } 27 | }()); 28 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.dashboard') 6 | .run(appRun); 7 | 8 | appRun.$inject = ['routerHelper']; 9 | /* @ngInject */ 10 | function appRun(routerHelper) { 11 | routerHelper.configureStates(getStates()); 12 | } 13 | 14 | function getStates() { 15 | return [ 16 | { 17 | state: 'dashboard', 18 | config: { 19 | url: '/dashboard', 20 | templateUrl: 'app/dashboard/dashboard.html', 21 | controller: 'DashboardController', 22 | controllerAs: 'vm', 23 | title: 'dashboard', 24 | settings: { 25 | nav: 1, 26 | content: 'Dashboard', 27 | roles: ['admin'], 28 | }, 29 | }, 30 | }, 31 | ]; 32 | } 33 | }()); 34 | -------------------------------------------------------------------------------- /src/client/app/home/createTaskModalController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.home') 6 | .controller('createTaskModalController', createTaskModalController); 7 | 8 | createTaskModalController.$inject = ['$uibModalInstance', 'logger', 'taskservice']; 9 | /* @ngInject */ 10 | function createTaskModalController($uibModalInstance, logger, taskservice) { 11 | const vm = this; 12 | vm.createTask = createTask; 13 | vm.cancel = cancel; 14 | vm.task = {}; 15 | activate(); 16 | 17 | function activate() {} 18 | 19 | function createTask() { 20 | return taskservice.createTask(vm.task).then(data => { 21 | $uibModalInstance.close(); 22 | }); 23 | } 24 | 25 | function cancel() { 26 | $uibModalInstance.dismiss('cancel'); 27 | } 28 | } 29 | }()); 30 | -------------------------------------------------------------------------------- /src/server/routes/core.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import acl from './../config/acl'; 3 | import coreService from './../services/core'; 4 | 5 | /** 6 | * Core policy 7 | * ACL configuration 8 | */ 9 | acl.allow([{ 10 | roles: ['guest', 'user'], 11 | allows: [{ 12 | resources: '/api/v1', 13 | permissions: '*', 14 | }], 15 | }]); 16 | 17 | const router = express.Router(); 18 | 19 | /** 20 | * @api {get} / API Status 21 | * @apiGroup Status 22 | * @apiSuccess {String} status API Status' message 23 | * @apiSuccessExample {json} Success 24 | * HTTP/1.1 200 OK 25 | * {"status": "mean API"} 26 | */ 27 | router.get('/api/v1', acl.checkRoles, (req, res) => { 28 | coreService.getIndexMessage() 29 | .then((status) => res.json(status)) 30 | .catch((error) => res.json(error)); 31 | }); 32 | 33 | export default router; 34 | -------------------------------------------------------------------------------- /src/client/app/home/editTaskModalController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.home') 6 | .controller('editTaskModalController', editTaskModalController); 7 | 8 | editTaskModalController.$inject = ['$uibModalInstance', 'logger', 'taskservice', 'task']; 9 | /* @ngInject */ 10 | function editTaskModalController($uibModalInstance, logger, taskservice, task) { 11 | const vm = this; 12 | vm.createTask = createTask; 13 | vm.cancel = cancel; 14 | vm.task = task; 15 | activate(); 16 | 17 | function activate() {} 18 | 19 | function createTask() { 20 | return taskservice.updateTask(vm.task.id, vm.task).then(data => { 21 | $uibModalInstance.close(); 22 | }); 23 | } 24 | 25 | function cancel() { 26 | $uibModalInstance.dismiss('cancel'); 27 | } 28 | } 29 | }()); 30 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.signup') 6 | .controller('SignupController', SignupController); 7 | 8 | SignupController.$inject = ['logger', 'authentication', '$state']; 9 | /* @ngInject */ 10 | function SignupController(logger, authentication, $state) { 11 | const vm = this; 12 | vm.title = 'Login'; 13 | vm.credentials = { 14 | email: '', 15 | username: '', 16 | }; 17 | vm.signup = signup; 18 | 19 | activate(); 20 | 21 | function activate() {} 22 | 23 | function signup(form) { 24 | if (form.$valid) { 25 | authentication.signup(vm.credentials).then(res => { 26 | logger.success(res.msg); 27 | $state.go(authentication.continueFrom()); 28 | }); 29 | } 30 | } 31 | } 32 | }()); 33 | -------------------------------------------------------------------------------- /src/server/config/db.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | 3 | import config from './config'; 4 | 5 | const db = { 6 | sequelize: null, 7 | Sequelize, 8 | }; 9 | 10 | if (!db.sequelize) { 11 | // bluebird promises 12 | Sequelize.Promise.config({ 13 | // Enable warnings 14 | warnings: false, 15 | // Enable long stack traces 16 | longStackTraces: true, 17 | // Enable cancellation 18 | cancellation: true, 19 | // Enable monitoring 20 | monitoring: true, 21 | }); 22 | 23 | if (process.env.DATABASE_URL && process.env.NODE_ENV === 'production') { 24 | db.sequelize = new Sequelize(process.env.DATABASE_URL, config.params); 25 | } else { 26 | db.sequelize = new Sequelize( 27 | config.database, 28 | config.username, 29 | config.password, 30 | config.params 31 | ); 32 | } 33 | } 34 | 35 | module.exports = db; 36 | -------------------------------------------------------------------------------- /src/client/app/home/createTaskModal.html: -------------------------------------------------------------------------------- 1 | 4 | 18 | 22 | -------------------------------------------------------------------------------- /src/client/app/signup/signup_validation_process.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.signup') 6 | .controller('SignupValidationEmailController', SignupValidationEmailController); 7 | 8 | SignupValidationEmailController.$inject = ['$state', '$stateParams', 'logger', 'authentication']; 9 | /* @ngInject */ 10 | function SignupValidationEmailController($state, $stateParams, logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Email validation'; 13 | vm.user = authentication.getUser(); 14 | 15 | activate(); 16 | 17 | function activate() { 18 | if ($stateParams.tokenValidate) { 19 | authentication.validateEmail($stateParams.tokenValidate).then(res => { 20 | logger.success(res.msg); 21 | $state.go(authentication.continueFrom()); 22 | }); 23 | } 24 | } 25 | } 26 | }()); 27 | -------------------------------------------------------------------------------- /src/client/app/signup/signup_validation.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.signup') 6 | .controller('SignupValidationController', SignupValidationController); 7 | 8 | SignupValidationController.$inject = ['$state', 'logger', 'authentication']; 9 | /* @ngInject */ 10 | function SignupValidationController($state, logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Email validation'; 13 | vm.user = authentication.getUser(); 14 | vm.resendValidation = resendValidation; 15 | vm.createAnotherAccount = createAnotherAccount; 16 | 17 | function resendValidation() { 18 | authentication.sendValidationEmail().then(res => { 19 | logger.success(res.msg); 20 | }); 21 | } 22 | 23 | function createAnotherAccount() { 24 | authentication.clearAll(); 25 | $state.go('signup'); 26 | } 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /src/server/config/env/production.js: -------------------------------------------------------------------------------- 1 | import logger from '../logger.js'; 2 | 3 | module.exports = { 4 | database: 'mean_relational', 5 | username: 'facundo', 6 | password: 'Kioscoel24.', 7 | params: { 8 | dialect: 'postgres', 9 | protocol: 'postgres', 10 | logging: (sql) => { 11 | logger.info(`[${new Date()}] ${sql}`); 12 | }, 13 | define: { 14 | underscored: true, 15 | }, 16 | }, 17 | jwtSecret: 'Mean-relational-AP1-prod', 18 | jwtSession: { session: false }, 19 | sessionExpiration: 800, 20 | emailService: 'Gmail', 21 | auth: { 22 | user: '', 23 | pass: '', 24 | }, 25 | mandrillAPIKEY: undefined, 26 | verifyEmail: true, 27 | urlBaseClient: '', 28 | urlBaseApi: '', 29 | FACEBOOK_SECRET: '', 30 | TWITTER_KEY: '', 31 | TWITTER_SECRET: '', 32 | INSTAGRAM_SECRET: '', 33 | GOOGLE_SECRET: '', 34 | PINTEREST_SECRET: '', 35 | PINTEREST_KEY: '', 36 | }; 37 | -------------------------------------------------------------------------------- /src/server/config/acl.js: -------------------------------------------------------------------------------- 1 | import ACL from 'acl'; 2 | 3 | let acl = null; 4 | 5 | if (!acl) { 6 | acl = new ACL(new ACL.memoryBackend()); 7 | } 8 | 9 | acl.checkRoles = (req, res, next) => { 10 | const roles = (req.User) ? [req.User.role] : ['guest']; 11 | // Check for user roles 12 | acl.areAnyRolesAllowed(roles, req.route.path, req.method.toLowerCase(), (err, isAllowed) => { 13 | if (err) { // An authorization error occurred. 14 | res.status(500).json({ msg: 'Unexpected authorization error' }); 15 | } else { 16 | if (isAllowed) { 17 | if (req.Session && req.Session.expired) { 18 | res.status(401).json({ msg: 'User session has expired' }); 19 | } else { 20 | next(); // Access granted! Invoke next middleware 21 | } 22 | } else { 23 | res.status(403).json({ msg: 'User is not authorized' }); 24 | } 25 | } 26 | }); 27 | }; 28 | 29 | module.exports = acl; 30 | -------------------------------------------------------------------------------- /src/client/app/signup/templates/signup_validation_process.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |
12 |

Validating your account

13 |

Hi {{ vm.user.username }}!

14 |

Please wait wile your account email address is validated.

15 |

You will be redirected after validation be completed.

16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/server/config/env/development.js: -------------------------------------------------------------------------------- 1 | import logger from '../logger.js'; 2 | 3 | module.exports = { 4 | database: 'mean_relational', 5 | username: 'root', 6 | password: 'root', 7 | params: { 8 | dialect: 'mysql', 9 | protocol: 'mysql', 10 | logging: (sql) => { 11 | logger.info(`[${new Date()}] ${sql}`); 12 | }, 13 | define: { 14 | underscored: true, 15 | }, 16 | }, 17 | jwtSecret: 'Mean-relational-AP1', 18 | jwtSession: { session: false }, 19 | sessionExpiration: 800, 20 | emailService: 'Gmail', 21 | auth: { 22 | user: '', 23 | pass: '', 24 | }, 25 | mandrillAPIKEY: undefined, 26 | verifyEmail: true, 27 | urlBaseClient: 'http://localhost:3000', 28 | urlBaseApi: 'http://localhost:3000/api/v1', 29 | FACEBOOK_SECRET: '', 30 | TWITTER_KEY: '', 31 | TWITTER_SECRET: '', 32 | INSTAGRAM_SECRET: '', 33 | GOOGLE_SECRET: '', 34 | PINTEREST_SECRET: '', 35 | PINTEREST_KEY: '', 36 | }; 37 | -------------------------------------------------------------------------------- /src/client/app/signup/signup_profile.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.signup') 6 | .controller('SignupProfileController', SignupProfileController); 7 | 8 | SignupProfileController.$inject = ['$state', 'logger', 'authentication']; 9 | /* @ngInject */ 10 | function SignupProfileController($state, logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Signup profile'; 13 | vm.user = authentication.getUser(); 14 | vm.saveProfile = saveProfile; 15 | vm.profile = { 16 | firstName: '', 17 | lastName: '', 18 | password: '', 19 | verifyPassword: '', 20 | }; 21 | 22 | function saveProfile(form) { 23 | if (form.$valid) { 24 | authentication.storeProfile(vm.profile).then(res => { 25 | logger.success(res.msg); 26 | $state.go(authentication.continueFrom()); 27 | }); 28 | } 29 | } 30 | } 31 | }()); 32 | -------------------------------------------------------------------------------- /src/client/app/layout/shell.controller.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0*/ 2 | describe('ShellController', function () { 3 | let controller; 4 | 5 | beforeEach(function () { 6 | bard.appModule('app.layout'); 7 | bard.inject('$controller', '$httpBackend', '$q', '$rootScope', '$timeout'); 8 | }); 9 | 10 | beforeEach(function () { 11 | controller = $controller('ShellController'); 12 | $rootScope.$apply(); 13 | }); 14 | 15 | afterEach(() => { 16 | $httpBackend.verifyNoOutstandingExpectation(false); 17 | $httpBackend.verifyNoOutstandingRequest(); 18 | }); 19 | 20 | describe('Shell controller', function () { 21 | it('should be created successfully', function () { 22 | expect(controller).to.be.defined; 23 | }); 24 | 25 | it('should hide splash screen after timeout', function (done) { 26 | $timeout(function () { 27 | expect($rootScope.showSplash).to.be.false; 28 | done(); 29 | }, 1000); 30 | $timeout.flush(); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/client/test-helpers/mock-data.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0*/ 2 | 3 | const mockData = (function () { 4 | return { 5 | getMockStates, 6 | getMockCountUsers, 7 | getMockDataCountAllTasks, 8 | getMockDataCountDoneTasks, 9 | getMockDataCountNotDoneTasks, 10 | }; 11 | 12 | function getMockStates() { 13 | return [ 14 | { 15 | state: 'home', 16 | config: { 17 | url: '/', 18 | template: '

Home

', 19 | title: 'home', 20 | settings: { 21 | nav: 1, 22 | content: ' Home', 23 | roles: ['guest', 'admin', 'user'], 24 | }, 25 | }, 26 | }, 27 | ]; 28 | } 29 | 30 | function getMockCountUsers() { 31 | return 2; 32 | } 33 | 34 | function getMockDataCountAllTasks() { 35 | return 25; 36 | } 37 | 38 | function getMockDataCountDoneTasks() { 39 | return 17; 40 | } 41 | 42 | function getMockDataCountNotDoneTasks() { 43 | return 8; 44 | } 45 | }()); 46 | -------------------------------------------------------------------------------- /src/client/app/layout/sidebar.controller.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0*/ 2 | 3 | describe('layout', () => { 4 | describe('sidebar', () => { 5 | let controller; 6 | 7 | beforeEach(() => { 8 | module('app.layout', bard.fakeToastr); 9 | bard.inject('$controller', '$httpBackend', '$location', 10 | '$rootScope', '$state', 'routerHelper'); 11 | }); 12 | 13 | beforeEach(() => { 14 | routerHelper.configureStates(mockData.getMockStates(), '/'); 15 | const scope = $rootScope.$new(); 16 | controller = $controller('SidebarController', { 17 | $scope: scope, 18 | }); 19 | $rootScope.$apply(); 20 | }); 21 | 22 | afterEach(() => { 23 | $httpBackend.verifyNoOutstandingExpectation(false); 24 | $httpBackend.verifyNoOutstandingRequest(); 25 | }); 26 | 27 | it('should have isCurrent() for / to return `current`', () => { 28 | $location.path('/'); 29 | $rootScope.$apply(); 30 | expect(controller.isCurrent($state.current)).to.equal('current'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/server/models/sessions.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import jwt from 'jwt-simple'; 3 | import _ from 'lodash'; 4 | import db from './../config/db'; 5 | import config from './../config/config'; 6 | 7 | const toJSON = function () { 8 | const privateAttributes = ['updatedOn', 'user_id', 'expired']; 9 | this.dataValues.authToken = jwt.encode(this.authToken, config.jwtSecret); 10 | return _.omit(this.dataValues, privateAttributes); 11 | }; 12 | 13 | const Sessions = db.sequelize.define('Sessions', { 14 | authToken: { 15 | type: Sequelize.STRING, 16 | primaryKey: true, 17 | }, 18 | expiresOn: { 19 | type: Sequelize.DATE, 20 | allowNull: false, 21 | validate: { 22 | notEmpty: true, 23 | }, 24 | }, 25 | expired: { 26 | type: Sequelize.VIRTUAL, 27 | }, 28 | }, { 29 | createdAt: 'issuedOn', 30 | updatedAt: 'updatedOn', 31 | classMethods: { 32 | associate: models => { 33 | Sessions.belongsTo(models.Users); 34 | }, 35 | }, 36 | instanceMethods: { 37 | toJSON, 38 | }, 39 | }); 40 | 41 | export default Sessions; 42 | -------------------------------------------------------------------------------- /src/client/app/widgets/ht-widget-header.directive.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.widgets') 6 | .directive('htWidgetHeader', htWidgetHeader); 7 | 8 | /* @ngInject */ 9 | function htWidgetHeader() { 10 | // Usage: 11 | //
12 | // Creates: 13 | //
16 | const directive = { 17 | scope: { 18 | 'title': '@', 19 | 'subtitle': '@', 20 | 'rightText': '@', 21 | 'allowCollapse': '@', 22 | }, 23 | templateUrl: 'app/widgets/widget-header.html', 24 | restrict: 'EA', 25 | link, 26 | }; 27 | return directive; 28 | 29 | function link(scope, element, attr) { 30 | scope.toggleContent = function () { 31 | if (scope.allowCollapse === 'true') { 32 | const content = angular.element(element).siblings('.widget-content'); 33 | content.toggle(); 34 | } 35 | }; 36 | } 37 | } 38 | }()); 39 | -------------------------------------------------------------------------------- /src/server/config/auth.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Strategy, ExtractJwt } from 'passport-jwt'; 3 | 4 | import config from './config'; 5 | import sessionsService from './../services/sessions'; 6 | 7 | 8 | const params = { 9 | secretOrKey: config.jwtSecret, 10 | jwtFromRequest: ExtractJwt.fromAuthHeader(), 11 | }; 12 | 13 | const strategy = new Strategy(params, (payload, done) => { 14 | sessionsService.validateSession(payload) 15 | .then(Session => { 16 | if (!Session) { 17 | return done(null, false); 18 | } 19 | 20 | return Session.getUser() 21 | .then(User => { 22 | if (!User) { 23 | return done(null, false); 24 | } 25 | 26 | return done(null, { User, Session }); 27 | }); 28 | }) 29 | .catch(error => { 30 | done(error, null); 31 | }); 32 | }); 33 | 34 | passport.use(strategy); 35 | 36 | export default class Auth { 37 | static initialize() { 38 | return passport.initialize(); 39 | } 40 | 41 | static authenticate(cb) { 42 | return passport.authenticate('jwt', config.jwtSession, cb); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/client/app/layout/ht-top-nav.html: -------------------------------------------------------------------------------- 1 | 2 | 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser" : "babel-eslint", 4 | "extends" : [ 5 | "airbnb/base" 6 | ], 7 | "plugins" : [ 8 | "flow-vars" 9 | ], 10 | "env" : { 11 | "es6": true, 12 | "node": true, 13 | "mocha": true 14 | }, 15 | globals: { 16 | app: true, 17 | expect: true, 18 | request: true, 19 | by: true, 20 | element: true, 21 | browser: true, 22 | jasmine: true, 23 | inject: true, 24 | angular: true, 25 | ApplicationConfiguration: true, 26 | io: true, 27 | $: true, 28 | moment: true, 29 | }, 30 | "rules": { 31 | "prefer-arrow-callback": 1, 32 | "semi" : [2, "always"], 33 | "no-param-reassign": [2, {"props": false}], 34 | "func-names": 0, 35 | "no-unused-expressions": 0, 36 | "no-console": 0, 37 | "one-var": 0, 38 | "new-cap": 0, 39 | "quote-props": 0, 40 | "prefer-template": 0, 41 | "arrow-body-style": 0, 42 | "no-empty-label": 0, 43 | "no-labels": 2, 44 | "no-unused-vars": [2, { "args": "none" }], 45 | "no-use-before-define": [2, { "functions": false, "classes": true }] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/app/core/controllers/alert.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.home') 6 | .controller('AlertController', AlertController); 7 | 8 | AlertController.$inject = ['$uibModalInstance', 'options']; 9 | /* @ngInject */ 10 | function AlertController($uibModalInstance, options) { 11 | const vm = this; 12 | vm.options = { 13 | title: 'Alert', 14 | body: 'Are you sure?', 15 | warning: null, 16 | }; 17 | 18 | vm.accept = accept; 19 | vm.cancel = cancel; 20 | 21 | activate(); 22 | 23 | function activate() { 24 | if (options) { 25 | if (options.title) { 26 | vm.options.title = options.title; 27 | } 28 | 29 | if (options.body) { 30 | vm.options.body = options.body; 31 | } 32 | 33 | if (options.warning) { 34 | vm.options.warning = options.warning; 35 | } 36 | } 37 | } 38 | 39 | function accept() { 40 | $uibModalInstance.close(); 41 | } 42 | 43 | function cancel() { 44 | $uibModalInstance.dismiss('cancel'); 45 | } 46 | } 47 | }()); 48 | -------------------------------------------------------------------------------- /src/client/app/core/templates/404.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
    7 |
  • 8 |
    9 |
    10 | 404Page Not Found 11 |
    12 |
    13 |
  • 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | No soup for you! 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.dashboard') 6 | .controller('DashboardController', DashboardController); 7 | 8 | DashboardController.$inject = ['taskservice', 'usersservice']; 9 | /* @ngInject */ 10 | function DashboardController(taskservice, usersservice) { 11 | const vm = this; 12 | vm.title = 'Dashboard'; 13 | 14 | activate(); 15 | 16 | function activate() { 17 | // Get users count 18 | usersservice.getCount() 19 | .then(count => { 20 | vm.userCount = count; 21 | }); 22 | 23 | // Get all task count 24 | taskservice.getCount() 25 | .then(count => { 26 | vm.taskCount = count; 27 | }); 28 | 29 | 30 | // Get complete tasks count 31 | taskservice.getCountDone() 32 | .then(count => { 33 | vm.taskDoneCount = count; 34 | }); 35 | 36 | // Get not done tasks count 37 | taskservice.getCountNotDone() 38 | .then(count => { 39 | vm.taskNotDoneCount = count; 40 | }); 41 | } 42 | } 43 | }()); 44 | -------------------------------------------------------------------------------- /src/server/config/certs/mean.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8zCCAdugAwIBAgIJAMltM6AKqPQxMA0GCSqGSIb3DQEBBQUAMBAxDjAMBgNV 3 | BAMMBW50YXNrMB4XDTE1MTAwNDE2MzIzNVoXDTI1MTAwMTE2MzIzNVowEDEOMAwG 4 | A1UEAwwFbnRhc2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ6P38 5 | AGoIpHpgOGdEYfh0/Tf5OL0XPFh2PK+wWsCCZzgmL5RUnfV+Gw3wR/rwfHeOJSXy 6 | cSwq7UJe0Rf0EnMULBpp+oeb2iGqbcAke7DTOzaihcp27l+zzNK/BvXq/BtI7Plm 7 | 4mziIuCf3QOeQemkPEiztGZKEJeEE/AFSZmx4J1ugJ0G+fqjkMWTXcYj0RWlqG2y 8 | LBmDFErQANaUeYOrjdSIp96MBFLC8afYnNp/XrItm1XqQ29mUpbwRw2qnR+WdM6S 9 | YZiNcEo/eXLDBnLYiJjSqoiY5hd6CiCO3yMv4IT/LdR1V+FTDg5KecQ7WD5/IKIA 10 | 44844xZzVGkgt3xPAgMBAAGjUDBOMB0GA1UdDgQWBBTAx3g/qsJRyaYpDS6AGTaT 11 | f43bLTAfBgNVHSMEGDAWgBTAx3g/qsJRyaYpDS6AGTaTf43bLTAMBgNVHRMEBTAD 12 | AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQC2Pwv5W/DQ2OFmHsGPVRwGBY0UP0PuTxpX 13 | 4rOe9ZtJDi2QRybA59iEdTT3pe7FX+LPd0DUiJswwYF8+BYDVjJRHiIdcFDC7Jqb 14 | nk3g1JdxzzMdhPpexHkkYzU8Zaxl1wkp3KDlrfqRD2dYSzaToiR8UjkDAwjXcfmA 15 | 489HPKBtZLWydbV/3J9EW/I6zmKgampdIn0qOp6FlCN2dgfarpD8bUxruLgN2wFu 16 | Bb8g3hzQo5ZC7Ws98TIh0B8MFidzMuLjqr1azm147wUr/NfnXS1a4w7/r302sGwa 17 | OuNmGHImnUpAnVpjV482M/DdUy5VYP0M45ImzX+rbJOUffFobx6E 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /src/client/app/blocks/logger/logger.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('blocks.logger') 6 | .factory('logger', logger); 7 | 8 | logger.$inject = ['$log', 'toastr']; 9 | 10 | /* @ngInject */ 11 | function logger($log, toastr) { 12 | const service = { 13 | showToasts: true, 14 | 15 | error, 16 | info, 17 | success, 18 | warning, 19 | 20 | // straight to console; bypass toastr 21 | log: $log.log, 22 | }; 23 | 24 | return service; 25 | // /////////////////// 26 | 27 | function error(message, data, title) { 28 | toastr.error(message, title); 29 | $log.error('Error: ' + message, data); 30 | } 31 | 32 | function info(message, data, title) { 33 | toastr.info(message, title); 34 | $log.info('Info: ' + message, data); 35 | } 36 | 37 | function success(message, data, title) { 38 | toastr.success(message, title); 39 | $log.info('Success: ' + message, data); 40 | } 41 | 42 | function warning(message, data, title) { 43 | toastr.warning(message, title); 44 | $log.warn('Warning: ' + message, data); 45 | } 46 | } 47 | }()); 48 | -------------------------------------------------------------------------------- /src/client/app/core/core.route.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0*/ 2 | describe('core', function () { 3 | describe('state', function () { 4 | const views = { 5 | four0four: 'app/core/templates/404.html', 6 | }; 7 | 8 | beforeEach(function () { 9 | module('app.core', bard.fakeToastr); 10 | bard.inject('$location', '$httpBackend', '$rootScope', '$state', '$templateCache'); 11 | $templateCache.put(views.four0four, ''); 12 | }); 13 | 14 | afterEach(() => { 15 | $httpBackend.verifyNoOutstandingExpectation(false); 16 | $httpBackend.verifyNoOutstandingRequest(); 17 | }); 18 | 19 | it('should map /404 route to 404 View template', function () { 20 | expect($state.get('404').templateUrl).to.equal(views.four0four); 21 | }); 22 | 23 | it('of dashboard should work with $state.go', function () { 24 | $location.path('/404'); 25 | $rootScope.$apply(); 26 | expect($state.is('404')); 27 | }); 28 | 29 | it('should route /invalid to the otherwise (404) route', function () { 30 | $location.path('/invalid'); 31 | $rootScope.$apply(); 32 | expect($state.current.templateUrl).to.equal(views.four0four); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/client/app/users/controllers/password_resetController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.users') 6 | .controller('PasswordResetController', PasswordResetController); 7 | 8 | PasswordResetController.$inject = ['$state', '$stateParams', 'logger', 'authentication']; 9 | /* @ngInject */ 10 | function PasswordResetController($state, $stateParams, logger, authentication) { 11 | const vm = this; 12 | vm.title = 'Password Reset'; 13 | vm.credentials = { 14 | password: '', 15 | verifyPassword: '', 16 | }; 17 | vm.reset = reset; 18 | 19 | activate(); 20 | 21 | function activate() { 22 | if (!$stateParams.tokenId) { 23 | logger.error('Invalid reset token'); 24 | } 25 | authentication.token($stateParams.tokenId) 26 | .then(data => { 27 | logger.success(data.msg); 28 | }) 29 | .catch(err => { 30 | $state.go('login'); 31 | }); 32 | } 33 | 34 | function reset(form) { 35 | if (form.$valid) { 36 | authentication.reset($stateParams.tokenId, vm.credentials).then(data => { 37 | logger.success(data.msg); 38 | $state.go('login'); 39 | }); 40 | } 41 | } 42 | } 43 | }()); 44 | -------------------------------------------------------------------------------- /src/client/app/home/editTaskModal.html: -------------------------------------------------------------------------------- 1 | 4 | 25 |
26 | 30 | -------------------------------------------------------------------------------- /src/client/app/signup/templates/signup_activate.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |

You are all set!

12 |

Thanks {{ vm.user.firstName }} {{ vm.user.lastName }} ({{ vm.user.username }}) for signup with us!.

13 |

Please click in Finish to end your signup process and activate your account.

14 |
15 |
16 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.route.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0*/ 2 | describe('dashboard routes', function () { 3 | describe('state', function () { 4 | const views = { 5 | dashboard: 'app/dashboard/dashboard.html', 6 | }; 7 | 8 | beforeEach(function () { 9 | module('app.dashboard', bard.fakeToastr); 10 | bard.inject( 11 | '$location', '$httpBackend', '$rootScope', '$state', '$templateCache', 'authentication' 12 | ); 13 | $templateCache.put(views.dashboard, ''); 14 | 15 | sinon.stub(authentication, 'getUser').returns({ 16 | id: 2, 17 | name: 'Admin', 18 | role: 'admin', 19 | status: 'active', 20 | }); 21 | sinon.stub(authentication, 'sessionHasExpired').returns(false); 22 | }); 23 | 24 | afterEach(() => { 25 | $httpBackend.verifyNoOutstandingExpectation(false); 26 | $httpBackend.verifyNoOutstandingRequest(); 27 | }); 28 | 29 | it('should map /dashboard route to Dashboard View template', function () { 30 | expect($state.get('dashboard').templateUrl).to.equal(views.dashboard); 31 | }); 32 | 33 | it('of dashboard should work with $state.go', function () { 34 | $location.path('/dashboard'); 35 | $rootScope.$apply(); 36 | expect($state.is('dashboard')); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/client/app/home/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 32 | 33 | 34 |
#TitleStatusCreatedAction
{{task.id}}{{task.title}}{{task.created_at | date: "MM/dd/yyyy 'at' h:mma"}} 29 | 30 | 31 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /src/client/app/users/templates/editUserModal.html: -------------------------------------------------------------------------------- 1 | 4 | 28 | 32 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mean-relational", 3 | "version": "0.0.2", 4 | "description": "mean-relational", 5 | "authors": [], 6 | "license": "MIT", 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "test", 12 | "tests" 13 | ], 14 | "devDependencies": { 15 | "angular-mocks": "^1.5.3", 16 | "sinon": "http://sinonjs.org/releases/sinon-1.12.1.js", 17 | "bardjs": "^0.1.8" 18 | }, 19 | "dependencies": { 20 | "jquery": "^2.2.2", 21 | "angular": "~1.5.3", 22 | "angular-sanitize": "~1.5.3", 23 | "bootstrap": "^3.3.6", 24 | "font-awesome": "^4.5.0", 25 | "moment": "momentjs#^2.15.2", 26 | "angular-ui-router": "^0.2.18", 27 | "toastr": "^2.1.2", 28 | "angular-animate": "^1.5.3", 29 | "angular-bootstrap": "^1.2.5", 30 | "ngstorage": "^0.3.11", 31 | "angular-chart.js": "^1.0.3", 32 | "angular-loading-bar": "^0.9.0", 33 | "angular-wysiwyg": "^1.2.4", 34 | "components-font-awesome": "^4.6.3", 35 | "angular-filepicker": "https://github.com/andreskuver/filepicker-angular.git#1.1.5", 36 | "angular-moment": "^1.0.0" 37 | }, 38 | "resolutions": { 39 | "angular": "1.5.3", 40 | "jquery": "^2.2.2" 41 | }, 42 | "overrides": { 43 | "bootstrap": { 44 | "main": [ 45 | "dist/css/bootstrap.css", 46 | "dist/js/bootstrap.js" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/client/app/signup/templates/signup_validation.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |
12 |

Please validate your account

13 |

An activation email has been sent to your address: {{ vm.user.email }}

14 |

You must click the link in this email before your account will be activated.

15 |

IMPORTANT: If you can't find the activation email in your inbox, be sure to look in your Junk folder and add mean@qactivo.com to your email client's trusted/whitelist. You will be notified when your account is active.

16 |
17 |
18 |

If you would like the email to be resent please click here or create a new account

19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /src/server/config/boot.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import db from './db'; 5 | import app from './express'; 6 | 7 | export default function () { 8 | loadModelRelationships() 9 | .then(syncDb) 10 | .then(runFixtures) 11 | .then(createServer); 12 | 13 | function loadModelRelationships() { 14 | const modelsPath = path.join(__dirname, '../models'); 15 | const models = []; 16 | 17 | fs.readdirSync(modelsPath).forEach(file => { 18 | const modelPath = path.join(modelsPath, file); 19 | const model = require(modelPath).default; 20 | models[model.name] = model; 21 | }); 22 | 23 | Object.keys(models).forEach(key => { 24 | models[key].associate(models); 25 | }); 26 | 27 | return Promise.resolve('ok'); 28 | } 29 | 30 | function syncDb() { 31 | return db.sequelize.sync(); 32 | } 33 | 34 | function runFixtures() { 35 | const fixturesPath = path.join(__dirname, '../fixtures'); 36 | const fixtures = fs.readdirSync(fixturesPath).map(file => { 37 | const fixturePath = path.join(fixturesPath, file); 38 | const fixtureModule = require(fixturePath); 39 | return fixtureModule(); 40 | }); 41 | 42 | return Promise.all(fixtures); 43 | } 44 | 45 | function createServer() { 46 | app.listen(app.get('port'), () => { 47 | const env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; 48 | console.log('Node Environment', env); 49 | console.log(`MEAN API - Port ${app.get('port')}`); 50 | }); 51 | } 52 | 53 | return app; 54 | } 55 | -------------------------------------------------------------------------------- /src/client/app/users/templates/addUserModal.html: -------------------------------------------------------------------------------- 1 | 4 | 32 | 36 | -------------------------------------------------------------------------------- /src/server/services/tokens.js: -------------------------------------------------------------------------------- 1 | import usersService from './../services/users'; 2 | import sessionsService from './../services/sessions'; 3 | 4 | const service = {}; 5 | 6 | /** 7 | * Do user signin, store new session for the logged user 8 | * with Authorization token and expiration date 9 | */ 10 | service.signin = (credentials) => { 11 | if (!credentials || !credentials.identification || !credentials.password) { 12 | return Promise.reject(new Error('Incomplete Credentials')); 13 | } 14 | 15 | const where = { 16 | $or: [{ 17 | username: credentials.identification, 18 | }, { 19 | email: credentials.identification, 20 | }], 21 | }; 22 | 23 | return usersService.findUser(where, true) 24 | .then(User => { 25 | if (!User || !User.isPassword(credentials.password)) { 26 | throw new Error('Invalid Username or Password'); 27 | } 28 | 29 | return sessionsService.removeExpiredSessions(User) 30 | .then(() => { 31 | return sessionsService.createNewSession(User); 32 | }) 33 | .then(Session => { 34 | return { User, Session }; 35 | }); 36 | }); 37 | }; 38 | 39 | /** 40 | * Do user signout, remove current user session from storage 41 | */ 42 | service.signout = (Session) => { 43 | return sessionsService.removeSession(Session) 44 | .then(() => { 45 | return { msg: 'Signout Successfully' }; 46 | }); 47 | }; 48 | 49 | /** 50 | * Finish all user sessions 51 | */ 52 | service.endSessions = (User) => { 53 | return sessionsService.removeAllSessions(User) 54 | .then(() => { 55 | return { msg: 'Sessions closed' }; 56 | }); 57 | }; 58 | 59 | module.exports = service; 60 | -------------------------------------------------------------------------------- /src/client/app/core/config.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const core = angular.module('app.core'); 5 | 6 | core.config(toastrConfig); 7 | 8 | toastrConfig.$inject = ['toastr']; 9 | /* @ngInject */ 10 | function toastrConfig(toastr) { 11 | toastr.options.timeOut = 4000; 12 | toastr.options.positionClass = 'toast-bottom-right'; 13 | } 14 | 15 | core.config(loadingBarConfig); 16 | 17 | loadingBarConfig.$inject = ['cfpLoadingBarProvider']; 18 | /* @ngInject */ 19 | function loadingBarConfig(cfpLoadingBarProvider) { 20 | // cfpLoadingBarProvider.includeSpinner = false; 21 | } 22 | 23 | const config = { 24 | appErrorPrefix: '[Mean Relational Error]', 25 | appTitle: 'Mean Relational', 26 | }; 27 | 28 | core.value('config', config); 29 | 30 | core.config(configure); 31 | 32 | configure.$inject = [ 33 | '$httpProvider', '$logProvider', 'routerHelperProvider', 'exceptionHandlerProvider', 34 | ]; 35 | /* @ngInject */ 36 | function configure($httpProvider, $logProvider, routerHelperProvider, exceptionHandlerProvider) { 37 | if ($logProvider.debugEnabled) { 38 | $logProvider.debugEnabled(true); 39 | } 40 | exceptionHandlerProvider.configure(config.appErrorPrefix); 41 | routerHelperProvider.configure({ docTitle: config.appTitle + ': ' }); 42 | $httpProvider.interceptors.push('RequestInterceptor'); 43 | } 44 | 45 | core.config(filePickerConfig); 46 | 47 | filePickerConfig.$inject = ['filepickerProvider']; 48 | /* @ngInject */ 49 | function filePickerConfig(filepickerProvider) { 50 | filepickerProvider.setKey('AFOad6qXmRXCJ7qZt0tMez'); 51 | } 52 | }()); 53 | -------------------------------------------------------------------------------- /src/server/services/tasks.js: -------------------------------------------------------------------------------- 1 | import Tasks from './../models/tasks'; 2 | 3 | const service = {}; 4 | 5 | service.getAll = (user) => { 6 | return Tasks.findAll({ 7 | where: { user_id: user.id }, 8 | }); 9 | }; 10 | 11 | service.getPaginated = (user, params) => { 12 | const query = {}; 13 | query.where = { user_id: user.id }; 14 | buildPagination(params, query); 15 | return Tasks.findAndCountAll(query); 16 | }; 17 | 18 | service.getCount = (params) => { 19 | const query = {}; 20 | 21 | if (params && params.done) { 22 | query.where = { 23 | done: params.done, 24 | }; 25 | } 26 | 27 | return Tasks.count(query); 28 | }; 29 | 30 | service.create = (task) => { 31 | return Tasks.create(task); 32 | }; 33 | 34 | service.findById = (id, user) => { 35 | const query = { where: { id } }; 36 | 37 | if (user) { 38 | query.where.user_id = user.id; 39 | } 40 | 41 | return Tasks.findOne(query); 42 | }; 43 | 44 | service.update = (id, task, user) => { 45 | const query = { where: { id } }; 46 | 47 | if (user) { 48 | query.where.user_id = user.id; 49 | } 50 | 51 | return Tasks.update(task, query); 52 | }; 53 | 54 | service.destroy = (id, user) => { 55 | const query = { where: { id } }; 56 | 57 | if (user) { 58 | query.where.user_id = user.id; 59 | } 60 | 61 | return Tasks.destroy(query); 62 | }; 63 | 64 | function buildPagination(params, query) { 65 | query.limit = 10; 66 | query.offset = 0; 67 | 68 | if (params.offset) { 69 | query.offset = parseInt(params.offset, 10) * parseInt(params.limit, 10); 70 | } 71 | 72 | if (params.limit) { 73 | query.limit = parseInt(params.limit, 10); 74 | } 75 | } 76 | 77 | module.exports = service; 78 | -------------------------------------------------------------------------------- /src/client/app/layout/sidebar.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.layout') 6 | .controller('SidebarController', SidebarController); 7 | 8 | SidebarController.$inject = ['$scope', '$state', 'routerHelper', 'authentication']; 9 | /* @ngInject */ 10 | function SidebarController($scope, $state, routerHelper, authentication) { 11 | const vm = this; 12 | const states = routerHelper.getStates(); 13 | let role = authentication.getUser() ? authentication.getUser().role : 'guest'; 14 | vm.isCurrent = isCurrent; 15 | vm.logout = authentication.logout; 16 | 17 | activate(); 18 | 19 | $scope.$on('user-login', (user) => { 20 | role = authentication.getUser() ? authentication.getUser().role : 'guest'; 21 | activate(); 22 | }); 23 | 24 | $scope.$on('user-logout', () => { 25 | role = 'guest'; 26 | activate(); 27 | }); 28 | 29 | function activate() { 30 | vm.user = authentication.getUser(); 31 | getNavRoutes(); 32 | } 33 | 34 | function getNavRoutes() { 35 | vm.navRoutes = states.filter(r => { 36 | if (r.settings && !r.settings.roles) { 37 | console.log('Please configure roles', r); 38 | } 39 | return r.settings && r.settings.nav && r.settings.roles.indexOf(role) !== -1; 40 | }).sort((r1, r2) => { 41 | return r1.settings.nav - r2.settings.nav; 42 | }); 43 | } 44 | 45 | function isCurrent(route) { 46 | if (!route.title || !$state.current || !$state.current.title) { 47 | return ''; 48 | } 49 | return $state.current.title.substr(0, route.title.length) === route.title ? 'current' : ''; 50 | } 51 | } 52 | }()); 53 | -------------------------------------------------------------------------------- /src/client/app/users/templates/password_recovery.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |
12 |

Password Recovery

13 |
14 |
15 | 16 |
17 | 18 |
19 |
Please, fill in your email or username.
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/server/config/certs/mean.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyej9/ABqCKR6YDhnRGH4dP03+Ti9FzxYdjyvsFrAgmc4Ji+U 3 | VJ31fhsN8Ef68Hx3jiUl8nEsKu1CXtEX9BJzFCwaafqHm9ohqm3AJHuw0zs2ooXK 4 | du5fs8zSvwb16vwbSOz5ZuJs4iLgn90DnkHppDxIs7RmShCXhBPwBUmZseCdboCd 5 | Bvn6o5DFk13GI9EVpahtsiwZgxRK0ADWlHmDq43UiKfejARSwvGn2Jzaf16yLZtV 6 | 6kNvZlKW8EcNqp0flnTOkmGYjXBKP3lywwZy2IiY0qqImOYXegogjt8jL+CE/y3U 7 | dVfhUw4OSnnEO1g+fyCiAOOPOOMWc1RpILd8TwIDAQABAoIBAHKTuv10Jre8zo0n 8 | tMJDbkDFKSxOHE/BONnv2isTdMcLV/ujaGMUOClVpPVDg41QtG9/eSc5Pb0mYlF4 9 | CkXA6nj6Bgs51haFFDGoki6h2lgj8/8KOTiAUOKxSq6IfqjYY4tgnq7Zsrwo2psd 10 | Sl5WPQWsB/2iU6GYBMM4pS369DLRp5RIu7fvGv4gT9hWxQldNCg0oDadsm185FBL 11 | 0TlKDd2fFnt0Be96yDpwli/Kd2xh3x58/J3AZNrC3dQl8LLPgoo+YvPSzz3Y/Ilj 12 | HImngbQY/c2k6F8wun8H1x0MhBFrN52Q8F0YSLMd3EDR9aaZzGUJfsbSjMsklSIz 13 | Q74mHCECgYEA622fv37f3+V2Uz/H8Tb/r/ym5kHtVxzxszQ84gpzNQjD8IZcfIbu 14 | AldJocHpKuWeN4W5Ttvi5WyH4o77FRz2OBHvmdTmWkrbISH0x7JKq4442PAYamD1 15 | iFCJrLBYVK+6mycBILRqm32eJARrTuBN5DHgvEiXg0DkX9GAxZrhSJcCgYEA242X 16 | FlSNV8OLzjCubJU+Vv0MijX7tMnzmziWCAabxOJUyWEsk4QjoQTL4gQjqNXIDfKt 17 | 162n7mKk7/GJZOG9gC7lZw7AIGwTQ9WyjfvgQIbFo2V9zDF2KGDX5oeW8qPISi52 18 | l0TocSKxy5T+7QQJMH+8Be7OSVvIlEAMu0LqaQkCgYAdPq/ibNNIj8uECd8/cpKO 19 | fPcKkVP3R0wq86lAdwXap60XWslwWp6EQe2On3TkdEOUKBNd3WixEStMFHDSLZfU 20 | XT4DQPQgcT4JPpuWluo5p2AeaqzNwh+eAEsp3XoLgwzOKykzs9WuXQtg8/+Ue76R 21 | QzTkjqvrjQsRcAfsBBJKHwKBgHKhDVZKVPWSkibYQelNTpwKSIbMwptUqYzMUYDl 22 | OmTkKpJt2uE2J4gFQhHCSX/4BhhKMTufXkNXW3gvaqWyOsd3NKzHBcanxrMvGqeI 23 | 7z+hXgT+k1yOInvYfEDPYB9VJdidQ6uc/aM8EwoQw7yp08ZvmpKaaTfh5OqKOlt3 24 | B35JAoGBALZ78bt2PJKdQyxO8WtSBUsD8lrB91qyNJRZhM2fMnAVe9eKAeOXtyD5 25 | DRsSp65CUMzBXN7f0JYutumBh5LSUX1RYzNtwcL4/Rj8mXMssYBE3TQ3Ajc8nAES 26 | W3uk7jU0ZNjRsAbpgaiGEH1uGXH7euC+XMpfDILAptxsam8r+ODA 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/client/app/users/users.route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.users') 6 | .run(appRun); 7 | 8 | appRun.$inject = ['routerHelper']; 9 | /* @ngInject */ 10 | function appRun(routerHelper) { 11 | routerHelper.configureStates(getStates()); 12 | } 13 | 14 | function getStates() { 15 | return [ 16 | { 17 | state: 'users', 18 | config: { 19 | url: '/users', 20 | templateUrl: 'app/users/templates/usersList.html', 21 | controller: 'UserListController', 22 | controllerAs: 'uc', 23 | title: 'Users', 24 | settings: { 25 | nav: 2, 26 | content: 'Users', 27 | roles: ['admin'], 28 | }, 29 | }, 30 | }, 31 | { 32 | state: 'password_recovery', 33 | config: { 34 | url: '/password_recovery', 35 | templateUrl: 'app/users/templates/password_recovery.html', 36 | controller: 'PasswordRecoveryController', 37 | controllerAs: 'prc', 38 | title: 'User password recovery', 39 | settings: { 40 | nav: 2, 41 | content: 'Users', 42 | roles: ['guest'], 43 | }, 44 | }, 45 | }, 46 | { 47 | state: 'password_reset', 48 | config: { 49 | url: '/password_reset/:tokenId', 50 | templateUrl: 'app/users/templates/password_reset.html', 51 | controller: 'PasswordResetController', 52 | controllerAs: 'prc', 53 | title: 'User password recovery', 54 | settings: { 55 | nav: 2, 56 | content: 'Users', 57 | roles: ['guest'], 58 | }, 59 | }, 60 | }, 61 | ]; 62 | } 63 | }()); 64 | -------------------------------------------------------------------------------- /src/server/routes/token.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jwt-simple'; 3 | import errors from './../error'; 4 | import acl from './../config/acl'; 5 | import tokenService from './../services/tokens'; 6 | import config from './../config/config'; 7 | 8 | /** 9 | * Token policy 10 | * ACL configuration 11 | */ 12 | acl.allow([{ 13 | roles: ['guest'], 14 | allows: [{ 15 | resources: '/api/v1/signin', 16 | permissions: 'post', 17 | }], 18 | }, { 19 | roles: ['admin', 'user'], 20 | allows: [{ 21 | resources: '/api/v1/signout', 22 | permissions: 'post', 23 | }, { 24 | resources: '/api/v1/signout/all', 25 | permissions: 'post', 26 | }], 27 | }]); 28 | 29 | const router = express.Router(); 30 | 31 | router.post('/api/v1/signin', acl.checkRoles, (req, res) => { 32 | tokenService.signin(req.body) 33 | .then(response => { 34 | res.setHeader('Authorization', jwt.encode(response.Session.authToken, config.jwtSecret)); 35 | res.setHeader('AuthExpiration', response.Session.expiresOn); 36 | res.json(response); 37 | }) 38 | .catch(error => res.status(412).json(errors.get(error))); 39 | }); 40 | 41 | router.post('/api/v1/signout', acl.checkRoles, (req, res) => { 42 | tokenService.signout(req.Session) 43 | .then(response => { 44 | res.removeHeader('Authorization'); 45 | res.removeHeader('AuthExpiration'); 46 | res.json(response); 47 | }) 48 | .catch(error => res.status(412).json(errors.get(error))); 49 | }); 50 | 51 | router.post('/api/v1/signout/all', acl.checkRoles, (req, res) => { 52 | tokenService.endSessions(req.User) 53 | .then(response => { 54 | res.removeHeader('Authorization'); 55 | res.removeHeader('AuthExpiration'); 56 | res.json(response); 57 | }) 58 | .catch(error => res.status(412).json(errors.get(error))); 59 | }); 60 | 61 | export default router; 62 | -------------------------------------------------------------------------------- /src/server/templates/_common.scss: -------------------------------------------------------------------------------- 1 | @import 'https://fonts.googleapis.com/css?family=Abel'; 2 | @import 'https://fonts.googleapis.com/css?family=Montserrat+Alternates'; 3 | 4 | .message { 5 | color: #7e7e7e; 6 | font-family: 'Abel', sans-serif; 7 | font-size: 14px; 8 | letter-spacing: 2px; 9 | padding-top: 25px; 10 | } 11 | .header { 12 | text-align: center; 13 | padding: 25px; 14 | background-color: #fff; 15 | border-bottom: 1px solid rgba(230, 230, 230, 0.7); 16 | } 17 | .header img { 18 | width: 250px; 19 | } 20 | .subject { 21 | background-color: #f0f0f0; 22 | text-align: center; 23 | font-family: 'Montserrat'; 24 | -webkit-border-radius: 0px; 25 | -moz-border-radius: 0px; 26 | border-radius: 0px; 27 | box-shadow: none; 28 | padding: 15px 0; 29 | margin: 0; 30 | border: none; 31 | text-transform: uppercase; 32 | display: block; 33 | color: #626262; 34 | font-size: 14px; 35 | font-weight: normal; 36 | letter-spacing: 0.01em; 37 | -webkit-font-smoothing: antialiased; 38 | } 39 | .content { 40 | text-align: center; 41 | color: #2c2c2c; 42 | } 43 | .footer { 44 | background-color: #f0f0f0; 45 | text-align: center; 46 | font-family: 'Montserrat'; 47 | -webkit-border-radius: 0px; 48 | -moz-border-radius: 0px; 49 | border-radius: 0px; 50 | box-shadow: none; 51 | padding: 40px 0; 52 | border: none; 53 | margin-top: 30px; 54 | display: block; 55 | color: #626262; 56 | font-size: 16px; 57 | -webkit-font-smoothing: antialiased; 58 | } 59 | .message-content p { 60 | display: block; 61 | font-size: 14px; 62 | font-weight: normal; 63 | letter-spacing: 0.01em; 64 | line-height: 22px; 65 | margin: 0px 0px 10px 0px; 66 | font-style: normal; 67 | white-space: normal; 68 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | const gulpConfig = require('./gulp.config')(); 3 | 4 | config.set({ 5 | // base path that will be used to resolve all patterns (eg. files, exclude) 6 | basePath: './', 7 | 8 | // frameworks to use 9 | // some available frameworks: https://npmjs.org/browse/keyword/karma-adapter 10 | frameworks: ['mocha', 'chai', 'sinon', 'chai-sinon'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: gulpConfig.karma.files, 14 | 15 | // list of files to exclude 16 | exclude: gulpConfig.karma.exclude, 17 | 18 | proxies: { 19 | '/': 'http://localhost:8888/', 20 | }, 21 | 22 | // preprocess matching files before serving them to the browser 23 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 24 | preprocessors: gulpConfig.karma.preprocessors, 25 | 26 | // test results reporter to use 27 | // possible values: 'dots', 'progress', 'coverage' 28 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 29 | // reporters: ['progress'], 30 | 31 | // web server port 32 | port: 9876, 33 | 34 | // enable / disable colors in the output (reporters and logs) 35 | colors: true, 36 | 37 | // level of logging 38 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 39 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 40 | logLevel: config.LOG_INFO, 41 | 42 | // enable / disable watching file and executing tests whenever any file changes 43 | autoWatch: true, 44 | 45 | // start these browsers 46 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 47 | // browsers: ['Chrome', 'ChromeCanary', 'FirefoxAurora', 'Safari', 'PhantomJS'], 48 | browsers: ['PhantomJS'], 49 | 50 | // Continuous Integration mode 51 | // if true, Karma captures browsers, runs the tests and exits 52 | singleRun: false, 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.controller.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0*/ 2 | describe('DashboardController', function () { 3 | let controller; 4 | const userCount = mockData.getMockCountUsers(); 5 | const taskCount = mockData.getMockDataCountAllTasks(); 6 | const taskDoneCount = mockData.getMockDataCountDoneTasks(); 7 | const taskNotDoneCount = mockData.getMockDataCountNotDoneTasks(); 8 | 9 | beforeEach(function () { 10 | bard.appModule('app.dashboard'); 11 | bard.inject('$controller', '$httpBackend', '$q', '$rootScope', 'usersservice', 12 | 'taskservice', 'authentication'); 13 | }); 14 | 15 | beforeEach(function () { 16 | sinon.stub(usersservice, 'getCount').returns($q.when(userCount)); 17 | sinon.stub(taskservice, 'getCount').returns($q.when(taskCount)); 18 | sinon.stub(taskservice, 'getCountDone').returns($q.when(taskDoneCount)); 19 | sinon.stub(taskservice, 'getCountNotDone').returns($q.when(taskNotDoneCount)); 20 | controller = $controller('DashboardController'); 21 | $rootScope.$apply(); 22 | }); 23 | 24 | afterEach(() => { 25 | $httpBackend.verifyNoOutstandingExpectation(false); 26 | $httpBackend.verifyNoOutstandingRequest(); 27 | }); 28 | 29 | describe('Dashboard controller', function () { 30 | it('should be created successfully', function () { 31 | expect(controller).to.be.defined; 32 | }); 33 | 34 | describe('after activate', function () { 35 | it('should have title of Dashboard', function () { 36 | expect(controller.title).to.equal('Dashboard'); 37 | }); 38 | 39 | it('should be equal to 2 the count of users', function () { 40 | expect(controller.userCount).to.equal(2); 41 | }); 42 | 43 | it('should be equal to 17 the count of done tasks', function () { 44 | expect(controller.taskDoneCount).to.equal(17); 45 | }); 46 | 47 | it('should be equal to 8 the count of not done tasks', function () { 48 | expect(controller.taskNotDoneCount).to.equal(8); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/server/services/sessions.js: -------------------------------------------------------------------------------- 1 | import uuid from 'node-uuid'; 2 | import Sequelize from 'sequelize'; 3 | 4 | import config from './../config/config'; 5 | import Sessions from './../models/sessions'; 6 | 7 | const service = {}; 8 | 9 | /** 10 | * Find one session by his Authorization token 11 | * And check session expiration date 12 | */ 13 | service.findSession = (authToken) => { 14 | const date = new Date().toISOString(); 15 | return Sessions.findOne({ 16 | where: { authToken }, 17 | attributes: [ 18 | 'user_id', 'authToken', 'expiresOn', 'issuedOn', 'updatedOn', 19 | Sequelize.literal(`expiresOn <= '${date}' AS expired`), 20 | ], 21 | }); 22 | }; 23 | 24 | /** 25 | * Find one session by his Authorization token 26 | * And updates expiration date 27 | */ 28 | service.validateSession = (authToken) => { 29 | return service.findSession(authToken) 30 | .then(Session => { 31 | if (Session && !Session.expired) { 32 | const expiresOn = new Date(); 33 | expiresOn.setSeconds(expiresOn.getSeconds() + config.sessionExpiration); 34 | Session.expiresOn = expiresOn; 35 | 36 | return Session.save(); 37 | } 38 | return Session; 39 | }); 40 | }; 41 | 42 | /** 43 | * Remove a user session 44 | */ 45 | service.removeSession = (Session) => { 46 | return Session.destroy(); 47 | }; 48 | 49 | /** 50 | * Close all sessions from an user 51 | */ 52 | service.removeAllSessions = (User) => { 53 | return Sessions.destroy({ 54 | where: { user_id: User.id }, 55 | }); 56 | }; 57 | 58 | /** 59 | * Remove expired sessions from an user 60 | */ 61 | service.removeExpiredSessions = (User) => { 62 | return Sessions.destroy({ 63 | where: { 64 | user_id: User.id, 65 | expiresOn: { $lt: new Date() }, 66 | }, 67 | }); 68 | }; 69 | 70 | /** 71 | * Create a new session for a user 72 | */ 73 | service.createNewSession = (User) => { 74 | const authToken = uuid.v1({ id: User.id }); 75 | const issuedOn = new Date(); 76 | const updatedOn = new Date(issuedOn.getTime()); 77 | const expiresOn = new Date(issuedOn.getTime()); 78 | expiresOn.setSeconds(expiresOn.getSeconds() + config.sessionExpiration); 79 | 80 | return User.createSession({ authToken, issuedOn, updatedOn, expiresOn }); 81 | }; 82 | 83 | module.exports = service; 84 | -------------------------------------------------------------------------------- /src/client/app/blocks/exception/exception-handler.provider.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0*/ 2 | describe('blocks.exception', function () { 3 | let exceptionHandlerProvider; 4 | const mocks = { 5 | errorMessage: 'fake error', 6 | prefix: '[TEST]: ', 7 | }; 8 | 9 | beforeEach(function () { 10 | bard.appModule('blocks.exception', function (_exceptionHandlerProvider_) { 11 | exceptionHandlerProvider = _exceptionHandlerProvider_; 12 | }); 13 | bard.inject('$rootScope'); 14 | }); 15 | 16 | bard.verifyNoOutstandingHttpRequests(); 17 | 18 | describe('exceptionHandlerProvider', function () { 19 | it('should have a dummy test', inject(function () { 20 | expect(true).to.equal(true); 21 | })); 22 | 23 | it('should have exceptionHandlerProvider defined', inject(function () { 24 | expect(exceptionHandlerProvider).to.be.defined; 25 | })); 26 | 27 | it('should have configuration', inject(function () { 28 | expect(exceptionHandlerProvider.config).to.be.defined; 29 | })); 30 | 31 | it('should have configuration', inject(function () { 32 | expect(exceptionHandlerProvider.configure).to.be.defined; 33 | })); 34 | 35 | describe('with appErrorPrefix', function () { 36 | beforeEach(function () { 37 | exceptionHandlerProvider.configure(mocks.prefix); 38 | }); 39 | 40 | it('should have appErrorPrefix defined', inject(function () { 41 | expect(exceptionHandlerProvider.$get().config.appErrorPrefix).to.be.defined; 42 | })); 43 | 44 | it('should have appErrorPrefix set properly', inject(function () { 45 | expect(exceptionHandlerProvider.$get().config.appErrorPrefix) 46 | .to.equal(mocks.prefix); 47 | })); 48 | 49 | it('should throw an error when forced', inject(function () { 50 | expect(functionThatWillThrow).to.throw(); 51 | })); 52 | 53 | it('manual error is handled by decorator', function () { 54 | exceptionHandlerProvider.configure(mocks.prefix); 55 | try { 56 | $rootScope.$apply(functionThatWillThrow); 57 | } catch (ex) { 58 | expect(ex.message).to.equal(mocks.prefix + mocks.errorMessage); 59 | } 60 | }); 61 | }); 62 | }); 63 | 64 | function functionThatWillThrow() { 65 | throw new Error(mocks.errorMessage); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /src/client/app/signup/templates/signup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
Please, write your username.
18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 |
Please, fill in your email.
27 |
Please, fill with valid email address.
28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/client/app/blocks/exception/exception-handler.provider.js: -------------------------------------------------------------------------------- 1 | // Include in index.html so that app level exceptions are handled. 2 | // Exclude from testRunner.html which should run exactly what it wants to run 3 | (function () { 4 | 'use strict'; 5 | 6 | angular 7 | .module('blocks.exception') 8 | .provider('exceptionHandler', exceptionHandlerProvider) 9 | .config(config); 10 | 11 | /** 12 | * Must configure the exception handling 13 | */ 14 | function exceptionHandlerProvider() { 15 | /* jshint validthis:true */ 16 | this.config = { 17 | appErrorPrefix: undefined, 18 | }; 19 | 20 | this.configure = function (appErrorPrefix) { 21 | this.config.appErrorPrefix = appErrorPrefix; 22 | }; 23 | 24 | this.$get = function () { 25 | return { config: this.config }; 26 | }; 27 | } 28 | 29 | config.$inject = ['$provide']; 30 | 31 | /** 32 | * Configure by setting an optional string value for appErrorPrefix. 33 | * Accessible via config.appErrorPrefix (via config value). 34 | * @param {Object} $provide 35 | */ 36 | /* @ngInject */ 37 | function config($provide) { 38 | $provide.decorator('$exceptionHandler', extendExceptionHandler); 39 | } 40 | 41 | extendExceptionHandler.$inject = ['$delegate', 'exceptionHandler', 'logger']; 42 | 43 | /** 44 | * Extend the $exceptionHandler service to also display a toast. 45 | * @param {Object} $delegate 46 | * @param {Object} exceptionHandler 47 | * @param {Object} logger 48 | * @return {Function} the decorated $exceptionHandler service 49 | */ 50 | function extendExceptionHandler($delegate, exceptionHandler, logger) { 51 | return function (exception, cause) { 52 | const appErrorPrefix = exceptionHandler.config.appErrorPrefix || ''; 53 | const errorData = { exception, cause }; 54 | exception.message = appErrorPrefix + exception.message; 55 | $delegate(exception, cause); 56 | /** 57 | * Could add the error to a service's collection, 58 | * add errors to $rootScope, log errors to remote web server, 59 | * or log locally. Or throw hard. It is entirely up to you. 60 | * throw exception; 61 | * 62 | * @example 63 | * throw { message: 'error message we added' }; 64 | */ 65 | logger.error(exception.message, errorData); 66 | }; 67 | } 68 | }()); 69 | -------------------------------------------------------------------------------- /src/client/app/core/interceptors/request.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .factory('RequestInterceptor', RequestInterceptor); 7 | 8 | 9 | RequestInterceptor.$inject = ['$q', '$injector', '$rootScope', '$localStorage', '$timeout']; 10 | /* @ngInject */ 11 | function RequestInterceptor($q, $injector, $rootScope, $localStorage, $timeout) { 12 | return { 13 | response: (response) => { 14 | const exp = response.headers().AuthExpiration; 15 | if (exp && $localStorage.session) { 16 | $localStorage.session.expiresOn = exp; // In Universal Time 17 | } 18 | return response; 19 | }, 20 | responseError: (rejection) => { 21 | if (!rejection.config.ignoreAuthModule) { 22 | switch (rejection.status) { 23 | case 401: // User session has expired (Unauthorized) 24 | // Clear Authorization token 25 | $injector.get('authentication').clearAll(); 26 | 27 | // Go to login 28 | $timeout(() => { $injector.get('$state').transitionTo('login'); }, 0); 29 | break; 30 | case 403: // User not authorized for this request (Forbidden) 31 | break; 32 | case 503: // Nothing special, no connection to internet 33 | break; 34 | default: 35 | break; 36 | } 37 | } 38 | // otherwise, default behaviour 39 | return $q.reject(rejection); 40 | }, 41 | request: ($config) => { 42 | // Check Authorization Token 43 | if ($localStorage.session) { 44 | // Only for api requests 45 | if ($config.url.includes('/api/v1/')) { 46 | // Set Authorization token 47 | $config.headers.Authorization = 'JWT ' + $localStorage.session.authToken; 48 | 49 | // Check Authorization Expiration 50 | if ($injector.get('authentication').sessionHasExpired()) { 51 | $config.status = 401; // Not Authorized session expired 52 | $config.config = { ignoreAuthModule: false }; 53 | $config.data = { msg: 'Your Session has expired' }; 54 | return $q.reject($config); 55 | } 56 | } 57 | } 58 | 59 | return $config; 60 | }, 61 | }; 62 | } 63 | }()); 64 | -------------------------------------------------------------------------------- /src/server/config/express.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import express from 'express'; 3 | import jwt from 'jwt-simple'; 4 | import morgan from 'morgan'; 5 | import cors from 'cors'; 6 | import helmet from 'helmet'; 7 | 8 | import config from './../config/config'; 9 | import routes from './../routes'; 10 | import logger from './logger'; 11 | import Auth from './auth'; 12 | 13 | const app = express(); 14 | const environment = process.env.NODE_ENV; 15 | const port = process.env.PORT; 16 | 17 | app.set('port', port || 3000); 18 | app.set('json spaces', 4); 19 | app.use(morgan('common', { 20 | stream: { 21 | write: (message) => { 22 | logger.info(message); 23 | }, 24 | }, 25 | })); 26 | app.use(helmet()); 27 | app.use(cors({})); 28 | app.use(bodyParser.json()); 29 | 30 | app.use(Auth.initialize()); 31 | 32 | app.use((req, res, next) => { 33 | Auth.authenticate((authErr, data, info) => { 34 | if (data && data.User && data.Session) { 35 | req.User = data.User; 36 | req.Session = data.Session; 37 | 38 | res.setHeader('Authorization', jwt.encode(data.Session.authToken, config.jwtSecret)); 39 | res.setHeader('AuthExpiration', data.Session.expiresOn); 40 | } 41 | next(authErr); 42 | })(req, res, next); 43 | }); 44 | 45 | app.use('/', routes); 46 | 47 | function send404(req, res, description) { 48 | const data = { 49 | status: 404, 50 | message: 'Not Found', 51 | description, 52 | url: req.url, 53 | }; 54 | res.status(404) 55 | .send(data) 56 | .end(); 57 | } 58 | 59 | switch (environment) { 60 | case 'production': 61 | app.use(express.static('./build/')); 62 | // Any invalid calls for templateUrls are under app/* and should return 404 63 | app.use('/app/*', (req, res, next) => { 64 | send404(req, res); 65 | }); 66 | // Any deep link calls should return index.html 67 | app.use('/*', express.static('./build/index.html')); 68 | break; 69 | default: 70 | app.use(express.static('./src/client/')); 71 | app.use(express.static('./')); 72 | app.use(express.static('./tmp')); 73 | // Any invalid calls for templateUrls are under app/* and should return 404 74 | app.use('/app/*', (req, res, next) => { 75 | send404(req, res); 76 | }); 77 | // Any deep link calls should return index.html 78 | app.use('/*', express.static('./src/client/index.html')); 79 | break; 80 | } 81 | 82 | export default app; 83 | -------------------------------------------------------------------------------- /src/client/app/core/services/users.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .factory('usersservice', usersservice); 7 | 8 | usersservice.$inject = ['$http', '$q', 'exception', 'logger']; 9 | /* @ngInject */ 10 | function usersservice($http, $q, exception, logger) { 11 | const service = { 12 | getUsers, 13 | createUser, 14 | editUser, 15 | removeUser, 16 | getCount, 17 | }; 18 | 19 | return service; 20 | 21 | function getUsers(params) { 22 | return $http.get('/api/v1/users', { params }) 23 | .then(success) 24 | .catch(fail); 25 | 26 | function success(response) { 27 | return response.data; 28 | } 29 | 30 | function fail(e) { 31 | return exception.catcher('XHR Failed for getUsers')(e); 32 | } 33 | } 34 | 35 | function createUser(user) { 36 | return $http.post('/api/v1/users', user) 37 | .then(success) 38 | .catch(fail); 39 | 40 | function success(response) { 41 | return response.data; 42 | } 43 | 44 | function fail(e) { 45 | return exception.catcher('XHR Failed for createUser')(e); 46 | } 47 | } 48 | 49 | function editUser(user) { 50 | return $http.put('/api/v1/users/' + user.id, user) 51 | .then(success) 52 | .catch(fail); 53 | 54 | function success(response) { 55 | return response.data; 56 | } 57 | 58 | function fail(e) { 59 | return exception.catcher('XHR Failed for editUser')(e); 60 | } 61 | } 62 | 63 | function removeUser(id) { 64 | return $http.delete('/api/v1/users/' + id) 65 | .then(success) 66 | .catch(fail); 67 | 68 | function success(response) { 69 | return response.data; 70 | } 71 | 72 | function fail(e) { 73 | return exception.catcher('XHR Failed for remove')(e); 74 | } 75 | } 76 | 77 | function getCount(query) { 78 | const params = {}; 79 | if (query) { 80 | params.params = query; 81 | } 82 | 83 | return $http.get('/api/v1/users/count', params) 84 | .then(success) 85 | .catch(fail); 86 | 87 | function success(response) { 88 | return response.data; 89 | } 90 | 91 | function fail(e) { 92 | return exception.catcher('XHR Failed for getCount')(e); 93 | } 94 | } 95 | } 96 | }()); 97 | -------------------------------------------------------------------------------- /src/client/app/home/homeController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.home') 6 | .controller('HomeController', HomeController); 7 | 8 | HomeController.$inject = ['$uibModal', 'logger', 'authentication', 'taskservice']; 9 | /* @ngInject */ 10 | function HomeController($uibModal, logger, authentication, taskservice) { 11 | const vm = this; 12 | vm.user = authentication.user; 13 | vm.title = 'Home'; 14 | vm.tasks = []; 15 | vm.showModal = showModal; 16 | vm.showEditTaskModal = showEditTaskModal; 17 | vm.removeTask = removeTask; 18 | 19 | activate(); 20 | 21 | function activate() { 22 | loadTasks(); 23 | } 24 | 25 | function showModal() { 26 | const modalInstance = $uibModal.open({ 27 | templateUrl: 'app/home/createTaskModal.html', 28 | controller: 'createTaskModalController', 29 | controllerAs: 'ctmc', 30 | resolve: { 31 | taskservice: () => { 32 | return taskservice; 33 | }, 34 | logger: () => { 35 | return logger; 36 | }, 37 | }, 38 | }); 39 | 40 | modalInstance.result 41 | .then(success); 42 | 43 | function success() { 44 | logger.success('Task created'); 45 | loadTasks(); 46 | } 47 | } 48 | 49 | function showEditTaskModal(task) { 50 | const modalInstance = $uibModal.open({ 51 | templateUrl: 'app/home/editTaskModal.html', 52 | controller: 'editTaskModalController', 53 | controllerAs: 'etmc', 54 | resolve: { 55 | taskservice: () => { 56 | return taskservice; 57 | }, 58 | logger: () => { 59 | return logger; 60 | }, 61 | task: () => { 62 | return task; 63 | }, 64 | }, 65 | }); 66 | 67 | modalInstance.result 68 | .then(success); 69 | 70 | function success() { 71 | logger.success('Task edited'); 72 | loadTasks(); 73 | } 74 | } 75 | 76 | function loadTasks() { 77 | taskservice.getTasks() 78 | .then((tasks) => { 79 | vm.tasks = tasks; 80 | }); 81 | } 82 | 83 | function removeTask(taskId) { 84 | taskservice.removeTask(taskId) 85 | .then(function success() { 86 | logger.success('Task deleted'); 87 | loadTasks(); 88 | }); 89 | } 90 | } 91 | }()); 92 | -------------------------------------------------------------------------------- /src/server/services/email.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import MandrillTransport from 'nodemailer-mandrill-transport'; 3 | import EmailTemplates from 'email-templates'; 4 | import Nodemailer from 'nodemailer'; 5 | import config from './../config/config'; 6 | 7 | const service = {}; 8 | const templatesDir = path.resolve(__dirname, '..', 'templates'); 9 | const transporter = config.mandrillAPIKEY ? 10 | Nodemailer.createTransport(MandrillTransport({ auth: { apiKey: config.mandrillAPIKEY } })) : 11 | Nodemailer.createTransport({ service: config.emailService, auth: config.auth }); 12 | 13 | function sendEmail(mailOptions) { 14 | return new Promise((resolve, reject) => { 15 | if (mailOptions.notification && !config.sendEmailNotifications) { 16 | resolve('ok'); 17 | return; 18 | } 19 | transporter.sendMail(mailOptions, (error, info) => { 20 | if (error) { 21 | reject(error); 22 | } else { 23 | resolve(info); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | function sendWithTemplate(templateData, User, subjectEmail, templateName, notification) { 30 | const template = new EmailTemplates.EmailTemplate(path.join(templatesDir, templateName)); 31 | return template.render(templateData) 32 | .then(results => { 33 | return sendEmail({ 34 | from: 'mean@qactivo.com', 35 | to: User.email, 36 | subject: subjectEmail, 37 | html: results.html, 38 | text: results.text, 39 | notification, 40 | }); 41 | }) 42 | .catch(err => { 43 | console.log(err); 44 | }); 45 | } 46 | 47 | service.sendValidateEmail = (User) => { 48 | const data = { 49 | url: `${config.urlBaseClient}/signup/validation/${User.tokenValidate}`, 50 | username: User.username, 51 | }; 52 | return sendWithTemplate(data, User, 'Validate your Email address', 'validate-email'); 53 | }; 54 | 55 | service.sendRecoveryEmail = (User) => { 56 | const data = { 57 | url: `${config.urlBaseClient}/password_reset/${User.tokenPassRecovery}`, 58 | username: User.username, 59 | }; 60 | return sendWithTemplate(data, User, 'Recovery Password', 'recovery-password-email'); 61 | }; 62 | 63 | service.sendWelcomeEmail = (User) => { 64 | const data = { 65 | url: `${config.urlBaseClient}/login`, 66 | firstName: User.firstName, 67 | lastName: User.lastName, 68 | username: User.username, 69 | }; 70 | return sendWithTemplate(data, User, 'Welcome!', 'welcome-email'); 71 | }; 72 | 73 | module.exports = service; 74 | -------------------------------------------------------------------------------- /src/client/app/login/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
Please, fill in your email or username.
18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 |
Please, fill in the password.
27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-angular-starter 2 | [![Build Status](https://travis-ci.org/toptive/express-angular-starter.svg)](https://travis-ci.org/toptive/express-angular-starter) 3 | [![Coverage Status](https://coveralls.io/repos/github/toptive/express-angular-starter/badge.svg)](https://coveralls.io/github/toptive/express-angular-starter) 4 | [![dependencies Status](https://david-dm.org/toptive/express-angular-starter/status.svg)](https://david-dm.org/toptive/express-angular-starter) 5 | 6 | ## Stack 7 | * [Express](https://www.npmjs.com/package/express) 8 | * Angular 1.5 ([John Papa Styleguide](https://github.com/johnpapa/angular-styleguide)) 9 | * [Gulp](https://gulpjs.com/) 10 | * [Babel](https://babeljs.io/) 11 | * [Sequalize](http://docs.sequelizejs.com/) 12 | * ACL 13 | * [Mocha](https://mochajs.org/) 14 | * [Karma](https://karma-runner.github.io/2.0/index.html) 15 | 16 | # Get Started 17 | 18 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 19 | 20 | ## Prerequisites 21 | Make sure you have installed all of the following prerequisites on your development machine: 22 | * Git - [Download & Install Git](https://git-scm.com/downloads). OSX and Linux machines typically have this already installed. 23 | * Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. If you encounter any problems, you can also use this [GitHub Gist](https://gist.github.com/isaacs/579814) to install Node.js. 24 | * [Postgres](https://www.postgresql.org/) 25 | * Yarn [Download & Install Yarn](https://yarnpkg.com/en/docs/install) 26 | * [Gulp](https://gulpjs.com/) 27 | * [Bower](https://bower.io/) 28 | 29 | ## Database 30 | 31 | ### Create postgres user 32 | ```bash 33 | createuser --pwprompt postgres 34 | postgres 35 | postgres 36 | ``` 37 | 38 | ### Create database 39 | ```bash 40 | createdb mean_relational 41 | ``` 42 | 43 | ## Quick Install 44 | 45 | To install the dependencies, run this in the application folder from the command-line: 46 | 47 | ```bash 48 | $ yarn 49 | $ bower install 50 | ``` 51 | ## Running The Application 52 | 53 | Run your application using npm: 54 | 55 | ```bash 56 | $ gulp serve-dev 57 | ``` 58 | 59 | ### Running in Production mode 60 | To run your application with *production* environment configuration, execute grunt as follows: 61 | 62 | ```bash 63 | $ gulp build 64 | $ gulp serve-build 65 | ``` 66 | 67 | ## Testing Your Application 68 | You can run the test suite: 69 | 70 | ```bash 71 | $ gulp test 72 | $ gulp server-tests 73 | ``` 74 | 75 | ## Linter 76 | You can run the linter: 77 | 78 | ```bash 79 | $ gulp vet 80 | ``` 81 | -------------------------------------------------------------------------------- /src/client/app/users/templates/password_reset.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | MEAN Relational 8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
Please subit your password.
18 |
Too short.
19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
This field is required.
28 |
Passwords must match.
29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | version: v1.5.0 2 | ignore: 3 | 'npm:connect:20130701': 4 | - browser-sync > browser-sync-ui > weinre > express > connect: 5 | reason: None given 6 | expires: '2016-11-21T15:29:07.920Z' 7 | 'npm:express:20140912': 8 | - browser-sync > browser-sync-ui > weinre > express: 9 | reason: None given 10 | expires: '2016-11-21T15:29:07.920Z' 11 | 'npm:qs:20140806': 12 | - browser-sync > browser-sync-ui > weinre > express > qs: 13 | reason: None given 14 | expires: '2016-11-21T15:29:07.921Z' 15 | 'npm:qs:20140806-1': 16 | - browser-sync > browser-sync-ui > weinre > express > qs: 17 | reason: None given 18 | expires: '2016-11-21T15:29:07.921Z' 19 | patch: 20 | 'npm:minimatch:20160620': 21 | - gulp > vinyl-fs > glob-watcher > gaze > globule > minimatch: 22 | patched: '2016-10-22T15:29:08.160Z' 23 | - browser-sync > foxy > resp-modifier > minimatch: 24 | patched: '2016-10-22T15:29:08.160Z' 25 | - gulp-nodemon > gulp > vinyl-fs > glob-stream > minimatch: 26 | patched: '2016-10-22T15:29:08.160Z' 27 | - gulp-traceur-compiler > traceur > glob > minimatch: 28 | patched: '2016-10-22T15:29:08.160Z' 29 | - gulp > vinyl-fs > glob-stream > glob > minimatch: 30 | patched: '2016-10-22T15:29:08.160Z' 31 | - gulp-nodemon > gulp > vinyl-fs > glob-stream > glob > minimatch: 32 | patched: '2016-10-22T15:29:08.160Z' 33 | - gulp-order > minimatch: 34 | patched: '2016-10-22T15:29:08.160Z' 35 | - gulp.spritesmith > minimatch: 36 | patched: '2016-10-22T15:29:08.160Z' 37 | - gulp > vinyl-fs > glob-stream > minimatch: 38 | patched: '2016-10-22T15:29:08.160Z' 39 | - gulp-nodemon > gulp > vinyl-fs > glob-watcher > gaze > globule > minimatch: 40 | patched: '2016-10-22T15:29:08.160Z' 41 | - gulp-useref > vinyl-fs > glob-watcher > gaze > globule > minimatch: 42 | patched: '2016-10-22T15:29:08.160Z' 43 | - gulp > vinyl-fs > glob-watcher > gaze > globule > glob > minimatch: 44 | patched: '2016-10-22T15:29:08.160Z' 45 | - gulp-nodemon > gulp > vinyl-fs > glob-watcher > gaze > globule > glob > minimatch: 46 | patched: '2016-10-22T15:29:08.160Z' 47 | - gulp-useref > vinyl-fs > glob-watcher > gaze > globule > glob > minimatch: 48 | patched: '2016-10-22T15:29:08.160Z' 49 | 'npm:request:20160119': 50 | - browser-sync > localtunnel > request: 51 | patched: '2016-10-22T15:29:08.160Z' 52 | 'npm:semver:20150403': 53 | - gulp-traceur-compiler > traceur > semver: 54 | patched: '2016-10-22T15:29:08.160Z' 55 | 'npm:tough-cookie:20160722': 56 | - browser-sync > localtunnel > request > tough-cookie: 57 | patched: '2016-10-22T15:29:08.160Z' 58 | -------------------------------------------------------------------------------- /src/client/app/users/templates/usersList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 |
UsernameLast NameFirst NameEmailRole
{{user.username}}{{user.lastName}}{{user.firstName}}{{user.email}}{{user.role}} 44 | 45 | 46 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 | 59 |
60 |
61 | 62 |
63 |
64 |
65 |
    66 |
    67 |
    68 |
    69 |
    70 | -------------------------------------------------------------------------------- /src/server/tests/routes/users.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jwt-simple'; 2 | import config from './../../config/config'; 3 | import Users from './../../models/users'; 4 | import Sessions from './../../models/sessions'; 5 | import sessionsService from './../../services/sessions'; 6 | 7 | describe('Routes: Users', () => { 8 | let token; 9 | beforeEach(done => { 10 | Sessions.destroy({ where: {} }) 11 | .then(() => { 12 | return Users.destroy({ where: {} }); 13 | }) 14 | .then(() => Users.create({ 15 | username: 'John', 16 | email: 'john@mail.net', 17 | password: '12345', 18 | role: 'admin', 19 | status: 'active', 20 | })) 21 | .then(user => { 22 | return sessionsService.createNewSession(user); 23 | }) 24 | .then(Session => { 25 | token = jwt.encode(Session.authToken, config.jwtSecret); 26 | done(); 27 | }); 28 | }); 29 | describe('GET /user', () => { 30 | describe('status 200', () => { 31 | it('returns an authenticated user', done => { 32 | request.get('/api/v1/users/me') 33 | .set('Authorization', `JWT ${token}`) 34 | .expect(200) 35 | .end((err, res) => { 36 | expect(res.body.username).to.eql('John'); 37 | expect(res.body.email).to.eql('john@mail.net'); 38 | done(err); 39 | }); 40 | }); 41 | }); 42 | }); 43 | describe('GET /users', () => { 44 | describe('status 200', () => { 45 | it('returns all users', done => { 46 | request.get('/api/v1/users') 47 | .set('Authorization', `JWT ${token}`) 48 | .expect(200) 49 | .end((err, res) => { 50 | expect(res.body.count).to.eql(1); 51 | expect(res.body.rows[0].username).to.eql('John'); 52 | done(err); 53 | }); 54 | }); 55 | }); 56 | }); 57 | describe('POST /users', () => { 58 | describe('status 200', () => { 59 | it('creates a new user', done => { 60 | request.post('/api/v1/users') 61 | .set('Authorization', `JWT ${token}`) 62 | .send({ 63 | username: 'Mary', 64 | email: 'mary@mail.net', 65 | password: '12345', 66 | }) 67 | .expect(200) 68 | .end((err, res) => { 69 | expect(res.body.username).to.eql('Mary'); 70 | expect(res.body.email).to.eql('mary@mail.net'); 71 | done(err); 72 | }); 73 | }); 74 | }); 75 | }); 76 | describe('GET /users', () => { 77 | describe('status 200', () => { 78 | it('returns users with params', done => { 79 | request.get('/api/v1/users?limit=1&page=1&filter=John') 80 | .set('Authorization', `JWT ${token}`) 81 | .expect(200) 82 | .end((err, res) => { 83 | expect(res.body.count).to.eql(1); 84 | expect(res.body.rows[0].username).to.eql('John'); 85 | done(err); 86 | }); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.route.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.signup') 6 | .run(appRun); 7 | 8 | appRun.$inject = ['routerHelper']; 9 | /* @ngInject */ 10 | function appRun(routerHelper) { 11 | routerHelper.configureStates(getStates()); 12 | } 13 | 14 | function getStates() { 15 | return [ 16 | { 17 | state: 'signup', 18 | config: { 19 | url: '/signup', 20 | templateUrl: 'app/signup/templates/signup.html', 21 | controller: 'SignupController', 22 | controllerAs: 'vm', 23 | title: 'Signup', 24 | settings: { 25 | nav: 0, 26 | content: ' Login', 27 | roles: ['guest'], 28 | }, 29 | }, 30 | }, 31 | { 32 | state: 'signup_validation', 33 | config: { 34 | url: '/signup/validation', 35 | templateUrl: 'app/signup/templates/signup_validation.html', 36 | controller: 'SignupValidationController', 37 | controllerAs: 'vm', 38 | title: 'Email validation', 39 | settings: { 40 | nav: 0, 41 | content: ' Login', 42 | roles: ['user'], 43 | status: ['not_validated'], 44 | }, 45 | }, 46 | }, 47 | { 48 | state: 'signup_validation_process', 49 | config: { 50 | url: '/signup/validation/:tokenValidate', 51 | templateUrl: 'app/signup/templates/signup_validation_process.html', 52 | controller: 'SignupValidationEmailController', 53 | controllerAs: 'vm', 54 | title: 'Email validation', 55 | settings: { 56 | nav: 0, 57 | content: ' Login', 58 | roles: ['user', 'guest'], 59 | status: ['not_validated', 'any'], 60 | }, 61 | }, 62 | }, 63 | { 64 | state: 'signup_profile', 65 | config: { 66 | url: '/signup/profile', 67 | templateUrl: 'app/signup/templates/signup_profile.html', 68 | controller: 'SignupProfileController', 69 | controllerAs: 'vm', 70 | title: 'Signup profile', 71 | settings: { 72 | nav: 0, 73 | content: ' Login', 74 | roles: ['user'], 75 | status: ['validated'], 76 | }, 77 | }, 78 | }, 79 | { 80 | state: 'signup_activate', 81 | config: { 82 | url: '/signup/activate', 83 | templateUrl: 'app/signup/templates/signup_activate.html', 84 | controller: 'SignupActivateController', 85 | controllerAs: 'vm', 86 | title: 'Signup activate', 87 | settings: { 88 | nav: 0, 89 | content: ' Login', 90 | roles: ['user'], 91 | status: ['not_active'], 92 | }, 93 | }, 94 | }, 95 | ]; 96 | } 97 | }()); 98 | -------------------------------------------------------------------------------- /src/client/app/users/controllers/usersListController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.users') 6 | .controller('UserListController', UserListController); 7 | 8 | UserListController.$inject = ['$state', '$stateParams', '$uibModal', 9 | 'logger', 'alert', 'usersservice', 'authentication']; 10 | 11 | /* @ngInject */ 12 | function UserListController($state, $stateParams, $uibModal, 13 | logger, alert, usersservice, authentication) { 14 | const vm = this; 15 | vm.title = 'Users'; 16 | vm.currentUser = authentication.user; 17 | vm.users = []; 18 | vm.openAddUserModal = openAddUserModal; 19 | vm.openEditUserModal = openEditUserModal; 20 | vm.removeUser = removeUser; 21 | vm.pagination = { 22 | filter: '', 23 | page: 1, 24 | lengthMenu: [10, 25, 50, 100], 25 | limit: 10, 26 | }; 27 | vm.changeLimit = changeLimit; 28 | vm.changePage = changePage; 29 | vm.loadUsers = loadUsers; 30 | 31 | activate(); 32 | 33 | function activate() { 34 | loadUsers(); 35 | } 36 | 37 | function openAddUserModal() { 38 | const modalInstance = $uibModal.open({ 39 | templateUrl: 'app/users/templates/addUserModal.html', 40 | controller: 'AddUserModalController', 41 | controllerAs: 'uc', 42 | resolve: { 43 | }, 44 | }); 45 | 46 | modalInstance.result 47 | .then(loadUsers) 48 | .then((res) => { 49 | logger.success('User created'); 50 | }); 51 | } 52 | 53 | function openEditUserModal(user) { 54 | const modalInstance = $uibModal.open({ 55 | templateUrl: 'app/users/templates/editUserModal.html', 56 | controller: 'EditUserModalController', 57 | controllerAs: 'uc', 58 | resolve: { 59 | user: angular.copy(user), 60 | }, 61 | }); 62 | 63 | modalInstance.result 64 | .then(loadUsers) 65 | .then((res) => { 66 | logger.success('User edited'); 67 | }); 68 | } 69 | 70 | function removeUser(user) { 71 | const opts = { 72 | title: 'Remove User', 73 | body: 'Are you sure you want to delete this User?', 74 | }; 75 | 76 | alert.show(opts) 77 | .then(accept) 78 | .then(loadUsers) 79 | .then((res) => { 80 | logger.success('User deleted.'); 81 | }); 82 | 83 | function accept() { 84 | return usersservice.removeUser(user.id); 85 | } 86 | } 87 | 88 | function changePage() { 89 | return loadUsers(vm.pagination.page); 90 | } 91 | 92 | function changeLimit() { 93 | return loadUsers(); 94 | } 95 | 96 | function loadUsers(page = 1) { 97 | const params = {}; 98 | vm.pagination.page = page; 99 | 100 | params.page = vm.pagination.page; 101 | params.limit = vm.pagination.limit; 102 | params.filter = (vm.pagination.filter !== '') ? vm.pagination.filter : null; 103 | 104 | return usersservice.getUsers(params) 105 | .then(users => { 106 | vm.users = users.rows; 107 | vm.count = users.count; 108 | }); 109 | } 110 | } 111 | }()); 112 | -------------------------------------------------------------------------------- /src/server/models/users.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import bcrypt from 'bcrypt-nodejs'; 3 | import _ from 'lodash'; 4 | import db from './../config/db'; 5 | 6 | const isPassword = function (password) { 7 | return bcrypt.compareSync(password, this.password); 8 | }; 9 | 10 | const toJSON = function () { 11 | const privateAttributes = [ 12 | 'password', 'emailValidate', 'tokenValidate', 13 | 'tokenPassRecovery', 'tokenPassRecoveryExpiryDate', 14 | ]; 15 | return _.omit(this.dataValues, privateAttributes); 16 | }; 17 | 18 | const Users = db.sequelize.define('Users', { 19 | id: { 20 | type: Sequelize.INTEGER, 21 | primaryKey: true, 22 | autoIncrement: true, 23 | }, 24 | username: { 25 | type: Sequelize.STRING, 26 | unique: { msg: 'Username alredy in use' }, 27 | allowNull: false, 28 | validate: { 29 | notEmpty: { msg: 'The username can\'t be empty' }, 30 | }, 31 | }, 32 | password: { 33 | type: Sequelize.STRING, 34 | allowNull: false, 35 | validate: { 36 | notEmpty: { msg: 'The password can\'t be empty' }, 37 | }, 38 | }, 39 | email: { 40 | type: Sequelize.STRING, 41 | unique: { msg: 'Email alredy in use' }, 42 | allowNull: false, 43 | validate: { 44 | notEmpty: { msg: 'The email address can\'t be empty' }, 45 | }, 46 | }, 47 | firstName: { 48 | type: Sequelize.STRING, 49 | allowNull: true, 50 | }, 51 | lastName: { 52 | type: Sequelize.STRING, 53 | allowNull: true, 54 | }, 55 | role: { 56 | type: Sequelize.STRING, 57 | allowNull: false, 58 | defaultValue: 'user', 59 | validate: { 60 | notEmpty: true, 61 | }, 62 | }, 63 | status: { 64 | type: Sequelize.ENUM('not_validated', 'validated', 'not_active', 'active'), 65 | allowNull: false, 66 | validate: { 67 | notEmpty: true, 68 | }, 69 | defaultValue: 'not_validated', 70 | }, 71 | emailValidate: { 72 | type: Sequelize.BOOLEAN, 73 | allowNull: false, 74 | defaultValue: false, 75 | }, 76 | picture: { 77 | type: Sequelize.STRING, 78 | allowNull: true, 79 | defaultValue: null, 80 | }, 81 | tokenValidate: { 82 | type: Sequelize.STRING, 83 | unique: true, 84 | allowNull: true, 85 | defaultValue: null, 86 | }, 87 | tokenPassRecovery: { 88 | type: Sequelize.STRING, 89 | unique: true, 90 | allowNull: true, 91 | defaultValue: null, 92 | }, 93 | tokenPassRecoveryExpiryDate: { 94 | type: Sequelize.DATE, 95 | allowNull: true, 96 | defaultValue: null, 97 | }, 98 | }, { 99 | hooks: { 100 | beforeCreate: user => { 101 | const salt = bcrypt.genSaltSync(); 102 | user.password = bcrypt.hashSync(user.password, salt); 103 | }, 104 | beforeValidate: (model, options, cb) => { // Workarround to change not null validation message 105 | model.email = model.email || ''; 106 | model.username = model.username || ''; 107 | model.password = model.password || ''; 108 | cb(null, model); 109 | }, 110 | }, 111 | classMethods: { 112 | associate: models => { 113 | Users.hasMany(models.Tasks); 114 | Users.hasMany(models.Sessions); 115 | }, 116 | }, 117 | instanceMethods: { 118 | isPassword, 119 | toJSON, 120 | }, 121 | }); 122 | 123 | export default Users; 124 | -------------------------------------------------------------------------------- /src/client/app/core/services/tasks.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.core') 6 | .factory('taskservice', taskservice); 7 | 8 | taskservice.$inject = ['$http', '$q', 'exception', 'logger']; 9 | /* @ngInject */ 10 | function taskservice($http, $q, exception, logger) { 11 | const service = { 12 | createTask, 13 | updateTask, 14 | getTasks, 15 | getPaginated, 16 | getMessageCount, 17 | getCount, 18 | getCountDone, 19 | getCountNotDone, 20 | removeTask, 21 | }; 22 | 23 | return service; 24 | 25 | function getMessageCount() { return $q.when(72); } 26 | 27 | function getTasks() { 28 | return $http.get('/api/v1/tasks') 29 | .then(success) 30 | .catch(fail); 31 | 32 | function success(response) { 33 | return response.data; 34 | } 35 | 36 | function fail(e) { 37 | return exception.catcher('XHR Failed for getTasks')(e); 38 | } 39 | } 40 | 41 | function getCount(query) { 42 | const params = {}; 43 | if (query) { 44 | params.params = query; 45 | } 46 | 47 | return $http.get('/api/v1/tasks/count', params) 48 | .then(success) 49 | .catch(fail); 50 | 51 | function success(response) { 52 | return response.data; 53 | } 54 | 55 | function fail(e) { 56 | return exception.catcher('XHR Failed for getCount')(e); 57 | } 58 | } 59 | 60 | function getCountDone() { 61 | return getCount({ done: true }); 62 | } 63 | 64 | function getCountNotDone() { 65 | return getCount({ done: false }); 66 | } 67 | 68 | function getPaginated(query) { 69 | const params = {}; 70 | 71 | if (query) { 72 | params.params = query; 73 | } 74 | 75 | return $http.get('/api/v1/tasks/paginated', params) 76 | .then(success) 77 | .catch(fail); 78 | 79 | function success(response) { 80 | return response.data; 81 | } 82 | 83 | function fail(e) { 84 | return exception.catcher('XHR Failed for getPaginated')(e); 85 | } 86 | } 87 | 88 | function createTask(task) { 89 | return $http.post('/api/v1/tasks', task) 90 | .then(success) 91 | .catch(fail); 92 | 93 | function success(response) { 94 | return response.data; 95 | } 96 | 97 | function fail(e) { 98 | return exception.catcher('XHR Failed for createTask')(e); 99 | } 100 | } 101 | 102 | function updateTask(taskId, task) { 103 | return $http.put('/api/v1/tasks/' + taskId, task) 104 | .then(success) 105 | .catch(fail); 106 | 107 | function success(response) { 108 | return response.data; 109 | } 110 | 111 | function fail(e) { 112 | return exception.catcher('XHR Failed for updateTask')(e); 113 | } 114 | } 115 | 116 | function removeTask(taskId) { 117 | return $http.delete('/api/v1/tasks/' + taskId) 118 | .then(success) 119 | .catch(fail); 120 | 121 | function success(response) { 122 | return response.data; 123 | } 124 | 125 | function fail(e) { 126 | return exception.catcher('XHR Failed for deleteTask')(e); 127 | } 128 | } 129 | } 130 | }()); 131 | -------------------------------------------------------------------------------- /src/client/app/signup/templates/signup_profile.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 | MEAN Relational 8 |
    9 |
    10 |
    11 | 12 |
    13 | 14 |
    15 | 16 |
    17 |
    Please, write your first name.
    18 |
    19 |
    20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 |
    Please, fill in your last name.
    27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 | 34 |
    35 |
    Please subit your password.
    36 |
    Too short.
    37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 | 44 |
    45 |
    This field is required.
    46 |
    Passwords must match.
    47 |
    48 |
    49 |
    50 |
    51 |
    52 | 53 |
    54 |
    55 | 56 |
    57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    63 | -------------------------------------------------------------------------------- /src/server/services/signup.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | 3 | import Users from './../models/users'; 4 | import config from './../config/config'; 5 | import sessionsService from './../services/sessions'; 6 | import emailService from './../services/email'; 7 | 8 | const service = {}; 9 | 10 | // Create new user 11 | service.create = (profile) => { 12 | const token = bcrypt.genSaltSync().replace(/\//g, '-').replace(/[^a-zA-Z0-9-_]/g, ''); 13 | 14 | return Users.create({ 15 | username: profile.username, 16 | email: profile.email, 17 | password: (Math.random() + 1).toString(36).substr(2, 10), 18 | role: 'user', 19 | status: config.verifyEmail ? 'not_validated' : 'validated', 20 | tokenValidate: config.verifyEmail ? token : null, 21 | emailValidate: !config.verifyEmail, 22 | }) 23 | .then(User => { 24 | if (config.verifyEmail && User.$options.isNewRecord) { 25 | emailService.sendValidateEmail(User); 26 | } 27 | 28 | return sessionsService.createNewSession(User) 29 | .then(Session => { 30 | return { 31 | User, 32 | Session, 33 | msg: 'Your account was created', 34 | }; 35 | }); 36 | }); 37 | }; 38 | 39 | // Send validation email 40 | service.sendValidationEmail = (User) => { 41 | if (User.emailValidate || !User.tokenValidate) { 42 | return Promise.reject(new Error('Invalid user email or already validated account')); 43 | } 44 | emailService.sendValidateEmail(User); 45 | return Promise.resolve({ msg: 'Validation email sent to ' + User.email }); 46 | }; 47 | 48 | // Validate User email 49 | service.validateEmail = (loggedUser, userSession, tokenValidate) => { 50 | if (loggedUser && userSession && tokenValidate) { 51 | if (loggedUser.tokenValidate === tokenValidate && !loggedUser.emailValidate) { 52 | return doValidation(loggedUser, userSession); 53 | } 54 | return Promise.reject(new Error('Invalid token or already validated account')); 55 | } 56 | 57 | return Users.findOne({ where: { tokenValidate, emailValidate: 0 } }) 58 | .then(User => { 59 | if (!User) { 60 | throw new Error('Invalid token or already validated account'); 61 | } 62 | return doValidation(User); 63 | }); 64 | 65 | function doValidation(User, Session) { 66 | // valid user then do email validation 67 | User.emailValidate = true; 68 | User.tokenValidate = null; 69 | User.status = 'validated'; 70 | return User.save() 71 | .then(updatedUser => { 72 | if (Session) { 73 | return { 74 | User: updatedUser, 75 | Session, 76 | msg: 'Your email account was validated', 77 | }; 78 | } 79 | 80 | return sessionsService.createNewSession(updatedUser) 81 | .then(newSession => { 82 | return { 83 | User: updatedUser, 84 | Session: newSession, 85 | msg: 'Your email account was validated', 86 | }; 87 | }); 88 | }); 89 | } 90 | }; 91 | 92 | // Continues signup process store user profile options 93 | service.storeProfile = (User, Session, profile) => { 94 | if (User.status !== 'validated') { 95 | return Promise.reject(new Error('Invalid user status')); 96 | } 97 | 98 | if (profile.password !== profile.verifyPassword) { 99 | return Promise.reject(new Error('Passwords must match')); 100 | } 101 | 102 | if (profile.password.length < 6) { 103 | return Promise.reject(new Error('Password too short')); 104 | } 105 | 106 | const salt = bcrypt.genSaltSync(); 107 | 108 | return User.update({ 109 | firstName: profile.firstName, 110 | lastName: profile.lastName, 111 | password: bcrypt.hashSync(profile.password, salt), 112 | status: 'not_active', 113 | }).then(updatedUser => { 114 | return { 115 | Session, 116 | User: updatedUser, 117 | msg: 'Your profile was saved', 118 | }; 119 | }); 120 | }; 121 | 122 | // Finish signup process: 'active' user profile and send welcome email 123 | service.activate = (User, Session) => { 124 | if (User.status !== 'not_active') { 125 | return Promise.reject(new Error('Invalid user')); 126 | } 127 | 128 | return User.update({ 129 | status: 'active', 130 | }).then(updatedUser => { 131 | emailService.sendWelcomeEmail(User); 132 | return { 133 | Session, 134 | User: updatedUser, 135 | msg: `Welcome ${User.firstName} ${User.lastName}!`, 136 | }; 137 | }); 138 | }; 139 | 140 | module.exports = service; 141 | -------------------------------------------------------------------------------- /src/server/routes/signup.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jwt-simple'; 3 | 4 | import errors from './../error'; 5 | import acl from './../config/acl'; 6 | import config from './../config/config'; 7 | import signupService from './../services/signup'; 8 | 9 | /** 10 | * Users policy 11 | * ACL configuration 12 | */ 13 | acl.allow([ 14 | { 15 | roles: ['guest'], 16 | allows: [{ 17 | resources: '/api/v1/signup', 18 | permissions: ['post'], 19 | }], 20 | }, { 21 | roles: ['user', 'guest'], 22 | allows: [{ 23 | resources: '/api/v1/signup/validate/:token', 24 | permissions: ['get'], 25 | }], 26 | }, { 27 | roles: ['user'], 28 | allows: [{ 29 | resources: '/api/v1/signup/activate', 30 | permissions: ['post'], 31 | }, { 32 | resources: '/api/v1/signup/profile', 33 | permissions: ['post'], 34 | }, { 35 | resources: '/api/v1/signup/validation', 36 | permissions: ['get'], 37 | }], 38 | }, 39 | ]); 40 | 41 | const router = express.Router(); 42 | 43 | /** 44 | * @api {post} /users Register a new user 45 | * @apiGroup User 46 | * @apiParam {String} name User name 47 | * @apiParam {String} email User email 48 | * @apiParam {String} password User password 49 | * @apiParamExample {json} Input 50 | * { 51 | * "name": "John Connor", 52 | * "email": "john@connor.net", 53 | * "password": "123456" 54 | * } 55 | * @apiSuccess {Number} id User id 56 | * @apiSuccess {String} name User name 57 | * @apiSuccess {String} email User email 58 | * @apiSuccess {String} password User encrypted password 59 | * @apiSuccess {Date} updated_at Update's date 60 | * @apiSuccess {Date} created_at Register's date 61 | * @apiSuccessExample {json} Success 62 | * HTTP/1.1 200 OK 63 | * { 64 | * "id": 1, 65 | * "name": "John Connor", 66 | * "email": "john@connor.net", 67 | * "password": "$2a$10$SK1B1", 68 | * "updated_at": "2016-02-10T15:20:11.700Z", 69 | * "created_at": "2016-02-10T15:29:11.700Z", 70 | * } 71 | * @apiErrorExample {json} Error 72 | * HTTP/1.1 412 Precondition Failed 73 | * { 74 | * "msg": "Not allowed or invalid fields", 75 | * } 76 | */ 77 | router.post('/api/v1/signup', acl.checkRoles, (req, res) => { 78 | signupService.create(req.body) 79 | .then(result => { 80 | res.setHeader('Authorization', jwt.encode(result.Session.authToken, config.jwtSecret)); 81 | res.setHeader('AuthExpiration', result.Session.expiresOn); 82 | res.json(result); 83 | }) 84 | .catch(error => res.status(412).json(errors.get(error))); 85 | }); 86 | 87 | /** 88 | * @api {get} /users/validate/validation?email=user@email.com Send user validation email 89 | * @apiGroup User 90 | * @apiSuccessExample {json} Success 91 | * HTTP/1.1 200 OK 92 | * { 93 | * "res": true, 94 | * } 95 | * @apiErrorExample {json} Error 96 | * HTTP/1.1 412 Precondition Failed 97 | * { 98 | * "msg": "Invalid user email", 99 | * } 100 | */ 101 | router.get('/api/v1/signup/validation', acl.checkRoles, (req, res) => { 102 | signupService.sendValidationEmail(req.User) 103 | .then(result => res.json(result)) 104 | .catch(error => res.status(412).json(errors.get(error))); 105 | }); 106 | 107 | /** 108 | * @api {get} /users/validate/:token Validate user email 109 | * @apiGroup User 110 | * @apiParam {String} token User tokenValidate 111 | * @apiSuccessExample {json} Success 112 | * HTTP/1.1 200 OK 113 | * { 114 | * "res": true, 115 | * } 116 | * @apiErrorExample {json} Error 117 | * HTTP/1.1 412 Precondition Failed 118 | * { 119 | * "msg": "Invalid token", 120 | * } 121 | */ 122 | router.get('/api/v1/signup/validate/:token', acl.checkRoles, (req, res) => { 123 | signupService.validateEmail(req.User, req.Session, req.params.token) 124 | .then(result => { 125 | res.setHeader('Authorization', jwt.encode(result.Session.authToken, config.jwtSecret)); 126 | res.setHeader('AuthExpiration', result.Session.expiresOn); 127 | res.json(result); 128 | }) 129 | .catch(error => res.status(412).json(errors.get(error))); 130 | }); 131 | 132 | /** 133 | * Update user profile, continues signup process 134 | */ 135 | router.post('/api/v1/signup/profile', acl.checkRoles, (req, res) => { 136 | signupService.storeProfile(req.User, req.Session, req.body) 137 | .then(result => res.json(result)) 138 | .catch(error => res.status(412).json(errors.get(error))); 139 | }); 140 | 141 | /** 142 | * Activate user profile, continues signup process 143 | */ 144 | router.post('/api/v1/signup/activate', acl.checkRoles, (req, res) => { 145 | signupService.activate(req.User, req.Session) 146 | .then(result => res.json(result)) 147 | .catch(error => res.status(412).json(errors.get(error))); 148 | }); 149 | 150 | export default router; 151 | -------------------------------------------------------------------------------- /src/server/tests/routes/token.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jwt-simple'; 2 | import config from './../../config/config'; 3 | import Users from './../../models/users'; 4 | import Sessions from './../../models/sessions'; 5 | import Tokens from './../../services/tokens'; 6 | 7 | describe('Routes: Token', () => { 8 | describe('POST /token', () => { 9 | beforeEach(done => { 10 | Sessions.destroy({ where: {} }) 11 | .then(() => { 12 | return Users.destroy({ where: {} }); 13 | }) 14 | .then(() => Users.create({ 15 | username: 'John', 16 | email: 'john@mail.net', 17 | password: '12345', 18 | emailValidate: 1, 19 | status: 'active', 20 | })) 21 | .then(user => { 22 | done(); 23 | }) 24 | .catch(err => { 25 | done(err); 26 | }); 27 | }); 28 | describe('status 200', () => { 29 | it('returns authenticated user token', done => { 30 | request.post('/api/v1/signin') 31 | .send({ 32 | identification: 'john@mail.net', 33 | password: '12345', 34 | }) 35 | .expect(200) 36 | .end((err, res) => { 37 | expect(res.body.Session).to.include.keys('authToken'); 38 | done(err); 39 | }); 40 | }); 41 | it('user session signout', done => { 42 | Tokens.signin({ 43 | identification: 'john@mail.net', 44 | password: '12345', 45 | }) 46 | .then(data => { 47 | expect(data.User).to.not.be.undefined; 48 | expect(data.User.username).to.eql('John'); 49 | expect(data.User.email).to.eql('john@mail.net'); 50 | expect(data.Session).to.not.be.undefined; 51 | expect(data.Session.authToken).to.not.be.undefined; 52 | const auth = jwt.encode(data.Session.authToken, config.jwtSecret); 53 | request.post('/api/v1/signout') 54 | .set('Authorization', `JWT ${auth}`) 55 | .expect(200) 56 | .end((err, res) => { 57 | expect(res.body.msg).to.eql('Signout Successfully'); 58 | Sessions.count().then(count => { 59 | expect(count).to.eql(0); 60 | done(err); 61 | }) 62 | .catch(error => { 63 | done(error); 64 | }); 65 | }); 66 | }) 67 | .catch(err => { 68 | done(err); 69 | }); 70 | }); 71 | it('all user sessions logout', done => { 72 | let auth; 73 | Tokens.signin({ 74 | identification: 'john@mail.net', 75 | password: '12345', 76 | }) 77 | .then(data => { 78 | return Tokens.signin({ 79 | identification: 'john@mail.net', 80 | password: '12345', 81 | }); 82 | }) 83 | .then(data => { 84 | expect(data.User).to.not.be.undefined; 85 | expect(data.User.username).to.eql('John'); 86 | expect(data.User.email).to.eql('john@mail.net'); 87 | expect(data.Session).to.not.be.undefined; 88 | expect(data.Session.authToken).to.not.be.undefined; 89 | auth = jwt.encode(data.Session.authToken, config.jwtSecret); 90 | 91 | return Sessions.count(); 92 | }) 93 | .then(count => { 94 | expect(count).to.eql(2); 95 | 96 | request.post('/api/v1/signout/all') 97 | .set('Authorization', `JWT ${auth}`) 98 | .expect(200) 99 | .end((err, res) => { 100 | expect(res.body.msg).to.eql('Sessions closed'); 101 | Sessions.count().then(finalCount => { 102 | expect(finalCount).to.eql(0); 103 | done(err); 104 | }) 105 | .catch(error => { 106 | done(error); 107 | }); 108 | }); 109 | }) 110 | .catch(err => { 111 | done(err); 112 | }); 113 | }); 114 | }); 115 | describe('status 401', () => { 116 | it('throws error when password is incorrect', done => { 117 | request.post('/api/v1/signin') 118 | .send({ 119 | identification: 'john@mail.net', 120 | password: 'WRONG_PASSWORD', 121 | }) 122 | .expect(412) 123 | .end((err, res) => { 124 | done(err); 125 | }); 126 | }); 127 | it('throws error when email not exist', done => { 128 | request.post('/api/v1/signin') 129 | .send({ 130 | identification: 'wrong@email.com', 131 | password: '12345', 132 | }) 133 | .expect(412) 134 | .end((err, res) => { 135 | done(err); 136 | }); 137 | }); 138 | it('throws error when email and password are blank', done => { 139 | request.post('/api/v1/signin') 140 | .expect(412) 141 | .end((err, res) => { 142 | done(err); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 | 9 |
    10 |
    11 |
    {{vm.userCount}}
    12 |
    Users
    13 |
    14 |
    15 |
    16 | 17 | 22 | 23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 | 31 |
    32 |
    33 |
    {{vm.taskCount}}
    34 |
    Tasks
    35 |
    36 |
    37 |
    38 | 39 | 44 | 45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 | 53 |
    54 |
    55 |
    {{vm.taskDoneCount}}
    56 |
    Complete Tasks
    57 |
    58 |
    59 |
    60 | 61 | 66 | 67 |
    68 |
    69 |
    70 |
    71 |
    72 |
    73 |
    74 | 75 |
    76 |
    77 |
    {{vm.taskNotDoneCount}}
    78 |
    Incomplete Tasks
    79 |
    80 |
    81 |
    82 | 83 | 88 | 89 |
    90 |
    91 |
    92 |
    93 | -------------------------------------------------------------------------------- /src/server/routes/social.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import errors from './../error'; 3 | import acl from './../config/acl'; 4 | 5 | import socialService from './../services/social'; 6 | 7 | /** 8 | * Users policy 9 | * ACL configuration 10 | */ 11 | acl.allow([]); 12 | 13 | const router = express.Router(); 14 | 15 | /** 16 | * @api {post} /users Register a new user (Facebook) 17 | * @apiGroup User 18 | * @apiParam {String} code 19 | * @apiParam {String} clientId 20 | * @apiParam {String} redirectUri 21 | * @apiParamExample {json} Input 22 | * { 23 | * "code": "abc", 24 | * "clientId": "abc", 25 | * "redirectUri": "http://redirect" 26 | * } 27 | * @apiSuccess {Object} user 28 | * @apiSuccess {String} token 29 | * @apiSuccessExample {json} Success 30 | * HTTP/1.1 200 OK 31 | * { 32 | * "user": { "name" : "John", "id": 1 }, 33 | * "token": "abczyx" 34 | * } 35 | * @apiErrorExample {json} Register error 36 | * HTTP/1.1 412 Precondition Failed 37 | */ 38 | router.post('/api/v1/auth/facebook', acl.checkRoles, (req, res) => { 39 | socialService.facebook(req.body.code, req.body.clientId, req.body.redirectUri) 40 | .then(result => res.json(result)) 41 | .catch(error => res.status(412).json(errors.get(error))); 42 | }); 43 | 44 | /** 45 | * @api {post} /users Register a new user (Twitter) 46 | * @apiGroup User 47 | * @apiParam {String} code 48 | * @apiParam {String} clientId 49 | * @apiParam {String} redirectUri 50 | * @apiParamExample {json} Input 51 | * { 52 | * "code": "abc", 53 | * "clientId": "abc", 54 | * "redirectUri": "http://redirect" 55 | * } 56 | * @apiSuccess {Object} user 57 | * @apiSuccess {String} token 58 | * @apiSuccessExample {json} Success 59 | * HTTP/1.1 200 OK 60 | * { 61 | * "user": { "name" : "John", "id": 1 }, 62 | * "token": "abczyx" 63 | * } 64 | * @apiErrorExample {json} Register error 65 | * HTTP/1.1 412 Precondition Failed 66 | */ 67 | router.post('/api/v1/auth/twitter', acl.checkRoles, (req, res) => { 68 | socialService.twitter(req.body) 69 | .then(result => res.json(result)) 70 | .catch(error => res.status(412).json(errors.get(error))); 71 | }); 72 | 73 | /** 74 | * @api {post} /users Register a new user (Instagram) 75 | * @apiGroup User 76 | * @apiParam {String} code 77 | * @apiParam {String} clientId 78 | * @apiParam {String} redirectUri 79 | * @apiParamExample {json} Input 80 | * { 81 | * "code": "abc", 82 | * "clientId": "abc", 83 | * "redirectUri": "http://redirect" 84 | * } 85 | * @apiSuccess {Object} user 86 | * @apiSuccess {String} token 87 | * @apiSuccessExample {json} Success 88 | * HTTP/1.1 200 OK 89 | * { 90 | * "user": { "name" : "John", "id": 1 }, 91 | * "token": "abczyx" 92 | * } 93 | * @apiErrorExample {json} Register error 94 | * HTTP/1.1 412 Precondition Failed 95 | */ 96 | router.post('/api/v1/auth/instagram', acl.checkRoles, (req, res) => { 97 | socialService.instagram(req.body.code, req.body.clientId, req.body.redirectUri) 98 | .then(result => res.json(result)) 99 | .catch(error => res.status(412).json(errors.get(error))); 100 | }); 101 | 102 | /** 103 | * @api {post} /users Register a new user (Google) 104 | * @apiGroup User 105 | * @apiParam {String} code 106 | * @apiParam {String} clientId 107 | * @apiParam {String} redirectUri 108 | * @apiParamExample {json} Input 109 | * { 110 | * "code": "abc", 111 | * "clientId": "abc", 112 | * "redirectUri": "http://redirect" 113 | * } 114 | * @apiSuccess {Object} user 115 | * @apiSuccess {String} token 116 | * @apiSuccessExample {json} Success 117 | * HTTP/1.1 200 OK 118 | * { 119 | * "user": { "name" : "John", "id": 1 }, 120 | * "token": "abczyx" 121 | * } 122 | * @apiErrorExample {json} Register error 123 | * HTTP/1.1 412 Precondition Failed 124 | */ 125 | router.post('/api/v1/auth/google', acl.checkRoles, (req, res) => { 126 | socialService.google(req.body.code, req.body.clientId, req.body.redirectUri) 127 | .then(result => res.json(result)) 128 | .catch(error => res.status(412).json(errors.get(error))); 129 | }); 130 | 131 | /** 132 | * @api {post} /users Register a new user (Pinterest) 133 | * @apiGroup User 134 | * @apiParam {String} code 135 | * @apiParam {String} clientId 136 | * @apiParam {String} redirectUri 137 | * @apiParamExample {json} Input 138 | * { 139 | * "code": "abc", 140 | * "clientId": "abc", 141 | * "redirectUri": "http://redirect" 142 | * } 143 | * @apiSuccess {Object} user 144 | * @apiSuccess {String} token 145 | * @apiSuccessExample {json} Success 146 | * HTTP/1.1 200 OK 147 | * { 148 | * "user": { "name" : "John", "id": 1 }, 149 | * "token": "abczyx" 150 | * } 151 | * @apiErrorExample {json} Register error 152 | * HTTP/1.1 412 Precondition Failed 153 | */ 154 | router.post('/api/v1/auth/pinterest', acl.checkRoles, (req, res) => { 155 | socialService.pinterest(req.body.code, req.body.clientId, req.body.redirectUri) 156 | .then(result => res.json(result)) 157 | .catch(error => res.status(412).json(errors.get(error))); 158 | }); 159 | 160 | export default router; 161 | -------------------------------------------------------------------------------- /src/client/app/blocks/router/router-helper.provider.js: -------------------------------------------------------------------------------- 1 | /* Help configure the state-base ui.router */ 2 | (function () { 3 | 'use strict'; 4 | 5 | angular 6 | .module('blocks.router') 7 | .provider('routerHelper', routerHelperProvider); 8 | 9 | routerHelperProvider.$inject = ['$locationProvider', '$stateProvider', '$urlRouterProvider']; 10 | /* @ngInject */ 11 | function routerHelperProvider($locationProvider, $stateProvider, $urlRouterProvider) { 12 | /* jshint validthis:true */ 13 | const config = { 14 | docTitle: undefined, 15 | resolveAlways: {}, 16 | }; 17 | 18 | if (!(window.history && window.history.pushState)) { 19 | window.location.hash = '/'; 20 | } 21 | 22 | $locationProvider.html5Mode(true); 23 | 24 | this.configure = function (cfg) { 25 | angular.extend(config, cfg); 26 | }; 27 | 28 | this.$get = RouterHelper; 29 | RouterHelper.$inject = ['$location', '$rootScope', '$state', 'logger', 'authentication']; 30 | /* @ngInject */ 31 | function RouterHelper($location, $rootScope, $state, logger, authentication) { 32 | let handlingStateChangeError = false; 33 | let hasOtherwise = false; 34 | const stateCounts = { 35 | errors: 0, 36 | changes: 0, 37 | }; 38 | 39 | const service = { 40 | configureStates, 41 | getStates, 42 | stateCounts, 43 | }; 44 | 45 | init(); 46 | 47 | return service; 48 | 49 | // ///////////// 50 | 51 | function configureStates(states, otherwisePath) { 52 | states.forEach(state => { 53 | state.config.resolve = angular.extend(state.config.resolve || {}, config.resolveAlways); 54 | $stateProvider.state(state.state, state.config); 55 | }); 56 | 57 | if (otherwisePath && !hasOtherwise) { 58 | hasOtherwise = true; 59 | $urlRouterProvider.otherwise(otherwisePath); 60 | } 61 | } 62 | 63 | function handleRoutingPermissions() { 64 | $rootScope.$on('$stateChangeStart', checkPermissions); 65 | 66 | function checkPermissions(event, toState, toParams, fromState, fromParams) { 67 | let allowed = false; 68 | 69 | if (toState.settings && toState.settings.roles && toState.settings.roles.length > 0) { 70 | const authenticated = typeof authentication.getUser() === 'object'; 71 | const role = authenticated ? authentication.getUser().role : 'guest'; 72 | const status = authenticated ? authentication.getUser().status : 'any'; 73 | 74 | if (toState.settings.roles && role) { 75 | allowed = toState.settings.roles.indexOf(role) !== -1; 76 | } 77 | 78 | if (toState.settings.status && status) { 79 | allowed = toState.settings.status.indexOf(status) !== -1; 80 | } 81 | 82 | if (!toState.settings.status && status !== 'active' && authenticated) { 83 | allowed = false; 84 | } 85 | 86 | if (!allowed) { 87 | event.preventDefault(); 88 | $state.go(authentication.continueFrom()); 89 | } 90 | 91 | // Check if user session has expired before continue 92 | if (allowed && authenticated && authentication.sessionHasExpired()) { 93 | event.preventDefault(); 94 | authentication.clearAll(); 95 | logger.error('Your Session has Expired'); 96 | $state.go(authentication.continueFrom()); 97 | } 98 | } 99 | } 100 | } 101 | 102 | // Route cancellation: 103 | // On routing error, go to the dashboard. 104 | // Provide an exit clause if it tries to do it twice. 105 | function handleRoutingErrors() { 106 | $rootScope.$on('$stateChangeError', handleErrors); 107 | 108 | function handleErrors(event, toState, toParams, fromState, fromParams, error) { 109 | if (handlingStateChangeError) { 110 | return; 111 | } 112 | 113 | stateCounts.errors++; 114 | handlingStateChangeError = true; 115 | 116 | const destination = (toState && 117 | (toState.title || toState.name || toState.loadedTemplateUrl)) || 'unknown target'; 118 | 119 | const msg = 'Error routing to ' + destination + '. ' + 120 | (error.data || '') + '.
    ' + (error.statusText || '') + ': ' + (error.status || ''); 121 | 122 | logger.warning(msg, [toState]); 123 | $location.path('/'); 124 | } 125 | } 126 | 127 | function init() { 128 | handleRoutingPermissions(); 129 | handleRoutingErrors(); 130 | updateDocTitle(); 131 | } 132 | 133 | function getStates() { 134 | return $state.get(); 135 | } 136 | 137 | function updateDocTitle() { 138 | $rootScope.$on('$stateChangeSuccess', updateTitle); 139 | 140 | function updateTitle(event, toState, toParams, fromState, fromParams) { 141 | stateCounts.changes++; 142 | handlingStateChangeError = false; 143 | $rootScope.title = `${config.docTitle}${toState.title || ''}`; // data bind to 144 | } 145 | } 146 | } 147 | } 148 | }()); 149 | -------------------------------------------------------------------------------- /gulp.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | const client = './src/client/'; 3 | const server = './src/server/'; 4 | const clientApp = client + 'app/'; 5 | const root = './'; 6 | const specRunnerFile = 'specs.html'; 7 | const temp = './.tmp/'; 8 | const wiredep = require('wiredep'); 9 | const bowerFiles = wiredep({ devDependencies: true }).js; 10 | const bower = { 11 | json: require('./bower.json'), 12 | directory: './bower_components/', 13 | ignorePath: '../..', 14 | }; 15 | const nodeModules = 'node_modules'; 16 | 17 | const config = { 18 | /** 19 | * File paths 20 | */ 21 | // all javascript that we want to vet 22 | alljs: [ 23 | './src/**/*.js', 24 | './*.js', 25 | ], 26 | serverSrcFiles: './src/server/**/*.js', 27 | build: './build/', 28 | client, 29 | css: temp + 'styles.css', 30 | fonts: [ 31 | bower.directory + 'bootstrap/fonts/**/*.*', 32 | bower.directory + 'font-awesome/fonts/**/*.*', 33 | ], 34 | html: client + '**/*.html', 35 | htmltemplates: clientApp + '**/*.html', 36 | images: client + 'images/**/*.*', 37 | index: client + 'index.html', 38 | // app js, with no specs 39 | js: [ 40 | clientApp + '**/*.module.js', 41 | clientApp + '**/*.js', 42 | '!' + clientApp + '**/*.spec.js', 43 | ], 44 | jsOrder: [ 45 | '**/app.module.js', 46 | '**/*.module.js', 47 | '**/*.js', 48 | ], 49 | less: client + 'styles/*.less', 50 | root, 51 | server, 52 | source: 'src/', 53 | stubsjs: [ 54 | bower.directory + 'angular-mocks/angular-mocks.js', 55 | client + 'stubs/**/*.js', 56 | ], 57 | temp, 58 | 59 | /** 60 | * optimized files 61 | */ 62 | optimized: { 63 | app: 'start.js', 64 | lib: 'lib.js', 65 | }, 66 | 67 | /** 68 | * plato 69 | */ 70 | plato: { js: clientApp + '**/*.js' }, 71 | 72 | /** 73 | * browser sync 74 | */ 75 | browserReloadDelay: 1000, 76 | 77 | /** 78 | * template cache 79 | */ 80 | templateCache: { 81 | file: 'templates.js', 82 | options: { 83 | module: 'app.core', 84 | root: 'app/', 85 | standalone: false, 86 | }, 87 | }, 88 | 89 | /** 90 | * Bower and NPM files 91 | */ 92 | bower, 93 | packages: [ 94 | './package.json', 95 | './bower.json', 96 | ], 97 | 98 | /** 99 | * specs.html, our HTML spec runner 100 | */ 101 | specRunner: client + specRunnerFile, 102 | specRunnerFile, 103 | 104 | /** 105 | * The sequence of the injections into specs.html: 106 | * 1 testlibraries 107 | * mocha setup 108 | * 2 bower 109 | * 3 js 110 | * 4 spechelpers 111 | * 5 specs 112 | * 6 templates 113 | */ 114 | testlibraries: [ 115 | nodeModules + '/mocha/mocha.js', 116 | nodeModules + '/chai/chai.js', 117 | nodeModules + '/sinon-chai/lib/sinon-chai.js', 118 | ], 119 | specHelpers: [client + 'test-helpers/*.js'], 120 | specs: [clientApp + '**/*.spec.js'], 121 | serverIntegrationSpecs: [client + '/tests/server-integration/**/*.spec.js'], 122 | serverHelpers: [server + '/tests/helpers'], 123 | serverSpecs: [server + '/tests/**/*.js'], 124 | 125 | /** 126 | * Node settings 127 | */ 128 | nodeServerDev: server + 'start.js', 129 | nodeServerBuild: server + 'start.js', 130 | defaultPort: '3000', 131 | }; 132 | 133 | /** 134 | * wiredep and bower settings 135 | */ 136 | config.getWiredepDefaultOptions = function () { 137 | const options = { 138 | bowerJson: config.bower.json, 139 | directory: config.bower.directory, 140 | ignorePath: config.bower.ignorePath, 141 | }; 142 | return options; 143 | }; 144 | 145 | /** 146 | * karma settings 147 | */ 148 | config.karma = getKarmaOptions(); 149 | 150 | return config; 151 | 152 | // ////////////// 153 | 154 | function getKarmaOptions() { 155 | const options = { 156 | files: [].concat( 157 | bowerFiles, 158 | config.specHelpers, 159 | clientApp + '**/*.module.js', 160 | clientApp + '**/*.js', 161 | temp + config.templateCache.file, 162 | config.serverIntegrationSpecs 163 | ), 164 | exclude: [], 165 | coverage: { 166 | dir: 'coverage/client', 167 | reporters: [ 168 | // reporters not supporting the `file` property 169 | { type: 'html', subdir: 'report-html' }, 170 | { type: 'lcov', subdir: '.' }, 171 | { type: 'text-summary' }, // , subdir: '.', file: 'text-summary.txt'} 172 | ], 173 | }, 174 | preprocessors: { 175 | 'src/**/*.js': ['babel'], 176 | }, 177 | babelPreprocessor: { 178 | options: { 179 | presets: ['es2015'], 180 | sourceMap: 'inline', 181 | }, 182 | filename(file) { 183 | return file.originalPath.replace(/\.js$/, '.es5.js'); 184 | }, 185 | sourceFileName(file) { 186 | return file.originalPath; 187 | }, 188 | }, 189 | }; 190 | options.preprocessors[clientApp + '**/!(*.spec)+(.js)'] = ['coverage']; 191 | return options; 192 | } 193 | }; 194 | -------------------------------------------------------------------------------- /src/server/services/users.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt-nodejs'; 2 | 3 | import Users from './../models/users'; 4 | import emailService from './../services/email'; 5 | 6 | const service = {}; 7 | 8 | service.getAll = (params) => { 9 | const query = {}; 10 | // attributes 11 | query.attributes = ['id', 'username', 'email', 'role', 'status', 'firstName', 'lastName']; 12 | // conditions 13 | if (params.filter) { 14 | query.where = { 15 | $or: [ 16 | { username: { $like: '%' + params.filter + '%' } }, 17 | { firstName: { $like: '%' + params.filter + '%' } }, 18 | { lastName: { $like: '%' + params.filter + '%' } }, 19 | { email: { $like: '%' + params.filter + '%' } }, 20 | { role: { $like: '%' + params.filter + '%' } }, 21 | ], 22 | }; 23 | } 24 | 25 | // pagination 26 | if (params.page) { 27 | const limit = (params.limit) ? parseInt(params.limit, 10) : 10; 28 | query.offset = (parseInt(params.page, 10) - 1) * limit; 29 | } 30 | if (params.limit) { 31 | query.limit = parseInt(params.limit, 10); 32 | } 33 | 34 | return Users.findAndCountAll(query); 35 | }; 36 | 37 | service.getCount = (query) => { 38 | return Users.count(query); 39 | }; 40 | 41 | // Find a user by his id and get all data / relations (used also in auth process) 42 | service.findById = (id, includePassword) => { 43 | return service.findUser({ id }, includePassword); 44 | }; 45 | 46 | // Find a user by his username and get all data / relations (used also in auth process) 47 | service.findByUsername = (username, includePassword) => { 48 | return service.findUser({ username }, includePassword); 49 | }; 50 | 51 | // Find a user by his email and get all data / relations (used also in auth process) 52 | service.findByEmail = (email, includePassword) => { 53 | return service.findUser({ email }, includePassword); 54 | }; 55 | 56 | // Find a user and realtions by custom where and check if include user password or not 57 | service.findUser = (where, includePassword) => { 58 | const attributes = [ 59 | 'id', 'username', 'email', 'role', 'emailValidate', 'firstName', 'lastName', 60 | 'picture', 'tokenValidate', 'tokenPassRecovery', 'tokenPassRecoveryExpiryDate', 'status', 61 | ]; 62 | 63 | if (includePassword) { 64 | attributes.push('password'); 65 | } 66 | 67 | return Users.findOne({ where, attributes }); 68 | }; 69 | 70 | service.destroy = (id) => { 71 | return Users.destroy({ where: { id } }); 72 | }; 73 | 74 | service.create = (user) => { 75 | user.tokenValidate = null; 76 | user.emailValidate = 1; 77 | user.status = 'active'; 78 | return Users.create(user); 79 | }; 80 | 81 | service.update = (id, body) => { 82 | const query = { where: { id } }; 83 | return Users.update(body, query); 84 | }; 85 | 86 | 87 | // Init forgot password process 88 | service.forgot = (identification) => { 89 | if (!identification) { 90 | throw new Error('Invalid user identification'); 91 | } 92 | 93 | return Users.findOne({ 94 | where: { 95 | $or: [{ 96 | username: identification, 97 | }, { 98 | email: identification, 99 | }], 100 | }, 101 | attributes: ['id', 'username', 'firstName', 'lastName', 'email'], 102 | }) 103 | .then(User => { 104 | if (!User) { 105 | throw new Error('User not registered'); 106 | } 107 | 108 | User.tokenPassRecovery = bcrypt.genSaltSync().replace(/[^a-zA-Z0-9-_]/g, ''); 109 | 110 | const expiry = new Date(); 111 | expiry.setHours(expiry.getHours() + 8); 112 | User.tokenPassRecoveryExpiryDate = expiry; 113 | 114 | return User.save() 115 | .then(updated => { 116 | emailService.sendRecoveryEmail(User); 117 | 118 | return { 119 | msg: `Recovery email sent to ${User.username} (${User.email}). 120 | Please follow the email instructions to recover your account.`, 121 | }; 122 | }); 123 | }); 124 | }; 125 | 126 | // Validate reset pasword token 127 | service.validateReset = (token) => { 128 | return Users.findOne({ 129 | where: { 130 | tokenPassRecovery: token, 131 | tokenPassRecoveryExpiryDate: { $gt: new Date() }, 132 | }, 133 | attributes: ['firstName', 'lastName', 'username'], 134 | }).then(user => { 135 | if (!user) { 136 | throw new Error('Invalid/Expired token.'); 137 | } 138 | return { msg: 'Your password recovery token is valid, please set your new password' }; 139 | }); 140 | }; 141 | 142 | // Do password reset 143 | service.resetPassword = (token, password, verifyPassword) => { 144 | if (!password || !verifyPassword || password !== verifyPassword) { 145 | throw new Error('Invalid password'); 146 | } 147 | 148 | return Users.findOne({ 149 | where: { 150 | tokenPassRecovery: token, 151 | tokenPassRecoveryExpiryDate: { $gt: new Date() }, 152 | }, 153 | }) 154 | .then(user => { 155 | if (!user) { 156 | throw new Error('Invalid or expired token'); 157 | } 158 | const salt = bcrypt.genSaltSync(); 159 | 160 | user.password = bcrypt.hashSync(password, salt); 161 | user.tokenPassRecovery = null; 162 | user.tokenPassRecoveryExpiryDate = null; 163 | 164 | // if recovery password then validate user email 165 | if (user.status === 'not_validated') { 166 | user.status = 'validated'; 167 | user.tokenValidate = null; 168 | } 169 | 170 | return user.save() 171 | .then(updated => { 172 | return { msg: 'Password successfully changed. Please login again to continue.' }; 173 | }); 174 | }); 175 | }; 176 | 177 | module.exports = service; 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mean-relational", 3 | "version": "1.0.1", 4 | "description": "API server boilerplate using Node/Express/Sequelize", 5 | "main": "api/index.js", 6 | "scripts": { 7 | "postinstall": "node node_modules/bower/bin/bower install", 8 | "start": "npm run apidoc && npm run clusters", 9 | "clusters": "babel-node api/clusters.server.js", 10 | "dev": "better-npm-run dev", 11 | "lint": "eslint client/web/app/modules", 12 | "lint:fix": "npm run lint -- --fix", 13 | "test": "NODE_ENV=test gulp test", 14 | "coverage": "gulp test-coverage", 15 | "coveralls": "gulp publish-coverage-report", 16 | "apidoc": "apidoc -i api/routes/ -o client/web/dist/apidoc", 17 | "deploy": "better-npm-run deploy", 18 | "snyk-protect": "snyk protect", 19 | "prepublish": "npm run snyk-protect" 20 | }, 21 | "apidoc": { 22 | "name": "Node Task API - Documentation", 23 | "template": { 24 | "forceLanguage": "en" 25 | } 26 | }, 27 | "betterScripts": { 28 | "dev": { 29 | "command": "nodemon --exec babel-node api/index.js", 30 | "env": { 31 | "NODE_ENV": "development", 32 | "DEBUG": "app:*" 33 | } 34 | }, 35 | "deploy": { 36 | "command": "echo TODO", 37 | "env": { 38 | "NODE_ENV": "production", 39 | "DEBUG": "app:*" 40 | } 41 | } 42 | }, 43 | "author": "Facundo Munoz", 44 | "dependencies": { 45 | "acl": "^0.4.9", 46 | "apidoc": "^0.15.1", 47 | "babel-cli": "^6.5.1", 48 | "babel-core": "^6.7.7", 49 | "babel-eslint": "^5.0.0", 50 | "babel-loader": "^6.2.4", 51 | "babel-plugin-transform-runtime": "^6.7.5", 52 | "babel-polyfill": "^6.7.4", 53 | "babel-preset-es2015": "^6.6.0", 54 | "babel-preset-stage-0": "^6.5.0", 55 | "babel-register": "^6.7.2", 56 | "babel-runtime": "^6.6.1", 57 | "bcrypt": "^0.8.5", 58 | "bcrypt-nodejs": "0.0.3", 59 | "better-npm-run": "0.0.5", 60 | "body-parser": "^1.15.0", 61 | "bower": "^1.7.9", 62 | "browser-sync": "~2.18.3", 63 | "chai": "^3.1.0", 64 | "chai-as-promised": "^5.1.0", 65 | "chalk": "^1.1.0", 66 | "compression": "^1.6.1", 67 | "consign": "^0.1.2", 68 | "cors": "^2.7.1", 69 | "dateformat": "^1.0.8-1.2.3", 70 | "debug": "^2.0.0", 71 | "del": "^1.2.0", 72 | "dirty-chai": "^1.2.2", 73 | "email-templates": "^2.5.3", 74 | "eslint": "^2.4.0", 75 | "eslint-config-airbnb": "^6.1.0", 76 | "eslint-plugin-flow-vars": "^0.1.3", 77 | "eslint-plugin-react": "^4.2.1", 78 | "estraverse-fb": "^1.3.1", 79 | "express": "^4.13.4", 80 | "glob": "^5.0.15", 81 | "gulp": "^3.9.1", 82 | "gulp-angular-templatecache": "^1.4.2", 83 | "gulp-apidoc": "^0.2.3", 84 | "gulp-autoprefixer": "^2.3.1", 85 | "gulp-babel": "^6.1.2", 86 | "gulp-bump": "^0.3.1", 87 | "gulp-bytediff": "^0.2.0", 88 | "gulp-cache": "^0.2.8", 89 | "gulp-concat": "^2.3.3", 90 | "gulp-eslint": "^2.0.0", 91 | "gulp-filter": "^2.0.2", 92 | "gulp-header": "^1.2.2", 93 | "gulp-if": "^2.0.0", 94 | "gulp-imagemin": "^2.3.0", 95 | "gulp-inject": "^1.0.1", 96 | "gulp-less": "^3.0.1", 97 | "gulp-load-plugins": "^1.0.0-rc.1", 98 | "gulp-minify-css": "^1.1.1", 99 | "gulp-minify-html": "^1.0.4", 100 | "gulp-mocha": "^3.0.0", 101 | "gulp-ng-annotate": "^1.0.0", 102 | "gulp-nodemon": "^2.0.3", 103 | "gulp-order": "^1.1.1", 104 | "gulp-plumber": "^1.0.1", 105 | "gulp-print": "^1.1.0", 106 | "gulp-protractor": "^3.0.0", 107 | "gulp-rev": "^5.1.0", 108 | "gulp-rev-replace": "^0.4.2", 109 | "gulp-sass": "^2.0.0", 110 | "gulp-size": "^1.2.1", 111 | "gulp-sourcemaps": "^1.6.0", 112 | "gulp-task-listing": "^1.0.0", 113 | "gulp-traceur-compiler": "^1.1.4", 114 | "gulp-uglify": "^1.0.1", 115 | "gulp-useref": "^2.0.0", 116 | "gulp-util": "^3.0.1", 117 | "gulp.spritesmith": "^6.0.0", 118 | "helmet": "^1.1.0", 119 | "istanbul": "^0.4.3", 120 | "jasmine": "^2.4.1", 121 | "jwt-simple": "^0.4.1", 122 | "karma": "^0.13.2", 123 | "karma-babel-preprocessor": "^6.0.1", 124 | "karma-chai": "^0.1.0", 125 | "karma-chai-sinon": "^0.1.3", 126 | "karma-chrome-launcher": "^0.2.0", 127 | "karma-cli": "^0.1.2", 128 | "karma-coverage": "^0.5.3", 129 | "karma-firefox-launcher": "^0.1.3", 130 | "karma-growl-reporter": "^0.1.1", 131 | "karma-jasmine": "^0.3.8", 132 | "karma-mocha": "^0.2.0", 133 | "karma-phantomjs-launcher": "^1.0.0", 134 | "karma-safari-launcher": "^0.1.1", 135 | "karma-sinon": "^1.0.3", 136 | "lodash": "^3.10.0", 137 | "main-bower-files": "^2.5.0", 138 | "method-override": "^2.3.4", 139 | "minimist": "^1.1.0", 140 | "mocha": "^3.0.0", 141 | "mongoose": "^4.5.9", 142 | "morgan": "^1.6.1", 143 | "node-bourbon": "^4.2.3", 144 | "node-notifier": "^4.0.3", 145 | "node-uuid": "^1.4.7", 146 | "nodemailer": "^2.3.0", 147 | "nodemailer-mandrill-transport": "^1.2.0", 148 | "nodemon": "^1.8.1", 149 | "opn": "^1.0.1", 150 | "passport": "^0.3.2", 151 | "passport-jwt": "^2.0.0", 152 | "pg": "^6.1.0", 153 | "phantomjs-prebuilt": "^2.1.4", 154 | "plato": "^1.2.0", 155 | "q": "^1.0.1", 156 | "run-sequence": "^1.2.2", 157 | "sequelize": "^3.19.2", 158 | "sinon": "^1.12.2", 159 | "sinon-chai": "^2.6.0", 160 | "snyk": "^1.19.1", 161 | "socket.io": "^1.4.6", 162 | "sqlite3": "^3.1.1", 163 | "supertest": "^1.2.0", 164 | "winston": "^2.1.1", 165 | "wiredep": "^3.0.1", 166 | "yargs": "^3.15.0" 167 | }, 168 | "eslintConfig": { 169 | "env": { 170 | "node": true, 171 | "browser": true 172 | }, 173 | "rules": { 174 | "quotes": [ 175 | 2, 176 | "single" 177 | ] 178 | } 179 | }, 180 | "devDependencies": { 181 | "gulp-coveralls": "^0.1.4", 182 | "gulp-istanbul": "^1.1.1", 183 | "isparta": "^4.0.0", 184 | "lcov-result-merger": "^1.2.0" 185 | }, 186 | "snyk": true 187 | } 188 | -------------------------------------------------------------------------------- /src/server/tests/routes/signup.js: -------------------------------------------------------------------------------- 1 | import Users from './../../models/users'; 2 | import Sessions from './../../models/sessions'; 3 | 4 | describe('Routes: Signup', () => { 5 | let session; 6 | before(done => { 7 | Sessions.destroy({ where: {} }) 8 | .then(() => { 9 | return Users.destroy({ where: {} }); 10 | }) 11 | .then(() => { 12 | done(); 13 | }); 14 | }); 15 | describe('POST /signup', () => { 16 | describe('status 200', () => { 17 | it('returns partial user and session', done => { 18 | request.post('/api/v1/signup') 19 | .send({ 20 | username: 'jhon', 21 | email: 'jhon.doe@yopmail.com', 22 | }) 23 | .expect(200) 24 | .end((err, res) => { 25 | expect(res.body.User.username).to.eql('jhon'); 26 | expect(res.body.User.email).to.eql('jhon.doe@yopmail.com'); 27 | expect(res.body.Session.authToken).to.not.be.undefined; 28 | done(err); 29 | }); 30 | }); 31 | }); 32 | }); 33 | describe('GET /signup/validate/:token', () => { 34 | describe('status 200', () => { 35 | it('validate signup token (email validation)', done => { 36 | Users.findOne({ where: { email: 'jhon.doe@yopmail.com' } }) 37 | .then(User => { 38 | request.get(`/api/v1/signup/validate/${User.dataValues.tokenValidate}`) 39 | .expect(200) 40 | .end((err, res) => { 41 | expect(res.body.User.username).to.eql('jhon'); 42 | expect(res.body.User.email).to.eql('jhon.doe@yopmail.com'); 43 | expect(res.body.User.status).to.eql('validated'); 44 | expect(res.body.msg).to.eql('Your email account was validated'); 45 | expect(res.body.Session.authToken).to.not.be.undefined; 46 | // 47 | session = res.body.Session; 48 | // 49 | done(err); 50 | }); 51 | }) 52 | .catch(err => { 53 | done(err); 54 | }); 55 | }); 56 | }); 57 | }); 58 | describe('POST /signup/profile', () => { 59 | describe('status 200', () => { 60 | it('store user basic profile, expect error for not matching passwords', done => { 61 | request.post('/api/v1/signup/profile') 62 | .set('Authorization', `JWT ${session.authToken}`) 63 | .send({ 64 | password: 'P@ssword!', 65 | verifyPassword: 'OtherPassword#', 66 | }) 67 | .expect(412) 68 | .end((err, res) => { 69 | expect(res.body.msg).to.eql('Passwords must match'); 70 | done(err); 71 | }); 72 | }); 73 | }); 74 | }); 75 | describe('POST /signup/profile', () => { 76 | describe('status 200', () => { 77 | it('store user basic profile, expect error for password to short', done => { 78 | request.post('/api/v1/signup/profile') 79 | .set('Authorization', `JWT ${session.authToken}`) 80 | .send({ 81 | password: 'short', 82 | verifyPassword: 'short', 83 | }) 84 | .expect(412) 85 | .end((err, res) => { 86 | expect(res.body.msg).to.eql('Password too short'); 87 | done(err); 88 | }); 89 | }); 90 | }); 91 | }); 92 | describe('POST /signup/profile', () => { 93 | describe('status 200', () => { 94 | it('store user basic profile', done => { 95 | request.post('/api/v1/signup/profile') 96 | .set('Authorization', `JWT ${session.authToken}`) 97 | .send({ 98 | password: 'P@ssword!', 99 | verifyPassword: 'P@ssword!', 100 | firstName: 'Jhon', 101 | lastName: 'Doe', 102 | }) 103 | .expect(200) 104 | .end((err, res) => { 105 | expect(res.body.Session).to.not.be.undefined; 106 | expect(res.body.User).to.not.be.undefined; 107 | expect(res.body.msg).to.eql('Your profile was saved'); 108 | expect(res.body.User.username).to.eql('jhon'); 109 | expect(res.body.User.email).to.eql('jhon.doe@yopmail.com'); 110 | expect(res.body.User.firstName).to.eql('Jhon'); 111 | expect(res.body.User.lastName).to.eql('Doe'); 112 | expect(res.body.User.status).to.eql('not_active'); 113 | session = res.body.Session; 114 | done(err); 115 | }); 116 | }); 117 | }); 118 | }); 119 | describe('POST /signup/profile', () => { 120 | describe('status 200', () => { 121 | it('store user basic profile, expect error when profile already stored', done => { 122 | request.post('/api/v1/signup/profile') 123 | .set('Authorization', `JWT ${session.authToken}`) 124 | .send({ 125 | password: 'P@ssword!', 126 | verifyPassword: 'P@ssword!', 127 | firstName: 'Jhon', 128 | lastName: 'Doe', 129 | }) 130 | .expect(412) 131 | .end((err, res) => { 132 | expect(res.body.msg).to.eql('Invalid user status'); 133 | done(err); 134 | }); 135 | }); 136 | }); 137 | }); 138 | describe('POST /signup/activate', () => { 139 | describe('status 200', () => { 140 | it('activate user account after fill user profile', done => { 141 | request.post('/api/v1/signup/activate') 142 | .set('Authorization', `JWT ${session.authToken}`) 143 | .expect(200) 144 | .end((err, res) => { 145 | expect(res.body.msg).to.eql('Welcome Jhon Doe!'); 146 | expect(res.body.User.status).to.eql('active'); 147 | done(err); 148 | }); 149 | }); 150 | }); 151 | }); 152 | describe('POST /signup/activate', () => { 153 | describe('status 200', () => { 154 | it('try to activate user account when already active or invalid status', done => { 155 | request.post('/api/v1/signup/activate') 156 | .set('Authorization', `JWT ${session.authToken}`) 157 | .expect(412) 158 | .end((err, res) => { 159 | expect(res.body.msg).to.eql('Invalid user'); 160 | done(err); 161 | }); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/client/styles/styles.less: -------------------------------------------------------------------------------- 1 | @import "theme.less"; 2 | 3 | #content { 4 | padding-top: 25px; 5 | } 6 | 7 | .navbar-default .navbar-brand { 8 | padding: 7px 15px; 9 | } 10 | 11 | .navbar-default .navbar-brand img { 12 | width: 170px; 13 | } 14 | 15 | #toast-container.toast-bottom-full-width > div, 16 | #toast-container.toast-top-full-width > div { 17 | margin: 4px auto; 18 | } 19 | 20 | .page-header { 21 | margin-top: 10px; 22 | margin-bottom: 30px; 23 | } 24 | 25 | .btn-models { 26 | margin-bottom: 20px; 27 | } 28 | 29 | #splash-page { 30 | z-index: 99999 !important; 31 | } 32 | 33 | #splash-page .bar { 34 | width: 100%; 35 | } 36 | 37 | .page-splash { 38 | z-index: 99999 !important; 39 | position: fixed !important; 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | background-color: @brand-primary; 45 | opacity: 0.9; 46 | pointer-events: auto; 47 | -webkit-backface-visibility: hidden; 48 | -moz-backface-visibility: hidden; 49 | -ms-backface-visibility: hidden; 50 | -o-backface-visibility: hidden; 51 | backface-visibility: hidden; 52 | -webkit-transition: opacity 0.3s linear; 53 | -moz-transition: opacity 0.3s linear; 54 | -o-transition: opacity 0.3s linear; 55 | transition: opacity 0.3s linear; 56 | } 57 | 58 | .page-splash-message { 59 | text-align: center; 60 | margin: 20% auto 0; 61 | font-size: 400%; 62 | font-family: "Segoe UI", Arial, Helvetica, sans-serif; 63 | font-weight: normal; 64 | -webkit-text-shadow: 2px 2px #000000; 65 | text-shadow: 2px 2px #000000; 66 | text-shadow: 2px 2px rgba(0, 0, 0, 0.15); 67 | text-transform: uppercase; 68 | text-decoration: none; 69 | color: #fff; 70 | padding: 0; 71 | } 72 | 73 | .page-splash-message.page-splash-message-subtle { 74 | margin: 30% auto 0; 75 | font-size: 200%; 76 | } 77 | 78 | .pointer { 79 | cursor: pointer; 80 | } 81 | 82 | .models-list-search { 83 | height: auto; 84 | max-height: 200px; 85 | overflow-x: hidden; 86 | } 87 | 88 | .table-reports { 89 | width: 100%; 90 | margin-bottom: 20px; 91 | } 92 | 93 | .width-10 { 94 | width: 10%; 95 | } 96 | 97 | .width-25 { 98 | width: 25%; 99 | } 100 | 101 | .width-40 { 102 | width: 40%; 103 | } 104 | 105 | .border { 106 | border: 1px solid black; 107 | } 108 | 109 | .border-l { 110 | border-left: 1px solid black; 111 | } 112 | 113 | .border-r { 114 | border-right: 1px solid black; 115 | } 116 | 117 | .border-t { 118 | border-top: 1px solid black; 119 | } 120 | 121 | .border-b { 122 | border-bottom: 1px solid black; 123 | } 124 | 125 | .padding-b-12 { 126 | padding-bottom: 12px; 127 | } 128 | 129 | .padding-b-20 { 130 | padding-bottom: 20px; 131 | } 132 | 133 | .padding-l-15 { 134 | padding-left: 15px; 135 | } 136 | 137 | .padding-t-20 { 138 | padding-top: 20px; 139 | } 140 | 141 | .margin-t-20 { 142 | margin-top: 20px; 143 | } 144 | 145 | .margin-b-20 { 146 | margin-bottom: 20px; 147 | } 148 | 149 | .css-form input.ng-invalid.ng-touched { 150 | border: 2px solid #FFBABA; 151 | } 152 | 153 | .css-form input.ng-valid.ng-touched { 154 | border: 2px solid #DFF2BF; 155 | } 156 | 157 | .css-form .error-msg { 158 | color: #D8000C; 159 | } 160 | 161 | .big-icon { 162 | font-size: 200%; 163 | } 164 | 165 | .login-div { 166 | margin-top: 5%; 167 | } 168 | 169 | @media print { 170 | .no-print { 171 | display: none !important; 172 | } 173 | 174 | .page-break { 175 | page-break-before: always !important; 176 | } 177 | } 178 | 179 | .pagination-margin { 180 | margin: 0px 0; 181 | } 182 | 183 | .bred { 184 | background: @color_important !important; 185 | color: #fff !important; 186 | border: 0 !important; 187 | } 188 | 189 | .widget { 190 | margin-top: 10px; 191 | margin-bottom: 20px; 192 | background: #fff; 193 | } 194 | 195 | .widget hr { 196 | margin: 4px 0; 197 | padding: 4px 0; 198 | border-top: 0; 199 | border-bottom: 1px solid #ddd; 200 | } 201 | 202 | .widget .table { 203 | margin: 0; 204 | width: 100%; 205 | } 206 | 207 | .widget .table-bordered { 208 | border: 0; 209 | } 210 | 211 | .widget .table-bordered th { 212 | border-bottom: 1px solid #ccc !important; 213 | } 214 | 215 | .widget .table-bordered td { 216 | border-top: 0 !important; 217 | border-bottom: 1px solid #ccc !important; 218 | } 219 | 220 | .widget .table-bordered td:first-child, 221 | .widget .table-bordered th:first-child { 222 | border-left: 0; 223 | } 224 | 225 | .widget .padd { 226 | padding: 15px; 227 | } 228 | 229 | .widget .widget-head { 230 | background-color: #f5f5f5; 231 | border: 1px solid #ddd; 232 | color: #777; 233 | font-size: 18px; 234 | padding: 12px 15px; 235 | 236 | &.collapsive { 237 | cursor: pointer; 238 | } 239 | } 240 | 241 | .widget .widget-head .widget-icons i { 242 | font-size: 14px; 243 | margin: 0 4px; 244 | } 245 | 246 | .widget .widget-head .widget-icons a { 247 | color: #aaa; 248 | } 249 | 250 | .widget .widget-head .widget-icons a:hover { 251 | color: #888; 252 | } 253 | 254 | .widget .widget-content { 255 | border-left: 1px solid #ddd; 256 | border-right: 1px solid #ddd; 257 | border-bottom: 1px solid #ddd; 258 | } 259 | 260 | .widget .widget-foot { 261 | background-color: #f9f9f9; 262 | border: 1px solid #ddd; 263 | border-top: 0; 264 | padding: 8px 15px; 265 | font-size: 13px; 266 | color: #555; 267 | } 268 | /* Widget colors */ 269 | .widget.wblue .widget-head { 270 | background-color: @color_blue; 271 | border: 1px solid @color_blue; 272 | color: #fff; 273 | } 274 | 275 | .widget.wblue .widget-head .widget-icons a { 276 | color: #fff; 277 | } 278 | 279 | .widget.wblue .widget-head .widget-icons a:hover { 280 | color: #eee; 281 | } 282 | /* Today datas */ 283 | .today-datas { 284 | list-style-type: none; 285 | padding: 0; 286 | margin: 10px 0; 287 | } 288 | 289 | .today-datas li { 290 | display: inline-block; 291 | margin-bottom: 5px; 292 | margin-right: 10px; 293 | padding: 1.5em 1em; 294 | background-color: #f8f8f8; 295 | background: #f8f8f8; 296 | border: 1px solid #ccc; 297 | max-width: 100%; 298 | text-align: center; 299 | } 300 | 301 | .today-datas li .datas-text { 302 | font-size: 13px; 303 | padding: 7px 0 0; 304 | font-weight: normal; 305 | } 306 | 307 | .today-datas li .datas-text span { 308 | display: block; 309 | font-size: 24px; 310 | margin-bottom: 5px; 311 | } 312 | 313 | .today-datas li i { 314 | font-size: 50px; 315 | margin-right: 10px; 316 | } 317 | -------------------------------------------------------------------------------- /src/server/routes/tasks.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import errors from './../error'; 3 | import acl from './../config/acl'; 4 | import tasksService from './../services/tasks'; 5 | 6 | /** 7 | * Tasks policy 8 | * ACL configuration 9 | */ 10 | acl.allow([{ 11 | roles: ['admin'], 12 | allows: [{ 13 | resources: '/api/v1/tasks', 14 | permissions: '*', 15 | }, 16 | { 17 | resources: '/api/v1/tasks/paginated', 18 | permissions: '*', 19 | }, { 20 | resources: '/api/v1/tasks/count', 21 | permissions: '*', 22 | }, { 23 | resources: '/api/v1/tasks/:taskId', 24 | permissions: '*', 25 | }], 26 | }, { 27 | roles: ['user'], 28 | allows: [{ 29 | resources: '/api/v1/tasks', 30 | permissions: ['get', 'post'], 31 | }, { 32 | resources: '/api/v1/tasks/:taskId', 33 | permissions: '*', 34 | }], 35 | }]); 36 | 37 | const router = express.Router(); 38 | 39 | /** 40 | * @api {get} /tasks List the user's tasks 41 | * @apiGroup Tasks 42 | * @apiHeader {String} Authorization Token of authenticated user 43 | * @apiHeaderExample {json} Header 44 | * {"Authorization": "JWT xyz.abc.123.hgf"} 45 | * @apiSuccess {Object[]} tasks Task's list 46 | * @apiSuccess {Number} tasks.id Task id 47 | * @apiSuccess {String} tasks.title Task title 48 | * @apiSuccess {Boolean} tasks.done Task is done? 49 | * @apiSuccess {Date} tasks.updated_at Update's date 50 | * @apiSuccess {Date} tasks.created_at Register's date 51 | * @apiSuccess {Number} tasks.user_id Id do usuário 52 | * @apiSuccessExample {json} Success 53 | * HTTP/1.1 200 OK 54 | * [{ 55 | * "id": 1, 56 | * "title": "Study", 57 | * "done": false 58 | * "updated_at": "2016-02-10T15:46:51.778Z", 59 | * "created_at": "2016-02-10T15:46:51.778Z", 60 | * "user_id": 1 61 | * }] 62 | * @apiErrorExample {json} List error 63 | * HTTP/1.1 412 Precondition Failed 64 | */ 65 | router.get('/api/v1/tasks', acl.checkRoles, (req, res) => { 66 | tasksService.getAll(req.User) 67 | .then(result => res.json(result)) 68 | .catch(error => res.status(412).json(errors.get(error))); 69 | }); 70 | 71 | router.get('/api/v1/tasks/paginated', acl.checkRoles, (req, res) => { 72 | tasksService.getPaginated(req.User, req.query) 73 | .then(result => res.json(result)) 74 | .catch(error => res.status(412).json(errors.get(error))); 75 | }); 76 | 77 | router.get('/api/v1/tasks/count', acl.checkRoles, (req, res) => { 78 | tasksService.getCount(req.query) 79 | .then(result => res.json(result)) 80 | .catch(error => res.status(412).json(errors.get(error))); 81 | }); 82 | 83 | 84 | /** 85 | * @api {post} /tasks Register a new task 86 | * @apiGroup Tasks 87 | * @apiHeader {String} Authorization Token of authenticated user 88 | * @apiHeaderExample {json} Header 89 | * {"Authorization": "JWT xyz.abc.123.hgf"} 90 | * @apiParam {String} title Task title 91 | * @apiParamExample {json} Input 92 | * {"title": "Study"} 93 | * @apiSuccess {Number} id Task id 94 | * @apiSuccess {String} title Task title 95 | * @apiSuccess {Boolean} done=false Task is done? 96 | * @apiSuccess {Date} updated_at Update's date 97 | * @apiSuccess {Date} created_at Register's date 98 | * @apiSuccess {Number} user_id User id 99 | * @apiSuccessExample {json} Success 100 | * HTTP/1.1 200 OK 101 | * { 102 | * "id": 1, 103 | * "title": "Study", 104 | * "done": false, 105 | * "updated_at": "2016-02-10T15:46:51.778Z", 106 | * "created_at": "2016-02-10T15:46:51.778Z", 107 | * "user_id": 1 108 | * } 109 | * @apiErrorExample {json} Register error 110 | * HTTP/1.1 412 Precondition Failed 111 | */ 112 | router.post('/api/v1/tasks', acl.checkRoles, (req, res) => { 113 | req.body.user_id = req.User.id; 114 | tasksService.create(req.body) 115 | .then(result => res.json(result)) 116 | .catch(error => res.status(412).json(errors.get(error))); 117 | }); 118 | 119 | 120 | /** 121 | * @api {get} /tasks/:id Get a task 122 | * @apiGroup Tasks 123 | * @apiHeader {String} Authorization Token of authenticated user 124 | * @apiHeaderExample {json} Header 125 | * {"Authorization": "JWT xyz.abc.123.hgf"} 126 | * @apiParam {id} id Task id 127 | * @apiSuccess {Number} id Task id 128 | * @apiSuccess {String} title Task title 129 | * @apiSuccess {Boolean} done Task is done? 130 | * @apiSuccess {Date} updated_at Update's date 131 | * @apiSuccess {Date} created_at Register's date 132 | * @apiSuccess {Number} user_id User id 133 | * @apiSuccessExample {json} Success 134 | * HTTP/1.1 200 OK 135 | * { 136 | * "id": 1, 137 | * "title": "Study", 138 | * "done": false 139 | * "updated_at": "2016-02-10T15:46:51.778Z", 140 | * "created_at": "2016-02-10T15:46:51.778Z", 141 | * "user_id": 1 142 | * } 143 | * @apiErrorExample {json} Task not found error 144 | * HTTP/1.1 404 Not Found 145 | * @apiErrorExample {json} Find error 146 | * HTTP/1.1 412 Precondition Failed 147 | */ 148 | router.get('/api/v1/tasks/:taskId', acl.checkRoles, (req, res) => { 149 | tasksService.findById(req.params.taskId, req.User) 150 | .then(result => { 151 | if (result) { 152 | res.json(result); 153 | } else { 154 | res.sendStatus(404); 155 | } 156 | }) 157 | .catch(error => res.status(412).json(errors.get(error))); 158 | }); 159 | 160 | 161 | /** 162 | * @api {put} /tasks/:id Update a task 163 | * @apiGroup Tasks 164 | * @apiHeader {String} Authorization Token of authenticated user 165 | * @apiHeaderExample {json} Header 166 | * {"Authorization": "JWT xyz.abc.123.hgf"} 167 | * @apiParam {id} id Task id 168 | * @apiParam {String} title Task title 169 | * @apiParam {Boolean} done Task is done? 170 | * @apiParamExample {json} Input 171 | * { 172 | * "title": "Work", 173 | * "done": true 174 | * } 175 | * @apiSuccessExample {json} Success 176 | * HTTP/1.1 204 No Content 177 | * @apiErrorExample {json} Update error 178 | * HTTP/1.1 412 Precondition Failed 179 | */ 180 | router.put('/api/v1/tasks/:taskId', acl.checkRoles, (req, res) => { 181 | tasksService.update(req.params.taskId, req.body, req.User) 182 | .then(result => res.sendStatus(204)) 183 | .catch(error => res.status(412).json(errors.get(error))); 184 | }); 185 | 186 | 187 | /** 188 | * @api {delete} /tasks/:id Remove a task 189 | * @apiGroup Tasks 190 | * @apiHeader {String} Authorization Token of authenticated user 191 | * @apiHeaderExample {json} Header 192 | * {"Authorization": "JWT xyz.abc.123.hgf"} 193 | * @apiParam {id} id Task id 194 | * @apiSuccessExample {json} Success 195 | * HTTP/1.1 204 No Content 196 | * @apiErrorExample {json} Delete error 197 | * HTTP/1.1 412 Precondition Failed 198 | */ 199 | router.delete('/api/v1/tasks/:taskId', acl.checkRoles, (req, res) => { 200 | tasksService.destroy(req.params.taskId, req.User) 201 | .then(result => res.sendStatus(204)) 202 | .catch(error => res.status(412).json(errors.get(error))); 203 | }); 204 | 205 | export default router; 206 | --------------------------------------------------------------------------------