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!
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]
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.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
#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.
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
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
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
: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%>
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#create are responsible for signing up an account, we want them to be accessible without being logged in.
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
#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
- Adding the password(even though we called it
userswe could still refer to it as
- 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 by
sessions#new which will then send a
post request to
Lastly we will update the
#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.
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!