diff --git a/Procfile b/Procfile new file mode 100755 index 0000000000..3360097a4d --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: ./node_modules/.bin/forever -m 5 server.js diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 0f4d5767f0..4355728d63 --- a/README.md +++ b/README.md @@ -1,4 +1,157 @@ -angular-quickstart -================== +# MEAN Stack -hybdrid between mean stack and angular seed and BBB +MEAN is a boilerplate that provides a nice starting point for [MongoDB](http://www.mongodb.org/), [Node.js](http://www.nodejs.org/), [Express](http://expressjs.com/), and [AngularJS](http://angularjs.org/) based applications. It is designed to give you quick and organized way to start developing of MEAN based web apps with useful modules like mongoose and passport pre-bundled and configured. We mainly try to take care of the connection points between existing popular frameworks and solve common integration problems. + +## Prerequisites +* Node.js - Download and Install [Node.js](http://www.nodejs.org/download/). You can also follow [this gist](https://gist.github.com/isaacs/579814) for a quick and easy way to install Node.js and npm +* MongoDB - Download and Install [MongoDB](http://www.mongodb.org/downloads) - Make sure it's running on the default port (27017). + +### Tools Prerequisites +* NPM - Node.js package manager, should be installed when you install node.js. +* Bower - Web package manager, installing [Bower](http://bower.io/) is simple when you have npm: + +``` +$ npm install -g bower +``` + +### Optional +* Grunt - Download and Install [Grunt](http://gruntjs.com). + +## Additional Packages +* Express - Defined as npm module in the [package.json](package.json) file. +* Mongoose - Defined as npm module in the [package.json](package.json) file. +* Passport - Defined as npm module in the [package.json](package.json) file. +* AngularJS - Defined as bower module in the [bower.json](bower.json) file. +* Twitter Bootstrap - Defined as bower module in the [bower.json](bower.json) file. +* UI Bootstrap - Defined as bower module in the [bower.json](bower.json) file. + +## Quick Install + The quickest way to get started with MEAN is to clone the project and utilize it like this: + + Install dependencies: + + $ npm install + + We recommend using [Grunt](https://github.com/gruntjs/grunt-cli) to start the server: + + $ grunt + + When not using grunt you can use: + + $ node server + + Then open a browser and go to: + + http://localhost:3000 + + +## Troubleshooting +During install some of you may encounter some issues, most of this issues can be solved by one of the following tips. +If you went through all this and still can't solve the issue, feel free to contact me(Amos), via the repository issue tracker or the links provided below. + +#### Update NPM, Bower or Grunt +Sometimes you may find there is a weird error during install like npm's *Error: ENOENT*, usually updating those tools to the latest version solves the issue. + +Updating NPM: +``` +$ npm update -g npm +``` + +Updating Grunt: +``` +$ npm update -g grunt-cli +``` + +Updating Bower: +``` +$ npm update -g bower +``` + +#### Cleaning NPM and Bower cache +NPM and Bower has a caching system for holding packages that you already installed. +We found that often cleaning the cache solves some troubles this system creates. + +NPM Clean Cache: +``` +$ npm cache clean +``` + +Bower Clean Cache: +``` +$ bower cache clean +``` + + +## Configuration +All configuration is specified in the [config](config/) folder, particularly the [config.js](config/config.js) file and the [env](config/env/) files. Here you will need to specify your application name, database name, as well as hook up any social app keys if you want integration with Twitter, Facebook, GitHub or Google. + +### Environmental Settings + +There are three environments provided by default, __development__, __test__, and __production__. Each of these environments has the following configuration options: +* __db__ - This is the name of the MongoDB database to use, and is set by default to __mean-dev__ for the development environment. +* __app.name__ - This is the name of your app or website, and can be different for each environment. You can tell which environment you are running by looking at the TITLE attribute that your app generates. +* __Social OAuth Keys__ - Facebook, GitHub, Google, Twitter. You can specify your own social application keys here for each platform: + * __clientID__ + * __clientSecret__ + * __callbackURL__ + +To run with a different environment, just specify NODE_ENV as you call grunt: + + $ NODE_ENV=test grunt + +If you are using node instead of grunt, it is very similar: + + $ NODE_ENV=test node server + +> NOTE: Running Node.js applications in the __production__ environment enables caching, which is disabled by default in all other environments. + +## Getting Started + We pre-included an article example, check it out: + * [The Model](https://github.com/linnovate/mean/blob/master/app/models/article.js) - Where we define our object schema. + * [The Controller](https://github.com/linnovate/mean/blob/master/app/controllers/articles.js) - Where we take care of our backend logic. + * [NodeJS Routes](https://github.com/linnovate/mean/blob/master/config/routes.js) - Where we define our REST service routes. + * [AngularJs Routes](https://github.com/linnovate/mean/blob/master/public/js/config.js) - Where we define our CRUD routes. + * [The AngularJs Service](https://github.com/linnovate/mean/blob/master/public/js/services/articles.js) - Where we connect to our REST service. + * [The AngularJs Controller](https://github.com/linnovate/mean/blob/master/public/js/controllers/articles.js) - Where we take care of our frontend logic. + * [The AngularJs Views Folder](https://github.com/linnovate/mean/blob/master/public/views/articles) - Where we keep our CRUD views. + +## Heroku Quick Deployment +Before you start make sure you have heroku toolbelt installed and an accessible mongo db instance - you can try mongohq which have an easy setup ) + +```bash +git init +git add . +git commit -m "initial version" +heroku apps:create +git push heroku master +``` + +## More Information + * Contact Amos Haviv on any issue via [E-Mail](mailto:mail@amoshaviv.com), [Facebook](http://www.facebook.com/amoshaviv), or [Twitter](http://www.twitter.com/amoshaviv). + * Visit us at [Linnovate.net](http://www.linnovate.net/). + * Visit our [Ninja's Zone](http://www.meanleanstartupmachine.com/) for extended support. + +## Credits +Inspired by the great work of [Madhusudhan Srinivasa](https://github.com/madhums/) + +## License +(The MIT License) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app/controllers/articles.js b/app/controllers/articles.js new file mode 100755 index 0000000000..0125fbfafa --- /dev/null +++ b/app/controllers/articles.js @@ -0,0 +1,90 @@ +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + Article = mongoose.model('Article'), + _ = require('underscore'); + + +/** + * Find article by id + */ +exports.article = function(req, res, next, id) { + Article.load(id, function(err, article) { + if (err) return next(err); + if (!article) return next(new Error('Failed to load article ' + id)); + req.article = article; + next(); + }); +}; + +/** + * Create a article + */ +exports.create = function(req, res) { + var article = new Article(req.body); + article.user = req.user; + + article.save(function(err) { + if (err) { + return res.send('users/signup', { + errors: err.errors, + article: article + }); + } else { + res.jsonp(article); + } + }); +}; + +/** + * Update a article + */ +exports.update = function(req, res) { + var article = req.article; + + article = _.extend(article, req.body); + + article.save(function(err) { + res.jsonp(article); + }); +}; + +/** + * Delete an article + */ +exports.destroy = function(req, res) { + var article = req.article; + + article.remove(function(err) { + if (err) { + res.render('error', { + status: 500 + }); + } else { + res.jsonp(article); + } + }); +}; + +/** + * Show an article + */ +exports.show = function(req, res) { + res.jsonp(req.article); +}; + +/** + * List of Articles + */ +exports.all = function(req, res) { + Article.find().sort('-created').populate('user', 'name username').exec(function(err, articles) { + if (err) { + res.render('error', { + status: 500 + }); + } else { + res.jsonp(articles); + } + }); +}; diff --git a/app/controllers/index.js b/app/controllers/index.js new file mode 100755 index 0000000000..c775a01667 --- /dev/null +++ b/app/controllers/index.js @@ -0,0 +1,12 @@ +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + _ = require('underscore'); + + +exports.render = function(req, res) { + res.render('index', { + user: req.user ? JSON.stringify(req.user) : "null" + }); +}; diff --git a/app/controllers/users.js b/app/controllers/users.js new file mode 100755 index 0000000000..30528152ff --- /dev/null +++ b/app/controllers/users.js @@ -0,0 +1,103 @@ +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * Auth callback + */ +exports.authCallback = function(req, res, next) { + res.redirect('/'); +}; + +/** + * Show login form + */ +exports.signin = function(req, res) { + res.render('users/signin', { + title: 'Signin', + message: req.flash('error') + }); +}; + +/** + * Show sign up form + */ +exports.signup = function(req, res) { + res.render('users/signup', { + title: 'Sign up', + user: new User() + }); +}; + +/** + * Logout + */ +exports.signout = function(req, res) { + req.logout(); + res.redirect('/'); +}; + +/** + * Session + */ +exports.session = function(req, res) { + res.redirect('/'); +}; + +/** + * Create user + */ +exports.create = function(req, res) { + var user = new User(req.body); + + user.provider = 'local'; + user.save(function(err) { + if (err) { + return res.render('users/signup', { + errors: err.errors, + user: user + }); + } + req.logIn(user, function(err) { + if (err) return next(err); + return res.redirect('/'); + }); + }); +}; + +/** + * Show profile + */ +exports.show = function(req, res) { + var user = req.profile; + + res.render('users/show', { + title: user.name, + user: user + }); +}; + +/** + * Send User + */ +exports.me = function(req, res) { + res.jsonp(req.user || null); +}; + +/** + * Find user by id + */ +exports.user = function(req, res, next, id) { + User + .findOne({ + _id: id + }) + .exec(function(err, user) { + if (err) return next(err); + if (!user) return next(new Error('Failed to load User ' + id)); + req.profile = user; + next(); + }); +}; \ No newline at end of file diff --git a/app/models/article.js b/app/models/article.js new file mode 100755 index 0000000000..ff1e186808 --- /dev/null +++ b/app/models/article.js @@ -0,0 +1,51 @@ +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + config = require('../../config/config'), + Schema = mongoose.Schema; + + +/** + * Article Schema + */ +var ArticleSchema = new Schema({ + created: { + type: Date, + default: Date.now + }, + title: { + type: String, + default: '', + trim: true + }, + content: { + type: String, + default: '', + trim: true + }, + user: { + type: Schema.ObjectId, + ref: 'User' + } +}); + +/** + * Validations + */ +ArticleSchema.path('title').validate(function(title) { + return title.length; +}, 'Title cannot be blank'); + +/** + * Statics + */ +ArticleSchema.statics = { + load: function(id, cb) { + this.findOne({ + _id: id + }).populate('user', 'name username').exec(cb); + } +}; + +mongoose.model('Article', ArticleSchema); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js new file mode 100755 index 0000000000..8c960a1207 --- /dev/null +++ b/app/models/user.js @@ -0,0 +1,124 @@ +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + Schema = mongoose.Schema, + crypto = require('crypto'), + _ = require('underscore'), + authTypes = ['github', 'twitter', 'facebook', 'google']; + + +/** + * User Schema + */ +var UserSchema = new Schema({ + name: String, + email: String, + username: { + type: String, + unique: true + }, + provider: String, + hashed_password: String, + salt: String, + facebook: {}, + twitter: {}, + github: {}, + google: {} +}); + +/** + * Virtuals + */ +UserSchema.virtual('password').set(function(password) { + this._password = password; + this.salt = this.makeSalt(); + this.hashed_password = this.encryptPassword(password); +}).get(function() { + return this._password; +}); + +/** + * Validations + */ +var validatePresenceOf = function(value) { + return value && value.length; +}; + +// the below 4 validations only apply if you are signing up traditionally +UserSchema.path('name').validate(function(name) { + // if you are authenticating by any of the oauth strategies, don't validate + if (authTypes.indexOf(this.provider) !== -1) return true; + return name.length; +}, 'Name cannot be blank'); + +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'); + +UserSchema.path('username').validate(function(username) { + // if you are authenticating by any of the oauth strategies, don't validate + if (authTypes.indexOf(this.provider) !== -1) return true; + return username.length; +}, 'Username cannot be blank'); + +UserSchema.path('hashed_password').validate(function(hashed_password) { + // if you are authenticating by any of the oauth strategies, don't validate + if (authTypes.indexOf(this.provider) !== -1) return true; + return hashed_password.length; +}, 'Password cannot be blank'); + + +/** + * Pre-save hook + */ +UserSchema.pre('save', function(next) { + if (!this.isNew) return next(); + + if (!validatePresenceOf(this.password) && 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.hashed_password; + }, + + /** + * Make salt + * + * @return {String} + * @api public + */ + makeSalt: function() { + return Math.round((new Date().valueOf() * Math.random())) + ''; + }, + + /** + * Encrypt password + * + * @param {String} password + * @return {String} + * @api public + */ + encryptPassword: function(password) { + if (!password) return ''; + return crypto.createHmac('sha1', this.salt).update(password).digest('hex'); + } +}; + +mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/views/404.jade b/app/views/404.jade new file mode 100755 index 0000000000..2f0d9e8663 --- /dev/null +++ b/app/views/404.jade @@ -0,0 +1,13 @@ +extends layouts/default + +block main + h1 Oops something went wrong + br + span 404 + +block content + #error-message-box + #error-stack-trace + pre + code!= error + diff --git a/app/views/500.jade b/app/views/500.jade new file mode 100755 index 0000000000..491b00084e --- /dev/null +++ b/app/views/500.jade @@ -0,0 +1,12 @@ +extends layouts/default + +block main + h1 Oops something went wrong + br + span 500 + +block content + #error-message-box + #error-stack-trace + pre + code!= error diff --git a/app/views/includes/foot.jade b/app/views/includes/foot.jade new file mode 100755 index 0000000000..09b1b54d0b --- /dev/null +++ b/app/views/includes/foot.jade @@ -0,0 +1,29 @@ +//AngularJS +script(type='text/javascript', src='/lib/angular/angular.js') +script(type='text/javascript', src='/lib/angular-cookies/angular-cookies.js') +script(type='text/javascript', src='/lib/angular-resource/angular-resource.js') + +//Angular UI +script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap-tpls.js') +script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap.js') +script(type='text/javascript', src='/lib/angular-ui-utils/modules/route/route.js') + +//Application Init +script(type='text/javascript', src='/js/app.js') +script(type='text/javascript', src='/js/config.js') +script(type='text/javascript', src='/js/directives.js') +script(type='text/javascript', src='/js/filters.js') + +//Application Services +script(type='text/javascript', src='/js/services/global.js') +script(type='text/javascript', src='/js/services/articles.js') + +//Application Controllers +script(type='text/javascript', src='/js/controllers/articles.js') +script(type='text/javascript', src='/js/controllers/index.js') +script(type='text/javascript', src='/js/controllers/header.js') +script(type='text/javascript', src='/js/init.js') + +if (req.host='localhost') + //Livereload script rendered + script(type='text/javascript', src='http://localhost:35729/livereload.js') diff --git a/app/views/includes/head.jade b/app/views/includes/head.jade new file mode 100755 index 0000000000..8daa225acf --- /dev/null +++ b/app/views/includes/head.jade @@ -0,0 +1,29 @@ +head + meta(charset='utf-8') + meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') + meta(name='viewport', content='width=device-width,initial-scale=1') + + title= appName+' - '+title + meta(http-equiv='Content-type', content='text/html;charset=UTF-8') + meta(name="keywords", content="node.js, express, mongoose, mongodb, angularjs") + meta(name="description", content="MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).") + + link(href='/img/icons/favicon.ico', rel='shortcut icon', type='image/x-icon') + + meta(property='fb:app_id', content='APP_ID') + meta(property='og:title', content='#{appName} - #{title}') + meta(property='og:description', content='MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).') + meta(property='og:type', content='website') + meta(property='og:url', content='APP_URL') + meta(property='og:image', content='APP_LOGO') + meta(property='og:site_name', content='MEAN - A Modern Stack') + meta(property='fb:admins', content='APP_ADMIN') + + link(rel='stylesheet', href='/lib/bootstrap/docs/assets/css/bootstrap.css') + //- link(rel='stylesheet', href='/lib/bootstrap/dist/css/bootstrap-responsive.css') + link(rel='stylesheet', href='/css/common.css') + + link(rel='stylesheet', href='/css/views/articles.css') + + //if lt IE 9 + script(src='http://html5shim.googlecode.com/svn/trunk/html5.js') diff --git a/app/views/index.jade b/app/views/index.jade new file mode 100755 index 0000000000..c121898992 --- /dev/null +++ b/app/views/index.jade @@ -0,0 +1,6 @@ +extends layouts/default + +block content + section(data-ng-view) + script(type="text/javascript"). + window.user = !{user}; diff --git a/app/views/layouts/default.jade b/app/views/layouts/default.jade new file mode 100755 index 0000000000..5e242b17a6 --- /dev/null +++ b/app/views/layouts/default.jade @@ -0,0 +1,9 @@ +!!! 5 +html(lang='en', xmlns='http://www.w3.org/1999/xhtml', xmlns:fb='https://www.facebook.com/2008/fbml', itemscope='itemscope', itemtype='http://schema.org/Product') + include ../includes/head + body + .navbar.navbar-inverse.navbar-fixed-top(data-ng-include="'views/header.html'", data-role="navigation") + section.content + section.container + block content + include ../includes/foot diff --git a/app/views/users/auth.jade b/app/views/users/auth.jade new file mode 100755 index 0000000000..de60489987 --- /dev/null +++ b/app/views/users/auth.jade @@ -0,0 +1,22 @@ +extends ../layouts/default + +block content + .row + .offset1.span5 + a(href="/auth/facebook") + img(src="/img/icons/facebook.png") + a(href="/auth/github") + img(src="/img/icons/github.png") + a(href="/auth/twitter") + img(src="/img/icons/twitter.png") + a(href="/auth/google") + img(src="/img/icons/google.png") + .span6 + if (typeof errors !== 'undefined') + .fade.in.alert.alert-block.alert-error + a.close(data-dismiss="alert", href="javascript:void(0)") x + ul + each error in errors + li= error.type + + block auth diff --git a/app/views/users/signin.jade b/app/views/users/signin.jade new file mode 100755 index 0000000000..ef96cb67f4 --- /dev/null +++ b/app/views/users/signin.jade @@ -0,0 +1,20 @@ +extends auth + +block auth + form.signin.form-horizontal(action="/users/session", method="post") + p.error= message + .control-group + label.control-label(for='email') Email + .controls + input#email(type='text', name="email", placeholder='Email') + + .control-group + label.control-label(for='password') Password + .controls + input#password(type='password', name="password", placeholder='Password') + + .form-actions + button.btn.btn-primary(type='submit') Sign in +   + | or  + a.show-signup(href="/signup") Sign up diff --git a/app/views/users/signup.jade b/app/views/users/signup.jade new file mode 100755 index 0000000000..b51229bccc --- /dev/null +++ b/app/views/users/signup.jade @@ -0,0 +1,29 @@ +extends auth + +block auth + form.signup.form-horizontal(action="/users", method="post") + .control-group + label.control-label(for='name') Full name + .controls + input#name(type='text', name="name", placeholder='Full name', value=user.name) + + .control-group + label.control-label(for='email') Email + .controls + input#email(type='text', name="email", placeholder='Email', value=user.email) + + .control-group + label.control-label(for='username') Username + .controls + input#username(type='text', name="username", placeholder='Username', value=user.username) + + .control-group + label.control-label(for='password') Password + .controls + input#password(type='password', name="password", placeholder='Password') + + .form-actions + button.btn.btn-primary(type='submit') Sign up +   + | or  + a.show-login(href="/signin") login diff --git a/bower.json b/bower.json new file mode 100755 index 0000000000..1ee578ef41 --- /dev/null +++ b/bower.json @@ -0,0 +1,12 @@ +{ + "name": "mean", + "version": "0.1.0", + "dependencies": { + "bootstrap": "2.3.2", + "angular": "1.0.6", + "angular-resource": "1.0.6", + "angular-cookies": "1.0.6", + "angular-bootstrap": "0.6.0", + "angular-ui-utils": "0.0.4" + } +} diff --git a/config/env/all.js b/config/env/all.js new file mode 100755 index 0000000000..fd5f16fabd --- /dev/null +++ b/config/env/all.js @@ -0,0 +1,8 @@ +var path = require('path'), +rootPath = path.normalize(__dirname + '/../..'); + +module.exports = { + root: rootPath, + port: process.env.PORT || 3000, + db: process.env.MONGOHQ_URL +} diff --git a/config/env/development.json b/config/env/development.json new file mode 100755 index 0000000000..cc3055f415 --- /dev/null +++ b/config/env/development.json @@ -0,0 +1,26 @@ +{ + "db": "mongodb://localhost/mean-dev", + "app": { + "name": "MEAN - A Modern Stack - Development" + }, + "facebook": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/facebook/callback" + }, + "twitter": { + "clientID": "CONSUMER_KEY", + "clientSecret": "CONSUMER_SECRET", + "callbackURL": "http://localhost:3000/auth/twitter/callback" + }, + "github": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/github/callback" + }, + "google": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/google/callback" + } +} \ No newline at end of file diff --git a/config/env/production.json b/config/env/production.json new file mode 100755 index 0000000000..f12ffb51e0 --- /dev/null +++ b/config/env/production.json @@ -0,0 +1,26 @@ +{ + "db": "mongodb://localhost/mean", + "app": { + "name": "MEAN - A Modern Stack - Production" + }, + "facebook": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/facebook/callback" + }, + "twitter": { + "clientID": "CONSUMER_KEY", + "clientSecret": "CONSUMER_SECRET", + "callbackURL": "http://localhost:3000/auth/twitter/callback" + }, + "github": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/github/callback" + }, + "google": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/google/callback" + } +} \ No newline at end of file diff --git a/config/env/test.json b/config/env/test.json new file mode 100755 index 0000000000..9fa16467ce --- /dev/null +++ b/config/env/test.json @@ -0,0 +1,27 @@ +{ + "db": "mongodb://localhost/mean-test", + "port": 3001, + "app": { + "name": "MEAN - A Modern Stack - Test" + }, + "facebook": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/facebook/callback" + }, + "twitter": { + "clientID": "CONSUMER_KEY", + "clientSecret": "CONSUMER_SECRET", + "callbackURL": "http://localhost:3000/auth/twitter/callback" + }, + "github": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/github/callback" + }, + "google": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/google/callback" + } +} \ No newline at end of file diff --git a/config/env/travis.json b/config/env/travis.json new file mode 100755 index 0000000000..2a2ccc980c --- /dev/null +++ b/config/env/travis.json @@ -0,0 +1,27 @@ +{ + "db": "mongodb://localhost/mean-travis", + "port": 3001, + "app": { + "name": "MEAN - A Modern Stack - Test on travis" + }, + "facebook": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/facebook/callback" + }, + "twitter": { + "clientID": "CONSUMER_KEY", + "clientSecret": "CONSUMER_SECRET", + "callbackURL": "http://localhost:3000/auth/twitter/callback" + }, + "github": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/github/callback" + }, + "google": { + "clientID": "APP_ID", + "clientSecret": "APP_SECRET", + "callbackURL": "http://localhost:3000/auth/google/callback" + } +} \ No newline at end of file diff --git a/config/express.js b/config/express.js new file mode 100755 index 0000000000..9a81a7e2b9 --- /dev/null +++ b/config/express.js @@ -0,0 +1,93 @@ +/** + * Module dependencies. + */ +var express = require('express'), + mongoStore = require('connect-mongo')(express), + flash = require('connect-flash'), + helpers = require('view-helpers'), + config = require('./config'); + +module.exports = function(app, passport, db) { + app.set('showStackError', true); + + //Prettify HTML + app.locals.pretty = true; + + //Should be placed before express.static + app.use(express.compress({ + filter: function(req, res) { + return (/json|text|javascript|css/).test(res.getHeader('Content-Type')); + }, + level: 9 + })); + + //Setting the fav icon and static folder + app.use(express.favicon()); + app.use(express.static(config.root + '/public')); + + //Don't use logger for test env + if (process.env.NODE_ENV !== 'test') { + app.use(express.logger('dev')); + } + + //Set views path, template engine and default layout + app.set('views', config.root + '/app/views'); + app.set('view engine', 'jade'); + + //Enable jsonp + app.enable("jsonp callback"); + + app.configure(function() { + //cookieParser should be above session + app.use(express.cookieParser()); + + //bodyParser should be above methodOverride + app.use(express.bodyParser()); + app.use(express.methodOverride()); + + //express/mongo session storage + app.use(express.session({ + secret: 'MEAN', + store: new mongoStore({ + db: db.connection.db, + collection: 'sessions' + }) + })); + + //connect flash for flash messages + app.use(flash()); + + //dynamic helpers + app.use(helpers(config.app.name)); + + //use passport session + app.use(passport.initialize()); + app.use(passport.session()); + + //routes should be at the last + app.use(app.router); + + //Assume "not found" in the error msgs is a 404. this is somewhat silly, but valid, you can do whatever you like, set properties, use instanceof etc. + app.use(function(err, req, res, next) { + //Treat as 404 + if (~err.message.indexOf('not found')) return next(); + + //Log it + console.error(err.stack); + + //Error page + res.status(500).render('500', { + error: err.stack + }); + }); + + //Assume 404 since no middleware responded + app.use(function(req, res, next) { + res.status(404).render('404', { + url: req.originalUrl, + error: 'Not found' + }); + }); + + }); +}; diff --git a/config/middlewares/authorization.js b/config/middlewares/authorization.js new file mode 100755 index 0000000000..9732488c0d --- /dev/null +++ b/config/middlewares/authorization.js @@ -0,0 +1,33 @@ +/** + * Generic require login routing middleware + */ +exports.requiresLogin = function(req, res, next) { + if (!req.isAuthenticated()) { + return res.send(401, 'User is not authorized'); + } + next(); +}; + +/** + * User authorizations routing middleware + */ +exports.user = { + hasAuthorization: function(req, res, next) { + if (req.profile.id != req.user.id) { + return res.send(401, 'User is not authorized'); + } + next(); + } +}; + +/** + * Article authorizations routing middleware + */ +exports.article = { + hasAuthorization: function(req, res, next) { + if (req.article.user.id != req.user.id) { + return res.send(401, 'User is not authorized'); + } + next(); + } +}; \ No newline at end of file diff --git a/config/passport.js b/config/passport.js new file mode 100755 index 0000000000..731c5ce2e1 --- /dev/null +++ b/config/passport.js @@ -0,0 +1,172 @@ +var mongoose = require('mongoose'), + LocalStrategy = require('passport-local').Strategy, + TwitterStrategy = require('passport-twitter').Strategy, + FacebookStrategy = require('passport-facebook').Strategy, + GitHubStrategy = require('passport-github').Strategy, + GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, + User = mongoose.model('User'), + config = require('./config'); + + +module.exports = function(passport) { + //Serialize sessions + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + passport.deserializeUser(function(id, done) { + User.findOne({ + _id: id + }, function(err, user) { + done(err, user); + }); + }); + + //Use local strategy + passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' + }, + function(email, password, done) { + User.findOne({ + email: email + }, function(err, user) { + if (err) { + return done(err); + } + if (!user) { + return done(null, false, { + message: 'Unknown user' + }); + } + if (!user.authenticate(password)) { + return done(null, false, { + message: 'Invalid password' + }); + } + return done(null, user); + }); + } + )); + + //Use twitter strategy + passport.use(new TwitterStrategy({ + consumerKey: config.twitter.clientID, + consumerSecret: config.twitter.clientSecret, + callbackURL: config.twitter.callbackURL + }, + function(token, tokenSecret, profile, done) { + User.findOne({ + 'twitter.id_str': profile.id + }, function(err, user) { + if (err) { + return done(err); + } + if (!user) { + user = new User({ + name: profile.displayName, + username: profile.username, + provider: 'twitter', + twitter: profile._json + }); + user.save(function(err) { + if (err) console.log(err); + return done(err, user); + }); + } else { + return done(err, user); + } + }); + } + )); + + //Use facebook strategy + passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.facebook.callbackURL + }, + function(accessToken, refreshToken, profile, done) { + User.findOne({ + 'facebook.id': profile.id + }, function(err, user) { + if (err) { + return done(err); + } + if (!user) { + user = new User({ + name: profile.displayName, + email: profile.emails[0].value, + username: profile.username, + provider: 'facebook', + facebook: profile._json + }); + user.save(function(err) { + if (err) console.log(err); + return done(err, user); + }); + } else { + return done(err, user); + } + }); + } + )); + + //Use github strategy + passport.use(new GitHubStrategy({ + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.github.callbackURL + }, + function(accessToken, refreshToken, profile, done) { + User.findOne({ + 'github.id': profile.id + }, function(err, user) { + if (!user) { + user = new User({ + name: profile.displayName, + email: profile.emails[0].value, + username: profile.username, + provider: 'github', + github: profile._json + }); + user.save(function(err) { + if (err) console.log(err); + return done(err, user); + }); + } else { + return done(err, user); + } + }); + } + )); + + //Use google strategy + passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.google.callbackURL + }, + function(accessToken, refreshToken, profile, done) { + User.findOne({ + 'google.id': profile.id + }, function(err, user) { + if (!user) { + user = new User({ + name: profile.displayName, + email: profile.emails[0].value, + username: profile.username, + provider: 'google', + google: profile._json + }); + user.save(function(err) { + if (err) console.log(err); + return done(err, user); + }); + } else { + return done(err, user); + } + }); + } + )); +}; \ No newline at end of file diff --git a/config/routes.js b/config/routes.js new file mode 100755 index 0000000000..173f6475d0 --- /dev/null +++ b/config/routes.js @@ -0,0 +1,78 @@ +module.exports = function(app, passport, auth) { + //User Routes + var users = require('../app/controllers/users'); + app.get('/signin', users.signin); + app.get('/signup', users.signup); + app.get('/signout', users.signout); + + //Setting up the users api + app.post('/users', users.create); + + app.post('/users/session', passport.authenticate('local', { + failureRedirect: '/signin', + failureFlash: 'Invalid email or password.' + }), users.session); + + app.get('/users/me', users.me); + app.get('/users/:userId', users.show); + + //Setting the facebook oauth routes + app.get('/auth/facebook', passport.authenticate('facebook', { + scope: ['email', 'user_about_me'], + failureRedirect: '/signin' + }), users.signin); + + app.get('/auth/facebook/callback', passport.authenticate('facebook', { + failureRedirect: '/signin' + }), users.authCallback); + + //Setting the github oauth routes + app.get('/auth/github', passport.authenticate('github', { + failureRedirect: '/signin' + }), users.signin); + + app.get('/auth/github/callback', passport.authenticate('github', { + failureRedirect: '/signin' + }), users.authCallback); + + //Setting the twitter oauth routes + app.get('/auth/twitter', passport.authenticate('twitter', { + failureRedirect: '/signin' + }), users.signin); + + app.get('/auth/twitter/callback', passport.authenticate('twitter', { + failureRedirect: '/signin' + }), users.authCallback); + + //Setting the google oauth routes + app.get('/auth/google', passport.authenticate('google', { + failureRedirect: '/signin', + scope: [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ] + }), users.signin); + + app.get('/auth/google/callback', passport.authenticate('google', { + failureRedirect: '/signin' + }), users.authCallback); + + //Finish with setting up the userId param + app.param('userId', users.user); + + //Article Routes + var articles = require('../app/controllers/articles'); + app.get('/articles', articles.all); + app.post('/articles', auth.requiresLogin, articles.create); + app.get('/articles/:articleId', articles.show); + app.put('/articles/:articleId', auth.requiresLogin, auth.article.hasAuthorization, articles.update); + app.del('/articles/:articleId', auth.requiresLogin, auth.article.hasAuthorization, articles.destroy); + + //Finish with setting up the articleId param + app.param('articleId', articles.article); + + //Home route + var index = require('../app/controllers/index'); + app.get('/', index.render); + +}; diff --git a/gruntfile.js b/gruntfile.js new file mode 100755 index 0000000000..2c41e8d626 --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,87 @@ +module.exports = function(grunt) { + // Project Configuration + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + watch: { + jade: { + files: ['app/views/**'], + options: { + livereload: true, + }, + }, + js: { + files: ['public/js/**', 'app/**/*.js'], + tasks: ['jshint'], + options: { + livereload: true, + }, + }, + html: { + files: ['public/views/**'], + options: { + livereload: true, + }, + }, + css: { + files: ['public/css/**'], + options: { + livereload: true + } + } + }, + jshint: { + all: ['gruntfile.js', 'public/js/**/*.js', 'test/**/*.js', 'app/**/*.js'] + }, + nodemon: { + dev: { + options: { + file: 'server.js', + args: [], + ignoredFiles: ['README.md', 'node_modules/**', '.DS_Store'], + watchedExtensions: ['js'], + watchedFolders: ['app', 'config'], + debug: true, + delayTime: 1, + env: { + PORT: 3000 + }, + cwd: __dirname + } + } + }, + concurrent: { + tasks: ['nodemon', 'watch'], + options: { + logConcurrentOutput: true + } + }, + mochaTest: { + options: { + reporter: 'spec' + }, + src: ['test/**/*.js'] + }, + env: { + test: { + NODE_ENV: 'test' + } + } + }); + + //Load NPM tasks + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-mocha-test'); + grunt.loadNpmTasks('grunt-nodemon'); + grunt.loadNpmTasks('grunt-concurrent'); + grunt.loadNpmTasks('grunt-env'); + + //Making grunt default to force in order not to break the project. + grunt.option('force', true); + + //Default task(s). + grunt.registerTask('default', ['jshint', 'concurrent']); + + //Test task. + grunt.registerTask('test', ['env:test', 'mochaTest']); +}; diff --git a/package.json b/package.json new file mode 100755 index 0000000000..7d0a69d01f --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "mean", + "description": "MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).", + "version": "0.1.0", + "private": false, + "author": "Amos Haviv", + "repository": { + "type": "git", + "url": "https://github.com/linnovate/mean.git" + }, + "engines": { + "node": "0.10.x", + "npm": "1.2.x" + }, + "scripts": { + "start": "node node_modules/grunt-cli/bin/grunt", + "test": "node node_modules/grunt-cli/bin/grunt test", + "postinstall": "bower install" + }, + "dependencies": { + "express": "latest", + "jade": "latest", + "mongoose": "latest", + "connect-mongo": "latest", + "connect-flash": "latest", + "crypto": "latest", + "passport": "latest", + "passport-local": "latest", + "passport-facebook": "latest", + "passport-twitter": "latest", + "passport-github": "latest", + "passport-google-oauth": "latest", + "underscore": "latest", + "view-helpers": "latest", + "mean-logger": "latest", + "forever": "latest", + "bower": "latest", + "grunt": "latest", + "grunt-cli": "latest", + "grunt-env": "latest" + }, + "devDependencies": { + "supertest": "latest", + "should": "latest", + "grunt-contrib-watch": "latest", + "grunt-contrib-jshint": "latest", + "grunt-nodemon": "latest", + "grunt-concurrent": "latest", + "grunt-mocha-test": "latest" + } +} diff --git a/public/css/common.css b/public/css/common.css new file mode 100755 index 0000000000..e91ae24026 --- /dev/null +++ b/public/css/common.css @@ -0,0 +1,25 @@ +.navbar .nav>li>a.brand { + padding-left:20px; + margin-left:0 +} + +.content { + margin-top:50px; + width:100% +} + +footer { + position:fixed; + left:0px; + bottom:0px; + height:30px; + width:100%; + background:#ddd; + -webkit-box-shadow:0 8px 6px 6px black; + -moz-box-shadow:0 8px 6px 6px black; + box-shadow:0 8px 6px 6px black +} + +footer p { + padding:5px 0 12px 10px +} \ No newline at end of file diff --git a/public/css/views/articles.css b/public/css/views/articles.css new file mode 100755 index 0000000000..163b1fa54d --- /dev/null +++ b/public/css/views/articles.css @@ -0,0 +1,7 @@ +h1 { + text-align:center +} + +ul.articles li:not(:last-child) { + border-bottom:1px solid #ccc +} \ No newline at end of file diff --git a/public/humans.txt b/public/humans.txt new file mode 100755 index 0000000000..5b037cf2e1 --- /dev/null +++ b/public/humans.txt @@ -0,0 +1,15 @@ +# humanstxt.org/ +# The humans responsible & technology colophon + +# TEAM + + -- -- + +# THANKS + + + +# TECHNOLOGY COLOPHON + + HTML5, CSS3 + jQuery, Modernizr diff --git a/public/img/.gitignore b/public/img/.gitignore new file mode 100755 index 0000000000..e69de29bb2 diff --git a/public/img/apple/apple-touch-icon-114x114-precomposed.png b/public/img/apple/apple-touch-icon-114x114-precomposed.png new file mode 100755 index 0000000000..172f1b621b Binary files /dev/null and b/public/img/apple/apple-touch-icon-114x114-precomposed.png differ diff --git a/public/img/apple/apple-touch-icon-144x144-precomposed.png b/public/img/apple/apple-touch-icon-144x144-precomposed.png new file mode 100755 index 0000000000..20746cf0ee Binary files /dev/null and b/public/img/apple/apple-touch-icon-144x144-precomposed.png differ diff --git a/public/img/apple/apple-touch-icon-57x57-precomposed.png b/public/img/apple/apple-touch-icon-57x57-precomposed.png new file mode 100755 index 0000000000..88ff2efea4 Binary files /dev/null and b/public/img/apple/apple-touch-icon-57x57-precomposed.png differ diff --git a/public/img/apple/apple-touch-icon-72x72-precomposed.png b/public/img/apple/apple-touch-icon-72x72-precomposed.png new file mode 100755 index 0000000000..61c017c743 Binary files /dev/null and b/public/img/apple/apple-touch-icon-72x72-precomposed.png differ diff --git a/public/img/apple/apple-touch-icon-precomposed.png b/public/img/apple/apple-touch-icon-precomposed.png new file mode 100755 index 0000000000..a70d9fd5d4 Binary files /dev/null and b/public/img/apple/apple-touch-icon-precomposed.png differ diff --git a/public/img/apple/apple-touch-icon.png b/public/img/apple/apple-touch-icon.png new file mode 100755 index 0000000000..94e2042f18 Binary files /dev/null and b/public/img/apple/apple-touch-icon.png differ diff --git a/public/img/apple/splash.png b/public/img/apple/splash.png new file mode 100755 index 0000000000..5d3dbbded5 Binary files /dev/null and b/public/img/apple/splash.png differ diff --git a/public/img/apple/splash2x.png b/public/img/apple/splash2x.png new file mode 100755 index 0000000000..977f37af77 Binary files /dev/null and b/public/img/apple/splash2x.png differ diff --git a/public/img/icons/facebook.png b/public/img/icons/facebook.png new file mode 100755 index 0000000000..3a04a89c80 Binary files /dev/null and b/public/img/icons/facebook.png differ diff --git a/public/img/icons/favicon.ico b/public/img/icons/favicon.ico new file mode 100755 index 0000000000..b459f5c485 Binary files /dev/null and b/public/img/icons/favicon.ico differ diff --git a/public/img/icons/github.png b/public/img/icons/github.png new file mode 100755 index 0000000000..15e02097f7 Binary files /dev/null and b/public/img/icons/github.png differ diff --git a/public/img/icons/google.png b/public/img/icons/google.png new file mode 100755 index 0000000000..401000a126 Binary files /dev/null and b/public/img/icons/google.png differ diff --git a/public/img/icons/twitter.png b/public/img/icons/twitter.png new file mode 100755 index 0000000000..6403a67e84 Binary files /dev/null and b/public/img/icons/twitter.png differ diff --git a/public/img/loaders/loader.gif b/public/img/loaders/loader.gif new file mode 100755 index 0000000000..72a9b35972 Binary files /dev/null and b/public/img/loaders/loader.gif differ diff --git a/public/img/sprites/glyphicons-halflings-white.png b/public/img/sprites/glyphicons-halflings-white.png new file mode 100755 index 0000000000..3bf6484a29 Binary files /dev/null and b/public/img/sprites/glyphicons-halflings-white.png differ diff --git a/public/img/sprites/glyphicons-halflings.png b/public/img/sprites/glyphicons-halflings.png new file mode 100755 index 0000000000..a996999320 Binary files /dev/null and b/public/img/sprites/glyphicons-halflings.png differ diff --git a/public/js/app.js b/public/js/app.js new file mode 100755 index 0000000000..a69b8d6761 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,4 @@ +window.app = angular.module('mean', ['ngCookies', 'ngResource', 'ui.bootstrap', 'ui.route', 'mean.system', 'mean.articles']); + +angular.module('mean.system', []); +angular.module('mean.articles', []); \ No newline at end of file diff --git a/public/js/config.js b/public/js/config.js new file mode 100755 index 0000000000..dc6ca36945 --- /dev/null +++ b/public/js/config.js @@ -0,0 +1,31 @@ +//Setting up route +window.app.config(['$routeProvider', + function($routeProvider) { + $routeProvider. + when('/articles', { + templateUrl: 'views/articles/list.html' + }). + when('/articles/create', { + templateUrl: 'views/articles/create.html' + }). + when('/articles/:articleId/edit', { + templateUrl: 'views/articles/edit.html' + }). + when('/articles/:articleId', { + templateUrl: 'views/articles/view.html' + }). + when('/', { + templateUrl: 'views/index.html' + }). + otherwise({ + redirectTo: '/' + }); + } +]); + +//Setting HTML5 Location Mode +window.app.config(['$locationProvider', + function($locationProvider) { + $locationProvider.hashPrefix("!"); + } +]); \ No newline at end of file diff --git a/public/js/controllers/articles.js b/public/js/controllers/articles.js new file mode 100755 index 0000000000..18bece8217 --- /dev/null +++ b/public/js/controllers/articles.js @@ -0,0 +1,52 @@ +angular.module('mean.articles').controller('ArticlesController', ['$scope', '$routeParams', '$location', 'Global', 'Articles', function ($scope, $routeParams, $location, Global, Articles) { + $scope.global = Global; + + $scope.create = function() { + var article = new Articles({ + title: this.title, + content: this.content + }); + article.$save(function(response) { + $location.path("articles/" + response._id); + }); + + this.title = ""; + this.content = ""; + }; + + $scope.remove = function(article) { + article.$remove(); + + for (var i in $scope.articles) { + if ($scope.articles[i] == article) { + $scope.articles.splice(i, 1); + } + } + }; + + $scope.update = function() { + var article = $scope.article; + if (!article.updated) { + article.updated = []; + } + article.updated.push(new Date().getTime()); + + article.$update(function() { + $location.path('articles/' + article._id); + }); + }; + + $scope.find = function() { + Articles.query(function(articles) { + $scope.articles = articles; + }); + }; + + $scope.findOne = function() { + Articles.get({ + articleId: $routeParams.articleId + }, function(article) { + $scope.article = article; + }); + }; +}]); \ No newline at end of file diff --git a/public/js/controllers/header.js b/public/js/controllers/header.js new file mode 100755 index 0000000000..1c1b04fcf2 --- /dev/null +++ b/public/js/controllers/header.js @@ -0,0 +1,13 @@ +angular.module('mean.system').controller('HeaderController', ['$scope', 'Global', function ($scope, Global) { + $scope.global = Global; + + $scope.menu = [{ + "title": "Articles", + "link": "articles" + }, { + "title": "Create New Article", + "link": "articles/create" + }]; + + $scope.isCollapsed = false; +}]); \ No newline at end of file diff --git a/public/js/controllers/index.js b/public/js/controllers/index.js new file mode 100755 index 0000000000..2687573559 --- /dev/null +++ b/public/js/controllers/index.js @@ -0,0 +1,3 @@ +angular.module('mean.system').controller('IndexController', ['$scope', 'Global', function ($scope, Global) { + $scope.global = Global; +}]); \ No newline at end of file diff --git a/public/js/directives.js b/public/js/directives.js new file mode 100755 index 0000000000..e69de29bb2 diff --git a/public/js/filters.js b/public/js/filters.js new file mode 100755 index 0000000000..e69de29bb2 diff --git a/public/js/init.js b/public/js/init.js new file mode 100755 index 0000000000..f77b4f2ae1 --- /dev/null +++ b/public/js/init.js @@ -0,0 +1,15 @@ +window.bootstrap = function() { + angular.bootstrap(document, ['mean']); +}; + +window.init = function() { + window.bootstrap(); +}; + +angular.element(document).ready(function() { + //Fixing facebook bug with redirect + if (window.location.hash == "#_=_") window.location.hash = ""; + + //Then init the app + window.init(); +}); \ No newline at end of file diff --git a/public/js/services/articles.js b/public/js/services/articles.js new file mode 100755 index 0000000000..2d496701ca --- /dev/null +++ b/public/js/services/articles.js @@ -0,0 +1,10 @@ +//Articles service used for articles REST endpoint +angular.module('mean.articles').factory("Articles", ['$resource', function($resource) { + return $resource('articles/:articleId', { + articleId: '@_id' + }, { + update: { + method: 'PUT' + } + }); +}]); \ No newline at end of file diff --git a/public/js/services/global.js b/public/js/services/global.js new file mode 100755 index 0000000000..95eb0117a0 --- /dev/null +++ b/public/js/services/global.js @@ -0,0 +1,9 @@ +angular.module('mean.system').factory("Global", [function() { + var _this = this; + _this._data = { + user: window.user, + authenticated: !! window.user + }; + + return _this._data; +}]); \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100755 index 0000000000..ee2cc216a6 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# robotstxt.org/ + +User-agent: * diff --git a/public/views/articles/create.html b/public/views/articles/create.html new file mode 100755 index 0000000000..222464accd --- /dev/null +++ b/public/views/articles/create.html @@ -0,0 +1,21 @@ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/public/views/articles/edit.html b/public/views/articles/edit.html new file mode 100755 index 0000000000..12d6c2b234 --- /dev/null +++ b/public/views/articles/edit.html @@ -0,0 +1,25 @@ +
+
+
+ + +
+ + +
+
+
+ + +
+ +
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/public/views/articles/list.html b/public/views/articles/list.html new file mode 100755 index 0000000000..40b4fba8f1 --- /dev/null +++ b/public/views/articles/list.html @@ -0,0 +1,11 @@ +
+
    +
  • + {{article.created | date:'medium'}} / + {{article.user.name}} +

    {{article.title}}

    +
    {{article.content}}
    +
  • +
