Featured image of post Twitor #2: Create User

Twitor #2: Create User

Let's create Twito!, a Tweet Clone App by using Ruby on Rails !

This project made based on Progate

Create User

The user features:

  1. Show list of users: handled by action users#index
  2. Show user details: handled by action users#show
  3. Sign up: handled by action users#new and users#create
  4. Edit account: handled by action users#edit and users#update
  5. Log in: handled by action users#login_form and users#login
  6. Log out: handled by action users#logout

Create Model and Table

We can create the User model and the users table with the command

1
rails g model User name:string email:string

We’ll add two pieces of data, name and email. The column_name: data_type can be used multiple times in a line to create multiple columns at the same time. string is used for short text line name and email

Adding Column

We will learn how to add column in existing table

Adding image_name

To add a column image_name, we need a migration file that can be generated by running rails g migration file_name. A migration file is created with a timestamp prepended to the file name. The file name can be anything, but it’s better to use a descriptive name, like add_image_name_to_users

Prior to migrate, we need to write the change method.

1
2
3
4
5
class AddImageNameToUsers < ActiveRecord::Migrate[5.0]
  def change
    add_column :users, :image_name, :string
  end
end

Adding Password

Create migration file with the name add_password_to_users using rails g migration like code below

1
rails g migration add_password_to_users

Change the content of the migration file as shown below

1
2
3
4
5
class AddPasswordToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :password, :string
  end
end

and run rails db:migrate

Adding Validation

Let’s add a validation to check for a “duplicate email” so that new users can’t register with an email already stored in the database. You can validate the uniquiness with uniqueness: true. In models/user.rb, put

1
2
3
4
5
class User < ApplicationRecord
  validates :name, {presence: true}
  validates :email, {presence: true, uniqueness: true}
  validates :password, {presence: true}
end

Adding Route

In the config/routes.rb add these routes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
  get "login" => "users#login_form"
  post "login" => "users#login"
  post "logout" => "users#logout"

  post "users/:id/update" => "users#update"
  get "users/:id/edit" => "users#edit"
  post "users/create" => "users#create"
  get "signup" => "users#new"
  get "users/index" => "users#index"
  get "users/:id" => "users#show"
...

It seems like the two routes with /login are the same, but get and post are treated as different routes. The link_to method looks for the get routing, while the form_tag method looks for the post routing.

We use post in logout because we need it to modify the value of the session variable.

Adding Action

The code in the ApplicationController can be used in all controllers. Like the codes below, if we define the :set_current_user method and set it as a before_action, @current_user will be define in all the actions of the controller

1
2
3
4
5
6
7
8
class ApplicationController < ActionController::Base
  before_action :set_current_user
  
  def set_current_user
    @current_user = User.find_by(id: session[:user_id])
  end

end

We create a method named authenticate_user in the Application controller to redirect users to the Login page. Also, We define a method named forbid_login_user in the Application controller. This method redirects the user to the Posts page if the user is loged in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ApplicationController < ActionController::Base
  before_action :set_current_user
  
  def set_current_user
    @current_user = User.find_by(id: session[:user_id])
  end
  
  def authenticate_user
    if @current_user == nil
      flash[:notice] = "You must be logged in"
      redirect_to("/login")
    end
  end
  
  def forbid_login_user
    if @current_user
      flash[:notice] = "You are already logged in"
      redirect_to("/posts/index")
    end
  end

end

Using rails g controller command, create a new controller named users with the index action for the Users page.

In the user_controller.rb, define action index, show, new, create, edit, and update

