├── .gitignore ├── README.md ├── course.json ├── images └── mean.jpg └── mean-tutorial.mdown /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | 4 | Gemfile.lock 5 | vendor/bundle 6 | log 7 | tmp 8 | .bundle 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Learn to Build Modern Web Apps with MEAN 2 | ======= 3 | 4 | **update**: This tutorial is no longer being maintained, updated, nor supported by me. 5 | 6 | This repository contains the markdown for the [MEAN Stack tutorial on Thinkster.io](https://thinkster.io/mean-stack-tutorial/). 7 | -------------------------------------------------------------------------------- /course.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Learn to Build Modern Web Apps with MEAN", 3 | "description": "This tutorial will teach you to use the MEAN stack (MongoDB, Express, AngularJS, and Node) to build a reddit clone", 4 | "url": "mean-stack-tutorial", 5 | "version": "v1.1", 6 | "twitter": "IAmMattGreen", 7 | "coverImage": "images/mean.jpg", 8 | "git": "https://github.com/emgeee/mean-tutorial", 9 | 10 | "author": { 11 | "github": "emgeee" 12 | }, 13 | "contributors": [ 14 | {"github": "apai4"}, 15 | {"github": "EricSimons"} 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /images/mean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emgeee/mean-tutorial/db59b02915f68fddd0b5afb7e6ad3fee57eec911/images/mean.jpg -------------------------------------------------------------------------------- /mean-tutorial.mdown: -------------------------------------------------------------------------------- 1 | 2 | [thinkster twitter]: http://twitter.com/AngularTutorial 3 | [thinkster email]: mailto:hello@thinkster.io 4 | [REST]: http://en.wikipedia.org/wiki/Representational_state_transfer 5 | [CRUD]: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete 6 | [install-node]: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager 7 | [install-mongo]: http://docs.mongodb.org/manual/installation/ 8 | [ng-repeat]: https://docs.angularjs.org/api/ng/directive/ngRepeat 9 | [ng-click]: https://docs.angularjs.org/api/ng/directive/ngClick 10 | [ng-submit]: https://docs.angularjs.org/api/ng/directive/ngSubmit 11 | [ng-model]: https://docs.angularjs.org/api/ng/directive/ngModel 12 | [ng-hide]: https://docs.angularjs.org/api/ng/directive/ngHide 13 | [ng-show]: https://docs.angularjs.org/api/ng/directive/ngShow 14 | [Angular expression]: https://docs.angularjs.org/guide/expression 15 | [ng-route]: https://docs.angularjs.org/api/ngRoute 16 | [ng http]: https://docs.angularjs.org/api/ng/service/$http 17 | [ng copy]: https://docs.angularjs.org/api/ng/function/angular.copy 18 | [services]: https://docs.angularjs.org/guide/services 19 | [data bindings]: https://docs.angularjs.org/guide/databinding 20 | [built in filters]: https://docs.angularjs.org/api/ng/filter 21 | [object reference]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects 22 | [angular style guide]: https://github.com/mgechev/angularjs-style-guide 23 | [factory vs service]: http://tylermcginnis.com/angularjs-factory-vs-service-vs-provider/ 24 | [ui otherwise]: https://github.com/angular-ui/ui-router/wiki/URL-Routing#otherwise-for-invalid-routes 25 | [ui view]: https://github.com/angular-ui/ui-router/wiki/Quick-Reference#ui-view 26 | [ui parameters]: https://github.com/angular-ui/ui-router/wiki/URL-Routing#url-parameters 27 | [ui stateParams]: https://github.com/angular-ui/ui-router/wiki/URL-Routing#stateparams-service 28 | [ui resolve]: https://github.com/angular-ui/ui-router/wiki#resolve 29 | [mongoose populate]: http://mongoosejs.com/docs/populate.html 30 | [mongoose objectid]: http://mongoosejs.com/docs/api.html#schema-objectid-js 31 | [mongoose query]: http://mongoosejs.com/docs/queries.html 32 | [mongo objectid]: http://api.mongodb.org/java/current/org/bson/types/ObjectId.html 33 | [express get]: http://expressjs.com/api#app.get 34 | [express json]: http://expressjs.com/api#res.json 35 | [express param]: http://expressjs.com/4x/api.html#app.param 36 | [express generator]: https://github.com/expressjs/generator 37 | 38 | [better way]: https://www.thinkster.io/angulartutorial/a-better-way-to-learn-angularjs/ 39 | [scopes]: https://www.thinkster.io/angulartutorial/a-better-way-to-learn-angularjs/#part-5-scope 40 | [filters]: https://www.thinkster.io/angulartutorial/a-better-way-to-learn-angularjs/#part-3-filters 41 | [config]: https://www.thinkster.io/egghead/the-config-function 42 | [inline template]: https://www.thinkster.io/egghead/templateurl 43 | [promises]: https://www.thinkster.io/egghead/promises 44 | [modern web apps firebase]: https://www.thinkster.io/angulartutorial/ 45 | 46 | # Introduction 47 | 48 | {intro-video: mean-intro} 49 | 50 | The goal of this tutorial is to guide you through the creation of a 51 | Reddit/Hacker News clone using the MEAN stack. By completing this tutorial, you 52 | will gain a basic understanding of the MEAN stack including building a REST 53 | interface with Express.js on top of Node.js and using that interface to perform 54 | CRUD operations on a database via an AngularJS frontend. 55 | 56 | 57 | ## Why MEAN Stack? 58 | 59 | The acronym "MEAN" stands for "[MongoDB](http://www.mongodb.org/) 60 | [Express.js](http://expressjs.com/) [AngularJS](https://angularjs.org/) 61 | [Node.js](http://nodejs.org/)" and represents a group of technologies which are 62 | known to synergize well together. The major benefit of the MEAN stack is that 63 | it's extremely quick to prototype with. Node.js allows you to use Javascript on 64 | the backend as well as the frontend which can save you from having to learn a 65 | separate language. In addition, the 66 | [NoSQL](http://www.mongodb.com/nosql-explained) nature of MongoDB allows you to 67 | quickly change and alter the data layer without having to worry about 68 | migrations, which is a very valuable attribute when you're trying to build a 69 | product without clear specifications. Finally, these technologies have a lot 70 | of community support behind them so finding answers to questions or hiring help 71 | is going to be much easier using these technologies. 72 | 73 | 74 | ## Prerequisites 75 | 76 | This course assumes knowledge of programming and at least basic knowledge of 77 | JavaScript. In addition, you should be comfortable with basic web application 78 | concepts including [REST][] and [CRUD][]. Before you begin, you will also need 79 | to have Node.js and MongoDB already installed. Because you will need to install 80 | various packages for Node.js, you should follow [these installation 81 | instructions][install-node] which use [npm](https://www.npmjs.org/). 82 | Installation instructions for MongoDB can be found [on the Mongo 83 | website][install-mongo]. This tutorial will be based on AngularJS v1.3.10, 84 | Node.js v0.10.31 and MongoDB 2.6.7. 85 | 86 | 87 | {x: install node} 88 | Install Node.js 89 | 90 | {x: install mongo} 91 | Install MongoDB 92 | 93 | 94 | ## Recommendations for Completing this Tutorial 95 | 96 | Throughout the course of this tutorial, links to additional concepts and 97 | information will be included. You can use these links as supplementary material 98 | which can help you gain insight into the stack and its various components. As 99 | always, if you have any questions Google and 100 | [Stackoverflow](http://stackoverflow.com/) are your best bet. If you're unsure 101 | about something specific to this tutorial, feel free to ping me on twitter 102 | at [@IAmMattGreen](https://twitter.com/IAmMattGreen)! 103 | 104 | We at Thinkster are firm believers in actually writing code. Therefor we 105 | **strongly** encourage you to type out all the code instead of copy+pasting it. 106 | 107 | 108 | ## Project Specifications 109 | 110 | Before beginning work on any project, it's usually a good idea to know what you're 111 | building. Below is a basic list of things we want our users to be able to do: 112 | 113 | * Create new posts 114 | * View all posts ordered by upvotes 115 | * Add comments about a given post 116 | * View comments for a given post 117 | * Upvote posts and comments 118 | 119 | 120 | In addition to technologies that make up MEAN, we're going to enlist the help 121 | of several other libraries to help us achieve our goals: 122 | 123 | * [Mongoose.js](http://mongoosejs.com/) for adding structure to MongoDB 124 | * [Angular ui-router](https://github.com/angular-ui/ui-router) for client-side routing 125 | * [Twitter Bootstrap](http://getbootstrap.com/) for some quick styling 126 | 127 | 128 | 129 | # Jumping in with Angular 130 | 131 | {video: ch-1} 132 | 133 | To begin this tutorial, we're going to start with the Angular side of things. 134 | AngularJS is a frontend framework developed by Google with the goal of making 135 | single page web applications easier to build, test, and maintain. Throughout 136 | this tutorials, we'll be linking to our [A Better Way to Learn Angular][better 137 | way] guide which can provide supplementary information. 138 | 139 | Without further ado, let's jump in... 140 | 141 | 142 | ## Getting Started 143 | 144 | As mentioned before, this tutorial will take you through building out a Hacker 145 | News/Reddit clone, which we're going to name "Flapper News". To keep things 146 | simple, we're going to kick things off with two files. 147 | 148 | 149 | {x: create basic angular app} 150 | Create two empty files called `index.html` (for writing the template) and `app.js` (for defining our AngularJS logic) 151 | 152 | To begin, our `index.html` will look like this: 153 | 154 | ```html 155 | 156 | 157 | My Angular App! 158 | 159 | 160 | 161 | 162 |
163 | {{test}} 164 |
165 | 166 | 167 | ``` 168 | 169 | Our `app.js` is going to look like this: 170 | 171 | ```javascript 172 | var app = angular.module('flapperNews', []); 173 | 174 | app.controller('MainCtrl', [ 175 | '$scope', 176 | function($scope){ 177 | $scope.test = 'Hello world!'; 178 | }]); 179 | ``` 180 | 181 | With these several lines of code, we've set up a new AngularJS app and created 182 | a new controller. You'll notice that we declare a variable `test` in the controller and display its contents 183 | using the AngularJS `{{}}` notation. This is demonstrating one of the most powerful features of 184 | AngularJS, which is its two-way data-bindings. 185 | 186 | 187 | 188 | {x: understand data bindings} 189 | If you aren't familiar with data-binding in AngularJS, read the AngularJS Guide on [ two-way data-binding ][data bindings] 190 | 191 | 192 | ## Displaying Lists 193 | 194 | One thing that's is going to be absolutely fundamental to our app is displaying 195 | lists. Fortunately, angular makes this really easy using the 196 | [ng-repeat][] directive. 197 | 198 | 199 | {x: add scope posts array} 200 | To begin, we're going to modify our 201 | controller to include a new `$scope` variable that defines a list of post 202 | titles. Add the following code inside the controller function in `app.js`: 203 | 204 | ```javascript 205 | $scope.posts = [ 206 | 'post 1', 207 | 'post 2', 208 | 'post 3', 209 | 'post 4', 210 | 'post 5' 211 | ]; 212 | ``` 213 | 214 | {info} 215 | The `$scope` variable serves as the bridge between Angular controllers and 216 | Angular templates. If you want something to be accessible in the template such 217 | as a function or variable, bind it to `$scope` 218 | 219 | 220 | {x: using ng-repeat} 221 | Now that we have a list of data we want to repeat, let's use `ng-repeat` to do it. Add the following code to line 8 of index.html, 222 | replacing the div that was there before: 223 | 224 | ```html 225 |
226 | {{post}} 227 |
228 | ``` 229 | 230 | When you refresh the page you should see a list of posts! 231 | 232 | Now what if we want to display additional information about our posts? 233 | `ng-repeat` lets us do that too! 234 | 235 | 236 | {x: amend posts objects} 237 | Let's amend our `posts` object to include some 238 | additional information we might be interested in displaying like the number of 239 | upvotes: 240 | 241 | ```javascript 242 | $scope.posts = [ 243 | {title: 'post 1', upvotes: 5}, 244 | {title: 'post 2', upvotes: 2}, 245 | {title: 'post 3', upvotes: 15}, 246 | {title: 'post 4', upvotes: 9}, 247 | {title: 'post 5', upvotes: 4} 248 | ]; 249 | ``` 250 | 251 | 252 | {x: display posts objects} 253 | Now we change our `ng-repeat` directive to display the new information: 254 | 255 | ```html 256 |
257 | {{post.title}} - upvotes: {{post.upvotes}} 258 |
259 | ``` 260 | 261 | Of course it is important to order posts by number of upvotes, and we can tap 262 | into an [angular filter][filters] to make it happen. 263 | 264 | 265 | {x: filter data} 266 | Add a filter to our posts based on the number of upvotes in descending order: 267 | 268 | ```html 269 |
270 | {{post.title}} - upvotes: {{post.upvotes}} 271 |
272 | ``` 273 | 274 | AngularJS comes with several [built in filters][built in filters] but you can 275 | also write custom filters tailored to your specific needs. 276 | 277 | 278 | # Getting User Input 279 | 280 | {video: ch-2} 281 | 282 | Now that we've learned how to display lists of information with Angular, 283 | it'd really be great if we could have the user add posts. To do this, 284 | we first need to add a function to our `$scope` variable. 285 | 286 | 287 | {x: create addpost function} 288 | Create a $scope function that will add an object into the posts array: 289 | 290 | ```javascript 291 | $scope.addPost = function(){ 292 | $scope.posts.push({title: 'A new post!', upvotes: 0}); 293 | }; 294 | ``` 295 | 296 | When this function is invoked, it will append a new post to our `$scope.posts` 297 | variable. Now we're going to have to allow the user to actually execute this 298 | function. 299 | 300 | 301 | {x: create addpost button} 302 | Create a button that executes our addPost $scope function using the [ng-click][ng-click] directive: 303 | 304 | ```html 305 | 306 | ``` 307 | 308 | Great, we can now click a button and have a new post show up! Let's extend this 309 | by allowing the user to actually specify what they want the title to be. First, 310 | we need to build out the form in HTML and sprinkle it with some Angular Magic. 311 | 312 | {x: form with ng-submit} 313 | Create a form below the ng-repeat div that will allow us to enter custom posts: 314 | 315 | ```html 316 |
317 | 318 | 319 |
320 | ``` 321 | 322 | Here we've created a form that encompasses our title text-box and 'Post' button. 323 | We are also now calling our `addPost()` function using the 324 | [ng-submit][ng-submit] directive, which has the added benefit of the user being 325 | able to press the 'enter' key to submit the form. Finally, we're using the 326 | [ng-model][ng-model] directive to bind the contents of the text box to 327 | `$scope`. This will allow our controller to access the contents of the text box 328 | using `$scope.title`. 329 | 330 | To accompany the changes to our template, we need to make some tweaks to 331 | `addPost()`. 332 | 333 | 334 | {x: tweak addPost} 335 | Have the addPost function retrieve the title entered into our form, which is bound to the $scope variable `title`, and set `title` to blank once it has been added to the posts array: 336 | 337 | ```Javascript 338 | $scope.addPost = function(){ 339 | $scope.posts.push({title: $scope.title, upvotes: 0}); 340 | $scope.title = ''; 341 | }; 342 | ``` 343 | 344 | When we add a post we are now getting the title from `$scope.title`, which we 345 | then clear after the post has been created. At this point, it makes sense to 346 | prevent the user from posting a blank title. 347 | 348 | 349 | {x: prevent blank title} 350 | Prevent a user from submitting a post with a blank title by adding the 351 | following line to the beginning of `addPost()`: 352 | 353 | ```javascript 354 | if(!$scope.title || $scope.title === '') { return; } 355 | ``` 356 | 357 | ## Enable Upvoting 358 | 359 | Now that we can add some new posts, why don't we allow a user to upvote 360 | existing ones? To get started, lets revisit our `ng-repeat` directive. 361 | 362 | 363 | {x: add upvote span} 364 | Next to each post, we need to place a click-able element that will increment the 365 | corresponding post's upvote counter: 366 | 367 | ```html 368 |
369 | ^ 370 | {{post.title}} - upvotes: {{post.upvotes}} 371 |
372 | ``` 373 | 374 | We've now added a `^` character inside a `` tag that when clicked, calls 375 | the `incrementUpvotes()` function in our controller, but we don't have 376 | that function in our controller yet! 377 | 378 | 379 | {x: add upvote function} 380 | Define the `incrementUpvotes()` function in our controller: 381 | 382 | ```javascript 383 | $scope.incrementUpvotes = function(post) { 384 | post.upvotes += 1; 385 | }; 386 | ``` 387 | 388 | Notice that for this function we are passing the current instance of `post` to 389 | the function. This is happening [by reference][object reference] so when we 390 | increment upvotes, it gets automatically reflected back to the HTML page. 391 | 392 | ## Submitting Links 393 | 394 | Ultimately, Flapper News is about sharing links to content, so lets enable users 395 | to submit links along with their titles. We'll start by adding a second text box 396 | to our form that a user can use to submit a link. We'll also add some placeholder 397 | text to make it clear which form is which: 398 | 399 | 400 | {x: link text field} 401 | Add a link field to our form that is bound to the scope variable `link`: 402 | 403 | ```javascript 404 |
405 | 406 |
407 | 408 |
409 | 410 |
411 | ``` 412 | 413 | 414 | {x: extend addPost for links} 415 | Next we're going to want to modify our `addPost()` function to include the link 416 | (notice that we aren't going to force the user to submit a link if they don't 417 | want to): 418 | 419 | ```javascript 420 | $scope.addPost = function(){ 421 | if(!$scope.title || $scope.title === '') { return; } 422 | $scope.posts.push({ 423 | title: $scope.title, 424 | link: $scope.link, 425 | upvotes: 0 426 | }); 427 | $scope.title = ''; 428 | $scope.link = ''; 429 | }; 430 | ``` 431 | 432 | Finally we need to modify the `ng-repeat` section to make the title a link 433 | to the content, but only if a link was specified. 434 | 435 | We'll do this by using a new directive called [ng-hide][], which hides elements 436 | when an [Angular expression][] evaluates to true. 437 | 438 | 439 | {x: hide blank link} 440 | Use to `ng-hide` to hide the linked version of the 441 | title if no link exists and correspondingly show the unlinked version: 442 | 443 | ```html 444 |
445 | ^ 446 | 447 | {{post.title}} 448 | 449 | 450 | {{post.title}} 451 | 452 | - upvotes: {{post.upvotes}} 453 |
454 | ``` 455 | 456 | It is worth noting that [ng-show][] is merely the inverse of `ng-hide`. You can 457 | use either one for programmatically displaying or hiding elements. 458 | 459 | # Adding Some Style 460 | 461 | {video: ch-3_4} 462 | 463 | At this point, we have the basics of an application - a user can add new posts 464 | which are automatically ordered based on the number of upvotes. Up until now, 465 | however, our interface has been lacking in the looks department. We can spruce 466 | it up a bit using some basic Bootstrap styling. 467 | 468 | 469 | {x: use bootstrap} 470 | Change your `index.html` file to include Twitter Bootstrap along with a new layout: 471 | 472 | ```html 473 | 474 | 475 | Flapper News 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 |
484 |
485 | 486 | 489 | 490 |
491 | 493 | {{post.upvotes}} 494 | 495 | 496 | {{post.title}} 497 | 498 | 499 | {{post.title}} 500 | 501 | 502 |
503 | 504 |
506 |

Add a new post

507 | 508 |
509 | 513 |
514 |
515 | 519 |
520 | 521 |
522 | 523 |
524 |
525 | 526 | 527 | ``` 528 | 529 | At the top we've included Bootstrap from a 530 | [CDN](http://en.wikipedia.org/wiki/Content_delivery_network). In the body tag, 531 | we've made use of Bootstrap's [grid system](http://getbootstrap.com/css/#grid) 532 | to align our content in the middle of the screen. We've also stylized the posts 533 | list and "Add a new post" form to make things a little easier to read. There's 534 | a lot more that could be done on this front so feel free to mess around with 535 | more styling before (or after) you continue. 536 | 537 | 538 | {x: add own styles} 539 | (optional) Add your own styles! 540 | 541 | # Angular Services 542 | 543 | Up to this point, we've been storing important data directly in the controller. 544 | While this works, it has some disadvantages: 545 | 546 | * when the controller goes out of scope, we lose the data 547 | * that data cannot be easily accessed from other controllers or directives 548 | * the data is difficult to mock, which is important when writing automated tests 549 | 550 | To rectify this problem, we're going to refactor our `$scope.posts` variable into a 551 | [service][services]. 552 | 553 | 554 | 555 | ## My First Service... Is Really a Factory 556 | 557 | In Angular, services are declared much like controllers. Inside `app.js`, 558 | we're going to attach a new service to our `flapperNews` module. 559 | 560 | 561 | {x: create factory} 562 | Create a factory for posts in `app.js` (our `MainCtrl` controller should appear below this): 563 | 564 | ```javascript 565 | app.factory('posts', [function(){ 566 | // service body 567 | }]) 568 | ``` 569 | 570 | {info} 571 | By [Angular conventions][angular style guide], lowerCamelCase is used for 572 | factory names that won't be `new`'ed. 573 | 574 | You may be wondering why we're using the keyword *factory* instead of 575 | *service*. In angular, factory and service are related in that they are both 576 | instances of a third entity called *provider*. 577 | 578 | 579 | {x: different types of services} 580 | The difference between them are nuanced but if you'd like to learn more check out [Angular Factory vs Service vs Provider][factory vs service]. 581 | 582 | {x: move posts to factory} 583 | Now that we've created our service, lets go ahead and move our posts variable to 584 | it: 585 | 586 | ```javascript 587 | app.factory('posts', [function(){ 588 | var o = { 589 | posts: [] 590 | }; 591 | return o; 592 | }]); 593 | ``` 594 | 595 | What we're doing here is creating a new object that has an array property 596 | called `posts`. We then return that variable so that our `o` object essentially 597 | becomes exposed to any other Angular module that cares to inject it. You'll 598 | note that we could have simply exported the `posts` array directly, however, by 599 | exporting an object that contains the `posts` array we can add new objects and 600 | methods to our services in the future. 601 | 602 | 603 | ## Injecting the Service 604 | 605 | Our next step is to inject the service into our controller so we can access its 606 | data. Simply add the name of the service as a parameter to the controller we 607 | wish to access it from: 608 | 609 | 610 | {x: inject post service} 611 | Inject posts service into `MainCtrl` 612 | 613 | ```javascript 614 | app.controller('MainCtrl', [ 615 | '$scope', 616 | 'posts', 617 | function($scope, posts){ 618 | ... 619 | 620 | )}; 621 | ``` 622 | 623 | As you'll recall, two-way data-binding only applies to variables bound to 624 | `$scope`. To display our array of posts that exist in the posts factory 625 | (`posts.posts`), we'll need to set a scope variable in our controller to mirror 626 | the array returned from the service. 627 | 628 | 629 | {x: mirror posts} 630 | Bind the `$scope.posts` variable in our controller to the posts array in our service: 631 | 632 | ```javascript 633 | $scope.posts = posts.posts; 634 | ``` 635 | 636 | Now any change or modification made to `$scope.posts` will be stored in the 637 | service and immediately accessible by any other module that injects the `posts` 638 | service. 639 | 640 | # Angular Routing 641 | 642 | {video: ch-5} 643 | 644 | 645 | Now that we understand the basics of Angular templates, controllers, and 646 | services, we're going to start diving into some of the concepts that make 647 | client side web applications so dynamic and powerful. To do this, we're going 648 | to need to learn how to deal with multiple views and controllers, which we will 649 | accomplish using the wonderful 650 | [ui-router](https://github.com/angular-ui/ui-router/) library. 651 | 652 | 653 | {x: add ui-router} 654 | To get started, lets include the library after we include Angular in our 655 | `index.html`: 656 | 657 | ```html 658 | 659 | ``` 660 | 661 | 662 | {x: include ui-router} 663 | Because we are adding an external module, we need to be sure to include it as a 664 | dependency in our app: 665 | 666 | ```javascript 667 | angular.module('flapperNews', ['ui.router']) 668 | ``` 669 | 670 | You may be wondering why we have chosen to use `ui-router` instead of the more 671 | standard [ngRoute][ng-route] module - `ui-router` is newer and provides more 672 | flexibility and features than `ngRoute`. We will be using a few of these in 673 | this tutorial. 674 | 675 | 676 | ## Adding a New State 677 | 678 | Now that we have `ui-router` included, we need to configure it. In our 679 | `app.js`, we're going to use the Angular `config()` function to setup a 680 | *home* state. 681 | 682 | 683 | {x: configure home state} 684 | Create the config block for our app and configure a home state using [$stateProvider](https://github.com/angular-ui/ui-router/wiki) and [$urlRouterProvider](https://github.com/angular-ui/ui-router/wiki/URL-Routing). Use `otherwise()` to redirect unspecified routes. 685 | 686 | ```javascript 687 | app.config([ 688 | '$stateProvider', 689 | '$urlRouterProvider', 690 | function($stateProvider, $urlRouterProvider) { 691 | 692 | $stateProvider 693 | .state('home', { 694 | url: '/home', 695 | templateUrl: '/home.html', 696 | controller: 'MainCtrl' 697 | }); 698 | 699 | $urlRouterProvider.otherwise('home'); 700 | }]); 701 | ``` 702 | 703 | Here we set up our *home* route. You'll notice that the state is given a name 704 | ('home'), URL ('/home'), and template URL ('/home.html'). We've also told 705 | Angular that our new state should be controlled by `MainCtrl`. Finally, using 706 | the [otherwise()][ui otherwise] method we've specified what should happen if 707 | the app receives a URL that is not defined. All that's left to do is define the 708 | `home.html` template. Instead of creating a new file, we are going to move most 709 | of our HTML into an [inline template][]. 710 | 711 | 712 | {x: add home template} 713 | Add our inline home template right before the `` tag in `index.html`: 714 | 715 | ```html 716 | 723 | ``` 724 | 725 | Using this syntax we can create templates inside our HTML and reference them 726 | in JavaScript. 727 | 728 | Finally, we need to tell `ui-router` where to place the template of the active 729 | state. 730 | 731 | 732 | {x: insert ui-view} 733 | Insert the `` tag where template should be rendered: 734 | 735 | ```html 736 | 737 |
738 |
739 | 740 |
741 |
742 | 743 | ... 744 | ``` 745 | 746 | Whenever `ui-router` detects a route change, it will place the new state's 747 | template inside the [][ui view] tag and initialize the controller we specified 748 | in our state configuration. Notice how we have removed the 749 | `ng-controller="MainCtrl"` line from the opening `` tag. 750 | 751 | # The Posts Page 752 | 753 | {video: ch-6} 754 | 755 | Now that we've figured out how to create a state with `ui-router`, let's create 756 | a new one called *posts* that will display comments associated with a post. 757 | 758 | 759 | {x: create posts state} 760 | In the config block in `app.js`, add a state where an individual post can be accessed: 761 | 762 | ```javascript 763 | .state('posts', { 764 | url: '/posts/{id}', 765 | templateUrl: '/posts.html', 766 | controller: 'PostsCtrl' 767 | }); 768 | ``` 769 | 770 | Notice that we define our URL with brackets around 'id'. This means that 'id' 771 | is actually a [route parameter][ui parameters] that will be made available to our controller. 772 | 773 | As with the *home* state, we're also going to need to define both a new 774 | template and a new controller. Because we're going to associate comments with 775 | posts, we want to ensure our `posts` factory is injected into this controller 776 | so that it may access the comments data. 777 | 778 | 779 | {x: create posts ctrl} 780 | Create a new controller in `app.js` called PostsCtrl and be sure to inject the `posts` service: 781 | 782 | ```javascript 783 | app.controller('PostsCtrl', [ 784 | '$scope', 785 | '$stateParams', 786 | 'posts', 787 | function($scope, $stateParams, posts){ 788 | 789 | }]); 790 | ``` 791 | 792 | ## Faking comment data 793 | 794 | Before we go any further, let's take a second to add some fake comment 795 | data to our `posts` model. This will help us mock up the basic comments 796 | view as well as ensure our routing is working properly. 797 | 798 | 799 | {x: add fake comments} 800 | In our addPost function in MainCtrl, add an array of fake comments to the posts object: 801 | 802 | ```javascript 803 | $scope.posts.push({ 804 | title: $scope.title, 805 | link: $scope.link, 806 | upvotes: 0, 807 | comments: [ 808 | {author: 'Joe', body: 'Cool post!', upvotes: 0}, 809 | {author: 'Bob', body: 'Great idea but everything is wrong!', upvotes: 0} 810 | ] 811 | }); 812 | ``` 813 | 814 | ## Getting the Right Post 815 | 816 | Since the *posts* page is about viewing the comments on a particular post, we 817 | need to use the `id` route parameter to grab the post and associated 818 | information. For now, we will consider the index of the post to be its id. We 819 | can use [$stateParams][ui stateParams] to retrieve the id from the URL and load the appropriate 820 | post. 821 | 822 | 823 | {x: get postid from stateparams} 824 | In PostsCtrl, set a scope object called `post` that grabs the appropriate post from the `posts` service using the id from $stateParams: 825 | 826 | ```javascript 827 | $scope.post = posts.posts[$stateParams.id]; 828 | ``` 829 | 830 | Now that we have the `post` variable in our controller, we can display that 831 | information in our template. 832 | 833 | 834 | {x: create posts template} 835 | Create a new inline template called "/posts.html" in `index.html` right before the `` tag: 836 | 837 | ```html 838 | 859 | ``` 860 | 861 | 862 | 863 | Finally, we'll add a link to the post's comment page next to the headline on the 864 | front page. 865 | 866 | 867 | {x: add cmt link} 868 | In the "/home.html" inline template, add the code below right after the title `
` tag: 869 | 870 | ```html 871 | 872 | Comments 873 | 874 | ``` 875 | 876 | {info} 877 | When iterating over an array, the `ng-repeat` directive makes an `$index` 878 | variable available along with each item in the array. 879 | 880 | 881 | ## Creating New Comments 882 | 883 | As with the creation of posts, we're going to want to allow our users to post new 884 | comments. This code looks very similar to what we've already written: 885 | 886 | 887 | {x: create addComment function} 888 | In our controller `PostsCtrl`, create an `addComment()` function: 889 | 890 | ```javascript 891 | $scope.addComment = function(){ 892 | if($scope.body === '') { return; } 893 | $scope.post.comments.push({ 894 | body: $scope.body, 895 | author: 'user', 896 | upvotes: 0 897 | }); 898 | $scope.body = ''; 899 | }; 900 | ``` 901 | 902 | {x: create addComment form} 903 | Add the form below at the end of our inline template "/posts.html": 904 | 905 | ```html 906 | 923 | ``` 924 | 925 | 926 | ## Recap 927 | 928 | In this first section, we've introduced you to some of the very basics of 929 | Angular.js including data-binding, controllers, services, and routing. In the 930 | process, we've created a skeleton web application that allows a user to create 931 | a posting that can contain a link or a title, then create comments that are 932 | associated with those postings. 933 | 934 | Up next we're going to learn how to use Node.js to implement a basic REST API 935 | for saving and retrieving posts and comments. Then we'll come back to the 936 | frontend and wire everything together into a single cohesive and functional web 937 | application. 938 | 939 | # Beginning Node 940 | 941 | {video: ch-7} 942 | 943 | Now that we have the basic front-end for our Flapper News coded up with 944 | Angular, we're going to start work on our backend. Because Angular is able to 945 | handle the templating and rendering, our backend server will mainly serve as a 946 | communication layer with our database. Over the next few sections, we are going 947 | to focus on modeling our data needs and creating a REST API that our Angular 948 | app can use to interact with this data. 949 | 950 | If you haven't already done so, make sure you have `node`, `npm`, and `mongodb` 951 | installed on your system. As a quick reminder, this tutorial assumes Node 952 | v0.10.31 and MongoDB 2.6.7. 953 | 954 | 955 | ## Creating A New Project 956 | 957 | {x: install express-generator} 958 | We will begin by using [express's generator][express generator]. We can install this using 959 | the following command: 960 | 961 | ```bash 962 | npm install -g express-generator 963 | ``` 964 | 965 | 966 | {x: create node project} 967 | Now we can initiate a new node project with the following command: 968 | 969 | ```bash 970 | express --ejs flapper-news 971 | cd flapper-news 972 | ``` 973 | 974 | This will create a new folder called *flapper-news* and populate it with 975 | various files and folders. 976 | 977 | {info} 978 | We are passing the `--ejs` flag because we'd like like our server to use 979 | standard HTML in its templates as opposed *jade*. Theoretically we are free to 980 | use Jade if we'd like to but the front-end is already written in plain HTML. 981 | 982 | Our first order of business is to add the relevant npm modules we're going to 983 | need. When starting a new project, a generator will include a list of packages 984 | that are required by default. 985 | 986 | 987 | {x: npm install} 988 | Install these packages using the following command: 989 | 990 | ```bash 991 | npm install 992 | ``` 993 | 994 | This will automatically download any packages specified in the `packages.json` 995 | file and store them in the `node_modules/` directory. 996 | 997 | Next, we are going to install Mongoose, which is a library that provides 998 | schemas and models on top of MongoDB. 999 | 1000 | 1001 | {x: install mongoose} 1002 | Run this command to install Mongoose via npm: 1003 | 1004 | ```bash 1005 | npm install --save mongoose 1006 | ``` 1007 | 1008 | {info} 1009 | The `--save` flag passed to npm install instructs the program to also save the 1010 | package to the `packages.json` file. This allows you (or your teammates) to 1011 | automatically install missing packages with the `npm install` command. 1012 | 1013 | If you're using any version control software such as 1014 | [git](http://git-scm.com/), now is a good time to make your initial commit. 1015 | 1016 | 1017 | ## The Anatomy of Node Project 1018 | 1019 | Right now the root directory of our Node project should look something like this: 1020 | 1021 | ```bash 1022 | app.js 1023 | bin/ 1024 | node_modules/ 1025 | package.json 1026 | public/ 1027 | routes/ 1028 | views/ 1029 | ``` 1030 | 1031 | Let's go step by step and explain what each folder/file is: 1032 | 1033 | * `app.js` - This file is the launching point for our app. We use it to import 1034 | all other server files including modules, configure routes, open database 1035 | connections, and just about anything else we can think of. 1036 | 1037 | * `bin/` - This directory is used to contain useful executable scripts. By 1038 | default it contains one called `www`. A quick peak inside reveals that this 1039 | script actually includes `app.js` and when invoked, starts our Node.js server. 1040 | 1041 | * `node_modules` - This directory is home to all external modules used in the 1042 | project. As mentioned earlier, these modules are usually installed using 1043 | `npm install`. You will most likely not have to touch anything here. 1044 | 1045 | * `package.json` - This file defines a JSON object that contains various 1046 | properties of our project including things such as name and version number. 1047 | It can also defined what versions of Node are required and what modules our 1048 | project depends on. A list of [possible 1049 | options](https://www.npmjs.org/doc/package.json.html) can be found in npm's 1050 | documentation. 1051 | 1052 | * `public/` - As the name alludes to, anything in this folder will be made 1053 | publicly available by the server. This is where we're going to store 1054 | JavaScript, CSS, images, and templates we want the client to use. 1055 | 1056 | * `routes/` - This directory houses our Node controllers and is usually where 1057 | most of the backend code will be stored. 1058 | 1059 | * `views/` - As the name says, we will put our views here. Because we specified 1060 | the `--ejs` flag when initializing our project, views will have the `.ejs` 1061 | extension as opposed to the '.jade' extension Jade views use. Although views 1062 | are ultimately HTML, they are slightly different than any HTML file we might 1063 | specify in the `public/` directory. Views are capable of being rendered 1064 | directly by Node using the `render()` function and can contain logic that 1065 | allows the server to dynamically change the content. Because we are using 1066 | Angular to create a dynamic experience, we won't be using this feature. 1067 | 1068 | In addition to the above files structure, we are going to add one more folder. 1069 | 1070 | 1071 | {x: create models folder} 1072 | Create a new folder called 'models': 1073 | 1074 | ```bash 1075 | mkdir models 1076 | ``` 1077 | 1078 | This folder will contain our Mongoose schema definitions. 1079 | 1080 | 1081 | ## Importing Our Angular Project 1082 | 1083 | The final step before we begin building out the backend is to import our 1084 | Angular portion into our Node.js project. First move the `index.html` file to 1085 | the `views/` directory. Because we're using the *ejs* engine, Node is looking 1086 | for files with the `.ejs` extension so we're going to have to rename our 1087 | `index.html` to `index.ejs`, replacing the existing one. 1088 | 1089 | Next, move the Angular `app.js` file to the `public/javascripts/` directory. To 1090 | avoid confusion with Node's `app.js`, also rename the Angular file to 1091 | `angularApp.js`. 1092 | 1093 | Finally let's update the ` 1098 | ``` 1099 | 1100 | Now we can start our application by running `npm start`. 1101 | 1102 | If we point our browser to http://localhost:3000 we should be greeted with our 1103 | Angular application. 1104 | 1105 | 1106 | {x: add angular to node} 1107 | Add our existing Angular code to the node project 1108 | 1109 | 1110 | 1111 | # Creating Schemas With Mongoose 1112 | 1113 | {video: ch-8} 1114 | 1115 | 1116 | Our first step in making a persistent data store is to configure our data 1117 | models. To do this, we are going to be adding a schema layer on top of 1118 | MongoDB using a nice library called Mongoose. Before we begin, let's 1119 | make sure our MongoDB server is running. 1120 | 1121 | 1122 | {x: start mongo} 1123 | If Mongo isn't running on your machine, enter this into your terminal: 1124 | 1125 | ```bash 1126 | mongod & 1127 | ``` 1128 | 1129 | Next, we need to tell Node to connect to our local database on start. 1130 | 1131 | 1132 | {x: connect to mongo} 1133 | Connect to our local MongoDB instance by adding the following code into our `app.js` file: 1134 | 1135 | ```javascript 1136 | var mongoose = require('mongoose'); 1137 | 1138 | mongoose.connect('mongodb://localhost/news'); 1139 | ``` 1140 | 1141 | This will open a connection with the `news` database running on our Mongo 1142 | server. Now we can create our first model. 1143 | 1144 | 1145 | {x: post schema} 1146 | In our `models/` directory create a new file called `Posts.js` and add the following code: 1147 | 1148 | ```javascript 1149 | var mongoose = require('mongoose'); 1150 | 1151 | var PostSchema = new mongoose.Schema({ 1152 | title: String, 1153 | link: String, 1154 | upvotes: {type: Number, default: 0}, 1155 | comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }] 1156 | }); 1157 | 1158 | mongoose.model('Post', PostSchema); 1159 | ``` 1160 | 1161 | Here we've defined a model called `Post` with several attributes corresponding 1162 | to the type of data we'd like to store. We've declared our `upvotes` field to 1163 | be initialized to 0 and we've set our `comments` field to an array of `Comment` 1164 | references. This will allow us to use Mongoose's build in [populate()][mongoose 1165 | populate] method to easily retrieve all comments associated with a given 1166 | `post`. 1167 | 1168 | Next we register that model with with the global `mongoose` object we imported 1169 | using `require()` so that it can be used to interact with the database anywhere 1170 | else `mongoose` is imported. 1171 | 1172 | 1173 | {x: register post schema} 1174 | In `app.js`, add the following line of code before our `mongoose.connect` call: 1175 | 1176 | ```javascript 1177 | var mongoose = require('mongoose'); 1178 | require('./models/Posts'); 1179 | 1180 | mongoose.connect('mongodb://localhost/news'); 1181 | ``` 1182 | 1183 | 1184 | {x: comment schema} 1185 | While were on the topic of making models, let's create a second model for 1186 | storing comments in `models/Comments.js`: 1187 | 1188 | ```javascript 1189 | var mongoose = require('mongoose'); 1190 | 1191 | var CommentSchema = new mongoose.Schema({ 1192 | body: String, 1193 | author: String, 1194 | upvotes: {type: Number, default: 0}, 1195 | post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' } 1196 | }); 1197 | 1198 | mongoose.model('Comment', CommentSchema); 1199 | ``` 1200 | 1201 | In Mongoose, we can create relationships between different data models using 1202 | the [ObjectId][mongoose objectid] type. The `ObjectId` data type refers to a 12 1203 | byte [MongoDB ObjectId][mongo objectid], which is actually what is stored in 1204 | the database. The `ref` property tells Mongoose what type of object the ID 1205 | references and enables us to retrieve both items simultaneously. 1206 | 1207 | 1208 | {x: register comments schema} 1209 | As always when you add a new file, remember to include it in `app.js`. Include this line of code right after the `require` for our Posts schema: 1210 | 1211 | ```javascript 1212 | require('./models/Comments'); 1213 | ``` 1214 | 1215 | 1216 | # Opening REST Routes 1217 | 1218 | {video: ch-9} 1219 | 1220 | 1221 | With our backend models in place, it's now time to open some routes that the 1222 | frontend client can interact with. The following are a list of actions a user 1223 | can perform: 1224 | 1225 | * view all posts 1226 | * Add a new post 1227 | * upvote a post 1228 | * view comments associated with a post 1229 | * add a comment 1230 | * upvote a comment 1231 | 1232 | The actions map directly to several routes, which are described as follows: 1233 | 1234 | * GET /posts - return a list of posts and associated metadata 1235 | * POST /posts - create a new post 1236 | * GET /posts/:id - return an individual post with associated comments 1237 | * PUT /posts/:id/upvote - upvote a post, notice we use the post ID in the URL 1238 | * POST /posts/:id/comments - add a new comment to a post by ID 1239 | * PUT /posts/:id/comments/:id/upvote - upvote a comment 1240 | 1241 | 1242 | ### Creating Our First Route 1243 | 1244 | To keep things simple we will be defining these routes in the `routes/index.js` 1245 | file. Let's begin by opening up the first route we listed, which should return 1246 | a JSON list containing all `posts`. 1247 | 1248 | 1249 | {x: get posts} 1250 | Start by creating a *GET* route for retrieving posts in `routes/index.js`. It should return a JSON list containing all `posts`: 1251 | 1252 | ```javascript 1253 | var mongoose = require('mongoose'); 1254 | var Post = mongoose.model('Post'); 1255 | var Comment = mongoose.model('Comment'); 1256 | 1257 | router.get('/posts', function(req, res, next) { 1258 | Post.find(function(err, posts){ 1259 | if(err){ return next(err); } 1260 | 1261 | res.json(posts); 1262 | }); 1263 | }); 1264 | ``` 1265 | 1266 | First, we need to make sure that `mongoose` is imported and that we have 1267 | handles to the `Post` and `Comment` models. Then we use the express 1268 | [get()][express get] method to define the URL for the route (`/posts`) and a 1269 | function to handle the request. Inside our request handler, we query the 1270 | database for all posts. If and error occurred, we pass the error to an error 1271 | handling function otherwise we use [res.json()][express json] to send the 1272 | retrieved posts back to the client. 1273 | 1274 | {info} 1275 | When defining routes with Express.js, two variables will get passed to the 1276 | handler function. `req`, which stands for "request", contains all the 1277 | information about the request that was made to the server including data 1278 | fields. `res`, which stands for "response", is the object used to respond to 1279 | the client. 1280 | 1281 | 1282 | {x: create posts} 1283 | Before we can test that this route works, we need to have some data in our 1284 | database. We can do this by creating a *POST* route for creating posts: 1285 | 1286 | Notice that we're using router.post instead of router.get. This means that we will be making a POST request to the server (not to be confused with our Flapper News 'post' models and routes). 1287 | 1288 | ```javascript 1289 | router.post('/posts', function(req, res, next) { 1290 | var post = new Post(req.body); 1291 | 1292 | post.save(function(err, post){ 1293 | if(err){ return next(err); } 1294 | 1295 | res.json(post); 1296 | }); 1297 | }); 1298 | ``` 1299 | 1300 | The structure of the route is similar as above, however, we are using the 1301 | `post()` method. We are also using `mongoose` to create a new `post` object in 1302 | memory before saving it to the database. 1303 | 1304 | 1305 | ## Testing the Initial Routes 1306 | 1307 | We can test these two routes using a command line tool called 1308 | [cURL](http://curl.haxx.se/). 1309 | 1310 | 1311 | {x: test create post} 1312 | First, we create a new `post`: 1313 | 1314 | ```sh 1315 | curl --data 'title=test&link=http://test.com' http://localhost:3000/posts 1316 | ``` 1317 | 1318 | 1319 | {x: test get post} 1320 | Next, we query the index route to insure it was saved: 1321 | 1322 | ```sh 1323 | curl http://localhost:3000/posts 1324 | ``` 1325 | 1326 | If everything is functioning properly, you should see a JSON array of size 1 1327 | printed on the console with `title` and `link` set to 'test' and 1328 | 'http://test.com' respectively. 1329 | 1330 | 1331 | ## Pre-loading Objects 1332 | 1333 | One thing you might notice about the remaining routes we need to implement is 1334 | that they all require us to load a `post` object by ID. Rather than replicating 1335 | the same code across several different request handler functions, we can use 1336 | Express's [param()][express param] function to automatically load an object. 1337 | 1338 | 1339 | {x: preload posts route} 1340 | Create a route for preloading `post` objects in `routes/index.js`: 1341 | 1342 | ```javascript 1343 | router.param('post', function(req, res, next, id) { 1344 | var query = Post.findById(id); 1345 | 1346 | query.exec(function (err, post){ 1347 | if (err) { return next(err); } 1348 | if (!post) { return next(new Error('can\'t find post')); } 1349 | 1350 | req.post = post; 1351 | return next(); 1352 | }); 1353 | }); 1354 | ``` 1355 | 1356 | In this example we are using mongoose's [query interface][mongoose query] 1357 | which simply provides a more flexible way of interacting with the database. 1358 | 1359 | Now when we define a route URL with `:post` in it, this function will be run 1360 | first. Assuming the `:post` parameter contains an ID, our function will 1361 | retrieve the `post` object from the database and attach it to the `req` object 1362 | after which the route handler function will be called. 1363 | 1364 | 1365 | {x: get single post} 1366 | To demonstrate this, let's go ahead and create our route for returning a single post: 1367 | 1368 | ```javascript 1369 | router.get('/posts/:post', function(req, res) { 1370 | res.json(req.post); 1371 | }); 1372 | ``` 1373 | 1374 | Because the post object was retrieved using the middleware function and 1375 | attached to the `req` object, all our request handler has to do is return the 1376 | JSON back to the client. 1377 | 1378 | 1379 | {x: mongoose query docs} 1380 | (optional) Check out the query interface docs for Mongoose 1381 | 1382 | ## Upvoting Posts 1383 | 1384 | Now let's implement the route to allow our users to upvote posts. We'll do 1385 | this by implementing a simple method in our `posts` schema to add one to the 1386 | upvote count then save it. 1387 | 1388 | 1389 | {x: upvote post schema method} 1390 | Add an `upvote()` method to the `Posts` schema in `Posts.js`: 1391 | 1392 | ```javascript 1393 | PostSchema.methods.upvote = function(cb) { 1394 | this.upvotes += 1; 1395 | this.save(cb); 1396 | }; 1397 | ``` 1398 | 1399 | 1400 | {x: upvote post route} 1401 | Now we create the route in our `index.js`: 1402 | 1403 | ```javascript 1404 | router.put('/posts/:post/upvote', function(req, res, next) { 1405 | req.post.upvote(function(err, post){ 1406 | if (err) { return next(err); } 1407 | 1408 | res.json(post); 1409 | }); 1410 | }); 1411 | ``` 1412 | 1413 | 1414 | {x: test upvote post} 1415 | To test this route, simply create a new post using the command above then run 1416 | 1417 | ```sh 1418 | curl -X PUT http://localhost:3000/posts//upvote 1419 | ``` 1420 | 1421 | You should see the post value returned back with the `upvote` property 1422 | incremented. 1423 | 1424 | 1425 | ## Finishing Off With Comments 1426 | 1427 | We've now completed all the basic routes for our `posts` object, now all we 1428 | need to do is do the same for `comments`. Many of the same concepts apply 1429 | with a few slight variations. 1430 | 1431 | Firstly, when creating a new comment we need to be sure to include the `post` 1432 | ID. Fortunately, this is already implicitly included in the request. In 1433 | addition to creating and saving the `comment`, we're going to need to attach a 1434 | reference to the new `comment` that refers to our `post` object. 1435 | 1436 | 1437 | {x: create comment route} 1438 | Create `comments` route for a particular `post`: 1439 | 1440 | ```javascript 1441 | router.post('/posts/:post/comments', function(req, res, next) { 1442 | var comment = new Comment(req.body); 1443 | comment.post = req.post; 1444 | 1445 | comment.save(function(err, comment){ 1446 | if(err){ return next(err); } 1447 | 1448 | req.post.comments.push(comment); 1449 | req.post.save(function(err, post) { 1450 | if(err){ return next(err); } 1451 | 1452 | res.json(comment); 1453 | }); 1454 | }); 1455 | }); 1456 | ``` 1457 | 1458 | 1459 | Now, we can also take the exact same `upvote` method from `posts` and apply 1460 | it to `comments`. 1461 | 1462 | {x: comment upvotes} 1463 | Implement the `/posts/:post/comments/:comment/upvote` route yourself. You 1464 | should also implement another `router.param()` middleware function to automatically 1465 | retrieve comments specified by the `:comment` route parameter. 1466 | 1467 | 1468 | Finally, we need to make a slight modification to our `GET /posts/:post` route 1469 | 1470 | {x: populate function} 1471 | Use the `populate()` function to retrieve `comments` along with `posts`: 1472 | 1473 | ```javascript 1474 | router.get('/posts/:post', function(req, res, next) { 1475 | req.post.populate('comments', function(err, post) { 1476 | if (err) { return next(err); } 1477 | 1478 | res.json(post); 1479 | }); 1480 | }); 1481 | ``` 1482 | Using the `populate()` method, we can automatically load all the comments 1483 | associated with that particular `post`. 1484 | 1485 | Congratulations! If you made it this far, you should now have a functioning 1486 | backend. There is still a significant amount of additional functionality 1487 | we could add, however, you should now understand some of the basics of 1488 | Node, Express, and Mongoose. 1489 | 1490 | Up next, we'll learn how to use these routes in conjunction with our 1491 | Angular.js frontend to create a web app where people can create `posts` 1492 | and add `comments`. 1493 | 1494 | # Wiring Everything Up 1495 | 1496 | {video: ch-10} 1497 | 1498 | Now that we have our basic backend implemented, we're going to wire up the 1499 | angular app we made in the first part of this tutorial. 1500 | 1501 | ## Loading Posts 1502 | 1503 | Our first step is going to be to query our new backend for all `posts` using 1504 | the index route. We do this by adding a new function inside our `posts` 1505 | service. 1506 | 1507 | 1508 | {x: get all posts} 1509 | Implement `getAll()` to retrieve posts in the `posts` service within `angularApp.js`: 1510 | 1511 | ```javascript 1512 | o.getAll = function() { 1513 | return $http.get('/posts').success(function(data){ 1514 | angular.copy(data, o.posts); 1515 | }); 1516 | }; 1517 | ``` 1518 | 1519 | {info} 1520 | It's important to use the [angular.copy()](https://docs.angularjs.org/api/ng/function/angular.copy) method to create a deep copy of the returned data. This ensures 1521 | that the `$scope.posts` variable in `MainCtrl` will also be updated, ensuring 1522 | the new values are reflect in our view. 1523 | 1524 | 1525 | {x: inject http} 1526 | Also be sure to inject the `$http` service: 1527 | 1528 | ```javascript 1529 | app.factory('posts', ['$http', function($http){ 1530 | ... 1531 | }); 1532 | ``` 1533 | 1534 | In this function we're using the Angular [$http][ng http] service to query our 1535 | `posts` route. The `success()` function allows us to bind function that will be 1536 | executed when the request returns. Because our route will return a list of 1537 | `posts`, all we need to do is copy that list to the client side `posts` object. 1538 | Notice that we're using the [angular.copy()][ng copy] function to do this as it 1539 | will make our UI update properly. 1540 | 1541 | Now we need to call this function at an appropriate time to load the data. We 1542 | do this by adding a property called [resolve][ui resolve] to our `home` state. 1543 | 1544 | 1545 | {x: resolve ui-router} 1546 | Use the `resolve` property of `ui-router` to ensure posts are loaded: 1547 | 1548 | ```javascript 1549 | .state('home', { 1550 | url: '/home', 1551 | templateUrl: '/home.html', 1552 | controller: 'MainCtrl', 1553 | resolve: { 1554 | postPromise: ['posts', function(posts){ 1555 | return posts.getAll(); 1556 | }] 1557 | } 1558 | }) 1559 | ``` 1560 | 1561 | By using the `resolve` property in this way, we are ensuring that anytime our 1562 | `home` state is entered, we will automatically query all posts from our 1563 | backend before the state actually finishes loading. 1564 | 1565 | Now, when you fire up the server and go to http://localhost:3000/#/home, you 1566 | should see all the posts that exist in the database. 1567 | 1568 | ## Creating New Posts 1569 | 1570 | Up next, we need to enable creating new `posts`. As with loading posts, we're 1571 | going to do this by adding another method to our `posts` service: 1572 | 1573 | 1574 | {x: create new posts service} 1575 | Add a method for creating new posts: 1576 | 1577 | ```javascript 1578 | o.create = function(post) { 1579 | return $http.post('/posts', post).success(function(data){ 1580 | o.posts.push(data); 1581 | }); 1582 | }; 1583 | ``` 1584 | 1585 | 1586 | {x: save post to server} 1587 | Now we can modify the `$scope.addPost()` method in `MainCtrl` to save `posts` to the server: 1588 | 1589 | ```javascript 1590 | $scope.addPost = function(){ 1591 | if(!$scope.title || $scope.title === '') { return; } 1592 | posts.create({ 1593 | title: $scope.title, 1594 | link: $scope.link, 1595 | }); 1596 | $scope.title = ''; 1597 | $scope.link = ''; 1598 | }; 1599 | ``` 1600 | 1601 | Refresh the page then try adding a new post. You should immediately see it 1602 | displayed and if you refresh the page, the post is still there! We now have 1603 | persistent data. 1604 | 1605 | 1606 | ## Upvoting Posts 1607 | 1608 | The last thing we need to wire up on the main page is upvoting posts. 1609 | 1610 | 1611 | {x: frontend upvote service} 1612 | Like in the previous examples, we'll add another method to our service: 1613 | 1614 | ```javascript 1615 | o.upvote = function(post) { 1616 | return $http.put('/posts/' + post._id + '/upvote') 1617 | .success(function(data){ 1618 | post.upvotes += 1; 1619 | }); 1620 | }; 1621 | ``` 1622 | 1623 | {x: frontend upvote scope} 1624 | And then in our controller, we simply replace `incrementUpvotes()` with this: 1625 | 1626 | ```javascript 1627 | $scope.incrementUpvotes = function(post) { 1628 | posts.upvote(post); 1629 | }; 1630 | ``` 1631 | 1632 | Here we use the `put()` method to upvote a `post`. When the call returns 1633 | successfully, we update our local copy to reflect the changes. Now when you 1634 | refresh the page, upvotes are persisted. 1635 | 1636 | 1637 | ## Finishing Off Comments 1638 | 1639 | If you remember back to when we first wrote the template, we were treating the 1640 | index of a `post` in the `posts` array to be its `id` field. Now that we are 1641 | actually dealing with database entries, we need to make sure that when you 1642 | click on the "Comments" link for a post, the application directs you to the 1643 | proper URL. 1644 | 1645 | 1646 | {x: comments link to correct route} 1647 | Modify the `Comments` link to point to the proper route: 1648 | 1649 | ```html 1650 | Comments 1651 | ``` 1652 | 1653 | {info} 1654 | MongoDB uses the `_id` property natively, so it's usually easier to just write 1655 | our application with that in mind rather than have to translate it to an `id` 1656 | field, which some might consider more intuitive. 1657 | 1658 | When you click on the "Comments" link for a post, you should be directed to a 1659 | new Angular URL that might look something like 1660 | `http://localhost:3000/#/posts/53e27c7c363cf85ad8a84723` 1661 | 1662 | What we are going to do is have Angular automatically load the full `post` 1663 | object with `comments` when we enter this state. Like we did with the `home` 1664 | state, we're going to use the `resolve` property in the route to accomplish this. 1665 | 1666 | 1667 | {x: retrieve single post angular} 1668 | First, lets add a simple method in our `posts` service to retrieve a single 1669 | `post` from our server: 1670 | 1671 | ```javascript 1672 | o.get = function(id) { 1673 | return $http.get('/posts/' + id).then(function(res){ 1674 | return res.data; 1675 | }); 1676 | }; 1677 | ``` 1678 | 1679 | Notice that instead of using the `success()` method we have traditionally used, 1680 | we are instead using a [promise](https://docs.angularjs.org/api/ng/service/$q). 1681 | 1682 | 1683 | {x: resolve for posts route} 1684 | Next we add the `resolve` object to our `posts` state: 1685 | 1686 | ```javascript 1687 | .state('posts', { 1688 | url: '/posts/{id}', 1689 | templateUrl: '/posts.html', 1690 | controller: 'PostsCtrl', 1691 | resolve: { 1692 | post: ['$stateParams', 'posts', function($stateParams, posts) { 1693 | return posts.get($stateParams.id); 1694 | }] 1695 | } 1696 | }); 1697 | ``` 1698 | 1699 | The Angular ui-router detects we are entering the `posts` state and will then 1700 | automatically query the server for the full post object, including `comments`. 1701 | Only after the request has returned will the state finish loading. 1702 | 1703 | To get access to the `post` object we just retrieved in the `PostsCtrl`, instead 1704 | of going through the `posts` service, the specific object will be directly injected 1705 | into our `PostsCtrl`. 1706 | 1707 | 1708 | {x: inject a post} 1709 | We can modify our controller to gain access to it like so: 1710 | 1711 | ```javascript 1712 | app.controller('PostsCtrl', [ 1713 | '$scope', 1714 | 'posts', 1715 | 'post', 1716 | function($scope, posts, post){ 1717 | $scope.post = post; 1718 | 1719 | ... 1720 | }); 1721 | ``` 1722 | 1723 | Notice that we no longer have need to inject `$stateParams` into our 1724 | controller. We're still going to want to inject `posts` to gain access to the 1725 | methods for manipulating `comments`, however. 1726 | 1727 | When you refresh the page, you should now see the title of the post displayed. 1728 | 1729 | To enable adding `comments`, we can use the same technique we used for adding 1730 | new `posts`. 1731 | 1732 | 1733 | {x: add comment service} 1734 | First add a method to the `posts` service: 1735 | 1736 | ```javascript 1737 | o.addComment = function(id, comment) { 1738 | return $http.post('/posts/' + id + '/comments', comment); 1739 | }; 1740 | ``` 1741 | 1742 | 1743 | {x: add comment scope} 1744 | Then call it from within the existing `addComment()` function: 1745 | 1746 | ```javascript 1747 | $scope.addComment = function(){ 1748 | if($scope.body === '') { return; } 1749 | posts.addComment(post._id, { 1750 | body: $scope.body, 1751 | author: 'user', 1752 | }).success(function(comment) { 1753 | $scope.post.comments.push(comment); 1754 | }); 1755 | $scope.body = ''; 1756 | }; 1757 | ``` 1758 | 1759 | Now you can add a comment that's persisted to the database. 1760 | 1761 | 1762 | {x: enable comment upvoting service} 1763 | Our final task is to enable the upvoting of comments: 1764 | 1765 | ```javascript 1766 | o.upvoteComment = function(post, comment) { 1767 | return $http.put('/posts/' + post._id + '/comments/'+ comment._id + '/upvote') 1768 | .success(function(data){ 1769 | comment.upvotes += 1; 1770 | }); 1771 | }; 1772 | ``` 1773 | 1774 | In `PostsCtrl`: 1775 | 1776 | ```javascript 1777 | $scope.incrementUpvotes = function(comment){ 1778 | posts.upvoteComment(post, comment); 1779 | }; 1780 | ``` 1781 | 1782 | 1783 | # Adding Authentication via Passport 1784 | 1785 | Now that we have a application with a working server and client, let's add in 1786 | the ability to create and authenticate users. To do this, we'll be using 1787 | [passport](http://passportjs.org/) for authentications and JWT tokens for 1788 | session management. [This 1789 | post](https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/) 1790 | has some great information about token vs session based authentication. 1791 | 1792 | ## Creating the User model 1793 | {video: create-user-model} 1794 | {x: create mongoose user} 1795 | Start by creating a mongoose model for our Users with the following code in `models/Users.js`: 1796 | 1797 | ```javascript 1798 | var mongoose = require('mongoose'); 1799 | 1800 | var UserSchema = new mongoose.Schema({ 1801 | username: {type: String, lowercase: true, unique: true}, 1802 | hash: String, 1803 | salt: String 1804 | }); 1805 | 1806 | mongoose.model('User', UserSchema); 1807 | ``` 1808 | 1809 | Our users will be logging in with a username and password. Since we don't want 1810 | to store our passwords in plain text, we'll need a field for storing the hash 1811 | of the password. We'll also be generating and saving a salt whenever we set the 1812 | password. 1813 | 1814 | Now let's implement setting and validating passwords. We'll be using the 1815 | `pbkdf2()` function which ships with node's native `crypto` module to hash our 1816 | passwords. 1817 | 1818 | {x: require crypto user} 1819 | Require the crypto module in our user model. 1820 | 1821 | ```javascript 1822 | var crypto = require('crypto'); 1823 | ``` 1824 | 1825 | {x: create setPassword} 1826 | Create a `setPassword()` method on the `User` model 1827 | that accepts a password then generates a salt and associated password hash. 1828 | 1829 | ```javascript 1830 | UserSchema.methods.setPassword = function(password){ 1831 | this.salt = crypto.randomBytes(16).toString('hex'); 1832 | 1833 | this.hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64).toString('hex'); 1834 | }; 1835 | ``` 1836 | 1837 | {info} 1838 | The `pbkdf2Sync()` function takes four parameters: password, salt, iterations, 1839 | and key length. We'll need to make sure the iterations and key length in our 1840 | `setPassword()` method match the ones in our `validPassword()` method 1841 | 1842 | {x: create validPassword} 1843 | Create a `validPassword()` method that accepts a password and compares it to the 1844 | hash stored, returning a boolean. 1845 | 1846 | ```javascript 1847 | UserSchema.methods.validPassword = function(password) { 1848 | var hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64).toString('hex'); 1849 | 1850 | return this.hash === hash; 1851 | }; 1852 | ``` 1853 | 1854 | Finally, let's create instance method for generating a JWT token for the 1855 | `user`. We'll be using the `jsonwebtoken()` module to help us generate tokens. 1856 | 1857 | {x: npm install jsonwebtoken} 1858 | Install the `jsonwebtoken` module via `npm`. 1859 | 1860 | ``` 1861 | npm install jsonwebtoken --save 1862 | ``` 1863 | 1864 | {x: require jsonwebtoken} 1865 | Require the `jsonwebtoken` module inside the User model. 1866 | 1867 | ```javascript 1868 | var jwt = require('jsonwebtoken'); 1869 | ``` 1870 | 1871 | {x: create generateJWT method} 1872 | Create the `generateJWT()` instance method on `UserSchema` with the following 1873 | code: 1874 | 1875 | ```javascript 1876 | UserSchema.methods.generateJWT = function() { 1877 | 1878 | // set expiration to 60 days 1879 | var today = new Date(); 1880 | var exp = new Date(today); 1881 | exp.setDate(today.getDate() + 60); 1882 | 1883 | return jwt.sign({ 1884 | _id: this._id, 1885 | username: this.username, 1886 | exp: parseInt(exp.getTime() / 1000) 1887 | }, 'SECRET'); 1888 | }; 1889 | ``` 1890 | 1891 | {info} 1892 | The first argument of the `jwt.sign()` method is the payload that gets signed. 1893 | Both the server and client will have access to the payload. The `exp` value in 1894 | the payload is a Unix timestamp in seconds that will specify when the token 1895 | expires. For this example we set it to 60 days in the future. The second 1896 | argument of `jwt.sign()` is the secret used to sign our tokens. We're 1897 | hard-coding it in this example, but it is **_strongly recommended_** that you 1898 | use an environment variable for referencing the secret and keep it out of your 1899 | codebase. 1900 | 1901 | ## Setting up Passport 1902 | 1903 | {video: setting-up-passport} 1904 | 1905 | Now that we have our methods in place on our `UserSchema` for creating and 1906 | authenticating users, let's install and setup Passport in our application. 1907 | Passport uses strategies for different authentication methods (password, GitHub, 1908 | Facebook, etc.) that are split out into separate modules. We'll be using the 1909 | `passport-local` strategy to handle username/password authentication. See the 1910 | [official Passport guide](http://passportjs.org/guide/) for more information 1911 | about Passport. 1912 | 1913 | {x: npm install passport passport-local} 1914 | Install `passport` and `passport-local` via `npm`. 1915 | 1916 | ``` 1917 | npm install passport passport-local --save 1918 | ``` 1919 | 1920 | {x: create config folder} 1921 | Create a folder `config` in the root of our application. 1922 | 1923 | {x: create config passport} 1924 | Create `config/passport.js` with the following code: 1925 | 1926 | ```javascript 1927 | var passport = require('passport'); 1928 | var LocalStrategy = require('passport-local').Strategy; 1929 | var mongoose = require('mongoose'); 1930 | var User = mongoose.model('User'); 1931 | 1932 | passport.use(new LocalStrategy( 1933 | function(username, password, done) { 1934 | User.findOne({ username: username }, function (err, user) { 1935 | if (err) { return done(err); } 1936 | if (!user) { 1937 | return done(null, false, { message: 'Incorrect username.' }); 1938 | } 1939 | if (!user.validPassword(password)) { 1940 | return done(null, false, { message: 'Incorrect password.' }); 1941 | } 1942 | return done(null, user); 1943 | }); 1944 | } 1945 | )); 1946 | 1947 | ``` 1948 | 1949 | {info} 1950 | Here we create a new `LocalStrategy` where we have our logic on how to 1951 | authenticate a user given a username and password. Note that this function 1952 | calls the `validPassword()` function that we just created. See the [official 1953 | passport configuration guide](http://passportjs.org/guide/configure/) for more 1954 | information. 1955 | 1956 | 1957 | ## Adding Passport to Our Application 1958 | 1959 | Now that we have a configuration file for passport ready to use, let's 1960 | incorporate it into our application. 1961 | 1962 | {x: require passport appjs} 1963 | Require passport in `app.js` after where we required `mongoose` 1964 | 1965 | ```javascript 1966 | var passport = require('passport'); 1967 | ``` 1968 | 1969 | {x: require users model} 1970 | Require the user model after where we required `Post` and `Comment` models 1971 | 1972 | ```javascript 1973 | require('./models/Users'); 1974 | ``` 1975 | 1976 | {x: require passport config} 1977 | Require the passport configuration we just created **after** where we required 1978 | our `User` model. 1979 | 1980 | ```javascript 1981 | require('./config/passport'); 1982 | ``` 1983 | {x: initialize passport} 1984 | Initialize passport after the `express.static` middleware. 1985 | 1986 | ```javascript 1987 | app.use(passport.initialize()); 1988 | ``` 1989 | 1990 | ## Creating Authentication Endpoints 1991 | 1992 | {video: create-auth-routes} 1993 | 1994 | Now that Passport is setup in our application, we can create a `/login` 1995 | endpoint to authenticate our users and return a JWT token. 1996 | 1997 | {x: create register route} 1998 | Add a `/register` route to `routes/index.js` that creates a user given a 1999 | username and password: 2000 | 2001 | ```javascript 2002 | router.post('/register', function(req, res, next){ 2003 | if(!req.body.username || !req.body.password){ 2004 | return res.status(400).json({message: 'Please fill out all fields'}); 2005 | } 2006 | 2007 | var user = new User(); 2008 | 2009 | user.username = req.body.username; 2010 | 2011 | user.setPassword(req.body.password) 2012 | 2013 | user.save(function (err){ 2014 | if(err){ return next(err); } 2015 | 2016 | return res.json({token: user.generateJWT()}) 2017 | }); 2018 | }); 2019 | ``` 2020 | 2021 | {x: require passport routes index} 2022 | Require `passport` in `routes/index.js` 2023 | 2024 | ```javascript 2025 | var passport = require('passport'); 2026 | ``` 2027 | 2028 | {x: import user model} 2029 | Import the `User` mongoose model 2030 | 2031 | ``` 2032 | var User = mongoose.model('User'); 2033 | ``` 2034 | 2035 | {x: create login route} 2036 | Add a `/login` route to `routes/index.js` that authenticates the user and 2037 | returns a token to the client: 2038 | 2039 | ```javascript 2040 | router.post('/login', function(req, res, next){ 2041 | if(!req.body.username || !req.body.password){ 2042 | return res.status(400).json({message: 'Please fill out all fields'}); 2043 | } 2044 | 2045 | passport.authenticate('local', function(err, user, info){ 2046 | if(err){ return next(err); } 2047 | 2048 | if(user){ 2049 | return res.json({token: user.generateJWT()}); 2050 | } else { 2051 | return res.status(401).json(info); 2052 | } 2053 | })(req, res, next); 2054 | }); 2055 | ``` 2056 | 2057 | The `passport.authenticate('local')` middleware uses the `LocalStrategy` we 2058 | created earlier. We're using a custom callback for the `authenticate` 2059 | middleware so we can return error messages to the client. If authentication is 2060 | successful we want to return a JWT token to the client just like our register 2061 | route does. 2062 | 2063 | 2064 | ## Securing Endpoints and Associating Posts and Comments with Users 2065 | 2066 | {x: install express-jwt} 2067 | Install the `express-jwt` module via `npm`. 2068 | 2069 | ``` 2070 | npm install express-jwt --save 2071 | ``` 2072 | 2073 | {x: require express-jwt} 2074 | Require `express-jwt` in `routes/index.js` 2075 | 2076 | ```javascript 2077 | var jwt = require('express-jwt'); 2078 | ``` 2079 | 2080 | {x: create auth middleware} 2081 | Create a middleware for authenticating jwt tokens in `routes/index.js`: 2082 | 2083 | ```javascript 2084 | var auth = jwt({secret: 'SECRET', userProperty: 'payload'}); 2085 | 2086 | ``` 2087 | 2088 | The `userPropery` option specifies which property on `req` to put our payload 2089 | from our tokens. By default it's set on `user` but we're using `payload` 2090 | instead to avoid any conflicts with passport (it shouldn't be an issue since we 2091 | aren't using both methods of authentication in the same request). This also 2092 | avoids confusion since the payload isn't an instance of our User model. 2093 | 2094 | 2095 | {info} 2096 | Make sure to use the same secret as the one in `models/User.js` for generating 2097 | tokens. Again, we're hard-coding the token in this example but it is *_strongly 2098 | recommended_* that you use an environment variable for referencing your secret. 2099 | 2100 | Now we can use the middleware we just defined to require authentication on 2101 | specific routes. In our case, we'd want to authenticate users whenever they try 2102 | to write to our application, such as when they're posting or commenting. 2103 | 2104 | {x: require auth create post} 2105 | Require authentication for creating a post 2106 | 2107 | ```javascript 2108 | router.post('/posts', auth, function(req, res, next) { 2109 | 2110 | ``` 2111 | 2112 | {x: require auth upvote post} 2113 | Require authentication for upvoting 2114 | 2115 | ```javascript 2116 | router.put('/posts/:post/upvote', auth, function(req, res, next) { 2117 | ``` 2118 | 2119 | {x: require auth commenting} 2120 | Require authentication for commenting and set the author for comments 2121 | 2122 | ```javascript 2123 | router.post('/posts/:post/comments', auth, function(req, res, next) { 2124 | ``` 2125 | 2126 | {x: require auth upvote comment} 2127 | Require Authentication for upvoting comments 2128 | 2129 | ```javascript 2130 | router.put('/posts/:post/comments/:comment/upvote', auth, function(req, res, next) { 2131 | ``` 2132 | 2133 | Finally, let's associate the authors with their posts and comments. Since we're 2134 | authenticating with JWT tokens, we can get the username directly from the 2135 | token's payload, saving us a trip to the database. 2136 | 2137 | {x: associate post authors} 2138 | Set the author field when creating posts 2139 | 2140 | ```javascript 2141 | router.post('/posts', auth, function(req, res, next) { 2142 | var post = new Post(req.body); 2143 | post.author = req.payload.username; 2144 | ``` 2145 | 2146 | {x: associate comment authors} 2147 | Set the author field when creating comments 2148 | 2149 | ```javascript 2150 | router.post('/posts/:post/comments', auth, function(req, res, next) { 2151 | var comment = new Comment(req.body); 2152 | comment.post = req.post; 2153 | comment.author = req.payload.username; 2154 | ``` 2155 | 2156 | Now that our backend is ready to register and authenticate users, let's update 2157 | the angular side of our application to handle this. 2158 | 2159 | 2160 | ## Creating an Angular Service for Authentication 2161 | 2162 | {video: wiring-frontend} 2163 | 2164 | We'll be using localStorage for persisting data to the client. This gives us 2165 | a much easier interface for persisting data across sessions without having to 2166 | deal with parsing cookies or handling cookies across multiple domains. If a 2167 | JWT token exists in localStorage, we can assume the user is logged in as long 2168 | as the token isn't expired. To log a user out, simply remove the token from 2169 | localStorage. Check out the [localStorage documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage) 2170 | for a better understanding of localStorage. 2171 | 2172 | {x: create auth factory} 2173 | Let's start out by creating our initial auth factory. We'll need to inject 2174 | `$http` for interfacing with our server, and `$window` for interfacing with 2175 | `localStorage`. 2176 | 2177 | ```javascript 2178 | .factory('auth', ['$http', '$window', function($http, $window){ 2179 | var auth = {}; 2180 | 2181 | return auth; 2182 | }]) 2183 | 2184 | ``` 2185 | 2186 | {x: create auth savetoken} 2187 | Next, let's create a `saveToken` and `getToken` function in our factory for 2188 | getting and setting our token to `localStorage` 2189 | 2190 | ```javascript 2191 | auth.saveToken = function (token){ 2192 | $window.localStorage['flapper-news-token'] = token; 2193 | }; 2194 | 2195 | auth.getToken = function (){ 2196 | return $window.localStorage['flapper-news-token']; 2197 | } 2198 | ``` 2199 | 2200 | {x: create auth isloggedin} 2201 | Now we'll need to add a function `isLoggedIn()` to our `auth` factory to 2202 | return a boolean value for if the user is logged in. 2203 | 2204 | ```javascript 2205 | auth.isLoggedIn = function(){ 2206 | var token = auth.getToken(); 2207 | 2208 | if(token){ 2209 | var payload = JSON.parse($window.atob(token.split('.')[1])); 2210 | 2211 | return payload.exp > Date.now() / 1000; 2212 | } else { 2213 | return false; 2214 | } 2215 | }; 2216 | ``` 2217 | 2218 | If a token exists, we'll need to check the payload to see if the token has 2219 | expired, otherwise we can assume the user is logged out. The payload is the 2220 | middle part of the token between the two `.`s. It's a JSON object that has been 2221 | base64'd. We can get it back to a stringified JSON by using `$window.atob()`, and 2222 | then back to a javascript object with `JSON.parse`. 2223 | 2224 | {x: create auth currentuser} 2225 | Let's create a function `currentUser()` that returns the username of the user that's 2226 | logged in. 2227 | 2228 | ```javascript 2229 | auth.currentUser = function(){ 2230 | if(auth.isLoggedIn()){ 2231 | var token = auth.getToken(); 2232 | var payload = JSON.parse($window.atob(token.split('.')[1])); 2233 | 2234 | return payload.username; 2235 | } 2236 | }; 2237 | ``` 2238 | 2239 | Finally, we'll need methods to log in, register, and log users out. 2240 | 2241 | {x: create auth register} 2242 | Create a register function that posts a user to our `/register` route and saves 2243 | the token returned. 2244 | 2245 | ```javascript 2246 | auth.register = function(user){ 2247 | return $http.post('/register', user).success(function(data){ 2248 | auth.saveToken(data.token); 2249 | }); 2250 | }; 2251 | ``` 2252 | 2253 | {x: create auth login} 2254 | Create a login function that posts a user to our `/login` route and saves the 2255 | token returned. 2256 | 2257 | ```javascript 2258 | auth.logIn = function(user){ 2259 | return $http.post('/login', user).success(function(data){ 2260 | auth.saveToken(data.token); 2261 | }); 2262 | }; 2263 | ``` 2264 | 2265 | {x: create auth log out} 2266 | Create a logout function that removes the user's token from localStorage, 2267 | logging the user out. 2268 | 2269 | ```javascript 2270 | auth.logOut = function(){ 2271 | $window.localStorage.removeItem('flapper-news-token'); 2272 | }; 2273 | ``` 2274 | 2275 | 2276 | ## Creating the Login and Register pages 2277 | 2278 | {video: auth-controllers} 2279 | 2280 | Now that our authentication factory is complete we can start using it in our 2281 | application. Let's create a controller for our login and register page. 2282 | 2283 | {x: create auth controller} 2284 | Create an authentication controller with the following code: 2285 | 2286 | ```javascript 2287 | .controller('AuthCtrl', [ 2288 | '$scope', 2289 | '$state', 2290 | 'auth', 2291 | function($scope, $state, auth){ 2292 | $scope.user = {}; 2293 | 2294 | $scope.register = function(){ 2295 | auth.register($scope.user).error(function(error){ 2296 | $scope.error = error; 2297 | }).then(function(){ 2298 | $state.go('home'); 2299 | }); 2300 | }; 2301 | 2302 | $scope.logIn = function(){ 2303 | auth.logIn($scope.user).error(function(error){ 2304 | $scope.error = error; 2305 | }).then(function(){ 2306 | $state.go('home'); 2307 | }); 2308 | }; 2309 | }]) 2310 | ``` 2311 | 2312 | We need to initialize a user on `$scope` for our form. Then, we can create 2313 | a `register` and `logIn()` method on `$scope` to call the respective methods on 2314 | the auth factory. We can then handle any errors and set `$scope.error` for 2315 | displaying error messages later. Finally, if no errors occur, we can send the 2316 | user back to the `home` state using a promise. Now we can go ahead and create 2317 | our login and registration templates 2318 | 2319 | 2320 | {x: create register template} 2321 | Create the following registration template in `views/index.ejs`: 2322 | 2323 | ```html 2324 | 2352 | ``` 2353 | 2354 | {x: create login template} 2355 | Create the following login template in `views/index.ejs`: 2356 | 2357 | ```html 2358 | 2386 | ``` 2387 | 2388 | Finally, let's add two new states that make our login and register pages 2389 | accessible: 2390 | 2391 | {x: create login register states} 2392 | Create two new states for the login and register page: 2393 | 2394 | ```javascript 2395 | .state('login', { 2396 | url: '/login', 2397 | templateUrl: '/login.html', 2398 | controller: 'AuthCtrl', 2399 | onEnter: ['$state', 'auth', function($state, auth){ 2400 | if(auth.isLoggedIn()){ 2401 | $state.go('home'); 2402 | } 2403 | }] 2404 | }) 2405 | .state('register', { 2406 | url: '/register', 2407 | templateUrl: '/register.html', 2408 | controller: 'AuthCtrl', 2409 | onEnter: ['$state', 'auth', function($state, auth){ 2410 | if(auth.isLoggedIn()){ 2411 | $state.go('home'); 2412 | } 2413 | }] 2414 | }); 2415 | ``` 2416 | 2417 | Here we're specifying an `onEnter` function to our states. This gives us the 2418 | ability to detect if the user is authenticated before entering the state, which 2419 | allows us to redirect them back to the `home` state if they're already logged 2420 | in. 2421 | 2422 | ## Adding Navigation 2423 | 2424 | Let's add a navbar to our application so we can easily tell if the user is 2425 | logged in or not. 2426 | 2427 | {x: create nav ctrl} 2428 | Start by making a simple controller for our navbar that exposes the 2429 | `isLoggedIn`, `currentUser`, and `logOut` methods from our `auth` factory. 2430 | 2431 | ```javascript 2432 | .controller('NavCtrl', [ 2433 | '$scope', 2434 | 'auth', 2435 | function($scope, auth){ 2436 | $scope.isLoggedIn = auth.isLoggedIn; 2437 | $scope.currentUser = auth.currentUser; 2438 | $scope.logOut = auth.logOut; 2439 | }]); 2440 | ``` 2441 | 2442 | {x: add nav tpl} 2443 | Now we can add the markup for our navbar to our application: 2444 | 2445 | ```html 2446 | 2447 | 2455 | ``` 2456 | 2457 | We're using `ng-show` and `ng-hide` along with our `isLoggedIn()` function to 2458 | determine if the user is authenticated. If they are, their username is 2459 | displayed, along with a log out link which calls our `logOut()` function. 2460 | If they aren't, a Log In and a Register link is shown instead. 2461 | 2462 | ## Making the Rest of Our Application User-Aware 2463 | 2464 | Since our routes that write to the database now require authentication, let's 2465 | update our `posts` service to send the JWT token to the server on authenticated 2466 | requests. 2467 | 2468 | {x: inject auth to posts} 2469 | First, we'll need to inject the `auth` service to our `posts` service 2470 | 2471 | ```javascript 2472 | .factory('posts', ['$http', 'auth', function($http, auth){ 2473 | ``` 2474 | 2475 | Next, we'll need to send up our JWT token as an `Authorization` header. The 2476 | format for this header should be `Authorization: Bearer TOKEN.GOES.HERE`. 2477 | 2478 | {x: add auth header to requests} 2479 | We'll need to pass the following object as the last argument for our `$http` 2480 | calls for the `create`, `upvote`, `addComment`, and `upvoteComment` methods in 2481 | our `posts` service: 2482 | 2483 | ```javascript 2484 | o.create = function(post) { 2485 | return $http.post('/posts', post, { 2486 | headers: {Authorization: 'Bearer '+auth.getToken()} 2487 | }).success(function(data){ 2488 | o.posts.push(data); 2489 | }); 2490 | }; 2491 | 2492 | o.upvote = function(post) { 2493 | return $http.put('/posts/' + post._id + '/upvote', null, { 2494 | headers: {Authorization: 'Bearer '+auth.getToken()} 2495 | }).success(function(data){ 2496 | post.upvotes += 1; 2497 | }); 2498 | }; 2499 | 2500 | o.addComment = function(id, comment) { 2501 | return $http.post('/posts/' + id + '/comments', comment, { 2502 | headers: {Authorization: 'Bearer '+auth.getToken()} 2503 | }); 2504 | }; 2505 | 2506 | o.upvoteComment = function(post, comment) { 2507 | return $http.put('/posts/' + post._id + '/comments/'+ comment._id + '/upvote', null, { 2508 | headers: {Authorization: 'Bearer '+auth.getToken()} 2509 | }).success(function(data){ 2510 | comment.upvotes += 1; 2511 | }); 2512 | }; 2513 | ``` 2514 | 2515 | {x: add author to posts} 2516 | We can now display the post authors in the `/home.html` template: 2517 | 2518 | ```html 2519 | 2520 | posted by {{post.author}} | 2521 | 2522 | ``` 2523 | 2524 | We only want to show the add post and add comment forms if the user is logged 2525 | in. To do this, our controllers need to be aware of the authentication state. 2526 | 2527 | {x: expose isloggedin post comments} 2528 | We'll want to inject `auth` into *both* `MainCtrl` and `PostsCtrl`, and expose 2529 | the `isLoggedIn` method to `$scope` 2530 | 2531 | ```javascript 2532 | $scope.isLoggedIn = auth.isLoggedIn; 2533 | ``` 2534 | 2535 | {x: hide post form} 2536 | Now we're able to only show the post form to authenticated users: 2537 | 2538 | ```html 2539 |
2542 | ``` 2543 | 2544 | {x: show signup msg post} 2545 | And display a message to users who aren't signed in 2546 | 2547 | ```html 2548 |
2549 |

You need to Log In or Register before you can add a post.

2550 |
2551 | ``` 2552 | 2553 | {x: hide comment form} 2554 | Finally, hide the comment form for unauthenticated users: 2555 | 2556 | ```html 2557 | 2560 | ``` 2561 | 2562 | {x: show auth msg comments} 2563 | And prompt them to log in or sign up instead 2564 | 2565 | ```html 2566 |
2567 |

You need to Log In or Register before you can comment.

2568 |
2569 | ``` 2570 | 2571 | We now have an application with authentication! 2572 | 2573 | # Where To Go Next 2574 | 2575 | Throughout this tutorial we've seen what a basic MEAN app might look like by 2576 | creating a basic Angular application with a persistent Node+Express backend. 2577 | By now, you should hopefully have an understanding of how these technologies 2578 | interact as well as the ability to make modifications and add new features. If 2579 | you're looking to sharpen your skills through practice, here are some 2580 | suggestions on modifications you can add to Flapper News: 2581 | 2582 | - feature downvote: Implement a 'downvoting' feature 2583 | - feature vote once: Only allow authenticated users to vote once. 2584 | - feature number of comments: Display the number of comments next to each post 2585 | on the main page 2586 | - feature hide new comments box: use `ng-hide` to hide the 'new comment' and 2587 | 'new post' input box until a user clicks a button to see the field 2588 | - feature specify name when commenting: Create an 'Author' field so people can 2589 | specify their name when commenting 2590 | 2591 | We sincerely hope this tutorial has been helpful. If you have any questions, 2592 | comments, or feedback, tweet us at [@GoThinkster](https://twitter.com/gothinkster). 2593 | You can also follow me at [@IAmMattGreen](https://twitter.com/IAmMattGreen) for 2594 | more updates about MEAN, AngularJS and more! 2595 | --------------------------------------------------------------------------------