+

No articles yet.
Why don't you Create One?

+
\ No newline at end of file diff --git a/public/views/articles/view.html b/public/views/articles/view.html new file mode 100755 index 0000000000..821d0b670c --- /dev/null +++ b/public/views/articles/view.html @@ -0,0 +1,6 @@ +
+ {{article.created | date:'medium'}} / + {{article.user.name}} +

{{article.title}} edit

+
{{article.content}}
+
\ No newline at end of file diff --git a/public/views/header.html b/public/views/header.html new file mode 100755 index 0000000000..7567812559 --- /dev/null +++ b/public/views/header.html @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/public/views/index.html b/public/views/index.html new file mode 100755 index 0000000000..f245897ac9 --- /dev/null +++ b/public/views/index.html @@ -0,0 +1,3 @@ +
+

This is the home view

+
\ No newline at end of file diff --git a/server.js b/server.js new file mode 100755 index 0000000000..f1fb60e37b --- /dev/null +++ b/server.js @@ -0,0 +1,61 @@ +/** + * Module dependencies. + */ +var express = require('express'), + fs = require('fs'), + passport = require('passport'), + logger = require('mean-logger'); + +/** + * Main application entry file. + * Please note that the order of loading is important. + */ + +//Load configurations +//if test env, load example file +var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development', + config = require('./config/config'), + auth = require('./config/middlewares/authorization'), + mongoose = require('mongoose'); + +//Bootstrap db connection +var db = mongoose.connect(config.db); + +//Bootstrap models +var models_path = __dirname + '/app/models'; +var walk = function(path) { + fs.readdirSync(path).forEach(function(file) { + var newPath = path + '/' + file; + var stat = fs.statSync(newPath); + if (stat.isFile()) { + if (/(.*)\.(js$|coffee$)/.test(file)) { + require(newPath); + } + } else if (stat.isDirectory()) { + walk(newPath); + } + }); +}; +walk(models_path); + +//bootstrap passport config +require('./config/passport')(passport); + +var app = express(); + +//express settings +require('./config/express')(app, passport, db); + +//Bootstrap routes +require('./config/routes')(app, passport, auth); + +//Start the app by listening on +var port = process.env.PORT || config.port; +app.listen(port); +console.log('Express app started on port ' + port); + +//Initializing logger +logger.init(app, passport, mongoose); + +//expose app +exports = module.exports = app; diff --git a/test/article/model.js b/test/article/model.js new file mode 100755 index 0000000000..9d1fd034c2 --- /dev/null +++ b/test/article/model.js @@ -0,0 +1,65 @@ +/** + * Module dependencies. + */ +var should = require('should'), + app = require('../../server'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + Article = mongoose.model('Article'); + +//Globals +var user; +var article; + +//The tests +describe('', function() { + describe('Model Article:', function() { + beforeEach(function(done) { + user = new User({ + name: 'Full name', + email: 'test@test.com', + username: 'user', + password: 'password' + }); + + user.save(function(err) { + article = new Article({ + title: 'Article Title', + content: 'Article Content', + user: user + }); + + done(); + }); + }); + + describe('Method Save', function() { + it('should be able to save without problems', function(done) { + return article.save(function(err) { + should.not.exist(err); + done(); + }); + }); + + it('should be able to show an error when try to save without title', function(done) { + article.title = ''; + + return article.save(function(err) { + should.exist(err); + done(); + }); + }); + }); + + afterEach(function(done) { + Article.remove({}); + User.remove({}); + done(); + }); + after(function(done){ + Article.remove().exec(); + User.remove().exec(); + done(); + }); + }); +}); diff --git a/test/user/model.js b/test/user/model.js new file mode 100755 index 0000000000..0ec6820a1f --- /dev/null +++ b/test/user/model.js @@ -0,0 +1,66 @@ +/** + * Module dependencies. + */ +var should = require('should'), + app = require('../../server'), + mongoose = require('mongoose'), + User = mongoose.model('User'); + +//Globals +var user; + +//The tests +describe('', function() { + describe('Model User:', function() { + before(function(done) { + user = new User({ + name: 'Full name', + email: 'test@test.com', + username: 'user', + password: 'password' + }); + user2 = new User({ + name: 'Full name', + email: 'test@test.com', + username: 'user', + password: 'password' + }); + + done(); + }); + + describe('Method Save', function() { + it('should begin with no users', function(done) { + User.find({}, function(err, users) { + users.should.have.length(0); + done(); + }); + }); + + it('should be able to save whithout problems', function(done) { + user.save(done); + }); + + it('should fail to save an existing user again', function(done) { + user.save(); + return user2.save(function(err) { + should.exist(err); + done(); + }); + }); + + it('should be able to show an error when try to save without name', function(done) { + user.name = ''; + return user.save(function(err) { + should.exist(err); + done(); + }); + }); + }); + + after(function(done) { + User.remove().exec(); + done(); + }); + }); +}); \ No newline at end of file