Let’s look at how to apply before_action to only certain actions of certain controllers because we don’t want to apply this to all the actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class UsersController < ApplicationController
  before_action :authenticate_user, {only: [:index, :show, :edit, :update]}
  before_action :forbid_login_user, {only: [:new, :create, :login_form, :login]}
  # Set the ensure_correct_user method as a before_action
  before_action :ensure_correct_user, {only: [:edit, :update]}
  
  def index
    @users = User.all
  end
  
  def show
    @user = User.find_by(id: params[:id])
  end
  
  def new
    @user = User.new
  end
  
  def create
    @user = User.new(
      name: params[:name],
      email: params[:email],
      image_name: "default_user.jpg",
      password: params[:password]
    )
    if @user.save
      session[:user_id] = @user.id
      flash[:notice] = "You have signed up successfully"
      redirect_to("/users/#{@user.id}")
    else
      render("users/new")
    end
  end
  
  def edit
    @user = User.find_by(id: params[:id])
  end
  
  def update
    @user = User.find_by(id: params[:id])
    @user.name = params[:name]
    @user.email = params[:email]
    
    if params[:image]
      @user.image_name = "#{@user.id}.jpg"
      image = params[:image]
      File.binwrite("public/user_images/#{@user.image_name}", image.read)
    end
    
    if @user.save
      flash[:notice] = "Your account has been updated successfully"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end
  
  def login_form
  end
  
  def login
    @user = User.find_by(email: params[:email], password: params[:password])
    if @user
      session[:user_id] = @user.id
      flash[:notice] = "You have logged in successfully"
      redirect_to("/posts/index")
    else
      @error_message = "Invalid email/password combination"
      @email = params[:email]
      @password = params[:password]
      render("users/login_form")
    end
  end
  
  def logout
    session[:user_id] = nil
    flash[:notice] = "You have logged out successfully"
    redirect_to("/login")
  end
  
  # Define the ensure_correct_user method
  def ensure_correct_user
    if @current_user.id != params[:id].to_i 
      flash[:notice] = "Unauthorized access"
      redirect_to("/posts/index")
    end
  end
  
end

To keep login information, We use a special variable known as session. The value assigned to session is saved in the browser. Rails can use this value to identify the logged in user.

In order to log out, we should make the value of session[:user_id] empty. We can do this by assigning nil to session[:user_id]

We define ensure_correct_user method to verify that the logged in user and the user being edited are the same. And we

For authenticate user, we shall add before_action also in post_controller.rb

1
2
3
class PostsController < ApplicationController
  before_action :authenticate_user
  ...

Adding views

in folder app/views/users create five files

  • To view all users, index.html.erb
  • To show detail users, show.html.erb
  • To edit user, edit.html.erb
  • To view login form, login_form.html.erb
  • To view signup form, new.html.erb

Index Page

This index page is to view all users,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<div class="main users-index">
  <div class="container">
    <h1 class="users-heading">All Users</h1>
    <% @users.each do |user| %>
      <div class="users-index-item">
        <div class="user-left">
          <img src="<%= "/user_images/#{user.image_name}" %>">
        </div>
        <div class="user-right">
          <%= link_to(user.name, "/users/#{user.id}") %>
        </div>
      </div>
    <% end %>
  </div>
</div>

Detail Page

This detail page is to show detail user

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="main user-show">
  <div class="container">
    <div class="user">
      <img src="<%= "/user_images/#{@user.image_name}" %>">
      <h2><%= @user.name %></h2>
      <p><%= @user.email %></p>
      <% if @user.id == @current_user.id %>
        <%= link_to("Edit", "/users/#{@user.id}/edit") %>
      <% end %>
    </div>
  </div>
</div>

Edit Form

This edit form is to show a form to change user data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="main users-edit">
  <div class="container">
    <div class="form-heading">Edit Account</div>
    <div class="form users-form">
      <div class="form-body">
        <% @user.errors.full_messages.each do |message| %>
          <div class="form-error">
            <%= message %>
          </div>
        <% end %>
      
        <%= form_tag("/users/#{@user.id}/update", {multipart: true}) do %>
          <p>Name</p>
          <input name="name" value="<%= @user.name %>">
          <p>Image</p>
          <input name="image" type="file">
          <p>Email</p>
          <input name="email" value="<%= @user.email %>">
          <input type="submit" value="Save">
        <% end %>
      </div>
    </div>
  </div>
</div>

