Well, hello there <username>!
Today we are going to start a Rails User Authentication from scratch. This will go step-by-step through everything you need to create basic user registration and login. Basic, people, basic. You can make it pretty later.
$ rails new user-auth --skip-spring --skip-test-unit --database=postgresql
$ cd user-auth
$ git init
Before we commit, let’s open up that Gemfile. Delete all those comments (you can leave bcrypt in there) and add the following:
group :test, :development do
gem 'rspec-rails', '~> 2.14.2'
gem 'capybara', '~> 2.2.0'
end
Personally, I haven’t really made great friends with RSpec 3. I am looking forward to that in the future.
$ bundle install
$ rails g rspec:install
Open up spec_helper
, delete all those comments, and add the following below the other require
:
require 'capybara/rspec'
Time to make our first commit! Feel free to git add .
here (and only here), there is a lot of new files that Rails just made for ya. Check it out in the browser if you want, but it might bore you to tears. Open up a new terminal window, navigate to our little directoy here, and $ rails s
. Then go back to your command line terminal window.
$ git status
$ git add .
$ git commit -m "Initial commit, RSpec, Capybara, PostgreSQL"
Time to get crackin! I’m just going to have you dump all the tests in at once, and you can remove the pending
after each commit. Below is your test, put it in a new folder called features
in a file user_auth_spec.rb
:
require 'spec_helper'
require 'capybara/rspec'
feature 'Homepage' do
scenario 'User can register' do
visit '/'
click_on 'Sign up'
fill_in 'Email', with: 'branwyn@example.com'
fill_in 'Password', with: 'password'
fill_in 'Password confirmation', with: 'password'
click_on 'Sign up'
expect(page).to have_content 'Welcome to the lending library branwyn@example.com'
end
scenario 'User can logout' do
pending
email_address = 'branwyn@example.com'
welcome_message = "Welcome to the lending library #{email_address}"
visit '/'
click_on 'Sign up'
fill_in 'Email', with: email_address
fill_in 'Password', with: 'password'
fill_in 'Password confirmation', with: 'password'
click_on 'Sign up'
expect(page).to have_content(welcome_message)
click_on 'Log out'
expect(page).to_not have_content(welcome_message)
end
scenario 'User can login with registered email and password' do
pending
email_address = 'branwyn@example.com'
password = 'password'
welcome_message = "Welcome to the lending library #{email_address}"
visit '/'
click_on 'Sign up'
fill_in 'Email', with: email_address
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password
click_on 'Sign up'
click_on 'Log out'
click_on 'Log in'
fill_in 'Email', with: email_address
fill_in 'Password', with: password
click_on 'Log in'
expect(page).to have_content(welcome_message)
end
scenario 'User cannot login if their email address does not exist' do
pending
email_address = 'branwyn@example.com'
password = '123456'
visit '/'
click_on 'Log in'
fill_in 'Email', with: email_address
fill_in 'Password', with: password
click_on 'Log in'
expect(page).to have_content 'Email / password is invalid'
end
scenario 'User cannot sign in with an invalid email and/or password' do
pending
email_address = 'branwyn@example.com'
password = '123456'
visit '/'
click_on 'Sign up'
fill_in 'Email', with: email_address
fill_in 'Password', with: password
fill_in 'Password confirmation', with: password
click_on 'Sign up'
click_on 'Log out'
click_on 'Log in'
fill_in 'Email', with: email_address
fill_in 'Password', with: 'password'
click_on 'Log in'
expect(page).to have_content 'Email / password is invalid'
end
end
Let’s run that first test!
$ rspec
Oh, create a database.
$ rake db:create
$ rspec
Looks like we need to “root” our app. Open /config/routes.rb
and delete all the comments. This should be your only route:
root 'welcome#index'
Run RSpec again. Just keep running it after every addition to see what is next.
$ rspec
Now we make add the WelcomeController. Add welcome_controller.rb
to /app/controllers/
with the following:
class WelcomeController < ApplicationController
def index
end
end
Running RSpec again tells us we need a view. Add index.html.erb
to the new file folder /app/views/welcome/
. Run RSpec again, and looks like we need to add a link. We are going to use some rails helpers, so before we actually add that link, add the following routes to our routes.rb
file:
resources :users
Are you curious what you really just did?
$ rake routes
Whoa. That’s a lot of routes right there. Moving on… back to our welcome/index.html.erb
:
<%= link_to 'Sign up', new_user_path %>
Now we need a users_controller.rb
in the same directory as our welcome_controller.rb
. Add the following:
class UsersController < ApplicationController
def new
end
end
And now a view… add new.html.erb
into a new file folder /app/views/users/
. Run RSpec again, and looks like we need to add a form for our new user to fill out. We are going to use some rails helpers again, and this is what it looks like:
<%= form_for @user do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
<%= f.submit 'Sign up' %>
<% end %>
We can’t pass in nil
? What does that even mean? Well, that @user
isn’t getting passed in from our controller… eventually we want that to be a User
object. But for that we need a migration. Go to the terminal, and…
$ rake g migration CreateUsers email:string password_digest:string
Feel free to check out that migration file in the db
folder. We will also need to add a model user.rb
in the /app/models/
folder. You can delete that .keep
file after you add the model file, it’s just holding that directory for you when there are no files within it. Within that model file, we will add the following:
class User < ActiveRecord::Base
has_secure_password
end
Uncomment your bcrypt gem in your Gemfile, bundle and run your migration!
$ bundle
$ rake db:migrate
Now, add the following within your new
method in your UsersController
:
@user = User.new
Yay! We are moving forward. RSpec tells us it is time to actually create this user… add the following below your Users new method:
def create
@user = User.new(allowed_parameters)
if @user.save
session[:current_user_id] = @user.id
redirect_to root_path
else
render :new
end
end
def allowed_parameters
params.require(:user).permit(:email, :password, :password_confirmation)
end
And we need to add something to our welcome/index.html.erb
above our link:
<h1>Welcome to the lending library
<% if @user %>
<%= @user.email %></h1>
<% else %>
</h1>
<%= link_to 'Sign up', new_user_path %>
<% end %>
And we need to pass in a user. Let’s just pass in the last one created for now, we will make that more workable with “sessions” later. Within the Welcome index method, add:
@user = User.last
This is obviously not going to work very well. But we have our first green test, and in our next session will refactor that nicely. For now, let’s check it out in the browser and commit.
$ rspec
$ git status
$ git add -N app/ db/ spec/
$ git add -p
$ git status
$ git commit -m "User can register"
The next step is to remove that pending
from our next test and run RSpec. Looks like we need a “Log out” button… however for logging in and out, we will create a new resource in our routes file:
resources :login
Then add this line to /app/views/welcome/index.html.erb
just below the @user.email
line:
<%= link_to 'Log out', login_path(@user), :method => :delete %>
Now we need a LoginController
. Add login_controller.rb
to /app/controllers/
with the following:
class LoginController < ApplicationController
def destroy
@_current_user = session[:current_user_id] = nil
redirect_to root_path
end
end
Now we eventually want to redirect to our root path. But first, we have to get that logged in thing working for real, not just that User.last
cheater. We want to add “sessions” to our little app. In our application_controller.rb
, we will add the following method:
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
And we want to change our WelcomeController
to pass in the following to the index
:
@user = User.find_by(id: session[:current_user_id])
RSpec says we are looking good! Let’s check it out in the browser and make another commit.
$ rspec
$ git status
$ git add -N app/
$ git add -p
$ git commit -m "User can logout, sessions have been added"
Nice! We are well on our way! Let’s remove another pending and see what RSpec and our test have in store. Looks like we need a new link to “Log in”. Add this above the “Sign up” link in our /app/views/welcome/index.html.erb
file:
<%= link_to 'Log in', new_login_path %>
Next we add the method new
to our LoginController
. This will create a “new” logged in session. Next we need to add the view. We are going to pass in a new user to the view, so go ahead and add that to our method new
in the LoginController
:
def new
@user = User.new
end
Now we will add the login form to the view page (new folder and file) /app/views/login/new.html.erb
:
<%= form_for @user, {url: '/login', :method => 'post'} do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit 'Log in' %>
<% end %>
That first line is a little weird… we are creating a view for @user
but we want it to post to /login
not /user
, so we need to add that hash in there. Now we need to have a create
method in our LoginController
:
def create
if user = User.find_by(:email => params[:user][:email]).try(:authenticate, params[:user][:password])
session[:current_user_id] = user.id
redirect_to root_url
end
end
Yay for green! Check it out in the browser, and let’s commit this baby.
$ rspec
$ git status
$ git add -N app/
$ git add -p
$ git status
$ git commit -m "User can log in"
Let’s un-pend the next test! What does RSpec say? Whoa, what happened? It’s like the login create method just totally broke. Check that LoginController
create
method out… oh! We have a “happy path” but what happens if the person trying to log in enters the wrong information? We will change that method to look like this:
def create
if user = User.find_by(:email => params[:user][:email]).try(:authenticate, params[:user][:password])
session[:current_user_id] = user.id
redirect_to root_url
else
@user = User.new
flash.now[:error] = 'Email / password is invalid'
render :new
end
end
Well, we still aren’t getting that flash message anywhere… where should we put that? Open the /app/code/layouts/application.html.erb
file and add the following just above the <%= yield %>
:
<% flash.each do |name, msg| %>
<%= content_tag :div, msg, class: name %>
<% end %>
RSpec approves! Let’s check it out in the browser and commit.
$ rspec
$ git status
$ git add -p
$ git commit -m "User cannot log in with a unregistered email"
Only one more test to go… let’s un-pend! Oh, wow, looks like our previous changes took care of this scenario too… a user cannot login with the incorrect password. Let’s amend that commit and commit this and the last together. Check it out in the browser to make sure all is good.
$ rspec
$ git status
$ git add -p
$ git commit --amend -m "User cannot log in with unregistered email or incorrect password"
Hooray! We made a super-basic User Authentication from scratch! There is so much more you could add to this project, but now you know the basics.