├── .buildignore ├── .gitattributes ├── Procfile ├── client ├── robots.txt ├── app │ ├── admin │ │ ├── admin.scss │ │ ├── admin.js │ │ ├── admin.controller.js │ │ └── admin.html │ ├── main │ │ ├── main.js │ │ ├── main.scss │ │ ├── main.controller.js │ │ ├── main.controller.spec.js │ │ └── main.html │ ├── account │ │ ├── account.js │ │ ├── settings │ │ │ ├── settings.controller.js │ │ │ └── settings.html │ │ ├── login │ │ │ ├── login.controller.js │ │ │ ├── login.scss │ │ │ └── login.html │ │ └── signup │ │ │ ├── signup.controller.js │ │ │ └── signup.html │ ├── app.scss │ └── app.js ├── assets │ └── images │ │ └── yeoman.png ├── components │ ├── socket │ │ ├── socket.mock.js │ │ └── socket.service.js │ ├── mongoose-error │ │ └── mongoose-error.directive.js │ ├── auth │ │ ├── user.service.js │ │ └── auth.service.js │ ├── modal │ │ ├── modal.html │ │ ├── modal.scss │ │ └── modal.service.js │ └── navbar │ │ ├── navbar.controller.js │ │ └── navbar.html ├── .jshintrc ├── favicon.ico ├── index.html └── .htaccess ├── .bowerrc ├── .gitignore ├── .travis.yml ├── server ├── .jshintrc-spec ├── config │ ├── environment │ │ ├── test.js │ │ ├── development.js │ │ ├── production.js │ │ └── index.js │ ├── local.env.sample.js │ ├── seed.js │ ├── socketio.js │ └── express.js ├── api │ ├── thing │ │ ├── thing.model.js │ │ ├── index.js │ │ ├── thing.spec.js │ │ ├── thing.socket.js │ │ └── thing.controller.js │ └── user │ │ ├── index.js │ │ ├── user.model.spec.js │ │ ├── user.controller.js │ │ └── user.model.js ├── .jshintrc ├── components │ └── errors │ │ └── index.js ├── auth │ ├── twitter │ │ ├── index.js │ │ └── passport.js │ ├── index.js │ ├── local │ │ ├── index.js │ │ └── passport.js │ ├── google │ │ ├── index.js │ │ └── passport.js │ └── auth.service.js ├── routes.js ├── app.js └── views │ └── 404.html ├── e2e └── main │ ├── main.po.js │ └── main.spec.js ├── .editorconfig ├── bower.json ├── readme.md ├── .yo-rc.json ├── protractor.conf.js ├── karma.conf.js ├── package.json └── Gruntfile.js /.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/app.js 2 | -------------------------------------------------------------------------------- /client/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "client/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /client/app/admin/admin.scss: -------------------------------------------------------------------------------- 1 | .trash { color:rgb(209, 91, 71); } 2 | -------------------------------------------------------------------------------- /client/assets/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/fullstack-demo/master/client/assets/images/yeoman.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | .tmp 4 | .sass-cache 5 | .idea 6 | client/bower_components 7 | dist 8 | /server/config/local.env.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.11' 5 | before_script: 6 | - npm install -g bower grunt-cli 7 | - gem install sass 8 | - bower install 9 | services: mongodb -------------------------------------------------------------------------------- /server/.jshintrc-spec: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".jshintrc", 3 | "globals": { 4 | "describe": true, 5 | "it": true, 6 | "before": true, 7 | "beforeEach": true, 8 | "after": true, 9 | "afterEach": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/config/environment/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Test specific configuration 4 | // =========================== 5 | module.exports = { 6 | // MongoDB connection options 7 | mongo: { 8 | uri: 'mongodb://localhost/demo-test' 9 | } 10 | }; -------------------------------------------------------------------------------- /client/app/main/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .config(function ($stateProvider) { 5 | $stateProvider 6 | .state('main', { 7 | url: '/', 8 | templateUrl: 'app/main/main.html', 9 | controller: 'MainCtrl' 10 | }); 11 | }); -------------------------------------------------------------------------------- /client/app/admin/admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .config(function ($stateProvider) { 5 | $stateProvider 6 | .state('admin', { 7 | url: '/admin', 8 | templateUrl: 'app/admin/admin.html', 9 | controller: 'AdminCtrl' 10 | }); 11 | }); -------------------------------------------------------------------------------- /server/api/thing/thing.model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'), 4 | Schema = mongoose.Schema; 5 | 6 | var ThingSchema = new Schema({ 7 | name: String, 8 | info: String, 9 | active: Boolean 10 | }); 11 | 12 | module.exports = mongoose.model('Thing', ThingSchema); -------------------------------------------------------------------------------- /server/config/environment/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Development specific configuration 4 | // ================================== 5 | module.exports = { 6 | // MongoDB connection options 7 | mongo: { 8 | uri: 'mongodb://localhost/demo-dev' 9 | }, 10 | 11 | seedDB: true 12 | }; 13 | -------------------------------------------------------------------------------- /server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "latedef": "nofunc", 8 | "newcap": true, 9 | "noarg": true, 10 | "regexp": true, 11 | "undef": true, 12 | "smarttabs": true, 13 | "asi": true, 14 | "debug": true 15 | } 16 | -------------------------------------------------------------------------------- /client/components/socket/socket.mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('socketMock', []) 4 | .factory('socket', function() { 5 | return { 6 | socket: { 7 | connect: function() {}, 8 | on: function() {}, 9 | emit: function() {}, 10 | receive: function() {} 11 | }, 12 | 13 | syncUpdates: function() {}, 14 | unsyncUpdates: function() {} 15 | }; 16 | }); -------------------------------------------------------------------------------- /server/api/thing/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var controller = require('./thing.controller'); 5 | 6 | var router = express.Router(); 7 | 8 | router.get('/', controller.index); 9 | router.get('/:id', controller.show); 10 | router.post('/', controller.create); 11 | router.put('/:id', controller.update); 12 | router.patch('/:id', controller.update); 13 | router.delete('/:id', controller.destroy); 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /e2e/main/main.po.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file uses the Page Object pattern to define the main page for tests 3 | * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var MainPage = function() { 9 | this.heroEl = element(by.css('.hero-unit')); 10 | this.h1El = this.heroEl.element(by.css('h1')); 11 | this.imgEl = this.heroEl.element(by.css('img')); 12 | }; 13 | 14 | module.exports = new MainPage(); 15 | 16 | -------------------------------------------------------------------------------- /server/components/errors/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error responses 3 | */ 4 | 5 | 'use strict'; 6 | 7 | module.exports[404] = function pageNotFound(req, res) { 8 | var viewFilePath = '404'; 9 | var statusCode = 404; 10 | var result = { 11 | status: statusCode 12 | }; 13 | 14 | res.status(result.status); 15 | res.render(viewFilePath, function (err) { 16 | if (err) { return res.json(result, result.status); } 17 | 18 | res.render(viewFilePath); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /client/components/mongoose-error/mongoose-error.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Removes server error when user updates input 5 | */ 6 | angular.module('demoApp') 7 | .directive('mongooseError', function () { 8 | return { 9 | restrict: 'A', 10 | require: 'ngModel', 11 | link: function(scope, element, attrs, ngModel) { 12 | element.on('keydown', function() { 13 | return ngModel.$setValidity('mongoose', true); 14 | }); 15 | } 16 | }; 17 | }); -------------------------------------------------------------------------------- /e2e/main/main.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Main View', function() { 4 | var page; 5 | 6 | beforeEach(function() { 7 | browser.get('/'); 8 | page = require('./main.po'); 9 | }); 10 | 11 | it('should include jumbotron with correct data', function() { 12 | expect(page.h1El.getText()).toBe('\'Allo, \'Allo!'); 13 | expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/); 14 | expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /client/app/admin/admin.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .controller('AdminCtrl', function ($scope, $http, Auth, User) { 5 | 6 | // Use the User $resource to fetch all users 7 | $scope.users = User.query(); 8 | 9 | $scope.delete = function(user) { 10 | User.remove({ id: user._id }); 11 | angular.forEach($scope.users, function(u, i) { 12 | if (u === user) { 13 | $scope.users.splice(i, 1); 14 | } 15 | }); 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /client/components/auth/user.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .factory('User', function ($resource) { 5 | return $resource('/api/users/:id/:controller', { 6 | id: '@_id' 7 | }, 8 | { 9 | changePassword: { 10 | method: 'PUT', 11 | params: { 12 | controller:'password' 13 | } 14 | }, 15 | get: { 16 | method: 'GET', 17 | params: { 18 | id:'me' 19 | } 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /server/auth/twitter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var passport = require('passport'); 5 | var auth = require('../auth.service'); 6 | 7 | var router = express.Router(); 8 | 9 | router 10 | .get('/', passport.authenticate('twitter', { 11 | failureRedirect: '/signup', 12 | session: false 13 | })) 14 | 15 | .get('/callback', passport.authenticate('twitter', { 16 | failureRedirect: '/signup', 17 | session: false 18 | }), auth.setTokenCookie); 19 | 20 | module.exports = router; -------------------------------------------------------------------------------- /client/app/admin/admin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

The delete user and user index api routes are restricted to users with the 'admin' role.

