You have been provided with MEAN Stack Application with user authentication fully baked in. Visitors to your site can sign up, login, and logout. Now it's time to add authorization so that only certain visitors (i.e. logged in users) can create
, update
, and destroy
blog posts.
Authentication is like the bouncer at a bar checking your drivers license. Does your face match the one in the picture (i.e. your username)? Do you know your birthdate? (i.e. your password)? Good. You're in.
Authorization is like the "Class c" on your license which gives you permission to drive cars. Only some motorists are allowed to ride 'motorcycles' and drive 'semi-trucks'.
-
Clone this repo.
-
From inside the project directoy, run
npm install
to install the required node modules. -
In one Terminal window, run
mongod
, and in another, runnodemon
. -
Run the seed tasks (
node seed.js
) to create a default user and some posts. -
Navigate to
localhost:9000
in the browser. You should see an empty page and an angry red error message in the Chrome console.
You should see
Error: ENOENT, no such file or directory '.env'
in your terminal
- To fix it, add a "dot env" file, called
.env
, to the root directory. Add this line to the file:
TOKEN_SECRET=yoursupersecrettoken
This is the secret your server will use to encode the JWT token for each user. Note that this file is listed in .gitignore
because you never want to expose your secret tokens on github.
- Before hooking up the front-end, test your server routes via Postman:
- Send a
GET
request to/api/me
. You should see the message: "Please make sure your request has an Authorization header." - Send a
POST
request to/auth/signup
with a testemail
andpassword
. You should see a token that was generated by the server.Make sure to use
x-www-form-urlencoded
and send your data in thebody
of the request). - Send a
POST
request to/auth/login
with theemail
andpassword
you just used to create a new user. You should again see a token that was generated by the server. - Bonus: If you add your JWT token in the
Authorization
header like so:"Bearer YOUR-UNIQUE-TOKEN"
you will be permitted/authorized to create/update/destroy blog posts.
You have been supplied with a posts#index
controller that displays the current blog posts.
Your goal is to: Build out angular templates, controllers, and routes for new
, edit
, and show
, with easy navigation between these pages.
To begin, let's create a new angular controller, template, and route for creating a single blog post.
- When a user visits
/posts/new
on the client...- they should see an empty form
- with a field for
title
(useng-model="postsNewCtrl.post.title"
) - and a field for
content
(useng-model="postsNewCtrl.post.content"
) - and a button to "save".
- with a field for
- they should see an empty form
- When a user clicks "save" (having filled out the form)...
- they should trigger a
on-submit="postsNewCtrl.create()"
method on theform
(!) - and the
create()
method should make an $http request toPOST /api/posts
using the data invm.post
. - and their post should be saved to the database
- and the server should respond with the newly created post and
_id
(e.g. 12345) - and the user should be redirect to e.g.
/posts/12345
.
- they should trigger a
Javascript Solution (Click Here)
```js PostsNewController.$inject = ["$location", "$http"]; // minification protection function PostsNewController ($location, $http) { var vm = this; vm.create = create; vm.post = {}; // form data////
function create() { $http .post('/api/posts', vm.post) .then(onCreateSuccess, onCreateError);
function onCreateSuccess(response){
$location.path('/posts/' + response.data._id)
}
function onCreateError(response){
console.error("Failed to create post", response);
}
}; }
</details>
<details>
<summary>HTML Solution (Click Here)</summary>
```html
<form ng-submit="postsNewCtrl.create()">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" ng-model="postsNewCtrl.post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Content" ng-model="postsNewCtrl.post.content"></textarea>
</div>
<input type="submit" value="Create Post" class="btn btn-block btn-info">
</form>
- When a user visits
/posts/12345
on the client...- they should make an
$http
request toGET /api/posts/12345
... - and they should see the
title
andcontent
of post 12345...- using e.g.
{{postsShowCtrl.title}}
- using e.g.
- and a button to
edit
- and be redirected to
/posts/12345/edit
- and be redirected to
- and a button to
delete
- and should trigger a
on-click="postsShowCtrl.destroy()"
method on thebutton
(!) - and the
destroy()
methods should make an$http
request toDELETE /api/posts/12345
- and on success, be redirected to
/
or/posts
(posts index). - BONUS: and see the message "Successfully deleted post" below the navbar.
- and should trigger a
- they should make an
Javascript Solution (Click Here)
```js PostsShowController.$inject = ["$location", "$http", "$routeParams"]; // minification protection function PostsShowController ($location, $http, $routeParams) { var vm = this; vm.post = {};var id = $routeParams.id; get(); // fetch one post (show)
////
function get() { $http .get('/api/posts/' + id) .then(onGetSuccess, onGetError);
function onGetSuccess(response){
vm.post = response.data;
}
function onGetError(response){
console.error("Failed to get post", response);
$location.path("/");
}
}; }
</details>
<details>
<summary>HTML Solution (Click Here)</summary>
```html
<h2>{{postsShowCtrl.post.title}}</h2>
<p>{{postsShowCtrl.post.content}}</p>
<a class="btn btn-primary" ng-href="/posts/{{postsShowCtrl.post._id}}/edit">Edit Post</a>
- When a user visits
/posts/12345/edit
on the client...- they should make an
$http
request toGET /api/posts/12345
... - and see a pre-populated form for post 12345...
- with a field for
title
(useng-model="postsEditCtrl.post.title"
) - and a field for
content
(useng-model="postsEditCtrl.post.content"
)
- with a field for
- and a button to
Discard Changes
- and be redirected to
/posts/12345
- BONUS: and a pop-up, confirmation dialog that says "Are you sure you want to discard your changes?"
- and be redirected to
- and a button to
Save Changes
- that should trigger a
on-submit="postsEditCtrl.update()"
on theform
(!) - and the
update()
method should make an$http
request to the server, using the data invm.post
- and on success, be redirected to
/posts/12345
(show).
- that should trigger a
- and a button to
Delete Post
- that should trigger a
on-click="postsEditCtrl.destroy()"
on thebutton
- and the
destroy()
method should make an$http
delete request to the server - and on success, redirect the user to
/
or/posts
(index) - BONUS: and a pop-up, confirmation dialog that says "Are you sure you want to delete this post?"
- that should trigger a
- they should make an
Javascript Solution (Click Here)
```js PostsEditController.$inject = ["$location", "$http", "$routeParams"]; // minification protection function PostsEditController ($location, $http, $routeParams) { var vm = this; vm.update = update; vm.destroy = destroy; vm.post = {}; // form datavar id = $routeParams.id; get(); // fetch one post (show)
////
function update() { $http .put('/api/posts/' + id, vm.post) .then(onUpdateSuccess, onUpdateError);
function onUpdateSuccess(response){
$location.path("/posts/" + id);
}
function onUpdateError(response){
console.error("Failed to update post", response);
}
}
function destroy() { $http .delete('/api/posts/' + id) .then(onDeleteSuccess, onDeleteError);
function onDeleteSuccess(response){
$location.path("/");
}
function onDeleteError(response){
console.error("Failed to delete post", response);
}
}
function get() { $http .get('/api/posts/' + id) .then(onGetSuccess, onGetError);
function onGetSuccess(response){
vm.post = response.data;
}
function onGetError(response){
console.error("Failed to get post", response);
$location.path("/");
}
}; }
</details>
<details>
<summary>HTML Solution (Click Here)</summary>
```html
<div class="pull-right col-xl-4">
<a class="btn btn-warning col-xl-2" ng-href="/">Discard Changes</a>
<a class="btn btn-danger col-xl-2" ng-click="postsEditCtrl.destroy()">Delete Post</a>
<hr>
</div>
<form ng-submit="postsEditCtrl.update()">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" ng-model="postsEditCtrl.post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Content" ng-model="postsEditCtrl.post.content"></textarea>
</div>
<input type="submit" value="Update Post" class="btn btn-block btn-info">
</form>
We are using Satellizer (an angular library), JWT Simple (an npm library for creating and parsing JWT tokens), and our own custom authentication
middleware (see middleware/auth.js
).
Before adding authorization*, please familiarize yourself with middleware/auth.js
. Try to answer the following questions:
- What is a JWT token?
- Where does it live?
- What does it mean to be "logged in"?
- How does the server "log in" a user?
- How does the client know a user is "logged in"?
- What does it mean to "log out"?
- How does the client "log out"?
Mega Hint: Authorization is already in place for client & server routes like
login
. How is it done?
Your goal is to:
- Protect sensitive endpoints on the server (like
post
,put
,delete
).- Only a logged in user should be able to hit API endpoints for creating, updating, and destroying blog posts (on the server).
- Protect sensitive endpoints on the client (like
new
,edit
, and the ability todelete
).- Only a logged in user should be able to see the forms and buttons for creating, updating, and destroying blog posts (on the client).
- Stretch Goal: A user should only be able to edit or delete their own blog posts (on the client). They should not see options to edit or delete on posts that do not belong to them.
You will want to use auth.ensureAuthenticated
(see middleware/auth.js
) in the route to find the current user (i.e. so that you can use req.user
to access the current user).
Protected Server Routes Hint (Click Here)
```js // server.js app.post('/api/posts', auth.ensureAuthenticated, postsCtrl.create); ```- Only logged in users should be able to see buttons for
new
,edit
, anddelete
.ng-show="main.currentUser.isLoggedIn()"
- BONUS: Only the owner of the blog post should see options to
edit
anddelete
the post.- Given that you have access to a
post
object, and thecurrentUser
object, inside your controllers, is there a way to determine ownership?
- Given that you have access to a
Ownership Hint (Click Here)
```js someCtrl.post.user._id === main.currentUser.user_id; // watch out for undefined! ```- Only a logged in user should be able to visit pages for
new
andedit
.- You will want to use
loginRequired
(seepublic/scripts/routes.js
) in the route to ensure that only a logged in user can go tonew
andedit
pages.
- You will want to use
Protected Routes Hint (Click Here)
```js // public/scripts/routes.js.when('/posts/new', { templateUrl: 'templates/posts/new.html', controller: 'PostsNewController', controllerAs: 'postsNewCtrl', resolve: { loginRequired: loginRequired // this is the important part } })
</details>
## Bonuses
1. Refactor to use a PostService (or a `Post` resource using `ngResource`), and inject your service into each of your post controllers.
1. Validate blog-posts! Ensure a user can't submit an empty title or content. (Use both backend AND frontend validations).
1. On the user profile page, the "Joined" date isn't formatted very nicely. Use Angular's built-in <a href="https://docs.angularjs.org/api/ng/filter/date" target="_blank">date filter</a> to display the date in this format: `January 25, 2016`.