Login Form

Create login_form.html.erb and put these lines

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="main users-new">
  <div class="container">
    <div class="form-heading">Log in</div>
    <div class="form users-form">
      <div class="form-body">
        <% if @error_message %>
          <div class="form-error">
            <%= @error_message %>
          </div>
        <% end %>
        <%= form_tag("/login") do %>
          <p>Email</p>
          <input name="email" value="<%= @email %>">
          <p>Password</p>
          <input type="password" name="password" value="<%= @password %>">
          <input type="submit" value="Log in">
        <% end %>
      </div>
    </div>
  </div>
</div>

Signup Form

For sign up, we create file views/users/new.html.erb and put these lines

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="main users-new">
  <div class="container">
    <div class="form-heading">Sign up</div>
    <div class="form users-form">
      <div class="form-body">
        <% @user.errors.full_messages.each do |message| %>
          <div class="form-error">
            <%= message %>
          </div>
        <% end %>
        
        <%= form_tag("/users/create") do %>
          <p>Name</p>
          <input name="name" value="<%= @user.name %>">
          <p>Email</p>
          <input name="email" value="<%= @user.email %>">
          <p>Password</p>
          <input type="password" name="password" value="<%= @user.password %>">
          <input type="submit" value="Sign up">
        <% end %>
      </div>
    </div>
  </div>
</div>

Sending File

Two steps need to take care, the front-end side to accept user input and the backend for handling the file.

Setting up The Form

Add form field with input tag, image name and file type.

1
2
3
4
5
6
<%= form_tag("/users/#{@user.id}/update", {multipart: true}) do %>
  ...
  <p>Image</p>
      <input name="image" type="file">
  ...
<% end %>

As we see the code above, We put {multipart: true} to the form_tag because sending an image is a special case. We need to know the detail later. For now just remember that {multipart: true} is necessary when sending image.

Creating File

To handle files with Ruby code, you can use the File class which is provided by Ruby by default. The write method of the File class creates a file. You can use it like:
File.write(file_location, file_content)

Let’s exercise to create file using Ruby in Rails console by running

1
File.write("public/sample.txt", "Hello World")

In the update action, we will save the image in the public folder, and save the name of the file in the database.

creating file

To save the image name and put file on the public directory, create function such:

1
2
3
4
5
6
7
8
9
def update
  ...
    if params[:image]
      @user.image_name = "#{@user.id}.jpg"
      image = params[:image]
      File.binwrite("public/user_images/#{@user.image_name}", image.read)
    end
  ...
end

We’ll use File.binwrite instead of File.write because image data is a special type of text. Also, the image data can be retrieved by using read method for the variable image as shown on the snippet above.

Login

Create Login Page

We’ll create following items for the Login page:

  1. The route
  2. The action
  3. The view

In the config/route.rb file, add

1
2
3
4
Rails.application.routes.draw do
  get "login" => "users#login_form"
  ...
end

In the app/controllers/users_controller.rb add action called login_form

1
2
3
4
5
class UsersController < ApplicationController
  # Add a new action called "login_form"
  def login_form
  end
end

In the app/controllers/view, create a file named login_form.html.erb and paste the HTML below in the newly created file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<div class="main users-new">
  <div class="container">
    <div class="form-heading">Log in</div>
    <div class="form users-form">
      <div class="form-body">
        <p>Email</p>
        <input>
        <p>Password</p>
        <input type="password">
        <input type="submit" value="Log in">
      </div>
    </div>
  </div>
</div>

Lastly, let’s create a link to Login page.

1
2
3
4
5
...
  <li>
    <%= link_to("Log in", "/login") %>
  </li>
...

Adding Login functionality

Add new route for the login action

1
2
3
4
5
Rails.application.routes.draw do
  get "login" => "users#login_form"
  post "login" => "users#login"
  ...
end