5 | 12 |
-------------------------------------------------------------------------------- /server/api/thing/thing.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var app = require('../../app'); 5 | var request = require('supertest'); 6 | 7 | describe('GET /api/things', function() { 8 | 9 | it('should respond with JSON array', function(done) { 10 | request(app) 11 | .get('/api/things') 12 | .expect(200) 13 | .expect('Content-Type', /json/) 14 | .end(function(err, res) { 15 | if (err) return done(err); 16 | res.body.should.be.instanceof(Array); 17 | done(); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/thing/thing.socket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Broadcast updates to client when the model changes 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var thing = require('./thing.model'); 8 | 9 | exports.register = function(socket) { 10 | thing.schema.post('save', function (doc) { 11 | onSave(socket, doc); 12 | }); 13 | thing.schema.post('remove', function (doc) { 14 | onRemove(socket, doc); 15 | }); 16 | } 17 | 18 | function onSave(socket, doc, cb) { 19 | socket.emit('thing:save', doc); 20 | } 21 | 22 | function onRemove(socket, doc, cb) { 23 | socket.emit('thing:remove', doc); 24 | } -------------------------------------------------------------------------------- /client/components/modal/modal.html: -------------------------------------------------------------------------------- 1 | 5 | 9 | -------------------------------------------------------------------------------- /client/components/modal/modal.scss: -------------------------------------------------------------------------------- 1 | .modal-primary, 2 | .modal-info, 3 | .modal-success, 4 | .modal-warning, 5 | .modal-danger { 6 | .modal-header { 7 | color: #fff; 8 | border-radius: 5px 5px 0 0; 9 | } 10 | } 11 | .modal-primary .modal-header { 12 | background: $brand-primary; 13 | } 14 | .modal-info .modal-header { 15 | background: $brand-info; 16 | } 17 | .modal-success .modal-header { 18 | background: $brand-success; 19 | } 20 | .modal-warning .modal-header { 21 | background: $brand-warning; 22 | } 23 | .modal-danger .modal-header { 24 | background: $brand-danger; 25 | } 26 | -------------------------------------------------------------------------------- /client/app/main/main.scss: -------------------------------------------------------------------------------- 1 | .thing-form { 2 | margin: 20px 0; 3 | } 4 | 5 | #banner { 6 | border-bottom: none; 7 | margin-top: -20px; 8 | } 9 | 10 | #banner h1 { 11 | font-size: 60px; 12 | line-height: 1; 13 | letter-spacing: -1px; 14 | } 15 | 16 | .hero-unit { 17 | position: relative; 18 | padding: 30px 15px; 19 | color: #F5F5F5; 20 | text-align: center; 21 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); 22 | background: #4393B9; 23 | } 24 | 25 | .footer { 26 | text-align: center; 27 | padding: 30px 0; 28 | margin-top: 70px; 29 | border-top: 1px solid #E5E5E5; 30 | } -------------------------------------------------------------------------------- /server/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var passport = require('passport'); 5 | var config = require('../config/environment'); 6 | var User = require('../api/user/user.model'); 7 | 8 | // Passport Configuration 9 | require('./local/passport').setup(User, config); 10 | require('./google/passport').setup(User, config); 11 | require('./twitter/passport').setup(User, config); 12 | 13 | var router = express.Router(); 14 | 15 | router.use('/local', require('./local')); 16 | router.use('/twitter', require('./twitter')); 17 | router.use('/google', require('./google')); 18 | 19 | module.exports = router; -------------------------------------------------------------------------------- /client/components/navbar/navbar.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .controller('NavbarCtrl', function ($scope, $location, Auth) { 5 | $scope.menu = [{ 6 | 'title': 'Home', 7 | 'link': '/' 8 | }]; 9 | 10 | $scope.isCollapsed = true; 11 | $scope.isLoggedIn = Auth.isLoggedIn; 12 | $scope.isAdmin = Auth.isAdmin; 13 | $scope.getCurrentUser = Auth.getCurrentUser; 14 | 15 | $scope.logout = function() { 16 | Auth.logout(); 17 | $location.path('/login'); 18 | }; 19 | 20 | $scope.isActive = function(route) { 21 | return route === $location.path(); 22 | }; 23 | }); -------------------------------------------------------------------------------- /server/auth/local/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var passport = require('passport'); 5 | var auth = require('../auth.service'); 6 | 7 | var router = express.Router(); 8 | 9 | router.post('/', function(req, res, next) { 10 | passport.authenticate('local', function (err, user, info) { 11 | var error = err || info; 12 | if (error) return res.json(401, error); 13 | if (!user) return res.json(404, {message: 'Something went wrong, please try again.'}); 14 | 15 | var token = auth.signToken(user._id, user.role); 16 | res.json({token: token}); 17 | })(req, res, next) 18 | }); 19 | 20 | module.exports = router; -------------------------------------------------------------------------------- /server/config/local.env.sample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use local.env.js for environment variables that grunt will set when the server starts locally. 4 | // Use for your api keys, secrets, etc. This file should not be tracked by git. 5 | // 6 | // You will need to set these on the server you deploy to. 7 | 8 | module.exports = { 9 | DOMAIN: 'http://localhost:9000', 10 | SESSION_SECRET: 'demo-secret', 11 | 12 | TWITTER_ID: 'app-id', 13 | TWITTER_SECRET: 'secret', 14 | 15 | GOOGLE_ID: 'app-id', 16 | GOOGLE_SECRET: 'secret', 17 | 18 | // Control debug level for modules using visionmedia/debug 19 | DEBUG: '' 20 | }; 21 | -------------------------------------------------------------------------------- /server/auth/google/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var passport = require('passport'); 5 | var auth = require('../auth.service'); 6 | 7 | var router = express.Router(); 8 | 9 | router 10 | .get('/', passport.authenticate('google', { 11 | failureRedirect: '/signup', 12 | scope: [ 13 | 'https://www.googleapis.com/auth/userinfo.profile', 14 | 'https://www.googleapis.com/auth/userinfo.email' 15 | ], 16 | session: false 17 | })) 18 | 19 | .get('/callback', passport.authenticate('google', { 20 | failureRedirect: '/signup', 21 | session: false 22 | }), auth.setTokenCookie); 23 | 24 | module.exports = router; -------------------------------------------------------------------------------- /client/app/account/account.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .config(function ($stateProvider) { 5 | $stateProvider 6 | .state('login', { 7 | url: '/login', 8 | templateUrl: 'app/account/login/login.html', 9 | controller: 'LoginCtrl' 10 | }) 11 | .state('signup', { 12 | url: '/signup', 13 | templateUrl: 'app/account/signup/signup.html', 14 | controller: 'SignupCtrl' 15 | }) 16 | .state('settings', { 17 | url: '/settings', 18 | templateUrl: 'app/account/settings/settings.html', 19 | controller: 'SettingsCtrl', 20 | authenticate: true 21 | }); 22 | }); -------------------------------------------------------------------------------- /client/app/app.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "/bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/"; 2 | $fa-font-path: "/bower_components/font-awesome/fonts"; 3 | 4 | @import 'bootstrap-sass-official/vendor/assets/stylesheets/bootstrap'; 5 | @import 'font-awesome/scss/font-awesome'; 6 | 7 | /** 8 | * App-wide Styles 9 | */ 10 | 11 | .browsehappy { 12 | margin: 0.2em 0; 13 | background: #ccc; 14 | color: #000; 15 | padding: 0.2em 0; 16 | } 17 | 18 | // Component styles are injected through grunt 19 | // injector 20 | @import 'account/login/login.scss'; 21 | @import 'admin/admin.scss'; 22 | @import 'main/main.scss'; 23 | @import 'modal/modal.scss'; 24 | // endinjector -------------------------------------------------------------------------------- /server/api/user/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var controller = require('./user.controller'); 5 | var config = require('../../config/environment'); 6 | var auth = require('../../auth/auth.service'); 7 | 8 | var router = express.Router(); 9 | 10 | router.get('/', auth.hasRole('admin'), controller.index); 11 | router.delete('/:id', auth.hasRole('admin'), controller.destroy); 12 | router.get('/me', auth.isAuthenticated(), controller.me); 13 | router.put('/:id/password', auth.isAuthenticated(), controller.changePassword); 14 | router.get('/:id', auth.isAuthenticated(), controller.show); 15 | router.post('/', controller.create); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": ">=1.2.*", 6 | "json3": "~3.3.1", 7 | "es5-shim": "~3.0.1", 8 | "jquery": "~1.11.0", 9 | "bootstrap-sass-official": "~3.1.1", 10 | "bootstrap": "~3.1.1", 11 | "angular-resource": ">=1.2.*", 12 | "angular-cookies": ">=1.2.*", 13 | "angular-sanitize": ">=1.2.*", 14 | "angular-bootstrap": "~0.11.0", 15 | "font-awesome": ">=4.1.0", 16 | "lodash": "~2.4.1", 17 | "angular-socket-io": "~0.6.0", 18 | "angular-ui-router": "~0.2.10" 19 | }, 20 | "devDependencies": { 21 | "angular-mocks": ">=1.2.*", 22 | "angular-scenario": ">=1.2.*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/config/environment/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Production specific configuration 4 | // ================================= 5 | module.exports = { 6 | // Server IP 7 | ip: process.env.OPENSHIFT_NODEJS_IP || 8 | process.env.IP || 9 | undefined, 10 | 11 | // Server port 12 | port: process.env.OPENSHIFT_NODEJS_PORT || 13 | process.env.PORT || 14 | 8080, 15 | 16 | // MongoDB connection options 17 | mongo: { 18 | uri: process.env.MONGOLAB_URI || 19 | process.env.MONGOHQ_URL || 20 | process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME || 21 | 'mongodb://localhost/demo' 22 | } 23 | }; -------------------------------------------------------------------------------- /client/app/account/settings/settings.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .controller('SettingsCtrl', function ($scope, User, Auth) { 5 | $scope.errors = {}; 6 | 7 | $scope.changePassword = function(form) { 8 | $scope.submitted = true; 9 | if(form.$valid) { 10 | Auth.changePassword( $scope.user.oldPassword, $scope.user.newPassword ) 11 | .then( function() { 12 | $scope.message = 'Password successfully changed.'; 13 | }) 14 | .catch( function() { 15 | form.password.$setValidity('mongoose', false); 16 | $scope.errors.other = 'Incorrect password'; 17 | $scope.message = ''; 18 | }); 19 | } 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application routes 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var errors = require('./components/errors'); 8 | 9 | module.exports = function(app) { 10 | 11 | // Insert routes below 12 | app.use('/api/things', require('./api/thing')); 13 | app.use('/api/users', require('./api/user')); 14 | 15 | app.use('/auth', require('./auth')); 16 | 17 | // All undefined asset or api routes should return a 404 18 | app.route('/:url(api|auth|components|app|bower_components|assets)/*') 19 | .get(errors[404]); 20 | 21 | // All other routes should redirect to the index.html 22 | app.route('/*') 23 | .get(function(req, res) { 24 | res.sendfile(app.get('appPath') + '/index.html'); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /client/app/main/main.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .controller('MainCtrl', function ($scope, $http, socket) { 5 | $scope.awesomeThings = []; 6 | 7 | $http.get('/api/things').success(function(awesomeThings) { 8 | $scope.awesomeThings = awesomeThings; 9 | socket.syncUpdates('thing', $scope.awesomeThings); 10 | }); 11 | 12 | $scope.addThing = function() { 13 | if($scope.newThing === '') { 14 | return; 15 | } 16 | $http.post('/api/things', { name: $scope.newThing }); 17 | $scope.newThing = ''; 18 | }; 19 | 20 | $scope.deleteThing = function(thing) { 21 | $http.delete('/api/things/' + thing._id); 22 | }; 23 | 24 | $scope.$on('$destroy', function () { 25 | socket.unsyncUpdates('thing'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "jQuery": true, 23 | "angular": true, 24 | "console": true, 25 | "$": true, 26 | "_": true, 27 | "moment": true, 28 | "describe": true, 29 | "beforeEach": true, 30 | "module": true, 31 | "inject": true, 32 | "it": true, 33 | "expect": true, 34 | "browser": true, 35 | "element": true, 36 | "by": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/app/account/login/login.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .controller('LoginCtrl', function ($scope, Auth, $location, $window) { 5 | $scope.user = {}; 6 | $scope.errors = {}; 7 | 8 | $scope.login = function(form) { 9 | $scope.submitted = true; 10 | 11 | if(form.$valid) { 12 | Auth.login({ 13 | email: $scope.user.email, 14 | password: $scope.user.password 15 | }) 16 | .then( function() { 17 | // Logged in, redirect to home 18 | $location.path('/'); 19 | }) 20 | .catch( function(err) { 21 | $scope.errors.other = err.message; 22 | }); 23 | } 24 | }; 25 | 26 | $scope.loginOauth = function(provider) { 27 | $window.location.href = '/auth/' + provider; 28 | }; 29 | }); 30 | -------------------------------------------------------------------------------- /server/auth/local/passport.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | var LocalStrategy = require('passport-local').Strategy; 3 | 4 | exports.setup = function (User, config) { 5 | passport.use(new LocalStrategy({ 6 | usernameField: 'email', 7 | passwordField: 'password' // this is the virtual field on the model 8 | }, 9 | function(email, password, done) { 10 | User.findOne({ 11 | email: email.toLowerCase() 12 | }, function(err, user) { 13 | if (err) return done(err); 14 | 15 | if (!user) { 16 | return done(null, false, { message: 'This email is not registered.' }); 17 | } 18 | if (!user.authenticate(password)) { 19 | return done(null, false, { message: 'This password is not correct.' }); 20 | } 21 | return done(null, user); 22 | }); 23 | } 24 | )); 25 | }; -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Fullstack demo generated using [Angular Fullstack][1] [![Code Climate](https://codeclimate.com/github/DaftMonk/fullstack-demo/badges/gpa.svg)](https://codeclimate.com/github/DaftMonk/fullstack-demo) 2 | 3 | [Angular Fullstack][1] is a MEAN generator. It scaffolds applications with MongoDB, Express, AngularJS, and Node. 4 | 5 | By running `yo angular-fullstack` and using defaults, you get a project very similar to this. This repo is to give you an idea of what that project looks like. 6 | 7 | ## Live Demo 8 | 9 | http://fullstack-demo.herokuapp.com/. 10 | 11 | ## Quick Install 12 | 13 | Easily create your own project as specified here: https://github.com/DaftMonk/generator-angular-fullstack#usage 14 | 15 | Then launch your express server in development mode using 16 | 17 | $ grunt serve 18 | 19 | [1]: https://github.com/DaftMonk/generator-angular-fullstack 20 | -------------------------------------------------------------------------------- /client/app/main/main.controller.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('demoApp')); 7 | beforeEach(module('socketMock')); 8 | 9 | var MainCtrl, 10 | scope, 11 | $httpBackend; 12 | 13 | // Initialize the controller and a mock scope 14 | beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) { 15 | $httpBackend = _$httpBackend_; 16 | $httpBackend.expectGET('/api/things') 17 | .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); 18 | 19 | scope = $rootScope.$new(); 20 | MainCtrl = $controller('MainCtrl', { 21 | $scope: scope 22 | }); 23 | })); 24 | 25 | it('should attach a list of things to the scope', function () { 26 | $httpBackend.flush(); 27 | expect(scope.awesomeThings.length).toBe(4); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /client/app/account/login/login.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | // -------------------------------------------------- 3 | 4 | $btnText: #fff; 5 | $btnTextAlt: #000; 6 | 7 | $btnTwitterBackground: #2daddc; 8 | $btnTwitterBackgroundHighlight: #0271bf; 9 | $btnGooglePlusBackground: #dd4b39; 10 | $btnGooglePlusBackgroundHighlight: #c53727; 11 | $btnGithubBackground: #fafafa; 12 | $btnGithubBackgroundHighlight: #ccc; 13 | 14 | // Social buttons 15 | // -------------------------------------------------- 16 | 17 | .btn-twitter { 18 | @include button-variant($btnText, $btnTwitterBackground, $btnTwitterBackgroundHighlight); 19 | } 20 | .btn-google-plus { 21 | @include button-variant($btnText, $btnGooglePlusBackground, $btnGooglePlusBackgroundHighlight); 22 | } 23 | .btn-github { 24 | @include button-variant($btnTextAlt, $btnGithubBackground, $btnGithubBackgroundHighlight); 25 | } 26 | -------------------------------------------------------------------------------- /server/auth/twitter/passport.js: -------------------------------------------------------------------------------- 1 | exports.setup = function (User, config) { 2 | var passport = require('passport'); 3 | var TwitterStrategy = require('passport-twitter').Strategy; 4 | 5 | passport.use(new TwitterStrategy({ 6 | consumerKey: config.twitter.clientID, 7 | consumerSecret: config.twitter.clientSecret, 8 | callbackURL: config.twitter.callbackURL 9 | }, 10 | function(token, tokenSecret, profile, done) { 11 | User.findOne({ 12 | 'twitter.id_str': profile.id 13 | }, function(err, user) { 14 | if (err) { 15 | return done(err); 16 | } 17 | if (!user) { 18 | user = new User({ 19 | name: profile.displayName, 20 | username: profile.username, 21 | role: 'user', 22 | provider: 'twitter', 23 | twitter: profile._json 24 | }); 25 | user.save(function(err) { 26 | if (err) return done(err); 27 | return done(err, user); 28 | }); 29 | } else { 30 | return done(err, user); 31 | } 32 | }); 33 | } 34 | )); 35 | }; -------------------------------------------------------------------------------- /server/auth/google/passport.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 3 | 4 | exports.setup = function (User, config) { 5 | passport.use(new GoogleStrategy({ 6 | clientID: config.google.clientID, 7 | clientSecret: config.google.clientSecret, 8 | callbackURL: config.google.callbackURL 9 | }, 10 | function(accessToken, refreshToken, profile, done) { 11 | User.findOne({ 12 | 'google.id': profile.id 13 | }, function(err, user) { 14 | if (!user) { 15 | user = new User({ 16 | name: profile.displayName, 17 | email: profile.emails[0].value, 18 | role: 'user', 19 | username: profile.username, 20 | provider: 'google', 21 | google: profile._json 22 | }); 23 | user.save(function(err) { 24 | if (err) done(err); 25 | return done(err, user); 26 | }); 27 | } else { 28 | return done(err, user); 29 | } 30 | }); 31 | } 32 | )); 33 | }; 34 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application file 3 | */ 4 | 5 | 'use strict'; 6 | 7 | // Set default node environment to development 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 9 | 10 | var express = require('express'); 11 | var mongoose = require('mongoose'); 12 | var config = require('./config/environment'); 13 | 14 | // Connect to database 15 | mongoose.connect(config.mongo.uri, config.mongo.options); 16 | 17 | // Populate DB with sample data 18 | if(config.seedDB) { require('./config/seed'); } 19 | 20 | // Setup server 21 | var app = express(); 22 | var server = require('http').createServer(app); 23 | var socketio = require('socket.io')(server, { 24 | serveClient: (config.env === 'production') ? false : true, 25 | path: '/socket.io-client' 26 | }); 27 | require('./config/socketio')(socketio); 28 | require('./config/express')(app); 29 | require('./routes')(app); 30 | 31 | // Start server 32 | server.listen(config.port, config.ip, function () { 33 | console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); 34 | }); 35 | 36 | // Expose app 37 | exports = module.exports = app; -------------------------------------------------------------------------------- /client/app/account/signup/signup.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .controller('SignupCtrl', function ($scope, Auth, $location, $window) { 5 | $scope.user = {}; 6 | $scope.errors = {}; 7 | 8 | $scope.register = function(form) { 9 | $scope.submitted = true; 10 | 11 | if(form.$valid) { 12 | Auth.createUser({ 13 | name: $scope.user.name, 14 | email: $scope.user.email, 15 | password: $scope.user.password 16 | }) 17 | .then( function() { 18 | // Account created, redirect to home 19 | $location.path('/'); 20 | }) 21 | .catch( function(err) { 22 | err = err.data; 23 | $scope.errors = {}; 24 | 25 | // Update validity of form fields that match the mongoose errors 26 | angular.forEach(err.errors, function(error, field) { 27 | form[field].$setValidity('mongoose', false); 28 | $scope.errors[field] = error.message; 29 | }); 30 | }); 31 | } 32 | }; 33 | 34 | $scope.loginOauth = function(provider) { 35 | $window.location.href = '/auth/' + provider; 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-angular-fullstack": { 3 | "insertRoutes": true, 4 | "registerRoutesFile": "server/routes.js", 5 | "routesNeedle": "// Insert routes below", 6 | "routesBase": "/api/", 7 | "pluralizeRoutes": true, 8 | "insertSockets": true, 9 | "registerSocketsFile": "server/config/socketio.js", 10 | "socketsNeedle": "// Insert sockets below", 11 | "filters": { 12 | "js": true, 13 | "html": true, 14 | "sass": true, 15 | "uirouter": true, 16 | "bootstrap": true, 17 | "uibootstrap": true, 18 | "socketio": true, 19 | "mongoose": true, 20 | "auth": true, 21 | "oauth": true, 22 | "googleAuth": true, 23 | "twitterAuth": true 24 | } 25 | }, 26 | "generator-ng-component": { 27 | "routeDirectory": "client/app/", 28 | "directiveDirectory": "client/app/", 29 | "filterDirectory": "client/app/", 30 | "serviceDirectory": "client/app/", 31 | "basePath": "client", 32 | "moduleName": "", 33 | "filters": [ 34 | "uirouter" 35 | ], 36 | "extensions": [ 37 | "js", 38 | "html", 39 | "scss" 40 | ], 41 | "directiveSimpleTemplates": "", 42 | "directiveComplexTemplates": "", 43 | "filterTemplates": "", 44 | "serviceTemplates": "", 45 | "factoryTemplates": "", 46 | "controllerTemplates": "", 47 | "decoratorTemplates": "", 48 | "providerTemplates": "", 49 | "routeTemplates": "" 50 | } 51 | } -------------------------------------------------------------------------------- /client/app/account/settings/settings.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

Change Password

7 |
8 |
9 |
10 | 11 |
12 | 13 | 14 | 16 |

17 | {{ errors.other }} 18 |

19 |
20 | 21 |
22 | 23 | 24 | 27 |

29 | Password must be at least 3 characters. 30 |

31 |
32 | 33 |

{{ message }}

34 | 35 | 36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /client/app/main/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | 11 |
12 |
13 |
14 |

Features:

15 | 18 |
19 |
20 | 21 |
22 | 23 |

24 | 25 | 26 | 27 | 28 |

29 |
30 |
31 | 32 | 39 | -------------------------------------------------------------------------------- /server/api/user/user.model.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var app = require('../../app'); 5 | var User = require('./user.model'); 6 | 7 | var user = new User({ 8 | provider: 'local', 9 | name: 'Fake User', 10 | email: 'test@test.com', 11 | password: 'password' 12 | }); 13 | 14 | describe('User Model', function() { 15 | before(function(done) { 16 | // Clear users before testing 17 | User.remove().exec().then(function() { 18 | done(); 19 | }); 20 | }); 21 | 22 | afterEach(function(done) { 23 | User.remove().exec().then(function() { 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should begin with no users', function(done) { 29 | User.find({}, function(err, users) { 30 | users.should.have.length(0); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should fail when saving a duplicate user', function(done) { 36 | user.save(function() { 37 | var userDup = new User(user); 38 | userDup.save(function(err) { 39 | should.exist(err); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | it('should fail when saving without an email', function(done) { 46 | user.email = ''; 47 | user.save(function(err) { 48 | should.exist(err); 49 | done(); 50 | }); 51 | }); 52 | 53 | it("should authenticate user if password is valid", function() { 54 | return user.authenticate('password').should.be.true; 55 | }); 56 | 57 | it("should not authenticate user if password is invalid", function() { 58 | return user.authenticate('blah').should.not.be.true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /client/components/navbar/navbar.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /client/app/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp', [ 4 | 'ngCookies', 5 | 'ngResource', 6 | 'ngSanitize', 7 | 'btford.socket-io', 8 | 'ui.router', 9 | 'ui.bootstrap' 10 | ]) 11 | .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { 12 | $urlRouterProvider 13 | .otherwise('/'); 14 | 15 | $locationProvider.html5Mode(true); 16 | $httpProvider.interceptors.push('authInterceptor'); 17 | }) 18 | 19 | .factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) { 20 | return { 21 | // Add authorization token to headers 22 | request: function (config) { 23 | config.headers = config.headers || {}; 24 | if ($cookieStore.get('token')) { 25 | config.headers.Authorization = 'Bearer ' + $cookieStore.get('token'); 26 | } 27 | return config; 28 | }, 29 | 30 | // Intercept 401s and redirect you to login 31 | responseError: function(response) { 32 | if(response.status === 401) { 33 | $location.path('/login'); 34 | // remove any stale tokens 35 | $cookieStore.remove('token'); 36 | return $q.reject(response); 37 | } 38 | else { 39 | return $q.reject(response); 40 | } 41 | } 42 | }; 43 | }) 44 | 45 | .run(function ($rootScope, $location, Auth) { 46 | // Redirect to login if route requires auth and you're not logged in 47 | $rootScope.$on('$stateChangeStart', function (event, next) { 48 | Auth.isLoggedInAsync(function(loggedIn) { 49 | if (next.authenticate && !loggedIn) { 50 | $location.path('/login'); 51 | } 52 | }); 53 | }); 54 | }); -------------------------------------------------------------------------------- /server/config/environment/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | 6 | function requiredProcessEnv(name) { 7 | if(!process.env[name]) { 8 | throw new Error('You must set the ' + name + ' environment variable'); 9 | } 10 | return process.env[name]; 11 | } 12 | 13 | // All configurations will extend these options 14 | // ============================================ 15 | var all = { 16 | env: process.env.NODE_ENV, 17 | 18 | // Root path of server 19 | root: path.normalize(__dirname + '/../../..'), 20 | 21 | // Server port 22 | port: process.env.PORT || 9000, 23 | 24 | // Should we populate the DB with sample data? 25 | seedDB: false, 26 | 27 | // Secret for session, you will want to change this and make it an environment variable 28 | secrets: { 29 | session: 'demo-secret' 30 | }, 31 | 32 | // List of user roles 33 | userRoles: ['guest', 'user', 'admin'], 34 | 35 | // MongoDB connection options 36 | mongo: { 37 | options: { 38 | db: { 39 | safe: true 40 | } 41 | } 42 | }, 43 | 44 | twitter: { 45 | clientID: process.env.TWITTER_ID || 'id', 46 | clientSecret: process.env.TWITTER_SECRET || 'secret', 47 | callbackURL: (process.env.DOMAIN || '') + '/auth/twitter/callback' 48 | }, 49 | 50 | google: { 51 | clientID: process.env.GOOGLE_ID || 'id', 52 | clientSecret: process.env.GOOGLE_SECRET || 'secret', 53 | callbackURL: (process.env.DOMAIN || '') + '/auth/google/callback' 54 | } 55 | }; 56 | 57 | // Export the config object based on the NODE_ENV 58 | // ============================================== 59 | module.exports = _.merge( 60 | all, 61 | require('./' + process.env.NODE_ENV + '.js') || {}); -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration 2 | // https://github.com/angular/protractor/blob/master/referenceConf.js 3 | 4 | 'use strict'; 5 | 6 | exports.config = { 7 | // The timeout for each script run on the browser. This should be longer 8 | // than the maximum time your application needs to stabilize between tasks. 9 | allScriptsTimeout: 110000, 10 | 11 | // A base URL for your application under test. Calls to protractor.get() 12 | // with relative paths will be prepended with this. 13 | baseUrl: 'http://localhost:' + (process.env.PORT || '9000'), 14 | 15 | // If true, only chromedriver will be started, not a standalone selenium. 16 | // Tests for browsers other than chrome will not run. 17 | chromeOnly: true, 18 | 19 | // list of files / patterns to load in the browser 20 | specs: [ 21 | 'e2e/**/*.spec.js' 22 | ], 23 | 24 | // Patterns to exclude. 25 | exclude: [], 26 | 27 | // ----- Capabilities to be passed to the webdriver instance ---- 28 | // 29 | // For a full list of available capabilities, see 30 | // https://code.google.com/p/selenium/wiki/DesiredCapabilities 31 | // and 32 | // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js 33 | capabilities: { 34 | 'browserName': 'chrome' 35 | }, 36 | 37 | // ----- The test framework ----- 38 | // 39 | // Jasmine and Cucumber are fully supported as a test and assertion framework. 40 | // Mocha has limited beta support. You will need to include your own 41 | // assertion framework if working with mocha. 42 | framework: 'jasmine', 43 | 44 | // ----- Options to be passed to minijasminenode ----- 45 | // 46 | // See the full list at https://github.com/juliemr/minijasminenode 47 | jasmineNodeOpts: { 48 | defaultTimeoutInterval: 30000 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /server/config/seed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with sample data on server start 3 | * to disable, edit config/environment/index.js, and set `seedDB: false` 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var Thing = require('../api/thing/thing.model'); 9 | var User = require('../api/user/user.model'); 10 | 11 | Thing.find({}).remove(function() { 12 | Thing.create({ 13 | name : 'Development Tools', 14 | info : 'Integration with popular tools such as Bower, Grunt, Karma, Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, Stylus, Sass, CoffeeScript, and Less.' 15 | }, { 16 | name : 'Server and Client integration', 17 | info : 'Built with a powerful and fun stack: MongoDB, Express, AngularJS, and Node.' 18 | }, { 19 | name : 'Smart Build System', 20 | info : 'Build system ignores `spec` files, allowing you to keep tests alongside code. Automatic injection of scripts and styles into your index.html' 21 | }, { 22 | name : 'Modular Structure', 23 | info : 'Best practice client and server structures allow for more code reusability and maximum scalability' 24 | }, { 25 | name : 'Optimized Build', 26 | info : 'Build process packs up your templates as a single JavaScript payload, minifies your scripts/css/images, and rewrites asset names for caching.' 27 | },{ 28 | name : 'Deployment Ready', 29 | info : 'Easily deploy your app to Heroku or Openshift with the heroku and openshift subgenerators' 30 | }); 31 | }); 32 | 33 | User.find({}).remove(function() { 34 | User.create({ 35 | provider: 'local', 36 | name: 'Test User', 37 | email: 'test@test.com', 38 | password: 'test' 39 | }, { 40 | provider: 'local', 41 | role: 'admin', 42 | name: 'Admin', 43 | email: 'admin@admin.com', 44 | password: 'admin' 45 | }, function() { 46 | console.log('finished populating users'); 47 | } 48 | ); 49 | }); -------------------------------------------------------------------------------- /server/config/socketio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket.io configuration 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var config = require('./environment'); 8 | 9 | // When the user disconnects.. perform this 10 | function onDisconnect(socket) { 11 | } 12 | 13 | // When the user connects.. perform this 14 | function onConnect(socket) { 15 | // When the client emits 'info', this listens and executes 16 | socket.on('info', function (data) { 17 | console.info('[%s] %s', socket.address, JSON.stringify(data, null, 2)); 18 | }); 19 | 20 | // Insert sockets below 21 | require('../api/thing/thing.socket').register(socket); 22 | } 23 | 24 | module.exports = function (socketio) { 25 | // socket.io (v1.x.x) is powered by debug. 26 | // In order to see all the debug output, set DEBUG (in server/config/local.env.js) to including the desired scope. 27 | // 28 | // ex: DEBUG: "http*,socket.io:socket" 29 | 30 | // We can authenticate socket.io users and access their token through socket.handshake.decoded_token 31 | // 32 | // 1. You will need to send the token in `client/components/socket/socket.service.js` 33 | // 34 | // 2. Require authentication here: 35 | // socketio.use(require('socketio-jwt').authorize({ 36 | // secret: config.secrets.session, 37 | // handshake: true 38 | // })); 39 | 40 | socketio.on('connection', function (socket) { 41 | socket.address = socket.handshake.address !== null ? 42 | socket.handshake.address.address + ':' + socket.handshake.address.port : 43 | process.env.DOMAIN; 44 | 45 | socket.connectedAt = new Date(); 46 | 47 | // Call onDisconnect. 48 | socket.on('disconnect', function () { 49 | onDisconnect(socket); 50 | console.info('[%s] DISCONNECTED', socket.address); 51 | }); 52 | 53 | // Call onConnect. 54 | onConnect(socket); 55 | console.info('[%s] CONNECTED', socket.address); 56 | }); 57 | }; -------------------------------------------------------------------------------- /server/api/thing/thing.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Using Rails-like standard naming convention for endpoints. 3 | * GET /things -> index 4 | * POST /things -> create 5 | * GET /things/:id -> show 6 | * PUT /things/:id -> update 7 | * DELETE /things/:id -> destroy 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var _ = require('lodash'); 13 | var Thing = require('./thing.model'); 14 | 15 | // Get list of things 16 | exports.index = function(req, res) { 17 | Thing.find(function (err, things) { 18 | if(err) { return handleError(res, err); } 19 | return res.json(200, things); 20 | }); 21 | }; 22 | 23 | // Get a single thing 24 | exports.show = function(req, res) { 25 | Thing.findById(req.params.id, function (err, thing) { 26 | if(err) { return handleError(res, err); } 27 | if(!thing) { return res.send(404); } 28 | return res.json(thing); 29 | }); 30 | }; 31 | 32 | // Creates a new thing in the DB. 33 | exports.create = function(req, res) { 34 | Thing.create(req.body, function(err, thing) { 35 | if(err) { return handleError(res, err); } 36 | return res.json(201, thing); 37 | }); 38 | }; 39 | 40 | // Updates an existing thing in the DB. 41 | exports.update = function(req, res) { 42 | if(req.body._id) { delete req.body._id; } 43 | Thing.findById(req.params.id, function (err, thing) { 44 | if (err) { return handleError(res, err); } 45 | if(!thing) { return res.send(404); } 46 | var updated = _.merge(thing, req.body); 47 | updated.save(function (err) { 48 | if (err) { return handleError(res, err); } 49 | return res.json(200, thing); 50 | }); 51 | }); 52 | }; 53 | 54 | // Deletes a thing from the DB. 55 | exports.destroy = function(req, res) { 56 | Thing.findById(req.params.id, function (err, thing) { 57 | if(err) { return handleError(res, err); } 58 | if(!thing) { return res.send(404); } 59 | thing.remove(function(err) { 60 | if(err) { return handleError(res, err); } 61 | return res.send(204); 62 | }); 63 | }); 64 | }; 65 | 66 | function handleError(res, err) { 67 | return res.send(500, err); 68 | } -------------------------------------------------------------------------------- /server/config/express.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Express configuration 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var express = require('express'); 8 | var favicon = require('serve-favicon'); 9 | var morgan = require('morgan'); 10 | var compression = require('compression'); 11 | var bodyParser = require('body-parser'); 12 | var methodOverride = require('method-override'); 13 | var cookieParser = require('cookie-parser'); 14 | var errorHandler = require('errorhandler'); 15 | var path = require('path'); 16 | var config = require('./environment'); 17 | var passport = require('passport'); 18 | var session = require('express-session'); 19 | var mongoStore = require('connect-mongo')(session); 20 | var mongoose = require('mongoose'); 21 | 22 | module.exports = function(app) { 23 | var env = app.get('env'); 24 | 25 | app.set('views', config.root + '/server/views'); 26 | app.engine('html', require('ejs').renderFile); 27 | app.set('view engine', 'html'); 28 | app.use(compression()); 29 | app.use(bodyParser.urlencoded({ extended: false })); 30 | app.use(bodyParser.json()); 31 | app.use(methodOverride()); 32 | app.use(cookieParser()); 33 | app.use(passport.initialize()); 34 | 35 | // Persist sessions with mongoStore 36 | // We need to enable sessions for passport twitter because its an oauth 1.0 strategy 37 | app.use(session({ 38 | secret: config.secrets.session, 39 | resave: true, 40 | saveUninitialized: true, 41 | store: new mongoStore({ mongoose_connection: mongoose.connection }) 42 | })); 43 | 44 | if ('production' === env) { 45 | app.use(favicon(path.join(config.root, 'public', 'favicon.ico'))); 46 | app.use(express.static(path.join(config.root, 'public'))); 47 | app.set('appPath', config.root + '/public'); 48 | app.use(morgan('dev')); 49 | } 50 | 51 | if ('development' === env || 'test' === env) { 52 | app.use(require('connect-livereload')()); 53 | app.use(express.static(path.join(config.root, '.tmp'))); 54 | app.use(express.static(path.join(config.root, 'client'))); 55 | app.set('appPath', 'client'); 56 | app.use(morgan('dev')); 57 | app.use(errorHandler()); // Error handler - has to be last 58 | } 59 | }; -------------------------------------------------------------------------------- /client/app/account/login/login.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

Login

7 |

Accounts are reset on server restart from server/config/seed.js. Default account is test@test.com / test

8 |

Admin account is admin@admin.com / admin

9 |
10 |
11 |
12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 |

27 | Please enter your email and password. 28 |

29 |

30 | Please enter a valid email. 31 |

32 | 33 |

{{ errors.other }}

34 |
35 | 36 |
37 | 40 | 41 | Register 42 | 43 |
44 | 45 |
46 |
47 | 48 | Connect with Google+ 49 | 50 | 51 | Connect with Twitter 52 | 53 |
54 |
55 |
56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /client/components/socket/socket.service.js: -------------------------------------------------------------------------------- 1 | /* global io */ 2 | 'use strict'; 3 | 4 | angular.module('demoApp') 5 | .factory('socket', function(socketFactory) { 6 | 7 | // socket.io now auto-configures its connection when we ommit a connection url 8 | var ioSocket = io('', { 9 | // Send auth token on connection, you will need to DI the Auth service above 10 | // 'query': 'token=' + Auth.getToken() 11 | path: '/socket.io-client' 12 | }); 13 | 14 | var socket = socketFactory({ 15 | ioSocket: ioSocket 16 | }); 17 | 18 | return { 19 | socket: socket, 20 | 21 | /** 22 | * Register listeners to sync an array with updates on a model 23 | * 24 | * Takes the array we want to sync, the model name that socket updates are sent from, 25 | * and an optional callback function after new items are updated. 26 | * 27 | * @param {String} modelName 28 | * @param {Array} array 29 | * @param {Function} cb 30 | */ 31 | syncUpdates: function (modelName, array, cb) { 32 | cb = cb || angular.noop; 33 | 34 | /** 35 | * Syncs item creation/updates on 'model:save' 36 | */ 37 | socket.on(modelName + ':save', function (item) { 38 | var oldItem = _.find(array, {_id: item._id}); 39 | var index = array.indexOf(oldItem); 40 | var event = 'created'; 41 | 42 | // replace oldItem if it exists 43 | // otherwise just add item to the collection 44 | if (oldItem) { 45 | array.splice(index, 1, item); 46 | event = 'updated'; 47 | } else { 48 | array.push(item); 49 | } 50 | 51 | cb(event, item, array); 52 | }); 53 | 54 | /** 55 | * Syncs removed items on 'model:remove' 56 | */ 57 | socket.on(modelName + ':remove', function (item) { 58 | var event = 'deleted'; 59 | _.remove(array, {_id: item._id}); 60 | cb(event, item, array); 61 | }); 62 | }, 63 | 64 | /** 65 | * Removes listeners for a models updates on the socket 66 | * 67 | * @param modelName 68 | */ 69 | unsyncUpdates: function (modelName) { 70 | socket.removeAllListeners(modelName + ':save'); 71 | socket.removeAllListeners(modelName + ':remove'); 72 | } 73 | }; 74 | }); 75 | -------------------------------------------------------------------------------- /server/auth/auth.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | var passport = require('passport'); 5 | var config = require('../config/environment'); 6 | var jwt = require('jsonwebtoken'); 7 | var expressJwt = require('express-jwt'); 8 | var compose = require('composable-middleware'); 9 | var User = require('../api/user/user.model'); 10 | var validateJwt = expressJwt({ secret: config.secrets.session }); 11 | 12 | /** 13 | * Attaches the user object to the request if authenticated 14 | * Otherwise returns 403 15 | */ 16 | function isAuthenticated() { 17 | return compose() 18 | // Validate jwt 19 | .use(function(req, res, next) { 20 | // allow access_token to be passed through query parameter as well 21 | if(req.query && req.query.hasOwnProperty('access_token')) { 22 | req.headers.authorization = 'Bearer ' + req.query.access_token; 23 | } 24 | validateJwt(req, res, next); 25 | }) 26 | // Attach user to request 27 | .use(function(req, res, next) { 28 | User.findById(req.user._id, function (err, user) { 29 | if (err) return next(err); 30 | if (!user) return res.send(401); 31 | 32 | req.user = user; 33 | next(); 34 | }); 35 | }); 36 | } 37 | 38 | /** 39 | * Checks if the user role meets the minimum requirements of the route 40 | */ 41 | function hasRole(roleRequired) { 42 | if (!roleRequired) throw new Error('Required role needs to be set'); 43 | 44 | return compose() 45 | .use(isAuthenticated()) 46 | .use(function meetsRequirements(req, res, next) { 47 | if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { 48 | next(); 49 | } 50 | else { 51 | res.send(403); 52 | } 53 | }); 54 | } 55 | 56 | /** 57 | * Returns a jwt token signed by the app secret 58 | */ 59 | function signToken(id) { 60 | return jwt.sign({ _id: id }, config.secrets.session, { expiresInMinutes: 60*5 }); 61 | } 62 | 63 | /** 64 | * Set token cookie directly for oAuth strategies 65 | */ 66 | function setTokenCookie(req, res) { 67 | if (!req.user) return res.json(404, { message: 'Something went wrong, please try again.'}); 68 | var token = signToken(req.user._id, req.user.role); 69 | res.cookie('token', JSON.stringify(token)); 70 | res.redirect('/'); 71 | } 72 | 73 | exports.isAuthenticated = isAuthenticated; 74 | exports.hasRole = hasRole; 75 | exports.signToken = signToken; 76 | exports.setTokenCookie = setTokenCookie; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'client/bower_components/jquery/dist/jquery.js', 15 | 'client/bower_components/angular/angular.js', 16 | 'client/bower_components/angular-mocks/angular-mocks.js', 17 | 'client/bower_components/angular-resource/angular-resource.js', 18 | 'client/bower_components/angular-cookies/angular-cookies.js', 19 | 'client/bower_components/angular-sanitize/angular-sanitize.js', 20 | 'client/bower_components/angular-route/angular-route.js', 21 | 'client/bower_components/angular-bootstrap/ui-bootstrap-tpls.js', 22 | 'client/bower_components/lodash/dist/lodash.compat.js', 23 | 'client/bower_components/angular-socket-io/socket.js', 24 | 'client/bower_components/angular-ui-router/release/angular-ui-router.js', 25 | 'client/app/app.js', 26 | 'client/app/app.coffee', 27 | 'client/app/**/*.js', 28 | 'client/app/**/*.coffee', 29 | 'client/components/**/*.js', 30 | 'client/components/**/*.coffee', 31 | 'client/app/**/*.jade', 32 | 'client/components/**/*.jade', 33 | 'client/app/**/*.html', 34 | 'client/components/**/*.html' 35 | ], 36 | 37 | preprocessors: { 38 | '**/*.jade': 'ng-jade2js', 39 | '**/*.html': 'html2js', 40 | '**/*.coffee': 'coffee', 41 | }, 42 | 43 | ngHtml2JsPreprocessor: { 44 | stripPrefix: 'client/' 45 | }, 46 | 47 | ngJade2JsPreprocessor: { 48 | stripPrefix: 'client/' 49 | }, 50 | 51 | // list of files / patterns to exclude 52 | exclude: [], 53 | 54 | // web server port 55 | port: 8080, 56 | 57 | // level of logging 58 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 59 | logLevel: config.LOG_INFO, 60 | 61 | 62 | // enable / disable watching file and executing tests whenever any file changes 63 | autoWatch: false, 64 | 65 | 66 | // Start these browsers, currently available: 67 | // - Chrome 68 | // - ChromeCanary 69 | // - Firefox 70 | // - Opera 71 | // - Safari (only Mac) 72 | // - PhantomJS 73 | // - IE (only Windows) 74 | browsers: ['PhantomJS'], 75 | 76 | 77 | // Continuous Integration mode 78 | // if true, it capture browsers, run tests and exit 79 | singleRun: false 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /client/components/modal/modal.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .factory('Modal', function ($rootScope, $modal) { 5 | /** 6 | * Opens a modal 7 | * @param {Object} scope - an object to be merged with modal's scope 8 | * @param {String} modalClass - (optional) class(es) to be applied to the modal 9 | * @return {Object} - the instance $modal.open() returns 10 | */ 11 | function openModal(scope, modalClass) { 12 | var modalScope = $rootScope.$new(); 13 | scope = scope || {}; 14 | modalClass = modalClass || 'modal-default'; 15 | 16 | angular.extend(modalScope, scope); 17 | 18 | return $modal.open({ 19 | templateUrl: 'components/modal/modal.html', 20 | windowClass: modalClass, 21 | scope: modalScope 22 | }); 23 | } 24 | 25 | // Public API here 26 | return { 27 | 28 | /* Confirmation modals */ 29 | confirm: { 30 | 31 | /** 32 | * Create a function to open a delete confirmation modal (ex. ng-click='myModalFn(name, arg1, arg2...)') 33 | * @param {Function} del - callback, ran when delete is confirmed 34 | * @return {Function} - the function to open the modal (ex. myModalFn) 35 | */ 36 | delete: function(del) { 37 | del = del || angular.noop; 38 | 39 | /** 40 | * Open a delete confirmation modal 41 | * @param {String} name - name or info to show on modal 42 | * @param {All} - any additional args are passed staight to del callback 43 | */ 44 | return function() { 45 | var args = Array.prototype.slice.call(arguments), 46 | name = args.shift(), 47 | deleteModal; 48 | 49 | deleteModal = openModal({ 50 | modal: { 51 | dismissable: true, 52 | title: 'Confirm Delete', 53 | html: '

Are you sure you want to delete ' + name + ' ?

', 54 | buttons: [{ 55 | classes: 'btn-danger', 56 | text: 'Delete', 57 | click: function(e) { 58 | deleteModal.close(e); 59 | } 60 | }, { 61 | classes: 'btn-default', 62 | text: 'Cancel', 63 | click: function(e) { 64 | deleteModal.dismiss(e); 65 | } 66 | }] 67 | } 68 | }, 'modal-danger'); 69 | 70 | deleteModal.result.then(function(event) { 71 | del.apply(event, args); 72 | }); 73 | }; 74 | } 75 | } 76 | }; 77 | }); 78 | -------------------------------------------------------------------------------- /server/api/user/user.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var User = require('./user.model'); 4 | var passport = require('passport'); 5 | var config = require('../../config/environment'); 6 | var jwt = require('jsonwebtoken'); 7 | 8 | var validationError = function(res, err) { 9 | return res.json(422, err); 10 | }; 11 | 12 | /** 13 | * Get list of users 14 | * restriction: 'admin' 15 | */ 16 | exports.index = function(req, res) { 17 | User.find({}, '-salt -hashedPassword', function (err, users) { 18 | if(err) return res.send(500, err); 19 | res.json(200, users); 20 | }); 21 | }; 22 | 23 | /** 24 | * Creates a new user 25 | */ 26 | exports.create = function (req, res, next) { 27 | var newUser = new User(req.body); 28 | newUser.provider = 'local'; 29 | newUser.role = 'user'; 30 | newUser.save(function(err, user) { 31 | if (err) return validationError(res, err); 32 | var token = jwt.sign({_id: user._id }, config.secrets.session, { expiresInMinutes: 60*5 }); 33 | res.json({ token: token }); 34 | }); 35 | }; 36 | 37 | /** 38 | * Get a single user 39 | */ 40 | exports.show = function (req, res, next) { 41 | var userId = req.params.id; 42 | 43 | User.findById(userId, function (err, user) { 44 | if (err) return next(err); 45 | if (!user) return res.send(401); 46 | res.json(user.profile); 47 | }); 48 | }; 49 | 50 | /** 51 | * Deletes a user 52 | * restriction: 'admin' 53 | */ 54 | exports.destroy = function(req, res) { 55 | User.findByIdAndRemove(req.params.id, function(err, user) { 56 | if(err) return res.send(500, err); 57 | return res.send(204); 58 | }); 59 | }; 60 | 61 | /** 62 | * Change a users password 63 | */ 64 | exports.changePassword = function(req, res, next) { 65 | var userId = req.user._id; 66 | var oldPass = String(req.body.oldPassword); 67 | var newPass = String(req.body.newPassword); 68 | 69 | User.findById(userId, function (err, user) { 70 | if(user.authenticate(oldPass)) { 71 | user.password = newPass; 72 | user.save(function(err) { 73 | if (err) return validationError(res, err); 74 | res.send(200); 75 | }); 76 | } else { 77 | res.send(403); 78 | } 79 | }); 80 | }; 81 | 82 | /** 83 | * Get my info 84 | */ 85 | exports.me = function(req, res, next) { 86 | var userId = req.user._id; 87 | User.findOne({ 88 | _id: userId 89 | }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt 90 | if (err) return next(err); 91 | if (!user) return res.json(401); 92 | res.json(user); 93 | }); 94 | }; 95 | 96 | /** 97 | * Authentication callback 98 | */ 99 | exports.authCallback = function(req, res, next) { 100 | res.redirect('/'); 101 | }; 102 | -------------------------------------------------------------------------------- /client/app/account/signup/signup.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

Sign up

7 |
8 |
9 |
10 | 11 |
13 | 14 | 15 | 17 |

18 | A name is required 19 |

20 |
21 | 22 |
24 | 25 | 26 | 29 |

30 | Doesn't look like a valid email. 31 |

32 |

33 | What's your email address? 34 |

35 |

36 | {{ errors.email }} 37 |

38 |
39 | 40 |
42 | 43 | 44 | 48 |

50 | Password must be at least 3 characters. 51 |

52 |

53 | {{ errors.password }} 54 |

55 |
56 | 57 |
58 | 61 | 62 | Login 63 | 64 |
65 | 66 |
67 |
68 | 69 | Connect with Google+ 70 | 71 | 72 | Connect with Twitter 73 | 74 |
75 |
76 |
77 |
78 |
79 |
80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "main": "server/app.js", 5 | "dependencies": { 6 | "express": "~4.0.0", 7 | "morgan": "~1.0.0", 8 | "body-parser": "~1.5.0", 9 | "method-override": "~1.0.0", 10 | "serve-favicon": "~2.0.1", 11 | "cookie-parser": "~1.0.1", 12 | "express-session": "~1.0.2", 13 | "errorhandler": "~1.0.0", 14 | "compression": "~1.0.1", 15 | "lodash": "~2.4.1", 16 | "ejs": "~0.8.4", 17 | "mongoose": "~3.8.8", 18 | "jsonwebtoken": "^0.3.0", 19 | "express-jwt": "^0.1.3", 20 | "passport": "~0.2.0", 21 | "passport-local": "~0.1.6", 22 | "passport-twitter": "latest", 23 | "passport-google-oauth": "latest", 24 | "composable-middleware": "^0.3.0", 25 | "connect-mongo": "^0.4.1", 26 | "socket.io": "^1.0.6", 27 | "socket.io-client": "^1.0.6", 28 | "socketio-jwt": "^2.0.2" 29 | }, 30 | "devDependencies": { 31 | "grunt": "~0.4.4", 32 | "grunt-autoprefixer": "~0.7.2", 33 | "grunt-wiredep": "~1.8.0", 34 | "grunt-concurrent": "~0.5.0", 35 | "grunt-contrib-clean": "~0.5.0", 36 | "grunt-contrib-concat": "~0.4.0", 37 | "grunt-contrib-copy": "~0.5.0", 38 | "grunt-contrib-cssmin": "~0.9.0", 39 | "grunt-contrib-htmlmin": "~0.2.0", 40 | "grunt-contrib-imagemin": "~0.7.1", 41 | "grunt-contrib-jshint": "~0.10.0", 42 | "grunt-contrib-uglify": "~0.4.0", 43 | "grunt-contrib-watch": "~0.6.1", 44 | "grunt-google-cdn": "~0.4.0", 45 | "grunt-newer": "~0.7.0", 46 | "grunt-ng-annotate": "^0.2.3", 47 | "grunt-rev": "~0.1.0", 48 | "grunt-svgmin": "~0.4.0", 49 | "grunt-usemin": "~2.1.1", 50 | "grunt-env": "~0.4.1", 51 | "grunt-node-inspector": "~0.1.5", 52 | "grunt-nodemon": "~0.2.0", 53 | "grunt-angular-templates": "^0.5.4", 54 | "grunt-dom-munger": "^3.4.0", 55 | "grunt-protractor-runner": "^1.1.0", 56 | "grunt-asset-injector": "^0.1.0", 57 | "grunt-karma": "~0.8.2", 58 | "grunt-build-control": "DaftMonk/grunt-build-control", 59 | "grunt-mocha-test": "~0.10.2", 60 | "grunt-contrib-sass": "^0.7.3", 61 | "jit-grunt": "^0.5.0", 62 | "time-grunt": "~0.3.1", 63 | "grunt-express-server": "~0.4.17", 64 | "grunt-open": "~0.2.3", 65 | "open": "~0.0.4", 66 | "jshint-stylish": "~0.1.5", 67 | "connect-livereload": "~0.4.0", 68 | "karma-ng-scenario": "~0.1.0", 69 | "karma-firefox-launcher": "~0.1.3", 70 | "karma-script-launcher": "~0.1.0", 71 | "karma-html2js-preprocessor": "~0.1.0", 72 | "karma-ng-jade2js-preprocessor": "^0.1.2", 73 | "karma-jasmine": "~0.1.5", 74 | "karma-chrome-launcher": "~0.1.3", 75 | "requirejs": "~2.1.11", 76 | "karma-requirejs": "~0.2.1", 77 | "karma-coffee-preprocessor": "~0.2.1", 78 | "karma-jade-preprocessor": "0.0.11", 79 | "karma-phantomjs-launcher": "~0.1.4", 80 | "karma": "~0.12.9", 81 | "karma-ng-html2js-preprocessor": "~0.1.0", 82 | "supertest": "~0.11.0", 83 | "should": "~3.3.1" 84 | }, 85 | "engines": { 86 | "node": ">=0.10.0" 87 | }, 88 | "scripts": { 89 | "start": "node server/app.js", 90 | "test": "grunt test", 91 | "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" 92 | }, 93 | "private": true 94 | } 95 | -------------------------------------------------------------------------------- /server/api/user/user.model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | var Schema = mongoose.Schema; 5 | var crypto = require('crypto'); 6 | var authTypes = ['github', 'twitter', 'facebook', 'google']; 7 | 8 | var UserSchema = new Schema({ 9 | name: String, 10 | email: { type: String, lowercase: true }, 11 | role: { 12 | type: String, 13 | default: 'user' 14 | }, 15 | hashedPassword: String, 16 | provider: String, 17 | salt: String, 18 | twitter: {}, 19 | google: {}, 20 | github: {} 21 | }); 22 | 23 | /** 24 | * Virtuals 25 | */ 26 | UserSchema 27 | .virtual('password') 28 | .set(function(password) { 29 | this._password = password; 30 | this.salt = this.makeSalt(); 31 | this.hashedPassword = this.encryptPassword(password); 32 | }) 33 | .get(function() { 34 | return this._password; 35 | }); 36 | 37 | // Public profile information 38 | UserSchema 39 | .virtual('profile') 40 | .get(function() { 41 | return { 42 | 'name': this.name, 43 | 'role': this.role 44 | }; 45 | }); 46 | 47 | // Non-sensitive info we'll be putting in the token 48 | UserSchema 49 | .virtual('token') 50 | .get(function() { 51 | return { 52 | '_id': this._id, 53 | 'role': this.role 54 | }; 55 | }); 56 | 57 | /** 58 | * Validations 59 | */ 60 | 61 | // Validate empty email 62 | UserSchema 63 | .path('email') 64 | .validate(function(email) { 65 | if (authTypes.indexOf(this.provider) !== -1) return true; 66 | return email.length; 67 | }, 'Email cannot be blank'); 68 | 69 | // Validate empty password 70 | UserSchema 71 | .path('hashedPassword') 72 | .validate(function(hashedPassword) { 73 | if (authTypes.indexOf(this.provider) !== -1) return true; 74 | return hashedPassword.length; 75 | }, 'Password cannot be blank'); 76 | 77 | // Validate email is not taken 78 | UserSchema 79 | .path('email') 80 | .validate(function(value, respond) { 81 | var self = this; 82 | this.constructor.findOne({email: value}, function(err, user) { 83 | if(err) throw err; 84 | if(user) { 85 | if(self.id === user.id) return respond(true); 86 | return respond(false); 87 | } 88 | respond(true); 89 | }); 90 | }, 'The specified email address is already in use.'); 91 | 92 | var validatePresenceOf = function(value) { 93 | return value && value.length; 94 | }; 95 | 96 | /** 97 | * Pre-save hook 98 | */ 99 | UserSchema 100 | .pre('save', function(next) { 101 | if (!this.isNew) return next(); 102 | 103 | if (!validatePresenceOf(this.hashedPassword) && authTypes.indexOf(this.provider) === -1) 104 | next(new Error('Invalid password')); 105 | else 106 | next(); 107 | }); 108 | 109 | /** 110 | * Methods 111 | */ 112 | UserSchema.methods = { 113 | /** 114 | * Authenticate - check if the passwords are the same 115 | * 116 | * @param {String} plainText 117 | * @return {Boolean} 118 | * @api public 119 | */ 120 | authenticate: function(plainText) { 121 | return this.encryptPassword(plainText) === this.hashedPassword; 122 | }, 123 | 124 | /** 125 | * Make salt 126 | * 127 | * @return {String} 128 | * @api public 129 | */ 130 | makeSalt: function() { 131 | return crypto.randomBytes(16).toString('base64'); 132 | }, 133 | 134 | /** 135 | * Encrypt password 136 | * 137 | * @param {String} password 138 | * @return {String} 139 | * @api public 140 | */ 141 | encryptPassword: function(password) { 142 | if (!password || !this.salt) return ''; 143 | var salt = new Buffer(this.salt, 'base64'); 144 | return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); 145 | } 146 | }; 147 | 148 | module.exports = mongoose.model('User', UserSchema); 149 | -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- 1 |   �( @   -2Op"=p�Jt��Jt��b���������������������������������������������������b���Jt��Jt��"=p�Op-2O`O�O�O�O�O�O�O� $\�Jt��������������v���v���������������Jt�� $\�O�O�O�O�O�O�O�O`O�O�O�O�O�O�O�O�O�O� ;n�s���>���>���>���>���s��� ;n�O�O�O�O�O�O�O�O�O�O�O`O�O�O�O�O�O�O�O�O�O� $\�]���^n��^n��]��� $\�O�O�O�O�O�O�O�O�O�O�O`O�O�O�O�O�O�O�O�O�O�O�n�*��*��n�O�O�O�O�O�O�O�O�O�O�O�  O�O�O�O�O�O�O�O�O�O�O�5>Y�5>Y�O�O�O�O�O�O�O�O�O�O�O�  -2O�O�O�O�O�O�O�O�O�O�&6e�&6e�O�O�O�O�O�O�O�O�O�O�-25r�4���E��� $\�O�O�O�O�O�O�O�O�O�O�O�O�O�O�O�O�O�O� $\�E���4���5r�5r�E���M���M���v���0\��O�O�O�O�O�O�O� $\� $\�O�O�O�O�O�O�O�0\��v���M���M���E���5r�)��p&��p��&��������������b���Jt��Jt��Jt��0\��#i��.r��.r��#i��0\��Jt��Jt��Jt��b���������������&��p��&��)��p4���&��-���_������������������]���]�������7���p�����������p���7�������]���]�������������������_��-���-���4���qֈp��p��p����������������������p���7���#i��p�����������p���#i��7���p�����������������������p��&��-���qֈ8��(p��p��I���v���v���]���7���n���v���p���#i��]���v���v���]���#i��p���v���n���7���]���v���v���I���-���-���8��(;��`-���M���7���7���7���.r��R��E��R��E��7���7���7���7���E��R��E��R��.r��7���7���7���M���M���;��`���������������������������z��������������������������� 2 | �  ��� 3 | � 9� 9� 9� 9� 9� 9� 9� 9� 4 |  �n�n� 5 |  � 9� 9� 9� 9� 9� 9� 9� 9� 6 | ����*�x*��*��*��*��*��*��*��n�&��#��&��&��n�*��*��*��*��*��*��*��*�x*ݟ*��*��*��*��*��*��!��#��&��#��&��*��!��!��*��*��*��*��*��*��*ݟ*ݿ*��*��*��*��*��*��n�*��*�� 9� 9�*��*���*��*��*��*��*��*��*ݿ*��*��*��*��*��*��*��!��#��&��&��&��*��#��!��*��*��*��*��*��*��*��  ��������I�&��&��&��&��I���������  U��������� 7 |  �n�n� 8 |  ����������-2z����������������������z������������������������ 9 | ����������������������� 10 | ������������������������� 11 | ������������������������-2����������������������U�������������������z5r������������������-25r�U�����������z  ������������������������������?��� -------------------------------------------------------------------------------- /client/index.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 | 28 | 29 | 30 |
31 | 32 | 33 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /client/components/auth/auth.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('demoApp') 4 | .factory('Auth', function Auth($location, $rootScope, $http, User, $cookieStore, $q) { 5 | var currentUser = {}; 6 | if($cookieStore.get('token')) { 7 | currentUser = User.get(); 8 | } 9 | 10 | return { 11 | 12 | /** 13 | * Authenticate user and save token 14 | * 15 | * @param {Object} user - login info 16 | * @param {Function} callback - optional 17 | * @return {Promise} 18 | */ 19 | login: function(user, callback) { 20 | var cb = callback || angular.noop; 21 | var deferred = $q.defer(); 22 | 23 | $http.post('/auth/local', { 24 | email: user.email, 25 | password: user.password 26 | }). 27 | success(function(data) { 28 | $cookieStore.put('token', data.token); 29 | currentUser = User.get(); 30 | deferred.resolve(data); 31 | return cb(); 32 | }). 33 | error(function(err) { 34 | this.logout(); 35 | deferred.reject(err); 36 | return cb(err); 37 | }.bind(this)); 38 | 39 | return deferred.promise; 40 | }, 41 | 42 | /** 43 | * Delete access token and user info 44 | * 45 | * @param {Function} 46 | */ 47 | logout: function() { 48 | $cookieStore.remove('token'); 49 | currentUser = {}; 50 | }, 51 | 52 | /** 53 | * Create a new user 54 | * 55 | * @param {Object} user - user info 56 | * @param {Function} callback - optional 57 | * @return {Promise} 58 | */ 59 | createUser: function(user, callback) { 60 | var cb = callback || angular.noop; 61 | 62 | return User.save(user, 63 | function(data) { 64 | $cookieStore.put('token', data.token); 65 | currentUser = User.get(); 66 | return cb(user); 67 | }, 68 | function(err) { 69 | this.logout(); 70 | return cb(err); 71 | }.bind(this)).$promise; 72 | }, 73 | 74 | /** 75 | * Change password 76 | * 77 | * @param {String} oldPassword 78 | * @param {String} newPassword 79 | * @param {Function} callback - optional 80 | * @return {Promise} 81 | */ 82 | changePassword: function(oldPassword, newPassword, callback) { 83 | var cb = callback || angular.noop; 84 | 85 | return User.changePassword({ id: currentUser._id }, { 86 | oldPassword: oldPassword, 87 | newPassword: newPassword 88 | }, function(user) { 89 | return cb(user); 90 | }, function(err) { 91 | return cb(err); 92 | }).$promise; 93 | }, 94 | 95 | /** 96 | * Gets all available info on authenticated user 97 | * 98 | * @return {Object} user 99 | */ 100 | getCurrentUser: function() { 101 | return currentUser; 102 | }, 103 | 104 | /** 105 | * Check if a user is logged in 106 | * 107 | * @return {Boolean} 108 | */ 109 | isLoggedIn: function() { 110 | return currentUser.hasOwnProperty('role'); 111 | }, 112 | 113 | /** 114 | * Waits for currentUser to resolve before checking if user is logged in 115 | */ 116 | isLoggedInAsync: function(cb) { 117 | if(currentUser.hasOwnProperty('$promise')) { 118 | currentUser.$promise.then(function() { 119 | cb(true); 120 | }).catch(function() { 121 | cb(false); 122 | }); 123 | } else if(currentUser.hasOwnProperty('role')) { 124 | cb(true); 125 | } else { 126 | cb(false); 127 | } 128 | }, 129 | 130 | /** 131 | * Check if a user is an admin 132 | * 133 | * @return {Boolean} 134 | */ 135 | isAdmin: function() { 136 | return currentUser.role === 'admin'; 137 | }, 138 | 139 | /** 140 | * Get auth token 141 | */ 142 | getToken: function() { 143 | return $cookieStore.get('token'); 144 | } 145 | }; 146 | }); 147 | -------------------------------------------------------------------------------- /server/views/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-08-29 using generator-angular-fullstack 2.0.13 2 | 'use strict'; 3 | 4 | module.exports = function (grunt) { 5 | var localConfig; 6 | try { 7 | localConfig = require('./server/config/local.env'); 8 | } catch(e) { 9 | localConfig = {}; 10 | } 11 | 12 | // Load grunt tasks automatically, when needed 13 | require('jit-grunt')(grunt, { 14 | express: 'grunt-express-server', 15 | useminPrepare: 'grunt-usemin', 16 | ngtemplates: 'grunt-angular-templates', 17 | cdnify: 'grunt-google-cdn', 18 | protractor: 'grunt-protractor-runner', 19 | injector: 'grunt-asset-injector', 20 | buildcontrol: 'grunt-build-control' 21 | }); 22 | 23 | // Time how long tasks take. Can help when optimizing build times 24 | require('time-grunt')(grunt); 25 | 26 | // Define the configuration for all the tasks 27 | grunt.initConfig({ 28 | 29 | // Project settings 30 | pkg: grunt.file.readJSON('package.json'), 31 | yeoman: { 32 | // configurable paths 33 | client: require('./bower.json').appPath || 'client', 34 | dist: 'dist' 35 | }, 36 | express: { 37 | options: { 38 | port: process.env.PORT || 9000 39 | }, 40 | dev: { 41 | options: { 42 | script: 'server/app.js', 43 | debug: true 44 | } 45 | }, 46 | prod: { 47 | options: { 48 | script: 'dist/server/app.js' 49 | } 50 | } 51 | }, 52 | open: { 53 | server: { 54 | url: 'http://localhost:<%= express.options.port %>' 55 | } 56 | }, 57 | watch: { 58 | injectJS: { 59 | files: [ 60 | '<%= yeoman.client %>/{app,components}/**/*.js', 61 | '!<%= yeoman.client %>/{app,components}/**/*.spec.js', 62 | '!<%= yeoman.client %>/{app,components}/**/*.mock.js', 63 | '!<%= yeoman.client %>/app/app.js'], 64 | tasks: ['injector:scripts'] 65 | }, 66 | injectCss: { 67 | files: [ 68 | '<%= yeoman.client %>/{app,components}/**/*.css' 69 | ], 70 | tasks: ['injector:css'] 71 | }, 72 | mochaTest: { 73 | files: ['server/**/*.spec.js'], 74 | tasks: ['env:test', 'mochaTest'] 75 | }, 76 | jsTest: { 77 | files: [ 78 | '<%= yeoman.client %>/{app,components}/**/*.spec.js', 79 | '<%= yeoman.client %>/{app,components}/**/*.mock.js' 80 | ], 81 | tasks: ['newer:jshint:all', 'karma'] 82 | }, 83 | injectSass: { 84 | files: [ 85 | '<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], 86 | tasks: ['injector:sass'] 87 | }, 88 | sass: { 89 | files: [ 90 | '<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], 91 | tasks: ['sass', 'autoprefixer'] 92 | }, 93 | gruntfile: { 94 | files: ['Gruntfile.js'] 95 | }, 96 | livereload: { 97 | files: [ 98 | '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.css', 99 | '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.html', 100 | '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js', 101 | '!{.tmp,<%= yeoman.client %>}{app,components}/**/*.spec.js', 102 | '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js', 103 | '<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}' 104 | ], 105 | options: { 106 | livereload: true 107 | } 108 | }, 109 | express: { 110 | files: [ 111 | 'server/**/*.{js,json}' 112 | ], 113 | tasks: ['express:dev', 'wait'], 114 | options: { 115 | livereload: true, 116 | nospawn: true //Without this option specified express won't be reloaded 117 | } 118 | } 119 | }, 120 | 121 | // Make sure code styles are up to par and there are no obvious mistakes 122 | jshint: { 123 | options: { 124 | jshintrc: '<%= yeoman.client %>/.jshintrc', 125 | reporter: require('jshint-stylish') 126 | }, 127 | server: { 128 | options: { 129 | jshintrc: 'server/.jshintrc' 130 | }, 131 | src: [ 132 | 'server/**/*.js', 133 | '!server/**/*.spec.js' 134 | ] 135 | }, 136 | serverTest: { 137 | options: { 138 | jshintrc: 'server/.jshintrc-spec' 139 | }, 140 | src: ['server/**/*.spec.js'] 141 | }, 142 | all: [ 143 | '<%= yeoman.client %>/{app,components}/**/*.js', 144 | '!<%= yeoman.client %>/{app,components}/**/*.spec.js', 145 | '!<%= yeoman.client %>/{app,components}/**/*.mock.js' 146 | ], 147 | test: { 148 | src: [ 149 | '<%= yeoman.client %>/{app,components}/**/*.spec.js', 150 | '<%= yeoman.client %>/{app,components}/**/*.mock.js' 151 | ] 152 | } 153 | }, 154 | 155 | // Empties folders to start fresh 156 | clean: { 157 | dist: { 158 | files: [{ 159 | dot: true, 160 | src: [ 161 | '.tmp', 162 | '<%= yeoman.dist %>/*', 163 | '!<%= yeoman.dist %>/.git*', 164 | '!<%= yeoman.dist %>/.openshift', 165 | '!<%= yeoman.dist %>/Procfile' 166 | ] 167 | }] 168 | }, 169 | server: '.tmp' 170 | }, 171 | 172 | // Add vendor prefixed styles 173 | autoprefixer: { 174 | options: { 175 | browsers: ['last 1 version'] 176 | }, 177 | dist: { 178 | files: [{ 179 | expand: true, 180 | cwd: '.tmp/', 181 | src: '{,*/}*.css', 182 | dest: '.tmp/' 183 | }] 184 | } 185 | }, 186 | 187 | // Debugging with node inspector 188 | 'node-inspector': { 189 | custom: { 190 | options: { 191 | 'web-host': 'localhost' 192 | } 193 | } 194 | }, 195 | 196 | // Use nodemon to run server in debug mode with an initial breakpoint 197 | nodemon: { 198 | debug: { 199 | script: 'server/app.js', 200 | options: { 201 | nodeArgs: ['--debug-brk'], 202 | env: { 203 | PORT: process.env.PORT || 9000 204 | }, 205 | callback: function (nodemon) { 206 | nodemon.on('log', function (event) { 207 | console.log(event.colour); 208 | }); 209 | 210 | // opens browser on initial server start 211 | nodemon.on('config:update', function () { 212 | setTimeout(function () { 213 | require('open')('http://localhost:8080/debug?port=5858'); 214 | }, 500); 215 | }); 216 | } 217 | } 218 | } 219 | }, 220 | 221 | // Automatically inject Bower components into the app 222 | wiredep: { 223 | target: { 224 | src: '<%= yeoman.client %>/index.html', 225 | ignorePath: '<%= yeoman.client %>/', 226 | exclude: [/bootstrap-sass-official/, /bootstrap.js/, '/json3/', '/es5-shim/', /bootstrap.css/, /font-awesome.css/ ] 227 | } 228 | }, 229 | 230 | // Renames files for browser caching purposes 231 | rev: { 232 | dist: { 233 | files: { 234 | src: [ 235 | '<%= yeoman.dist %>/public/{,*/}*.js', 236 | '<%= yeoman.dist %>/public/{,*/}*.css', 237 | '<%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 238 | '<%= yeoman.dist %>/public/assets/fonts/*' 239 | ] 240 | } 241 | } 242 | }, 243 | 244 | // Reads HTML for usemin blocks to enable smart builds that automatically 245 | // concat, minify and revision files. Creates configurations in memory so 246 | // additional tasks can operate on them 247 | useminPrepare: { 248 | html: ['<%= yeoman.client %>/index.html'], 249 | options: { 250 | dest: '<%= yeoman.dist %>/public' 251 | } 252 | }, 253 | 254 | // Performs rewrites based on rev and the useminPrepare configuration 255 | usemin: { 256 | html: ['<%= yeoman.dist %>/public/{,*/}*.html'], 257 | css: ['<%= yeoman.dist %>/public/{,*/}*.css'], 258 | js: ['<%= yeoman.dist %>/public/{,*/}*.js'], 259 | options: { 260 | assetsDirs: [ 261 | '<%= yeoman.dist %>/public', 262 | '<%= yeoman.dist %>/public/assets/images' 263 | ], 264 | // This is so we update image references in our ng-templates 265 | patterns: { 266 | js: [ 267 | [/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images'] 268 | ] 269 | } 270 | } 271 | }, 272 | 273 | // The following *-min tasks produce minified files in the dist folder 274 | imagemin: { 275 | dist: { 276 | files: [{ 277 | expand: true, 278 | cwd: '<%= yeoman.client %>/assets/images', 279 | src: '{,*/}*.{png,jpg,jpeg,gif}', 280 | dest: '<%= yeoman.dist %>/public/assets/images' 281 | }] 282 | } 283 | }, 284 | 285 | svgmin: { 286 | dist: { 287 | files: [{ 288 | expand: true, 289 | cwd: '<%= yeoman.client %>/assets/images', 290 | src: '{,*/}*.svg', 291 | dest: '<%= yeoman.dist %>/public/assets/images' 292 | }] 293 | } 294 | }, 295 | 296 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 297 | // minsafe compatible so Uglify does not destroy the ng references 298 | ngAnnotate: { 299 | dist: { 300 | files: [{ 301 | expand: true, 302 | cwd: '.tmp/concat', 303 | src: '*/**.js', 304 | dest: '.tmp/concat' 305 | }] 306 | } 307 | }, 308 | 309 | // Package all the html partials into a single javascript payload 310 | ngtemplates: { 311 | options: { 312 | // This should be the name of your apps angular module 313 | module: 'demoApp', 314 | htmlmin: { 315 | collapseBooleanAttributes: true, 316 | collapseWhitespace: true, 317 | removeAttributeQuotes: true, 318 | removeEmptyAttributes: true, 319 | removeRedundantAttributes: true, 320 | removeScriptTypeAttributes: true, 321 | removeStyleLinkTypeAttributes: true 322 | }, 323 | usemin: 'app/app.js' 324 | }, 325 | main: { 326 | cwd: '<%= yeoman.client %>', 327 | src: ['{app,components}/**/*.html'], 328 | dest: '.tmp/templates.js' 329 | }, 330 | tmp: { 331 | cwd: '.tmp', 332 | src: ['{app,components}/**/*.html'], 333 | dest: '.tmp/tmp-templates.js' 334 | } 335 | }, 336 | 337 | // Replace Google CDN references 338 | cdnify: { 339 | dist: { 340 | html: ['<%= yeoman.dist %>/public/*.html'] 341 | } 342 | }, 343 | 344 | // Copies remaining files to places other tasks can use 345 | copy: { 346 | dist: { 347 | files: [{ 348 | expand: true, 349 | dot: true, 350 | cwd: '<%= yeoman.client %>', 351 | dest: '<%= yeoman.dist %>/public', 352 | src: [ 353 | '*.{ico,png,txt}', 354 | '.htaccess', 355 | 'bower_components/**/*', 356 | 'assets/images/{,*/}*.{webp}', 357 | 'assets/fonts/**/*', 358 | 'index.html' 359 | ] 360 | }, { 361 | expand: true, 362 | cwd: '.tmp/images', 363 | dest: '<%= yeoman.dist %>/public/assets/images', 364 | src: ['generated/*'] 365 | }, { 366 | expand: true, 367 | dest: '<%= yeoman.dist %>', 368 | src: [ 369 | 'package.json', 370 | 'server/**/*' 371 | ] 372 | }] 373 | }, 374 | styles: { 375 | expand: true, 376 | cwd: '<%= yeoman.client %>', 377 | dest: '.tmp/', 378 | src: ['{app,components}/**/*.css'] 379 | } 380 | }, 381 | 382 | buildcontrol: { 383 | options: { 384 | dir: 'dist', 385 | commit: true, 386 | push: true, 387 | connectCommits: false, 388 | message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%' 389 | }, 390 | heroku: { 391 | options: { 392 | remote: 'heroku', 393 | branch: 'master' 394 | } 395 | }, 396 | openshift: { 397 | options: { 398 | remote: 'openshift', 399 | branch: 'master' 400 | } 401 | } 402 | }, 403 | 404 | // Run some tasks in parallel to speed up the build process 405 | concurrent: { 406 | server: [ 407 | 'sass', 408 | ], 409 | test: [ 410 | 'sass', 411 | ], 412 | debug: { 413 | tasks: [ 414 | 'nodemon', 415 | 'node-inspector' 416 | ], 417 | options: { 418 | logConcurrentOutput: true 419 | } 420 | }, 421 | dist: [ 422 | 'sass', 423 | 'imagemin', 424 | 'svgmin' 425 | ] 426 | }, 427 | 428 | // Test settings 429 | karma: { 430 | unit: { 431 | configFile: 'karma.conf.js', 432 | singleRun: true 433 | } 434 | }, 435 | 436 | mochaTest: { 437 | options: { 438 | reporter: 'spec' 439 | }, 440 | src: ['server/**/*.spec.js'] 441 | }, 442 | 443 | protractor: { 444 | options: { 445 | configFile: 'protractor.conf.js' 446 | }, 447 | chrome: { 448 | options: { 449 | args: { 450 | browser: 'chrome' 451 | } 452 | } 453 | } 454 | }, 455 | 456 | env: { 457 | test: { 458 | NODE_ENV: 'test' 459 | }, 460 | prod: { 461 | NODE_ENV: 'production' 462 | }, 463 | all: localConfig 464 | }, 465 | 466 | // Compiles Sass to CSS 467 | sass: { 468 | server: { 469 | options: { 470 | loadPath: [ 471 | '<%= yeoman.client %>/bower_components', 472 | '<%= yeoman.client %>/app', 473 | '<%= yeoman.client %>/components' 474 | ], 475 | compass: false 476 | }, 477 | files: { 478 | '.tmp/app/app.css' : '<%= yeoman.client %>/app/app.scss' 479 | } 480 | } 481 | }, 482 | 483 | injector: { 484 | options: { 485 | 486 | }, 487 | // Inject application script files into index.html (doesn't include bower) 488 | scripts: { 489 | options: { 490 | transform: function(filePath) { 491 | filePath = filePath.replace('/client/', ''); 492 | filePath = filePath.replace('/.tmp/', ''); 493 | return ''; 494 | }, 495 | starttag: '', 496 | endtag: '' 497 | }, 498 | files: { 499 | '<%= yeoman.client %>/index.html': [ 500 | ['{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js', 501 | '!{.tmp,<%= yeoman.client %>}/app/app.js', 502 | '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js', 503 | '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js'] 504 | ] 505 | } 506 | }, 507 | 508 | // Inject component scss into app.scss 509 | sass: { 510 | options: { 511 | transform: function(filePath) { 512 | filePath = filePath.replace('/client/app/', ''); 513 | filePath = filePath.replace('/client/components/', ''); 514 | return '@import \'' + filePath + '\';'; 515 | }, 516 | starttag: '// injector', 517 | endtag: '// endinjector' 518 | }, 519 | files: { 520 | '<%= yeoman.client %>/app/app.scss': [ 521 | '<%= yeoman.client %>/{app,components}/**/*.{scss,sass}', 522 | '!<%= yeoman.client %>/app/app.{scss,sass}' 523 | ] 524 | } 525 | }, 526 | 527 | // Inject component css into index.html 528 | css: { 529 | options: { 530 | transform: function(filePath) { 531 | filePath = filePath.replace('/client/', ''); 532 | filePath = filePath.replace('/.tmp/', ''); 533 | return ''; 534 | }, 535 | starttag: '', 536 | endtag: '' 537 | }, 538 | files: { 539 | '<%= yeoman.client %>/index.html': [ 540 | '<%= yeoman.client %>/{app,components}/**/*.css' 541 | ] 542 | } 543 | } 544 | }, 545 | }); 546 | 547 | // Used for delaying livereload until after server has restarted 548 | grunt.registerTask('wait', function () { 549 | grunt.log.ok('Waiting for server reload...'); 550 | 551 | var done = this.async(); 552 | 553 | setTimeout(function () { 554 | grunt.log.writeln('Done waiting!'); 555 | done(); 556 | }, 1500); 557 | }); 558 | 559 | grunt.registerTask('express-keepalive', 'Keep grunt running', function() { 560 | this.async(); 561 | }); 562 | 563 | grunt.registerTask('serve', function (target) { 564 | if (target === 'dist') { 565 | return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'open', 'express-keepalive']); 566 | } 567 | 568 | if (target === 'debug') { 569 | return grunt.task.run([ 570 | 'clean:server', 571 | 'env:all', 572 | 'injector:sass', 573 | 'concurrent:server', 574 | 'injector', 575 | 'wiredep', 576 | 'autoprefixer', 577 | 'concurrent:debug' 578 | ]); 579 | } 580 | 581 | grunt.task.run([ 582 | 'clean:server', 583 | 'env:all', 584 | 'injector:sass', 585 | 'concurrent:server', 586 | 'injector', 587 | 'wiredep', 588 | 'autoprefixer', 589 | 'express:dev', 590 | 'wait', 591 | 'open', 592 | 'watch' 593 | ]); 594 | }); 595 | 596 | grunt.registerTask('server', function () { 597 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 598 | grunt.task.run(['serve']); 599 | }); 600 | 601 | grunt.registerTask('test', function(target) { 602 | if (target === 'server') { 603 | return grunt.task.run([ 604 | 'env:all', 605 | 'env:test', 606 | 'mochaTest' 607 | ]); 608 | } 609 | 610 | else if (target === 'client') { 611 | return grunt.task.run([ 612 | 'clean:server', 613 | 'env:all', 614 | 'injector:sass', 615 | 'concurrent:test', 616 | 'injector', 617 | 'autoprefixer', 618 | 'karma' 619 | ]); 620 | } 621 | 622 | else if (target === 'e2e') { 623 | return grunt.task.run([ 624 | 'clean:server', 625 | 'env:all', 626 | 'env:test', 627 | 'injector:sass', 628 | 'concurrent:test', 629 | 'injector', 630 | 'wiredep', 631 | 'autoprefixer', 632 | 'express:dev', 633 | 'protractor' 634 | ]); 635 | } 636 | 637 | else grunt.task.run([ 638 | 'test:server', 639 | 'test:client' 640 | ]); 641 | }); 642 | 643 | grunt.registerTask('build', [ 644 | 'clean:dist', 645 | 'injector:sass', 646 | 'concurrent:dist', 647 | 'injector', 648 | 'wiredep', 649 | 'useminPrepare', 650 | 'autoprefixer', 651 | 'ngtemplates', 652 | 'concat', 653 | 'ngAnnotate', 654 | 'copy:dist', 655 | 'cdnify', 656 | 'cssmin', 657 | 'uglify', 658 | 'rev', 659 | 'usemin' 660 | ]); 661 | 662 | grunt.registerTask('default', [ 663 | 'newer:jshint', 664 | 'test', 665 | 'build' 666 | ]); 667 | }; 668 | -------------------------------------------------------------------------------- /client/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Configuration File 2 | 3 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have access 4 | # to the main server config file (usually called `httpd.conf`), you should add 5 | # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. 6 | 7 | # ############################################################################## 8 | # # CROSS-ORIGIN RESOURCE SHARING (CORS) # 9 | # ############################################################################## 10 | 11 | # ------------------------------------------------------------------------------ 12 | # | Cross-domain AJAX requests | 13 | # ------------------------------------------------------------------------------ 14 | 15 | # Enable cross-origin AJAX requests. 16 | # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 17 | # http://enable-cors.org/ 18 | 19 | # 20 | # Header set Access-Control-Allow-Origin "*" 21 | # 22 | 23 | # ------------------------------------------------------------------------------ 24 | # | CORS-enabled images | 25 | # ------------------------------------------------------------------------------ 26 | 27 | # Send the CORS header for images when browsers request it. 28 | # https://developer.mozilla.org/en/CORS_Enabled_Image 29 | # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 30 | # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 31 | 32 | 33 | 34 | 35 | SetEnvIf Origin ":" IS_CORS 36 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 37 | 38 | 39 | 40 | 41 | # ------------------------------------------------------------------------------ 42 | # | Web fonts access | 43 | # ------------------------------------------------------------------------------ 44 | 45 | # Allow access from all domains for web fonts 46 | 47 | 48 | 49 | Header set Access-Control-Allow-Origin "*" 50 | 51 | 52 | 53 | 54 | # ############################################################################## 55 | # # ERRORS # 56 | # ############################################################################## 57 | 58 | # ------------------------------------------------------------------------------ 59 | # | 404 error prevention for non-existing redirected folders | 60 | # ------------------------------------------------------------------------------ 61 | 62 | # Prevent Apache from returning a 404 error for a rewrite if a directory 63 | # with the same name does not exist. 64 | # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews 65 | # http://www.webmasterworld.com/apache/3808792.htm 66 | 67 | Options -MultiViews 68 | 69 | # ------------------------------------------------------------------------------ 70 | # | Custom error messages / pages | 71 | # ------------------------------------------------------------------------------ 72 | 73 | # You can customize what Apache returns to the client in case of an error (see 74 | # http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: 75 | 76 | ErrorDocument 404 /404.html 77 | 78 | 79 | # ############################################################################## 80 | # # INTERNET EXPLORER # 81 | # ############################################################################## 82 | 83 | # ------------------------------------------------------------------------------ 84 | # | Better website experience | 85 | # ------------------------------------------------------------------------------ 86 | 87 | # Force IE to render pages in the highest available mode in the various 88 | # cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. 89 | 90 | 91 | Header set X-UA-Compatible "IE=edge" 92 | # `mod_headers` can't match based on the content-type, however, we only 93 | # want to send this header for HTML pages and not for the other resources 94 | 95 | Header unset X-UA-Compatible 96 | 97 | 98 | 99 | # ------------------------------------------------------------------------------ 100 | # | Cookie setting from iframes | 101 | # ------------------------------------------------------------------------------ 102 | 103 | # Allow cookies to be set from iframes in IE. 104 | 105 | # 106 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 107 | # 108 | 109 | # ------------------------------------------------------------------------------ 110 | # | Screen flicker | 111 | # ------------------------------------------------------------------------------ 112 | 113 | # Stop screen flicker in IE on CSS rollovers (this only works in 114 | # combination with the `ExpiresByType` directives for images from below). 115 | 116 | # BrowserMatch "MSIE" brokenvary=1 117 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 118 | # BrowserMatch "Opera" !brokenvary 119 | # SetEnvIf brokenvary 1 force-no-vary 120 | 121 | 122 | # ############################################################################## 123 | # # MIME TYPES AND ENCODING # 124 | # ############################################################################## 125 | 126 | # ------------------------------------------------------------------------------ 127 | # | Proper MIME types for all files | 128 | # ------------------------------------------------------------------------------ 129 | 130 | 131 | 132 | # Audio 133 | AddType audio/mp4 m4a f4a f4b 134 | AddType audio/ogg oga ogg 135 | 136 | # JavaScript 137 | # Normalize to standard type (it's sniffed in IE anyways): 138 | # http://tools.ietf.org/html/rfc4329#section-7.2 139 | AddType application/javascript js jsonp 140 | AddType application/json json 141 | 142 | # Video 143 | AddType video/mp4 mp4 m4v f4v f4p 144 | AddType video/ogg ogv 145 | AddType video/webm webm 146 | AddType video/x-flv flv 147 | 148 | # Web fonts 149 | AddType application/font-woff woff 150 | AddType application/vnd.ms-fontobject eot 151 | 152 | # Browsers usually ignore the font MIME types and sniff the content, 153 | # however, Chrome shows a warning if other MIME types are used for the 154 | # following fonts. 155 | AddType application/x-font-ttf ttc ttf 156 | AddType font/opentype otf 157 | 158 | # Make SVGZ fonts work on iPad: 159 | # https://twitter.com/FontSquirrel/status/14855840545 160 | AddType image/svg+xml svg svgz 161 | AddEncoding gzip svgz 162 | 163 | # Other 164 | AddType application/octet-stream safariextz 165 | AddType application/x-chrome-extension crx 166 | AddType application/x-opera-extension oex 167 | AddType application/x-shockwave-flash swf 168 | AddType application/x-web-app-manifest+json webapp 169 | AddType application/x-xpinstall xpi 170 | AddType application/xml atom rdf rss xml 171 | AddType image/webp webp 172 | AddType image/x-icon ico 173 | AddType text/cache-manifest appcache manifest 174 | AddType text/vtt vtt 175 | AddType text/x-component htc 176 | AddType text/x-vcard vcf 177 | 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # | UTF-8 encoding | 182 | # ------------------------------------------------------------------------------ 183 | 184 | # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. 185 | AddDefaultCharset utf-8 186 | 187 | # Force UTF-8 for certain file formats. 188 | 189 | AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml 190 | 191 | 192 | 193 | # ############################################################################## 194 | # # URL REWRITES # 195 | # ############################################################################## 196 | 197 | # ------------------------------------------------------------------------------ 198 | # | Rewrite engine | 199 | # ------------------------------------------------------------------------------ 200 | 201 | # Turning on the rewrite engine and enabling the `FollowSymLinks` option is 202 | # necessary for the following directives to work. 203 | 204 | # If your web host doesn't allow the `FollowSymlinks` option, you may need to 205 | # comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the 206 | # performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 207 | 208 | # Also, some cloud hosting services require `RewriteBase` to be set: 209 | # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site 210 | 211 | 212 | Options +FollowSymlinks 213 | # Options +SymLinksIfOwnerMatch 214 | RewriteEngine On 215 | # RewriteBase / 216 | 217 | 218 | # ------------------------------------------------------------------------------ 219 | # | Suppressing / Forcing the "www." at the beginning of URLs | 220 | # ------------------------------------------------------------------------------ 221 | 222 | # The same content should never be available under two different URLs especially 223 | # not with and without "www." at the beginning. This can cause SEO problems 224 | # (duplicate content), therefore, you should choose one of the alternatives and 225 | # redirect the other one. 226 | 227 | # By default option 1 (no "www.") is activated: 228 | # http://no-www.org/faq.php?q=class_b 229 | 230 | # If you'd prefer to use option 2, just comment out all the lines from option 1 231 | # and uncomment the ones from option 2. 232 | 233 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 234 | 235 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 | 237 | # Option 1: rewrite www.example.com → example.com 238 | 239 | 240 | RewriteCond %{HTTPS} !=on 241 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 242 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 243 | 244 | 245 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 246 | 247 | # Option 2: rewrite example.com → www.example.com 248 | 249 | # Be aware that the following might not be a good idea if you use "real" 250 | # subdomains for certain parts of your website. 251 | 252 | # 253 | # RewriteCond %{HTTPS} !=on 254 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 255 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 256 | # 257 | 258 | 259 | # ############################################################################## 260 | # # SECURITY # 261 | # ############################################################################## 262 | 263 | # ------------------------------------------------------------------------------ 264 | # | Content Security Policy (CSP) | 265 | # ------------------------------------------------------------------------------ 266 | 267 | # You can mitigate the risk of cross-site scripting and other content-injection 268 | # attacks by setting a Content Security Policy which whitelists trusted sources 269 | # of content for your site. 270 | 271 | # The example header below allows ONLY scripts that are loaded from the current 272 | # site's origin (no inline scripts, no CDN, etc). This almost certainly won't 273 | # work as-is for your site! 274 | 275 | # To get all the details you'll need to craft a reasonable policy for your site, 276 | # read: http://html5rocks.com/en/tutorials/security/content-security-policy (or 277 | # see the specification: http://w3.org/TR/CSP). 278 | 279 | # 280 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 281 | # 282 | # Header unset Content-Security-Policy 283 | # 284 | # 285 | 286 | # ------------------------------------------------------------------------------ 287 | # | File access | 288 | # ------------------------------------------------------------------------------ 289 | 290 | # Block access to directories without a default document. 291 | # Usually you should leave this uncommented because you shouldn't allow anyone 292 | # to surf through every directory on your server (which may includes rather 293 | # private places like the CMS's directories). 294 | 295 | 296 | Options -Indexes 297 | 298 | 299 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 300 | 301 | # Block access to hidden files and directories. 302 | # This includes directories used by version control systems such as Git and SVN. 303 | 304 | 305 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 306 | RewriteCond %{SCRIPT_FILENAME} -f 307 | RewriteRule "(^|/)\." - [F] 308 | 309 | 310 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 311 | 312 | # Block access to backup and source files. 313 | # These files may be left by some text editors and can pose a great security 314 | # danger when anyone has access to them. 315 | 316 | 317 | Order allow,deny 318 | Deny from all 319 | Satisfy All 320 | 321 | 322 | # ------------------------------------------------------------------------------ 323 | # | Secure Sockets Layer (SSL) | 324 | # ------------------------------------------------------------------------------ 325 | 326 | # Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: 327 | # prevent `https://www.example.com` when your certificate only allows 328 | # `https://secure.example.com`. 329 | 330 | # 331 | # RewriteCond %{SERVER_PORT} !^443 332 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 333 | # 334 | 335 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 336 | 337 | # Force client-side SSL redirection. 338 | 339 | # If a user types "example.com" in his browser, the above rule will redirect him 340 | # to the secure version of the site. That still leaves a window of opportunity 341 | # (the initial HTTP connection) for an attacker to downgrade or redirect the 342 | # request. The following header ensures that browser will ONLY connect to your 343 | # server via HTTPS, regardless of what the users type in the address bar. 344 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 345 | 346 | # 347 | # Header set Strict-Transport-Security max-age=16070400; 348 | # 349 | 350 | # ------------------------------------------------------------------------------ 351 | # | Server software information | 352 | # ------------------------------------------------------------------------------ 353 | 354 | # Avoid displaying the exact Apache version number, the description of the 355 | # generic OS-type and the information about Apache's compiled-in modules. 356 | 357 | # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! 358 | 359 | # ServerTokens Prod 360 | 361 | 362 | # ############################################################################## 363 | # # WEB PERFORMANCE # 364 | # ############################################################################## 365 | 366 | # ------------------------------------------------------------------------------ 367 | # | Compression | 368 | # ------------------------------------------------------------------------------ 369 | 370 | 371 | 372 | # Force compression for mangled headers. 373 | # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping 374 | 375 | 376 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 377 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 378 | 379 | 380 | 381 | # Compress all output labeled with one of the following MIME-types 382 | # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` 383 | # and can remove the `` and `` lines 384 | # as `AddOutputFilterByType` is still in the core directives). 385 | 386 | AddOutputFilterByType DEFLATE application/atom+xml \ 387 | application/javascript \ 388 | application/json \ 389 | application/rss+xml \ 390 | application/vnd.ms-fontobject \ 391 | application/x-font-ttf \ 392 | application/x-web-app-manifest+json \ 393 | application/xhtml+xml \ 394 | application/xml \ 395 | font/opentype \ 396 | image/svg+xml \ 397 | image/x-icon \ 398 | text/css \ 399 | text/html \ 400 | text/plain \ 401 | text/x-component \ 402 | text/xml 403 | 404 | 405 | 406 | 407 | # ------------------------------------------------------------------------------ 408 | # | Content transformations | 409 | # ------------------------------------------------------------------------------ 410 | 411 | # Prevent some of the mobile network providers from modifying the content of 412 | # your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. 413 | 414 | # 415 | # Header set Cache-Control "no-transform" 416 | # 417 | 418 | # ------------------------------------------------------------------------------ 419 | # | ETag removal | 420 | # ------------------------------------------------------------------------------ 421 | 422 | # Since we're sending far-future expires headers (see below), ETags can 423 | # be removed: http://developer.yahoo.com/performance/rules.html#etags. 424 | 425 | # `FileETag None` is not enough for every server. 426 | 427 | Header unset ETag 428 | 429 | 430 | FileETag None 431 | 432 | # ------------------------------------------------------------------------------ 433 | # | Expires headers (for better cache control) | 434 | # ------------------------------------------------------------------------------ 435 | 436 | # The following expires headers are set pretty far in the future. If you don't 437 | # control versioning with filename-based cache busting, consider lowering the 438 | # cache time for resources like CSS and JS to something like 1 week. 439 | 440 | 441 | 442 | ExpiresActive on 443 | ExpiresDefault "access plus 1 month" 444 | 445 | # CSS 446 | ExpiresByType text/css "access plus 1 year" 447 | 448 | # Data interchange 449 | ExpiresByType application/json "access plus 0 seconds" 450 | ExpiresByType application/xml "access plus 0 seconds" 451 | ExpiresByType text/xml "access plus 0 seconds" 452 | 453 | # Favicon (cannot be renamed!) 454 | ExpiresByType image/x-icon "access plus 1 week" 455 | 456 | # HTML components (HTCs) 457 | ExpiresByType text/x-component "access plus 1 month" 458 | 459 | # HTML 460 | ExpiresByType text/html "access plus 0 seconds" 461 | 462 | # JavaScript 463 | ExpiresByType application/javascript "access plus 1 year" 464 | 465 | # Manifest files 466 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 467 | ExpiresByType text/cache-manifest "access plus 0 seconds" 468 | 469 | # Media 470 | ExpiresByType audio/ogg "access plus 1 month" 471 | ExpiresByType image/gif "access plus 1 month" 472 | ExpiresByType image/jpeg "access plus 1 month" 473 | ExpiresByType image/png "access plus 1 month" 474 | ExpiresByType video/mp4 "access plus 1 month" 475 | ExpiresByType video/ogg "access plus 1 month" 476 | ExpiresByType video/webm "access plus 1 month" 477 | 478 | # Web feeds 479 | ExpiresByType application/atom+xml "access plus 1 hour" 480 | ExpiresByType application/rss+xml "access plus 1 hour" 481 | 482 | # Web fonts 483 | ExpiresByType application/font-woff "access plus 1 month" 484 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 485 | ExpiresByType application/x-font-ttf "access plus 1 month" 486 | ExpiresByType font/opentype "access plus 1 month" 487 | ExpiresByType image/svg+xml "access plus 1 month" 488 | 489 | 490 | 491 | # ------------------------------------------------------------------------------ 492 | # | Filename-based cache busting | 493 | # ------------------------------------------------------------------------------ 494 | 495 | # If you're not using a build process to manage your filename version revving, 496 | # you might want to consider enabling the following directives to route all 497 | # requests such as `/css/style.12345.css` to `/css/style.css`. 498 | 499 | # To understand why this is important and a better idea than `*.css?v231`, read: 500 | # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring 501 | 502 | # 503 | # RewriteCond %{REQUEST_FILENAME} !-f 504 | # RewriteCond %{REQUEST_FILENAME} !-d 505 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 506 | # 507 | 508 | # ------------------------------------------------------------------------------ 509 | # | File concatenation | 510 | # ------------------------------------------------------------------------------ 511 | 512 | # Allow concatenation from within specific CSS and JS files, e.g.: 513 | # Inside of `script.combined.js` you could have 514 | # 515 | # 516 | # and they would be included into this single file. 517 | 518 | # 519 | # 520 | # Options +Includes 521 | # AddOutputFilterByType INCLUDES application/javascript application/json 522 | # SetOutputFilter INCLUDES 523 | # 524 | # 525 | # Options +Includes 526 | # AddOutputFilterByType INCLUDES text/css 527 | # SetOutputFilter INCLUDES 528 | # 529 | # 530 | 531 | # ------------------------------------------------------------------------------ 532 | # | Persistent connections | 533 | # ------------------------------------------------------------------------------ 534 | 535 | # Allow multiple requests to be sent over the same TCP connection: 536 | # http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. 537 | 538 | # Enable if you serve a lot of static content but, be aware of the 539 | # possible disadvantages! 540 | 541 | # 542 | # Header set Connection Keep-Alive 543 | # 544 | --------------------------------------------------------------------------------