Hi everyone! As for my second blog, I wanted to go through the step by step process of how we created a log in and a log out feature to our simple app project. I'm going to try my best to keep it concise!
Step 1 - Creating a user
In order to be able to log in you need to create a user model. In our User model we added username as one of the columns.
rails g resource User username
Here I am generating a User resource with a column username and because I didn't provide a data type for username, the default data type is a string. This provides me the User MVC(Model, Views, Controller), the migration, and the routes.
#app/config/routes.rb
resources :users
Since the only goal is to 'register' a user we just need to render a form and submit the inputted data to the database. The controller-action pair users#new
will display the form and once the submit button is clicked a post request will be sent to users#create
which will create the user object and save it to the database for us. Because we're only using those routes for this example let's specify our routes real quick to:
#config/routes.rb
resources :users, only:[:new, :create]
Display a form and submit data
Our UsersController should look like this:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
And we're going to use form_for:
#app/views/users/new.html.erb
<h1>Sign up</h1>
<%= form_for @user do |f|%>
<%= f.label :username%>
<%= f.text_field :username%>
<%= f.submit%>
<% end %>
The inputted data in the text field will then be posted to the users#create
action. Our create action will look like this:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
...
def create
@user = User.new(username:params[:user][:username])
@user.save
redirect_to user_path(@user)
end
You might ask "Why do we have to call User.new
and @user.save
instead of just using User.create?
" A quick explanation to this is because you don't want bad data to be stored in your database. An example of a bad data is a duplicate of email used or even a blank email. It would cause a lot of bugs down the line. A quick fix to this is adding validation to your User
model.
#app/models/user.rb
class User < ApplicationRecord
validates :username, presence: true, uniqueness: true
...
end
@user.save
will check those validations first to see if username
is not blank(presence) and if username
is not already in the database(uniqueness). Our create action will now look like this:
#app/controllers/users_controller.rb
def create
@user = User.new(username:params[:user][:username])
if @user.save
redirect_to @user
else
render :new
end
end
If it passes all validations, it will then be saved to the database and it will redirect us to the @user
showpage otherwise we are rendering back the same form.
Step 2 - Create a Sessions_Controller
Through the sessions controller is where we're going to have our login page, login our current user, and log out our user.
rails g controller Sessions
Then we're going to specify our routes
#config/routes.rb
get '/login', to: 'sessions#new', as: 'login'
post '/sessions', to: 'sessions#create', as: 'sessions'
delete '/sessions', to: 'sessions#destroy'
sessions#new
will display our log in form:
#app/views/sessions/new.html.erb
<h1>Login</h1>
<%=form_tag (sessions_path) do %>
<%=text_field_tag :email %>
<%=submit_tag %>
<% end %>
We are using the form tag because this form is not based on an active record object. In order for this form to know where to submit the data, we supply the (sessions_path)
and once the submit button is clicked, this form will send a post
request to the action controller sessions#create
.
Going to our sessions controller we will define our #create action:
#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:username])
session[:user_id] = user.id
redirect_to user_path(user)
end
def destroy
session.clear
redirect_to login_path
end
end
In our create
action we are going to grab the user object from the db using the :username
provided in the login. We will then make a key in the session
called :user_id
and assign the user.id
as the value. Now that we have this, we can technically say that if there is a session[:user_id]
then someone must be logged in and if it is not present then they must be logged out. That is what the destroy method is doing here. Creating a link_to that calls that action#destroy
on the sessions controller will clear the session
and therefore logging out the user and redirecting back to the login_path.
<%=link_to 'Log Out', sessions_path, method: :delete%>
Step 3 - Use the Application_Controller
Since the rest of the controllers inherit from the application controller
, we will use this to our advantage to define methods so that all the other controllers can share the same functions.
Here we will define the methods:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def logged_in?
session[:user_id]
end
def require_login
if !logged_in?
redirect_to login_path
end
def current_user
@current_user ||= User.find(session[:user_id]) if
session[:user_id]
end
The first method, logged_in?
is the method we call to check if the user is logged in.
The second method, require_login
is the method we use to basically make certain controller actions inaccessible if a user is not logged in.
The last method, current_user
enables us to have access to the specific user object.
The code is written in way so that the first time it is called @current_user
is nil, it will then go to the or (||) condition and look into the database, grab that user, and assign that user to itself. The next time it is called @current_user
is present and will return the found user the first time it was called.
We are then going to declare some of these methods as helper methods so that these functions are accessible to all views as well.
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user
helper_method :logged_in?
before_action :require_login
Remembering that all other controllers inherit from the application controller, declaring a before_action :require_login
, also means this before_action applies to all other controllers as well. It means in order for a user to be able to do anything that involves with any of the controllers, a user MUST be logged in.
If you want certain actions to still be accessible you can explicitly do so by going to that specific controller and use skip_before_action:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
skip_before_action :require_login, only:[:new, :create]
In this case because our users#new
and users#create
are responsible for signing up an account, we want them to be accessible without being logged in.
Step 4 - Adding Passwords
When storing passwords in the database, you want them to be encrypted so for any chance your database gets hacked, the hacker will not have access to the real passwords. This is when the has_secure_password
come in!
In order to use this you need to add gem 'bcrypt'
(usually in your gemfile already, just need to uncomment it) and this will also require a column in your user model called password_digest
which will store the encrypted password. has_secure_password
will allow us to write the password and encrypt it before storing it to password_digest.
With that said we create a new migration to add password_digest
column to the Users
table. Once that's all migrated, we need to declare it in the User model
#app/models.user.rb
class User < ApplicationRecord
has_secure_password
...
end
We then need to update our sign up form:
#app/views/users/new.html.erb
<h1>Sign up</h1>
<%= form_for @user do |f|%>
<%= f.label :user%>
<%= f.text_field :user%>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit%>
<% end %>
Then were going to update the users#create
by:
- Adding the password(even though we called it
password_digest
in ourusers
we could still refer to it aspassword
) - and also logging them in after they created their account
session[:user_id] = @user.id
#app/controllers/users_controller.rb
def create
@user = User.new(username:params[:user][:username])
@user.password = params[:user][:password]
if @user.save
session[:user_id] = @user.id
redirect_to @user
else
render :new
end
end
Now we're going to update the our log in form:
#app/views/sessions/new.html.erb
<h1>Login</h1>
<%=form_tag (sessions_path) do %>
<%=text_field_tag :user %>
<%=text_field_tag :password %>
<%=submit_tag %>
<% end %>
has_secure_password
also give us the method .authenticate
which takes in a string and checks to see if the encrypted password matches the string passed in. If the string matches it will return the user
object otherwise it will return false. The string comes from the inputted data when a user fills in the login form displayed bysessions#new
which will then send a post
request to sessions#create
.
Lastly we will update the sessions#create
:
#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(username: params[:username])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to user_path(user)
else
render 'login_path'
end
end
def destroy
session.clear
redirect_to login_path
end
end
Now our create action is not only checking if the user exists in the db. If it exists then it authenticates the password and if it returns a user object we log them in, otherwise we send them back to the login page.
Conclusion
Hopefully this helps you set up your log in and log out features to your app. I'm sure there are tons of other ways to do it, but for now this is what my newbie brain can offer! If you have other suggestions please reach out to me, I'd love to learn!
Top comments (0)