Now we’ll create action for login in the User controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class UsersController < ApplicationController
  ...
  def login
    @user = User.find_by(email: params[:email], password: params[:password])
    if @user
      session[:user_id] = @user.id 
      flash[:notice] = "You have logged in successfully"
      redirect_to("/posts/index")
    else
      @error_message = "Invalid email/password combination"
      @email = params[:email]
      @password = params[:password]
      render("users/login_form")
    end
  end
  ...
end

The code above tries to find User data by given email and password, and store in @user variable. If user find, user id will be stored in session, notification will be appear and page will be redirected to /posts/index.

If user not found, there will be error message and the parameters will be stored in default value since it would be convenient for the user to get the email and password they inputted when the form is redisplayed.

The render method is for redisplay login_form.

In the login_form.html.erb, we add functinality to send user input and set default value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
...
<div class="form users-form">
  <div class="form-body">
    <% if @error_message %>
      <div class="form-error">
        <%= @error_message %>
      </div>
    <% end %>
    <%= form_tag("/login") do %>
      <p>Email</p>
      <input name="email" value="<%= @email %>">
      <p>Password</p>
      <input type="password" name="password" value="<%= @password %>">
      <input type="submit" value="Log in">
    <% end %>
  </div>
</div>
...

To check your session feature, put this lines to show user_id on header

1
2
3
4
5
6
7
8
9
  <ul class="header-menus">
        <% if session[:user_id] %>
          <li>
            Current user ID: 
            <%= session[:user_id] %>
          </li>
        <% end %>
  ...
  </ul>

Logout

In this section we’ll learn hot to use before_action

Restricted User

Display the following in the header when a user is logged in:

  • The current user’s ID
  • Post(/posts/index)
  • New Post(/posts/index)
  • Users(/users/index)
  • Log out(/logout)

When there’s no logged in user, the following links should be shown in the header:

  • About(/about)
  • Sign up(/signup)
  • Log in(/login)

These requirements can be achieved by putting this logic in header tag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<ul class="header-menus">
  <% if @current_user %>
    <li>
      <%= link_to(@current_user.name, "/users/#{@current_user.id}") %>
    </li>
    <li>
      <%= link_to("Posts", "/posts/index") %>
    </li>
    <li>
      <%= link_to("New post", "/posts/new") %>
    </li>
    <li>
      <%= link_to("Users", "/users/index") %>
    </li>
    <li>
      <%= link_to("Log out", "/logout", {method: :post}) %>
    </li>
  <% else %>
    <li>
      <%= link_to("About", "/about") %>
    </li>
    <li>
      <%= link_to("Sign up", "/signup") %>
    </li>
    <li>
      <%= link_to("Log in", "/login") %>
    </li>
  <% end %>
</ul>

Post Restriction

In the Application Controller we already put

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ApplicationController < ActionController::Base
  before_action :set_current_user
  
  def set_current_user
    @current_user = User.find_by(id: session[:user_id])
  end

  def authenticate_user
    if @current_user == nil
      flash[:notice] = "You must be logged in"
      redirect_to("/login")
    end
  end
...

This authentication_user method limits access the controller action. To use it on all actions in the the Post Controoler, We put before_action with this method.

1
2
3
4
class PostsController < ApplicationController
  before_action :authenticate_user
  ...
end

User Restriction

In the Application Controller, we also put method forbid_login_user

1
2
3
4
5
6
7
8
...
  def forbid_login_user
    if @current_user
      flash[:notice] = "You are already logged in"
      redirect_to("/posts/index")
    end
  end
...

This method prevent already logged user to access login or signup page. To use this method, put this on User Controller after before_action and use only argument to put actions related to login and signup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class UsersController < ApplicationController
  before_action :authenticate_user, {only: [:index, :show, :edit, :update]}
  before_action :forbid_login_user, {only: [:new, :create, :login_form, :login]}
  before_action :ensure_correct_user, {only: [:edit, :update]}
  ...
    def ensure_correct_user
    if @current_user.id != params[:id].to_i 
      flash[:notice] = "Unauthorized access"
      redirect_to("/posts/index")
    end
  ...
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy