This project made based on Progate
Create User
The user features:
- Show list of users: handled by action
users#index
- Show user details: handled by action
users#show
- Sign up: handled by action
users#new
and users#create
- Edit account: handled by action
users#edit
and users#update
- Log in: handled by action
users#login_form
and users#login
- 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>
|
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>
|
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>
|
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.
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.
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:
- The route
- The action
- 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
...
|