diff --git a/app/index.js b/app/index.js index 401ec8cbd..767a0b82c 100644 --- a/app/index.js +++ b/app/index.js @@ -250,8 +250,17 @@ Generator.prototype.askForMongo = function askForMongo() { name: 'mongo', message: 'Would you like to include MongoDB with Mongoose?', default: false + }, { + type: 'confirm', + name: 'mongoPassportUser', + message: 'Would you like to include a Passport authentication boilerplate?', + default: false, + when: function (props) { + return props.mongo; + } }], function (props) { this.mongo = props.mongo; + this.mongoPassportUser = props.mongoPassportUser; cb(); }.bind(this)); @@ -335,21 +344,24 @@ function appendFilesToJade(jadeOrOptions, fileType, optimizedPath, sourceFileLis return updatedContent; } -Generator.prototype.navBarScript = function navBarScript() { - var ext = 'js'; - var folder = 'javascript'; - var minsafe = ''; +var copyScriptWithEnvOptions = function copyScriptWithEnvOptions(that, fileToCopy, destinationFolder) { + var ext = 'js', + minsafe = '', + sourceFolder = 'javascript'; - if(this.env.options.coffee) { + if(that.env.options.coffee) { ext = 'coffee'; - folder = 'coffeescript'; + sourceFolder = 'coffeescript'; } - if(this.env.options.minsafe) { + if(that.env.options.minsafe) { minsafe = '-min'; } + that.copy('../../templates/' + sourceFolder + minsafe + '/' + fileToCopy + '.' + ext, destinationFolder + fileToCopy + '.' + ext); +}; - this.copy('../../templates/' + folder + minsafe + '/navbar.' + ext, 'app/scripts/controllers/navbar.' + ext); +Generator.prototype.navBarScript = function navBarScript() { + copyScriptWithEnvOptions(this, 'controllers/navbar', 'app/scripts/'); }; Generator.prototype.appJs = function appJs() { @@ -360,6 +372,18 @@ Generator.prototype.appJs = function appJs() { sourceFileList: ['scripts/app.js', 'scripts/controllers/main.js', 'scripts/controllers/navbar.js'], searchPath: ['.tmp', 'app'] }; + + // only reference authentication controllers when required + if (this.mongoPassportUser) { + appendOptions.sourceFileList.push('scripts/controllers/login.js'); + appendOptions.sourceFileList.push('scripts/controllers/signup.js'); + appendOptions.sourceFileList.push('scripts/controllers/settings.js'); + appendOptions.sourceFileList.push('scripts/services/auth.js'); + appendOptions.sourceFileList.push('scripts/services/session.js'); + appendOptions.sourceFileList.push('scripts/services/user.js'); + appendOptions.sourceFileList.push('scripts/directives/mongooseError.js'); + } + if (this.jade) { this.indexFile = appendFilesToJade(appendOptions); } else { @@ -380,6 +404,11 @@ Generator.prototype.addJadeViews = function addHtmlJade() { if(this.jade) { this.copy('../../templates/views/jade/partials/main.jade', 'app/views/partials/main.jade'); this.copy('../../templates/views/jade/partials/navbar.jade', 'app/views/partials/navbar.jade'); + if(this.mongoPassportUser) { + this.copy('../../templates/views/jade/partials/login.jade', 'app/views/partials/login.jade'); + this.copy('../../templates/views/jade/partials/signup.jade', 'app/views/partials/signup.jade'); + this.copy('../../templates/views/jade/partials/settings.jade', 'app/views/partials/settings.jade'); + } this.copy('../../templates/views/jade/404.jade', 'app/views/404.jade'); } }; @@ -388,6 +417,11 @@ Generator.prototype.addHtmlViews = function addHtmlViews() { if(!this.jade) { this.copy('../../templates/views/html/partials/main.html', 'app/views/partials/main.html'); this.copy('../../templates/views/html/partials/navbar.html', 'app/views/partials/navbar.html'); + if(this.mongoPassportUser) { + this.copy('../../templates/views/html/partials/login.html', 'app/views/partials/login.html'); + this.copy('../../templates/views/html/partials/signup.html', 'app/views/partials/signup.html'); + this.copy('../../templates/views/html/partials/settings.html', 'app/views/partials/settings.html'); + } this.copy('../../templates/views/html/404.html', 'app/views/404.html'); } }; @@ -443,11 +477,39 @@ Generator.prototype.serverFiles = function () { }; Generator.prototype.mongoFiles = function () { + if (!this.mongo) { return; // Skip if disabled. } + this.env.options.mongo = this.mongo; this.template('../../templates/express/mongo/mongo.js', 'lib/db/mongo.js'); this.template('../../templates/express/mongo/dummydata.js', 'lib/db/dummydata.js'); this.template('../../templates/express/mongo/thing.js', 'lib/models/thing.js'); + + if(!this.mongoPassportUser) { + return; // Skip if disabled. + } + this.env.options.mongoPassportUser = this.mongoPassportUser; + + // frontend + copyScriptWithEnvOptions(this, 'controllers/login', 'app/scripts/'); + copyScriptWithEnvOptions(this, 'controllers/signup', 'app/scripts/'); + copyScriptWithEnvOptions(this, 'controllers/settings', 'app/scripts/'); + + copyScriptWithEnvOptions(this, 'services/auth', 'app/scripts/'); + copyScriptWithEnvOptions(this, 'services/session', 'app/scripts/'); + copyScriptWithEnvOptions(this, 'services/user', 'app/scripts/'); + + copyScriptWithEnvOptions(this, 'directives/mongooseError', 'app/scripts/'); + + // middleware + this.template('../../templates/express/middleware.js', 'lib/middleware.js'); + // config + this.template('../../templates/express/config/passport.js', 'lib/config/passport.js'); + // models + this.template('../../templates/express/mongo/user.js', 'lib/models/user.js'); + // controllers + this.template('../../templates/express/session.js', 'lib/controllers/session.js'); + this.template('../../templates/express/users.js', 'lib/controllers/users.js'); }; diff --git a/main/index.js b/main/index.js index 000bcd62c..f2903edff 100644 --- a/main/index.js +++ b/main/index.js @@ -13,6 +13,8 @@ util.inherits(Generator, ScriptBase); Generator.prototype.createAppFile = function createAppFile() { this.angularModules = this.env.options.angularDeps; + this.mongo = this.env.options.mongo; + this.mongoPassportUser = this.env.options.mongoPassportUser; this.ngRoute = this.env.options.ngRoute; this.appTemplate('app', 'scripts/app'); }; diff --git a/package.json b/package.json index 3c24aa9c0..b27cbe2f1 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Yeoman generator for AngularJS with Express server", "keywords": [ "yeoman-generator", + "mean", "scaffold", "express", "fullstack", diff --git a/templates/coffeescript-min/app.coffee b/templates/coffeescript-min/app.coffee index 8898744c2..704a9acac 100644 --- a/templates/coffeescript-min/app.coffee +++ b/templates/coffeescript-min/app.coffee @@ -6,6 +6,16 @@ angular.module('<%= scriptAppName %>', [<%= angularModules %>])<% if (ngRoute) { .when '/', templateUrl: 'partials/main' controller: 'MainCtrl' + <% if(mongo && mongoPassportUser) {%> + .when '/login', + templateUrl: 'partials/login' + controller: 'LoginCtrl' + .when '/logout', + controller: 'LogoutCtrl' + .when '/signup', + templateUrl: 'partials/signup' + controller: 'SignupCtrl' + <% } %> .otherwise redirectTo: '/' $locationProvider.html5Mode(true) diff --git a/templates/coffeescript-min/auth.coffee b/templates/coffeescript-min/auth.coffee new file mode 100644 index 000000000..e69de29bb diff --git a/templates/coffeescript-min/navbar.coffee b/templates/coffeescript-min/navbar.coffee index 160bb4485..21361b1cf 100644 --- a/templates/coffeescript-min/navbar.coffee +++ b/templates/coffeescript-min/navbar.coffee @@ -1,17 +1,24 @@ 'use strict' angular.module('<%= scriptAppName %>') - .controller 'NavbarCtrl', ['$scope', '$location', ($scope, $location) -> - $scope.menu = [ - title: 'Home' - link: '/' - , - title: 'About' - link: '#' - , - title: 'Contact' - link: '#' - ] - $scope.isActive = (route) -> - route is $location.path() - ] \ No newline at end of file + .controller 'NavbarCtrl', ['$scope', '$location', ($scope, $location) -> + $scope.menu = [ + title: 'Home' + link: '/' + , + title: 'About' + link: '#' + , + title: 'Contact' + link: '#' + <% if(mongo && mongoPassportUser) { %>, + title: 'Sign Up' + link: '#/signup' + , + title: 'Login' + link: '#/login' + }<% } %> + ] + $scope.isActive = (route) -> + route is $location.path() + ] \ No newline at end of file diff --git a/templates/coffeescript/app.coffee b/templates/coffeescript/app.coffee index ab162b3d5..cf99c9f5c 100644 --- a/templates/coffeescript/app.coffee +++ b/templates/coffeescript/app.coffee @@ -1,11 +1,37 @@ 'use strict' angular.module('<%= scriptAppName %>', [<%= angularModules %>])<% if (ngRoute) { %> - .config ($routeProvider, $locationProvider) -> + .config ($routeProvider, $locationProvider<% if (mongoPassportUser) { %>, $httpProvider<% } %>) -> $routeProvider .when '/', templateUrl: 'partials/main' controller: 'MainCtrl' + <% if(mongoPassportUser) {%> + .when '/login', + templateUrl: 'partials/login' + controller: 'LoginCtrl' + .when '/signup', + templateUrl: 'partials/signup' + controller: 'SignupCtrl' + .when '/settings', + templateUrl: 'partials/settings' + controller: 'SettingsCtrl' + authenticate: true<% } %> .otherwise redirectTo: '/' - $locationProvider.html5Mode(true)<% } %> \ No newline at end of file + + $locationProvider.html5Mode true<% if (mongoPassportUser) { %> + + $httpProvider.interceptors.push ['$q', '$location', ($q, $location) -> + responseError: (response) -> + if response.status is 401 or response.status is 403 + $location.path '/login' + $q.reject response + else + $q.reject response + ] + .run ($rootScope, $location, Auth) -> + + # Redirect to login if route requires auth and you're not logged in + $rootScope.$on '$routeChangeStart', (event, next) -> + $location.path '/login' if next.authenticate and not Auth.isLoggedIn()<% } %><% } %> \ No newline at end of file diff --git a/templates/coffeescript/controllers/login.coffee b/templates/coffeescript/controllers/login.coffee new file mode 100644 index 000000000..e62a6b58f --- /dev/null +++ b/templates/coffeescript/controllers/login.coffee @@ -0,0 +1,21 @@ +'use strict' + +angular.module('<%= scriptAppName %>') + .controller 'LoginCtrl', ($scope, Auth, $location) -> + $scope.user = {} + $scope.errors = {} + + $scope.login = (form) -> + $scope.submitted = true + + if form.$valid + Auth.login( + email: $scope.user.email + password: $scope.user.password + ) + .then -> + # Logged in, redirect to home + $location.path '/' + .catch (err) -> + err = err.data; + $scope.errors.other = err.message; diff --git a/templates/coffeescript/controllers/navbar.coffee b/templates/coffeescript/controllers/navbar.coffee new file mode 100644 index 000000000..bfe59adc6 --- /dev/null +++ b/templates/coffeescript/controllers/navbar.coffee @@ -0,0 +1,18 @@ +'use strict' + +angular.module('<%= scriptAppName %>') + .controller 'NavbarCtrl', ($scope, $location<% if (mongoPassportUser) { %>, Auth<% } %>) -> + $scope.menu = [ + title: 'Home' + link: '/' + <% if(mongoPassportUser) { %>, + title: 'Settings' + link: '/settings' + <% } %>] + <% if(mongoPassportUser) { %> + $scope.logout = -> + Auth.logout().then -> + $location.path "/login" + <% } %> + $scope.isActive = (route) -> + route is $location.path() \ No newline at end of file diff --git a/templates/coffeescript/controllers/settings.coffee b/templates/coffeescript/controllers/settings.coffee new file mode 100644 index 000000000..82a7fb9d5 --- /dev/null +++ b/templates/coffeescript/controllers/settings.coffee @@ -0,0 +1,17 @@ +'use strict' + +angular.module('<%= scriptAppName %>') + .controller 'SettingsCtrl', ($scope, User, Auth) -> + $scope.errors = {} + + $scope.changePassword = (form) -> + $scope.submitted = true + + if form.$valid + Auth.changePassword($scope.user.oldPassword, $scope.user.newPassword) + .then(-> + $scope.message = 'Password successfully changed.' + ).catch( -> + form.password.$setValidity 'mongoose', false + $scope.errors.other = 'Incorrect password' + ) \ No newline at end of file diff --git a/templates/coffeescript/controllers/signup.coffee b/templates/coffeescript/controllers/signup.coffee new file mode 100644 index 000000000..b5ebcc970 --- /dev/null +++ b/templates/coffeescript/controllers/signup.coffee @@ -0,0 +1,27 @@ +'use strict' + +angular.module('<%= scriptAppName %>') + .controller 'SignupCtrl', ($scope, Auth, $location) -> + $scope.user = {} + $scope.errors = {} + + $scope.register = (form) -> + $scope.submitted = true + + if form.$valid + Auth.createUser( + name: $scope.user.name + email: $scope.user.email + password: $scope.user.password + ).then( -> + # Account created, redirect to home + $location.path '/' + ).catch( (err) -> + err = err.data + $scope.errors = {} + + # Update validity of form fields that match the mongoose errors + angular.forEach err.errors, (error, field) -> + form[field].$setValidity 'mongoose', false + $scope.errors[field] = error.type + ) \ No newline at end of file diff --git a/templates/coffeescript/directives/mongooseError.coffee b/templates/coffeescript/directives/mongooseError.coffee new file mode 100644 index 000000000..d5ecd16a2 --- /dev/null +++ b/templates/coffeescript/directives/mongooseError.coffee @@ -0,0 +1,13 @@ +'use strict' + +### +Removes server error when user updates input +### +angular.module('<%= scriptAppName %>') + .directive 'mongooseError', -> + restrict: 'A' + require: 'ngModel' + link: (scope, element, attrs, ngModel) -> + element.on 'keydown', -> + ngModel.$setValidity 'mongoose', true + diff --git a/templates/coffeescript/navbar.coffee b/templates/coffeescript/navbar.coffee deleted file mode 100644 index a734e22c4..000000000 --- a/templates/coffeescript/navbar.coffee +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -angular.module('<%= scriptAppName %>') - .controller 'NavbarCtrl', ($scope, $location) -> - $scope.menu = [ - title: 'Home' - link: '/' - , - title: 'About' - link: '#' - , - title: 'Contact' - link: '#' - ] - $scope.isActive = (route) -> - route is $location.path() \ No newline at end of file diff --git a/templates/coffeescript/services/auth.coffee b/templates/coffeescript/services/auth.coffee new file mode 100644 index 000000000..714d996c1 --- /dev/null +++ b/templates/coffeescript/services/auth.coffee @@ -0,0 +1,99 @@ +'use strict' + +angular.module('<%= scriptAppName %>') + .factory 'Auth', Auth = ($location, $rootScope, Session, User, $cookieStore) -> + + # Get currentUser from cookie + $rootScope.currentUser = $cookieStore.get('user') or null + $cookieStore.remove 'user' + + ### + Authenticate user + + @param {Object} user - login info + @param {Function} callback - optional + @return {Promise} + ### + login: (user, callback) -> + cb = callback or angular.noop + Session.save( + email: user.email + password: user.password + , (user) -> + $rootScope.currentUser = user + cb() + , (err) -> + cb err + ).$promise + + + ### + Unauthenticate user + + @param {Function} callback - optional + @return {Promise} + ### + logout: (callback) -> + cb = callback or angular.noop + Session.delete(-> + $rootScope.currentUser = null + cb() + , (err) -> + cb err + ).$promise + + + ### + Create a new user + + @param {Object} user - user info + @param {Function} callback - optional + @return {Promise} + ### + createUser: (user, callback) -> + cb = callback or angular.noop + User.save(user, (user) -> + $rootScope.currentUser = user + cb user + , (err) -> + cb err + ).$promise + + + ### + Change password + + @param {String} oldPassword + @param {String} newPassword + @param {Function} callback - optional + @return {Promise} + ### + changePassword: (oldPassword, newPassword, callback) -> + cb = callback or angular.noop + User.update( + oldPassword: oldPassword + newPassword: newPassword + , (user) -> + cb user + , (err) -> + cb err + ).$promise + + + ### + Gets all available info on authenticated user + + @return {Object} user + ### + currentUser: -> + User.get() + + + ### + Simple check to see if a user is logged in + + @return {Boolean} + ### + isLoggedIn: -> + user = $rootScope.currentUser + !!user diff --git a/templates/coffeescript/services/session.coffee b/templates/coffeescript/services/session.coffee new file mode 100644 index 000000000..30da5dcc5 --- /dev/null +++ b/templates/coffeescript/services/session.coffee @@ -0,0 +1,5 @@ +'use strict' + +angular.module('<%= scriptAppName %>') + .factory 'Session', ($resource) -> + $resource '/api/session/' diff --git a/templates/coffeescript/services/user.coffee b/templates/coffeescript/services/user.coffee new file mode 100644 index 000000000..90e133ead --- /dev/null +++ b/templates/coffeescript/services/user.coffee @@ -0,0 +1,16 @@ +"use strict" + +angular.module("<%= scriptAppName %>") + .factory "User", ($resource) -> + $resource "/api/users/:id", + id: "@id" + , + update: + method: "PUT" + params: {} + + get: + method: "GET" + params: + id: "me" + diff --git a/templates/common/_bower.json b/templates/common/_bower.json index 8a8e8f5f3..da12c309c 100644 --- a/templates/common/_bower.json +++ b/templates/common/_bower.json @@ -17,4 +17,4 @@ "angular-mocks": "1.2.6", "angular-scenario": "1.2.6" } -} \ No newline at end of file +} diff --git a/templates/common/_package.json b/templates/common/_package.json index 0061ce49a..1eea0a017 100644 --- a/templates/common/_package.json +++ b/templates/common/_package.json @@ -3,11 +3,14 @@ "version": "0.0.0", "dependencies": { "express": "~3.4.3"<% if (mongo) { %>, - "mongoose": "~3.5.5", - "async": "~0.2.9"<% } %><% if (jade) { %>, + "async": "~0.2.9", + "mongoose": "~3.5.5"<% } %><% if (mongo && mongoPassportUser) { %>, + "mongoose-unique-validator": "~0.3.0", + "passport": "latest", + "passport-local": "latest", + "bcrypt": "~0.7.7"<% } %><% if (jade) { %>, "jade": "latest"<% } %><% if (!jade) { %>, - "ejs": "~0.8.4" - <% } %> + "ejs": "~0.8.4"<% } %> }, "devDependencies": { "grunt": "~0.4.1", diff --git a/templates/express/api.js b/templates/express/api.js index 6d384263d..3f5f5b2cd 100644 --- a/templates/express/api.js +++ b/templates/express/api.js @@ -1,6 +1,20 @@ 'use strict'; -<% if (!mongo) { %> -exports.awesomeThings = function(req, res) { +<% if (mongo) { %> +var mongoose = require('mongoose'), + Thing = mongoose.model('Thing'), + async = require('async'); +<% } %> +/** + * Get awesome things + */ +exports.awesomeThings = function(req, res) {<% if (mongo) { %> + return Thing.find(function (err, things) { + if (!err) { + return res.json(things); + } else { + return res.send(err); + } + });<% } %><% if (!mongo) { %> res.json([ { name : 'HTML5 Boilerplate', @@ -19,20 +33,5 @@ exports.awesomeThings = function(req, res) { info : 'Flexible and minimalist web application framework for node.js.', awesomeness: 10 } - ]); -}; -<% } %><% if (mongo) { %> -var mongoose = require('mongoose'), - Thing = mongoose.model('Thing'), - async = require('async'); - -exports.awesomeThings = function(req, res) { - return Thing.find(function (err, things) { - if (!err) { - return res.json(things); - } else { - return res.send(err); - } - }); -}; -<% } %> \ No newline at end of file + ]);<% } %> +}; \ No newline at end of file diff --git a/templates/express/config/express.js b/templates/express/config/express.js index 2f014e696..3549c7053 100644 --- a/templates/express/config/express.js +++ b/templates/express/config/express.js @@ -1,7 +1,8 @@ 'use strict'; var express = require('express'), - path = require('path'); + path = require('path')<% if (mongo && mongoPassportUser) { %>, + passport = require('passport')<% } %>; module.exports = function(app) { var rootPath = path.normalize(__dirname + '/../..'); @@ -12,9 +13,9 @@ module.exports = function(app) { // Disable caching of scripts for easier testing app.use(function noCache(req, res, next) { if (req.url.indexOf('/scripts/') === 0) { - res.header("Cache-Control", "no-cache, no-store, must-revalidate"); - res.header("Pragma", "no-cache"); - res.header("Expires", 0); + res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.header('Pragma', 'no-cache'); + res.header('Expires', 0); } next(); }); @@ -38,7 +39,16 @@ module.exports = function(app) { app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride()); - + <% if(mongo && mongoPassportUser) { %> + app.use(express.cookieParser()); + app.use(express.session({ + secret: 'generator-angular-fullstack-supersecret!', + })); + + //use passport session + app.use(passport.initialize()); + app.use(passport.session()); + <% } %> // Router needs to be last app.use(app.router); }); diff --git a/templates/express/config/passport.js b/templates/express/config/passport.js new file mode 100644 index 000000000..7da20c11d --- /dev/null +++ b/templates/express/config/passport.js @@ -0,0 +1,45 @@ +'use strict'; + +var mongoose = require('mongoose'), + User = mongoose.model('User'), + passport = require('passport'), + LocalStrategy = require('passport-local').Strategy; + +module.exports = function() { + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + passport.deserializeUser(function(id, done) { + User.findOne({ + _id: id + }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt + done(err, user); + }); + }); + + // add other strategies for more authentication flexibility + passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' // this is the virtual field on the model + }, + function(email, password, done) { + User.findOne({ + email: email + }, function(err, user) { + if (err) return done(err); + + if (!user) { + return done(null, false, { + message: 'This email is not registered.' + }); + } + if (!user.authenticate(password)) { + return done(null, false, { + message: 'This password is not correct.' + }); + } + return done(null, user); + }); + } + )); +}; \ No newline at end of file diff --git a/templates/express/index.js b/templates/express/index.js index faf92a4e7..db7f30d4c 100644 --- a/templates/express/index.js +++ b/templates/express/index.js @@ -2,18 +2,24 @@ var path = require('path'); +/** + * Send partial, or 404 if it doesn't exist + */ exports.partials = function(req, res) { var stripped = req.url.split('.')[0]; var requestedView = path.join('./', stripped); res.render(requestedView, function(err, html) { if(err) { - res.render('404'); + res.send(404); } else { res.send(html); } }); }; -exports.index = function(req, res) { +/** + * Send our single page app + */ +exports.index = function(req, res) { res.render('index'); }; diff --git a/templates/express/jshintrc b/templates/express/jshintrc index b2a3a69ac..b5787d9be 100644 --- a/templates/express/jshintrc +++ b/templates/express/jshintrc @@ -1,10 +1,7 @@ { "node": true, - "browser": true, "esnext": true, "bitwise": true, - "camelcase": true, - "curly": true, "eqeqeq": true, "immed": true, "indent": 2, @@ -13,6 +10,5 @@ "noarg": true, "regexp": true, "undef": true, - "strict": true, "smarttabs": true -} +} \ No newline at end of file diff --git a/templates/express/middleware.js b/templates/express/middleware.js new file mode 100644 index 000000000..7bfc8b679 --- /dev/null +++ b/templates/express/middleware.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Custom middleware used by the application. + */ +module.exports = { + + /** + * Protect routes on your api from unauthenticated access + */ + auth: function auth(req, res, next) { + if (req.isAuthenticated()) return next(); + res.send(401); + }, + + /** + * Set a cookie for angular so it knows we have an http session + */ + setUserCookie: function(req, res, next) { + if(req.user) { + res.cookie('user', JSON.stringify(req.user.userInfo)); + } + next(); + } +}; \ No newline at end of file diff --git a/templates/express/mongo/dummydata.js b/templates/express/mongo/dummydata.js index e0b5d556d..329647962 100644 --- a/templates/express/mongo/dummydata.js +++ b/templates/express/mongo/dummydata.js @@ -1,7 +1,8 @@ 'use strict'; -var mongoose = require('mongoose'), - Thing = mongoose.model('Thing'); +var mongoose = require('mongoose'),<% if(mongo && mongoPassportUser) { %> + User = mongoose.model('User'),<% } %> + Thing = mongoose.model('Thing'); //Clear old things, then add things in Thing.find({}).remove(function() { @@ -29,4 +30,17 @@ Thing.find({}).remove(function() { console.log('finished populating things'); } ); -}); \ No newline at end of file +}); +<% if(mongo && mongoPassportUser) { %> +// Clear old users, then add a default user +User.find({}).remove(function() { + User.create({ + name: 'Test User', + email: 'test@test.com', + password: 'test' + }, function(err) { + console.log('finished populating users'); + } + ); +}); +<% } %> \ No newline at end of file diff --git a/templates/express/mongo/mongo.js b/templates/express/mongo/mongo.js index bb65e5da8..71dbf3646 100644 --- a/templates/express/mongo/mongo.js +++ b/templates/express/mongo/mongo.js @@ -2,8 +2,6 @@ var mongoose = require('mongoose'); -exports.mongoose = mongoose; - // Configure for possible deployment var uristring = process.env.MONGOLAB_URI || @@ -13,10 +11,10 @@ var uristring = var mongoOptions = { db: { safe: true } }; // Connect to Database -mongoose.connect(uristring, mongoOptions, function (err, res) { +module.exports = mongoose.connect(uristring, mongoOptions, function (err, res) { if (err) { console.log ('ERROR connecting to: ' + uristring + '. ' + err); } else { console.log ('Successfully connected to: ' + uristring); } -}); +}); \ No newline at end of file diff --git a/templates/express/mongo/user.js b/templates/express/mongo/user.js new file mode 100755 index 000000000..cb6f5814f --- /dev/null +++ b/templates/express/mongo/user.js @@ -0,0 +1,150 @@ +'use strict'; + +var mongoose = require('mongoose'), + uniqueValidator = require('mongoose-unique-validator'), + Schema = mongoose.Schema, + bcrypt = require('bcrypt'); + +var authTypes = ['github', 'twitter', 'facebook', 'google'], + SALT_WORK_FACTOR = 10; + +/** + * User Schema + */ +var UserSchema = new Schema({ + name: String, + email: { + type: String, + unique: true + }, + role: { + type: String, + default: 'user' + }, + hashedPassword: String, + provider: String, + salt: String, + facebook: {}, + twitter: {}, + github: {}, + google: {} +}); + +/** + * Virtuals + */ +UserSchema + .virtual('password') + .set(function(password) { + this._password = password; + this.salt = this.makeSalt(); + this.hashedPassword = this.encryptPassword(password, this.salt); + }) + .get(function() { + return this._password; + }); + +// Basic info to identify the current authenticated user in the app +UserSchema + .virtual('userInfo') + .get(function() { + return { + 'name': this.name, + 'role': this.role, + 'provider': this.provider + }; + }); + +// Public profile information +UserSchema + .virtual('profile') + .get(function() { + return { + 'name': this.name, + 'role': this.role + }; + }); + +/** + * Validations + */ +var validatePresenceOf = function(value) { + return value && value.length; +}; + +// Validate empty email +UserSchema + .path('email') + .validate(function(email) { + // if you are authenticating by any of the oauth strategies, don't validate + if (authTypes.indexOf(this.provider) !== -1) return true; + return email.length; + }, 'Email cannot be blank'); + +// Validate empty password +UserSchema + .path('hashedPassword') + .validate(function(hashedPassword) { + // if you are authenticating by any of the oauth strategies, don't validate + if (authTypes.indexOf(this.provider) !== -1) return true; + return hashedPassword.length; + }, 'Password cannot be blank'); + +// /** +// * Plugins +// */ + +UserSchema.plugin(uniqueValidator, { message: 'Value is not unique.' }); + +/** + * Pre-save hook + */ +UserSchema + .pre('save', function(next) { + if (!this.isNew) return next(); + + if (!validatePresenceOf(this.hashedPassword) && authTypes.indexOf(this.provider) === -1) + next(new Error('Invalid password')); + else + next(); + }); + +/** + * Methods + */ +UserSchema.methods = { + /** + * Authenticate - check if the passwords are the same + * + * @param {String} plainText + * @return {Boolean} + * @api public + */ + authenticate: function(plainText) { + return this.encryptPassword(plainText, this.salt) === this.hashedPassword; + }, + + /** + * Make salt + * + * @return {String} + * @api public + */ + makeSalt: function() { + return bcrypt.genSaltSync(SALT_WORK_FACTOR); + }, + + /** + * Encrypt password + * + * @param {String} password + * @return {String} + * @api public + */ + encryptPassword: function(password, salt) { + // hash the password using our new salt + return bcrypt.hashSync(password, salt); + } +}; + +mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/templates/express/server.js b/templates/express/server.js index c98b24644..862d4b0e7 100644 --- a/templates/express/server.js +++ b/templates/express/server.js @@ -1,7 +1,6 @@ 'use strict'; -// Module dependencies. -var express = require('express')<% if (mongo) { %>, +var express = require('express')<% if (mongo) { %>, path = require('path'), fs = require('fs')<% } %>; @@ -18,21 +17,33 @@ fs.readdirSync(modelsPath).forEach(function (file) { // Populate empty DB with dummy data require('./lib/db/dummydata'); -<% } %> - -// Express Configuration +<% } %><% if(mongoPassportUser) { %> +// Configuration +require('./lib/config/passport')();<% } %> require('./lib/config/express')(app); // Controllers var api = require('./lib/controllers/api'), - index = require('./lib/controllers'); + index = require('./lib/controllers')<% if(mongoPassportUser) { %>, + users = require('./lib/controllers/users'), + session = require('./lib/controllers/session'); + +var middleware = require('./lib/middleware')<% } %>; // Server Routes app.get('/api/awesomeThings', api.awesomeThings); +<% if(mongoPassportUser) { %> +app.post('/api/users', users.create); +app.put('/api/users', users.changePassword); +app.get('/api/users/me', users.me); +app.get('/api/users/:id', users.show); + +app.post('/api/session', session.login); +app.del('/api/session', session.logout);<% } %> // Angular Routes app.get('/partials/*', index.partials); -app.get('/*', index.index); +app.get('/*',<% if(mongoPassportUser) { %> middleware.setUserCookie,<% } %> index.index); // Start server var port = process.env.PORT || 3000; diff --git a/templates/express/session.js b/templates/express/session.js new file mode 100644 index 000000000..d11d1daff --- /dev/null +++ b/templates/express/session.js @@ -0,0 +1,28 @@ +'use strict'; + +var mongoose = require('mongoose'), + passport = require('passport'); + +/** + * Logout + */ +exports.logout = function (req, res) { + req.logout(); + res.send(200); +}; + +/** + * Login + */ +exports.login = function (req, res, next) { + passport.authenticate('local', function(err, user, info) { + var error = err || info; + if (error) return res.json(401, error); + + req.logIn(user, function(err) { + + if (err) return res.send(err); + res.json(req.user.userInfo); + }); + })(req, res, next); +}; \ No newline at end of file diff --git a/templates/express/users.js b/templates/express/users.js new file mode 100644 index 000000000..614d01924 --- /dev/null +++ b/templates/express/users.js @@ -0,0 +1,78 @@ +'use strict'; + +var mongoose = require('mongoose'), + User = mongoose.model('User'), + passport = require('passport'); + +/** + * Create user + */ +exports.create = function (req, res, next) { + var newUser = new User(req.body); + newUser.provider = 'local'; + + newUser.save(function(err) { + if (err) { + // Manually provide our own message for 'unique' validation errors, can't do it from schema + if(err.errors.email.type === 'Value is not unique.') { + err.errors.email.type = 'The specified email address is already in use.'; + } + return res.json(400, err); + } + + req.logIn(newUser, function(err) { + if (err) return next(err); + + return res.json(req.user.userInfo); + }); + }); +}; + +/** + * Get profile of specified user + */ +exports.show = function (req, res, next) { + var userId = req.params.id; + + User.findById(userId, function (err, user) { + if (err) return next(new Error('Failed to load User')); + + if (user) { + res.send({ profile: user.profile }); + } else { + res.send(404, 'USER_NOT_FOUND'); + } + }); +}; + +/** + * Change password + */ +exports.changePassword = function(req, res, next) { + var userId = req.user._id; + var oldPass = String(req.body.oldPassword); + var newPass = String(req.body.newPassword); + + User.findById(userId, function (err, user) { + if(user.authenticate(oldPass)) { + + user.password = newPass; + user.save(function(err) { + if (err) { + res.send(500, err); + } else { + res.send(200); + } + }); + } else { + res.send(400); + } + }); +}; + +/** + * Get current user + */ +exports.me = function(req, res) { + res.json(req.user || null); +}; \ No newline at end of file diff --git a/templates/javascript-min/app.js b/templates/javascript-min/app.js index a8af4582a..73a806bee 100644 --- a/templates/javascript-min/app.js +++ b/templates/javascript-min/app.js @@ -6,9 +6,20 @@ angular.module('<%= scriptAppName %>', [<%= angularModules %>])<% if (ngRoute) { .when('/', { templateUrl: 'partials/main', controller: 'MainCtrl' + })<% if(mongo && mongoPassportUser) {%> + .when('/login', { + templateUrl: 'partials/login', + controller: 'LoginCtrl' }) + .when('/logout', { + controller: 'LogoutCtrl' + }) + .when('/signup', { + templateUrl: 'partials/signup', + controller: 'SignupCtrl' + })<% } %> .otherwise({ redirectTo: '/' }); $locationProvider.html5Mode(true); - }])<% } %>; \ No newline at end of file + }])<% } %>; diff --git a/templates/javascript-min/auth.js b/templates/javascript-min/auth.js new file mode 100644 index 000000000..c4b54f30b --- /dev/null +++ b/templates/javascript-min/auth.js @@ -0,0 +1,35 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .controller('LoginCtrl', ['$scope', '$http', function ($scope, $http) { + console.log('LoginCtrl'); + $scope.email = 'testusers@email.com'; + $scope.password = 'changeme'; + + $scope.login = function() { + console.log('login(email=' + $scope.email + ', password=' + $scope.password + ')'); + + var credentials = { + email: $scope.email, + password: $scope.password + }; + $http.post('/users/session', credentials).success(function() { + console.log('Congratulartions, you are logged IN!'); + }); + }; + }]); + +angular.module('<%= scriptAppName %>') + .controller('LogoutCtrl', ['$scope', '$http', function ($scope, $http) { + console.log('LogoutCtrl'); + $http.get('/signout').success(function() { + console.log('Congratulartions, you are logged OUT!'); + }); + }]); + +angular.module('<%= scriptAppName %>') + .controller('SignupCtrl', ['$scope', '$http', function ($scope, $http) { + console.log('SignupCtrl'); + $scope.email = 'email'; + $scope.password = 'changeme'; + }]); \ No newline at end of file diff --git a/templates/javascript/app.js b/templates/javascript/app.js index 2f366875a..5112d3090 100644 --- a/templates/javascript/app.js +++ b/templates/javascript/app.js @@ -1,14 +1,53 @@ 'use strict'; angular.module('<%= scriptAppName %>', [<%= angularModules %>])<% if (ngRoute) { %> - .config(function ($routeProvider, $locationProvider) { + .config(function ($routeProvider, $locationProvider<% if (mongoPassportUser) { %>, $httpProvider<% } %>) { $routeProvider .when('/', { templateUrl: 'partials/main', controller: 'MainCtrl' + })<% if (mongoPassportUser) { %> + .when('/login', { + templateUrl: 'partials/login', + controller: 'LoginCtrl' }) + .when('/signup', { + templateUrl: 'partials/signup', + controller: 'SignupCtrl' + }) + .when('/settings', { + templateUrl: 'partials/settings', + controller: 'SettingsCtrl', + authenticate: true + })<% } %> .otherwise({ redirectTo: '/' }); - $locationProvider.html5Mode(true); + + $locationProvider.html5Mode(true);<% if (mongoPassportUser) { %> + + // Intercept 401s and 403s and redirect you to login + $httpProvider.interceptors.push(['$q', '$location', function($q, $location) { + return { + 'responseError': function(response) { + if(response.status === 401 || response.status === 403) { + $location.path('/login'); + return $q.reject(response); + } + else { + return $q.reject(response); + } + } + }; + }]); + }) + .run(function ($rootScope, $location, Auth) { + + // Redirect to login if route requires auth and you're not logged in + $rootScope.$on('$routeChangeStart', function (event, next) { + + if (next.authenticate && !Auth.isLoggedIn()) { + $location.path('/login'); + } + });<% } %> })<% } %>; \ No newline at end of file diff --git a/templates/javascript/controllers/login.js b/templates/javascript/controllers/login.js new file mode 100644 index 000000000..ef93455da --- /dev/null +++ b/templates/javascript/controllers/login.js @@ -0,0 +1,26 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .controller('LoginCtrl', function ($scope, Auth, $location) { + $scope.user = {}; + $scope.errors = {}; + + $scope.login = function(form) { + $scope.submitted = true; + + if(form.$valid) { + Auth.login({ + email: $scope.user.email, + password: $scope.user.password + }) + .then( function() { + // Logged in, redirect to home + $location.path('/'); + }) + .catch( function(err) { + err = err.data; + $scope.errors.other = err.message; + }); + } + }; + }); \ No newline at end of file diff --git a/templates/javascript/controllers/navbar.js b/templates/javascript/controllers/navbar.js new file mode 100644 index 000000000..a5eb531b9 --- /dev/null +++ b/templates/javascript/controllers/navbar.js @@ -0,0 +1,23 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .controller('NavbarCtrl', function ($scope, $location<% if(mongoPassportUser) { %>, Auth<% } %>) { + $scope.menu = [{ + 'title': 'Home', + 'link': '/' + }<% if(mongoPassportUser) { %>, { + 'title': 'Settings', + 'link': '/settings' + }<% } %>]; + <% if(mongoPassportUser) { %> + $scope.logout = function() { + Auth.logout() + .then(function() { + $location.path('/login'); + }); + }; + <% } %> + $scope.isActive = function(route) { + return route === $location.path(); + }; + }); diff --git a/templates/javascript/controllers/settings.js b/templates/javascript/controllers/settings.js new file mode 100644 index 000000000..9d96baeb8 --- /dev/null +++ b/templates/javascript/controllers/settings.js @@ -0,0 +1,21 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .controller('SettingsCtrl', function ($scope, User, Auth) { + $scope.errors = {}; + + $scope.changePassword = function(form) { + $scope.submitted = true; + + if(form.$valid) { + Auth.changePassword( $scope.user.oldPassword, $scope.user.newPassword ) + .then( function() { + $scope.message = 'Password successfully changed.'; + }) + .catch( function() { + form.password.$setValidity('mongoose', false); + $scope.errors.other = 'Incorrect password'; + }); + } + }; + }); diff --git a/templates/javascript/controllers/signup.js b/templates/javascript/controllers/signup.js new file mode 100644 index 000000000..f48021eb4 --- /dev/null +++ b/templates/javascript/controllers/signup.js @@ -0,0 +1,33 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .controller('SignupCtrl', function ($scope, Auth, $location) { + $scope.user = {}; + $scope.errors = {}; + + $scope.register = function(form) { + $scope.submitted = true; + + if(form.$valid) { + Auth.createUser({ + name: $scope.user.name, + email: $scope.user.email, + password: $scope.user.password + }) + .then( function() { + // Account created, redirect to home + $location.path('/'); + }) + .catch( function(err) { + err = err.data; + $scope.errors = {}; + + // Update validity of form fields that match the mongoose errors + angular.forEach(err.errors, function(error, field) { + form[field].$setValidity('mongoose', false); + $scope.errors[field] = error.type; + }); + }); + } + }; + }); \ No newline at end of file diff --git a/templates/javascript/directives/mongooseError.js b/templates/javascript/directives/mongooseError.js new file mode 100644 index 000000000..982943eb2 --- /dev/null +++ b/templates/javascript/directives/mongooseError.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + + /** + * Removes server error when user updates input + */ + .directive('mongooseError', function () { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attrs, ngModel) { + element.on('keydown', function() { + return ngModel.$setValidity('mongoose', true); + }); + } + }; + }); \ No newline at end of file diff --git a/templates/javascript/navbar.js b/templates/javascript/navbar.js deleted file mode 100644 index 8921337f7..000000000 --- a/templates/javascript/navbar.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -angular.module('<%= scriptAppName %>') - .controller('NavbarCtrl', function ($scope, $location) { - $scope.menu = [{ - 'title': 'Home', - 'link': '/' - }, - { - 'title': 'About', - 'link': '#' - }, - { - 'title': 'Contact', - 'link': '#' - }]; - - $scope.isActive = function(route) { - return route === $location.path(); - }; - }); diff --git a/templates/javascript/services/auth.js b/templates/javascript/services/auth.js new file mode 100644 index 000000000..09c53a671 --- /dev/null +++ b/templates/javascript/services/auth.js @@ -0,0 +1,111 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .factory('Auth', function Auth($location, $rootScope, Session, User, $cookieStore) { + + // Get currentUser from cookie + $rootScope.currentUser = $cookieStore.get('user') || null; + $cookieStore.remove('user'); + + return { + + /** + * Authenticate user + * + * @param {Object} user - login info + * @param {Function} callback - optional + * @return {Promise} + */ + login: function(user, callback) { + var cb = callback || angular.noop; + + return Session.save({ + email: user.email, + password: user.password + }, function(user) { + $rootScope.currentUser = user; + return cb(); + }, function(err) { + return cb(err); + }).$promise; + }, + + /** + * Unauthenticate user + * + * @param {Function} callback - optional + * @return {Promise} + */ + logout: function(callback) { + var cb = callback || angular.noop; + + return Session.delete(function() { + $rootScope.currentUser = null; + return cb(); + }, + function(err) { + return cb(err); + }).$promise; + }, + + /** + * Create a new user + * + * @param {Object} user - user info + * @param {Function} callback - optional + * @return {Promise} + */ + createUser: function(user, callback) { + var cb = callback || angular.noop; + + return User.save(user, + function(user) { + $rootScope.currentUser = user; + return cb(user); + }, + function(err) { + return cb(err); + }).$promise; + }, + + /** + * Change password + * + * @param {String} oldPassword + * @param {String} newPassword + * @param {Function} callback - optional + * @return {Promise} + */ + changePassword: function(oldPassword, newPassword, callback) { + var cb = callback || angular.noop; + + return User.update({ + oldPassword: oldPassword, + newPassword: newPassword + }, function(user) { + return cb(user); + }, function(err) { + return cb(err); + }).$promise; + }, + + /** + * Gets all available info on authenticated user + * + * @return {Object} user + */ + currentUser: function() { + return User.get(); + }, + + /** + * Simple check to see if a user is logged in + * + * @return {Boolean} + */ + isLoggedIn: function() { + var user = $rootScope.currentUser; + return !!user; + }, + }; + }); \ No newline at end of file diff --git a/templates/javascript/services/session.js b/templates/javascript/services/session.js new file mode 100644 index 000000000..80a300cbc --- /dev/null +++ b/templates/javascript/services/session.js @@ -0,0 +1,6 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .factory('Session', function ($resource) { + return $resource('/api/session/'); + }); diff --git a/templates/javascript/services/user.js b/templates/javascript/services/user.js new file mode 100644 index 000000000..d59886a95 --- /dev/null +++ b/templates/javascript/services/user.js @@ -0,0 +1,19 @@ +'use strict'; + +angular.module('<%= scriptAppName %>') + .factory('User', function ($resource) { + return $resource('/api/users/:id', { + id: '@id' + }, { //parameters default + update: { + method: 'PUT', + params: {} + }, + get: { + method: 'GET', + params: { + id:'me' + } + } + }); + }); diff --git a/templates/views/html/partials/login.html b/templates/views/html/partials/login.html new file mode 100644 index 000000000..2463f52a9 --- /dev/null +++ b/templates/views/html/partials/login.html @@ -0,0 +1,40 @@ +
+ +
+
+

Login

+

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

+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+

+ Please enter your email and password. +

+

{{ errors.other }}

+
+ + +
+
+
+ +
+
+
+ Not registered? +
+
\ No newline at end of file diff --git a/templates/views/html/partials/main.html b/templates/views/html/partials/main.html index 7c47e7176..5b2c6eb60 100644 --- a/templates/views/html/partials/main.html +++ b/templates/views/html/partials/main.html @@ -1,7 +1,7 @@
-

'Allo, 'Allo!

+

'Allo, 'Allo<% if(mongoPassportUser) { %>{{ currentUser.name }}<% } %>!

I'm Yeoman
Always a pleasure scaffolding your apps. diff --git a/templates/views/html/partials/navbar.html b/templates/views/html/partials/navbar.html index 66811cdc1..ff7a538b8 100644 --- a/templates/views/html/partials/navbar.html +++ b/templates/views/html/partials/navbar.html @@ -2,7 +2,10 @@

<%= appname %>

\ No newline at end of file diff --git a/templates/views/html/partials/settings.html b/templates/views/html/partials/settings.html new file mode 100644 index 000000000..2e66903f0 --- /dev/null +++ b/templates/views/html/partials/settings.html @@ -0,0 +1,37 @@ +
+ +
+
+

Change Password

+
+
+
+ +
+ + + +

+ {{ errors.other }} +

+
+ +
+ + + +

+ Password must be at least 3 characters. +

+
+ +

{{ message }}

+ + +
+
+
diff --git a/templates/views/html/partials/signup.html b/templates/views/html/partials/signup.html new file mode 100644 index 000000000..71d75fedd --- /dev/null +++ b/templates/views/html/partials/signup.html @@ -0,0 +1,66 @@ +
+ +
+
+

Sign up

+
+
+
+ +
+ + + +

+ A name is required +

+
+ +
+ + + +

+ Doesn't look like a valid email. +

+

+ What's your email address? +

+

+ {{ errors.email }} +

+
+ +
+ + + +

+ Password must be at least 3 characters. +

+

+ {{ errors.password }} +

+
+ + +
+
+
+ +
+
+
+ Have an account? +
+
\ No newline at end of file diff --git a/templates/views/jade/partials/login.jade b/templates/views/jade/partials/login.jade new file mode 100644 index 000000000..05f07fd2a --- /dev/null +++ b/templates/views/jade/partials/login.jade @@ -0,0 +1,37 @@ +div(ng-include='\'partials/navbar.html\'') + +.row + .col-sm-12 + h1 Login + p + | Accounts are reset on server restart from + code dummydata.js + | . Default account is + code test@test.com + | / + code test + + .col-sm-12 + form.form(name='form', ng-submit='login(form)', novalidate='novalidate') + + .form-group + label Email + input.form-control(type='text', name='email', ng-model='user.email') + + .form-group + label Password + input.form-control(type='password', name='password', ng-model='user.password') + + .form-group.has-error + p.help-block(ng-show='form.email.$error.required && form.password.$error.required && submitted') + | Please enter your email and password. + p.help-block {{ errors.other }} + + button.btn.btn-lg.btn-primary(type='submit') Sign in + span.clearfix + +.row + .col-sm-12 + hr + | Not registered? + a.text-center.new-account(href='/signup') Create an account. \ No newline at end of file diff --git a/templates/views/jade/partials/main.jade b/templates/views/jade/partials/main.jade index 28cd4974a..403e8711e 100644 --- a/templates/views/jade/partials/main.jade +++ b/templates/views/jade/partials/main.jade @@ -1,16 +1,22 @@ -div(ng-include="'partials/navbar.html'") +div(ng-include='\'partials/navbar\'') .jumbotron - h1 'Allo, 'Allo! + h1 + | 'Allo, 'Allo<% if(mongoPassportUser) { %> + span(ng-show='currentUser') {{ currentUser.name }} + | <% } %>! + p.lead I'm Yeoman br | Always a pleasure scaffolding your apps. p a.btn.btn-lg.btn-success(ng-href='#') Splendid! + .row.marketing div(ng-repeat='thing in awesomeThings') h4 {{thing.name}} p {{thing.info}} + .footer p ♥ from the Yeoman team \ No newline at end of file diff --git a/templates/views/jade/partials/navbar.jade b/templates/views/jade/partials/navbar.jade index 8f6506439..01c82e0e9 100644 --- a/templates/views/jade/partials/navbar.jade +++ b/templates/views/jade/partials/navbar.jade @@ -1,5 +1,11 @@ .header(ng-controller='NavbarCtrl') ul.nav.nav-pills.pull-right li(ng-repeat='item in menu', ng-class='{active: isActive(item.link)}') - a(ng-href='{{item.link}}') {{item.title}} - h3.text-muted <%= scriptAppName %> \ No newline at end of file + a(ng-href='{{item.link}}') {{item.title}}<% if(mongoPassportUser) { %> + li(ng-hide='currentUser', ng-class='{active: isActive("/login")}') + a(href='/login') Login + li(ng-hide='currentUser', ng-class='{active: isActive("/signup")}') + a(href='/signup') Sign up + li(ng-show='currentUser', ng-class='{active: isActive("/logout")}') + a(href='', ng-click='logout()') Logout<% } %> + h3.text-muted <%= appname %> diff --git a/templates/views/jade/partials/settings.jade b/templates/views/jade/partials/settings.jade new file mode 100644 index 000000000..5c07d5d9b --- /dev/null +++ b/templates/views/jade/partials/settings.jade @@ -0,0 +1,24 @@ +div(ng-include='\'partials/navbar.html\'') + +.row + .col-sm-12 + h1 Change Password + + .col-sm-12 + form.form(name='form', ng-submit='changePassword(form)', novalidate='') + + .form-group + label Current Password + input.form-control(type='password', name='password', ng-model='user.oldPassword', mongoose-error='') + p.help-block(ng-show='form.password.$error.mongoose') + | {{ errors.other }} + + .form-group + label New Password + input.form-control(type='password', name='newPassword', ng-model='user.newPassword', ng-minlength='3', required='') + p.help-block(ng-show='(form.newPassword.$error.minlength || form.newPassword.$error.required) && (form.newPassword.$dirty || submitted)') + | Password must be at least 3 characters. + + p.help-block {{ message }} + + button.btn.btn-lg.btn-primary(type='submit') Save changes diff --git a/templates/views/jade/partials/signup.jade b/templates/views/jade/partials/signup.jade new file mode 100644 index 000000000..023eee339 --- /dev/null +++ b/templates/views/jade/partials/signup.jade @@ -0,0 +1,38 @@ +div(ng-include='\'partials/navbar.html\'') + +.row + .col-sm-12 + h1 Sign up + + .col-sm-12 + form.form(name='form', ng-submit='register(form)', novalidate='') + .form-group(ng-class='{ \'has-success\': form.name.$valid && submitted, \'has-error\': form.name.$invalid && submitted }') + label Name + input.form-control(type='text', name='name', ng-model='user.name', required='') + p.help-block(ng-show='form.name.$error.required && submitted') + | A name is required + + .form-group(ng-class='{ \'has-success\': form.email.$valid && submitted, \'has-error\': form.email.$invalid && submitted }') + label Email + input.form-control(type='email', name='email', ng-model='user.email', required='', mongoose-error='') + p.help-block(ng-show='form.email.$error.email && submitted') + | Doesn't look like a valid email. + p.help-block(ng-show='form.email.$error.required && submitted') + | What's your email address? + p.help-block(ng-show='form.email.$error.mongoose') + | {{ errors.email }} + + .form-group(ng-class='{ \'has-success\': form.password.$valid && submitted, \'has-error\': form.password.$invalid && submitted }') + label Password + input.form-control(type='password', name='password', ng-model='user.password', ng-minlength='3', required='', mongoose-error='') + p.help-block(ng-show='(form.password.$error.minlength || form.password.$error.required) && submitted') + | Password must be at least 3 characters. + p.help-block(ng-show='form.password.$error.mongoose') + | {{ errors.password }} + + button.btn.btn-lg.btn-primary(type='submit') Sign up +.row + .col-sm-12 + hr + | Have an account? + a.text-center.new-account(href='/login') Log in. \ No newline at end of file