Right now, your Job Tracker app is available to use by anyone. Wouldn't it be nice if we could make it so that only logged-in users could access the site?
Today, you'll attempt to implement authentication in your Job Tracker app. This challenge is not meant to give you step-by-step instructions for implementing authentication. Instead, you'll be prompted with places to start, things to cosider, and resources to reference, but you'll want to consider the implications of your choices along the way.
Before you start, read this article which talks about the difference between authentication and authorization. We'll focus most of this challenge on authentication, although you will see bits of authorization.
The purpose of the challenge is to figure out the moving pieces of authentication, not to TDD the authentication process. Therefore, don't worry about writing tests.
As you go through this challenge, push to try to get as many pieces figured out. However, don't freak out if you get stuck or don't get through all of the challenges. We want you to try it out yourself before diving into how hand-rolled authentication is typically set up in a Rails app tomorrow. As a good baseline, aim to complete/attempt Parts I and II, and dive into Parts III, IV, V, and VI if time permits.
If we want users to be able to log in, we need to store those users in the database. Go ahead and create a users table and a User model. You'll also want to consider what credentials you want the user to use when logging in. For example: username or email? How will you store the password? Here's some documentation you may want to look at: ActiveModel::SecurePassword. Take note of the fact that the documentation states "This mechanism requires you to have a password_digest attribute.". This means you must have a password_digest column in your users table.
Try it out:
Create some users from the console. Insert a few users into your database with the following lines (change the name/email/username portion if necessary to match your model, but do not change the fact that the password is entered in a password: 'something' format even though the column in the database is called password_digest).
User.create(username: 'miked', password: 'mypassword')
User.create(username: 'rachelw', password: 'pa$$word')
User.create(username: 'laurenf', password: 'secretpass')Now that you have some users in your development database, let's see if we can log them in. Keep in mind that we are not creating functionality where a user creates a new account -- the following situation and steps assume that the user already has an account in the database and just wants to get logged into the app.
Let's consider the process before we dive into code: We'll want the user to navigate to a URL, see a form on that URL that asks for a username + password (or email + password), click "login", and be redirected to somewhere. Where they are redirected doesn't really matter as long as there is some kind of confirmation that they are, in fact, logged in. (For example: "Welcome, Lauren" listed in the upper right-hand corner of every page they visit or something like that).
Now that we understand the flow, let's implement it.
- Add a route in your
routes.rbfile that will be for the sign in form. Hand-write the route; do not useresources :somethingfor this. Things to consider as you write this route: What controller will it route to -- one that already exists or one that is new? What action within that controller? - Create the controller and action for the route you just made.
- Make a view with a form. You may consider using a form_tag instead of a
form_for(though it is totally possible to doform_foras well). - Create a route for this form to post to. Consider the controller and action for this route.
- Add the action to the controller and determine what should happen in that action. Stuck? Google "bcrypt authenticate". Also consider how you can keep track of the "state" of a user being logged in. Since HTTP is stateless, we'll need a way to pass info from the server to client to server to client... about who is logged in. Think about your sessions/cookies/flashes lesson.
- Choose somewhere to redirect the user once they are logged in. If they do not successfully log in (like wrong password, etc.), re-render the login form.
- Add some sort of confirmation to the
application.html.erbview to show that the user is logged in. This could be as simple as having the logged-in user's name appear at the top.
Try it out! Hopefully you've done this along the way, but if you have not, now would be a great time to start up the server and see if you can get any of your users logged in. Since we don't have a way to log users out of our app yet, it might be a good idea to use an incognito window so that cookies aren't shared between windows (although they will still be shared between tabs of the same window).
Also, don't worry about the user being able to access all parts of the site -- that's an authorization issue that we aren't concerned with right now.
Right now, we can tell that a user is logged in because their name appears at the top of each page they visit. We want a user to be able to click on something that says "logout" and have their name no longer appear on any of the pages they visit.
Now create a link or button on the application.html.erb that says "Logout". You'll need a route that this button hits, and you'll need to determine the controller and action this route will trigger.
Think about the mechanism used to keep track of the fact that a user is logged in. Inside the controller action that is triggered when a user clicks logout, do something to that mechanism so that the logged in user is "forgotten".
Ensure that the user's name no longer appears at the top of any page they visit.
In our job tracker app, a user should only be able to see the jobs that they have personally entered. A job belongs to a user, and a user can have many jobs. Being able to see, edit, and delete only the jobs that belong to the logged-in user is an authorization concern.
Figure out what needs to happen at both the database layer and model layer to set up this association. You may need to update existing data in your database and/or modify your seeds file so that you don't see errors.
Depending on how you have your routes set up, you may have '/jobs'/ or '/companies/:id/jobs'. For whichever one(s) you have, make it so that the user only seeds the jobs that belong to them.
Ensure that a user cannot see the show or edit pages for a job manually entered in the url that doesn't belong to them. For example, if job with ID of 5 was created by someone else, I should not be able to navigage to '/jobs/5' or '/companies/:id/jobs/5' and see that job.
Finally, make sure that a job cannot be deleted by someone who is not the owner of that job.
Let's back up a bit. We had user accounts in our database because we had created them through the console. But your users should not have to ask you to create an account for them before they log in. Let's create a way for users to sign up for an account through your web interface.
Here's the flow: A user goes to some URL like 'localhost:3000/signup' or something. They see a form where they enter their required details (name, username/email, password, anything else you're storing about your users, etc.). They click a button that says "sign up" and then they are taken to their profile page (probably users#show) where they see some sort of confirmation that they are logged in ("Welcome, Rachel!" at the top of the page, etc.).
Figure out what routes, controllers/actions, and views you need in order to make this happen.
Right now, all of our users are of one type: default. Can we carry information about the type of user someone is in our database? For example, we may have a user with a role type of "admin" and another user with a role type of "default".
Figure out how to protect the creating, editing, and deleting of categories so that only a user who has a role of "admin" can do that (or even access these pages).