10 User Sessions - getfretless/elevennote GitHub Wiki
We need to keep track of when users are and aren't authenticated. Let's make a sessions controller with the following actions:
-
new
for the login page -
create
for actually logging in -
destroy
for logging out.
There's no point in showing, listing, or updating sessions, so we'll leave them out of the routes. We'll use a RESTful route for create
only, and define named routes for destroy
and new
.
config/routes.rb
resources :sessions, only: [:create]
delete 'logout' => 'sessions#destroy', as: :logout
get 'login' => 'sessions#new', as: :login
We'll require the DELETE
HTTP verb for logouts, just as Devise does.
Let's create the SessionsController
. Once again, we'll use the welcome
layout, rather than the default application
layout.
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
layout 'landing'
def new
@user = User.new
end
end
Let's make a login form to go with it.
app/views/sessions/new.html.erb
<h3>Sign In</h3>
<%= form_for @user, url: sessions_path do |f| %>
<p>
<%= f.label :username %><br>
<%= f.text_field :username %>
</p>
<p>
<%= f.label :password %><br>
<%= f.password_field :password %>
</p>
<%= f.submit 'Sign In', class: 'btn btn-default' %>
<% end %>
You should now see a lovely form at /login. If you actually try to sign in, however, it will blow up upon trying to render create
.
To implement sessions#create
, we need to check if the user exists and whether the password entered encrypts to match the password_digest
value in our database. We won't make that comparison manually. has_secure_password
adds an instance method to the User
model called authenticate
that handles it for us. Pretty cool, huh?
If the user successfully authenticates, we'll assign the user_id
to a session variable. This is stored in an encrypted cookie, and can be read in on page load to set current_user
. Upon setting the session variable, we'll redirect to the root_url
.
If authentication fails, we'll render the login form, making available a @user
instance variable with just the username
set.
Don't forget to permit the username
and password
fields for mass assignment.
app/controllers/sessions_controller.rb
def create
user = User.find_by username: user_params[:username]
if user.present? && user.authenticate(user_params[:password])
session[:user_id] = user.id
redirect_to root_url, notice: t('session.flash.create.success')
else
@user = User.new username: user_params[:username]
flash.now.alert = t('session.flash.create.failure')
render :new
end
end
private
def user_params
params.require(:user).permit(:username, :password)
end
Add the English text for our session-related flash messages to the locale file.
config/locales/en.yml
en:
hello: "Hello world"
user:
flash:
create:
success: "Thanks for signing up!"
failure: "There was a problem with your registration."
session:
flash:
create:
success: "Welcome!"
failure: "There was a problem logging in with those credentials."
Visit /login and try signing in with the user you added earlier.
It works, but aside from the flash message, there's no indicated that we're logged in. Let's create a current_user
helper method available to all controllers. Define the new method in ApplicationController
.
app/controllers/application_controller.rb
#...
helper_method :current_user
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
Now we need a way to sign out. Our sessions#destroy
method just needs to clear the session variable and redirect to root_url
.
app/controllers/sessions_controller.rb
def destroy
session[:user_id] = nil
redirect_to root_url, notice: t('session.flash.destroy.success')
end
We'll also want to add a flash message to en.yml
.
destroy:
success: "You are now logged out."
When a user is signed in, let's show their name if name
is set. Otherwise, we'll show their username. We'll add a display_name
method to User
to achieve that.
app/models/user.rb
def display_name
name.presence || username
end
Now let's add a logout link to the application
layout. It will need to use the DELETE
HTTP method.
<header>
<div class="well">
ElevenNote
<div class="user-links">
<% if current_user.present? -%>
Signed in as <%= current_user.display_name %>.
<%= link_to 'Logout', logout_path, method: :delete %>
<% else -%>
<%= link_to 'Sign Up', sign_up_path %> or
<%= link_to 'Login', login_path %>
<% end -%>
</div>
</div>
</header>
Let's go back to our sign up page and add a link to the login page.
app/views/users/new.html.erb
<h3>Sign Up for ElevenNote</h3>
<%= form_for @user do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.label :username %><br>
<%= f.text_field :username %>
</p>
<p>
<%= f.label :password %><br>
<%= f.password_field :password %>
</p>
<%= f.submit 'Sign Up', class: 'btn btn-default' %>
<span class="login">Already have an account? <%= link_to 'Sign in', login_path %>.</span>
<% end %>
Let's make a link in the other direction too. Edit the login page (sessions/new
).
app/views/sessions/new.html.erb
<h3>Sign In</h3>
<%= form_for @user, url: sessions_path do |f| %>
<p>
<%= f.label :username %><br>
<%= f.text_field :username %>
</p>
<p>
<%= f.label :password %><br>
<%= f.password_field :password %>
</p>
<%= f.submit 'Sign In', class: 'btn btn-default' %>
<span class="login">Don't have an account? <%= link_to 'Sign up!', sign_up_path %>.</span>
<% end %>
Now that we have current_user
, let's make an application-wide method to redirect unauthenticated users to the login page.
app/controllers/application_controller.rb
def authorize_user
redirect_to login_path, alert: 'Please sign in to view that page.' if current_user.nil?
end
Later we'll use this as a before_action
in our other controllers in order to restrict access.
Everything look good? Commit!
$ git add .
$ git commit -m "Implement authentication."