User Profile

To update a user profile we need the edit and update action. Let's beginning with writing tests for the edit action:

# spec/app/controller/users_controller_spec.rb

...

describe "GET /users/:id/edit" do
  let(:user) { build(:user) }
  let(:user_second) { build(:user) }

  it "redirects if user is not signed in" do
    get "/users/-1/edit", {}, { 'rack.session' => { current_user: nil}}
    expect(last_response).to be_redirect
    expect(last_response.header['Location']).to include('/login')
  end

  it "redirects if user is signed in and tries to call a different user" do
    expect(User).to receive(:find_by_id).and_return(user, user_second)
    get "/users/2/edit"
    expect(last_response).to be_redirect
    expect(last_response.header['Location']).to include('/login')
  end

  it "render the view for editing a user" do
    expect(User).to receive(:find_by_id).and_return(user, user, user)
    get "/users/#{user.id}/edit", {}, { 'rack.session' => { current_user: user } }
    expect(last_response).to be_ok
    expect(last_response.body).to include('Edit your profile')
  end
end

The fist interesting part above is the and_return(user, user_second) call. This is the way to return different return values when a method is called several times - the number of arguments is the number of the functions call. The second thing is that we check the last_response.header and last_response.body. A typical Rack::MockResponse looks like the following:

=> #<Rack::MockResponse:0xabd6f00
 @block=nil,
 @body=[],
 @body_string=nil,
 @chunked=false,
 @errors="",
 @header=
  {"Content-Type"=>"text/html;charset=utf-8",
   "Location"=>"http://example.org/login",
   "Content-Length"=>"0",
   "X-XSS-Protection"=>"1; mode=block",
   "X-Content-Type-Options"=>"nosniff",
   "X-Frame-Options"=>"SAMEORIGIN",
   "Set-Cookie"=>
    "rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTk3ZjI...; path=/; HttpOnly"},
 @length=0,
 @original_headers=
  {"Content-Type"=>"text/html;charset=utf-8",
   "Location"=>"http://example.org/login",
   "Content-Length"=>"0",
   "X-XSS-Protection"=>"1; mode=block",
   "X-Content-Type-Options"=>"nosniff",
   "X-Frame-Options"=>"SAMEORIGIN",
   "Set-Cookie"=>
    "rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTk3ZjI...; path=/; HttpOnly"},
 @status=302,
 @writer=
  #<Proc:0xabd6d0c@/home/wm/.rvm/gems/ruby-2.2.1/
  # gems/rack-1.5.5/lib/rack/response.rb:27 (lambda)>>

Let's look at the implementation:

# app/controllers/users.rb

JobVacancy::App.controllers :users do
  before :edit, :update  do
    redirect('/login') unless signed_in?
    @user = User.find_by_id(params[:id])
    redirect('/login') unless current_user?(@user)
  end
  ...

  get :edit, :map => '/users/:id/edit' do
    @user = User.find_by_id(params[:id])
    render 'edit'
  end
end

We don't want that everybody can edit the profile for other users. Before we are going to call these actions we set a before route filter. They are evaluated before each requests within the context of the requests and it is possible to define variables, change the response and request. For the get :edit action we are using namespaced route aliases. They have the advantage that you can refer to them with the url_for method - you can always reference to them and don't have change the actual string for the method.

Let's write the tests for the update action:

# spec/app/controllers/users_controller_spec.rb

...
describe "PUT /users/:id" do
  let(:user) { build(:user) }
  let(:user_second) { build(:user) }

  it "redirects if user is not signed in", :current do
    put "/users/1", {}, { 'rack.session' => { current_user: nil}}
    expect(last_response).to be_redirect
    expect(last_response.header['Location']).to include('/login')
  end

  it "redirects if user is signed in and tries to call a different user" do
    expect(User).to receive(:find_by_id).and_return(user, user_second)
    put "/users/1"
    expect(last_response).to be_redirect
    expect(last_response.header['Location']).to include('/login')
  end

  it "redirects to /edit if user has valid account changes" do
    expect(User).to receive(:find_by_id).and_return(user, user, user)
    put "/users/1"
    expect(last_response).to be_redirect
    expect(last_response.header['Location']).to include('/edit')
  end

  it "redirects to /edit if user has valid account changes" do
    expect(User).to receive(:find_by_id).and_return(user, user, user)
    put "/users/1"
    expect(last_response).to be_redirect
    expect(last_response.body).to eq 'You have updated your profile.'
    expect(last_response.header['Location']).to include('/edit')
  end

  it "redirects to /edit if user has not valid account changes" do
    user.password = 'real'
    user.password_confirmation = 'fake'
    expect(User).to receive(:find_by_id).and_return(user, user, user)
    put "/users/1"
    expect(last_response).to be_redirect
    expect(last_response.body).to eq 'Your profile was not updated.'
    expect(last_response.header['Location']).to include('/edit')
  end
end
...

And the implementation:

# app/controllers/users.rb

put :update, :map => '/users/:id' do
  @user = User.find_by_id(params[:id])

  route = url(:users, :edit, :id => @user.id)
  if @user.update_attributes(params[:user])
    redirect route, flash[:notice] = "You have updated your profile."
  else
    redirect route, flash[:error] = "Your profile was not updated."
  end
end

Please note that the update_attributes method is making a valid? method before the changes are saved.

Making this test pass took me a while. The HTTP specification only understands GET and POST in the <form> method attribute. How can we solve this? We need to use a hidden form with the put method:

<%# app/views/users/edit.erb %>

<h2>Edit your profile</h2>

<% form_for @user, url(:users, :update, :id => @user.id), method: :put do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <%= error_message_on @user, :name, :class => "text-error",
    :prepend => "The name " %>

  <%= f.label :email %>
  <%= f.text_field :email %>
  <%= error_message_on @user, :email, :class => "text-error",
    :prepend => "The email " %>

  <%= f.label :password %>
  <%= f.password_field :password %>
  <%= error_message_on @user, :password, :class => "text-error",
    :prepend => "The password "%>

  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation %>
  <%= error_message_on @user, :password_confirmation, :class => "text-error" %>

  <p>
    <%= f.submit "Save changes", class: "btn btn-large btn-primary" %>
  </p>
<% end %>

You can specify the HTTP methods with method: <action>. So method: :put will be translated into:

<input type="hidden" value="put" name="_method"</input>

Authorization

\label{sec:authorization}

The controller actions are ready and we used many method from the session_helper.rb. Before we add now the action for signup and registration to the view, it's time to test the helper. A normal helper does look like the following:

# app/helpers/page_helper.rb

JobVacancy::App.helpers do
end

This syntax is a shortcut for:

helpers = Module.new do
end

JobVacancy::App.helpers helpers

The helpers are an anonymous module and its hard to reference something that is anonymous. The solution is to make the module explicit. This is something I learned from Florian in his comment on GitHub. Let's transform the spec_helper.rb into this new form:

# app/helpers/sessions_helper.rb

module SessionsHelper
  def current_user=(user)
    @current_user = user
  end

  def current_user
    @current_user ||= User.find_by_id(session[:current_user])
  end

  def current_user?(user)
    user == current_user
  end

  def sign_in(user)
    session[:current_user] = user.id
    self.current_user = user
  end

  def sign_out
    session.delete(:current_user)
  end

  def signed_in?
    !current_user.nil?
  end
end

JobVacancy::App.helpers SessionsHelper

Padrino isn't requiring helper to be tested automatically. Since we are planing to be consistence with the folder structure of our app within the tests folder, we need to add all helpers files in app/helpers/*.rb. Let's made all helper files availabe in our specs:

# spec/spec_helper.rb

RACK_ENV = 'test' unless defined?(RACK_ENV)
...
Dir[File.dirname(__FILE__) + '/../app/helpers/**.rb'].each { |file| require file }
...

Here is the outline of the tests:

# spec/app/helpers/sessions_helper_spec.rb

require 'spec_helper'

describe SessionsHelper do
  before do
    class SessionsHelperKlass
      include SessionsHelper
    end

    @session_helper = SessionsHelperKlass.new
  end

  describe "#current_user" do
    xit "returns the current user if user is set"
    xit "returns the current user from session"
  end

  describe "#current_user?" do
    xit "returns true if current user is logged in"
    xit "returns false if user is not logged in"
  end

  describe "#sign_in" do
    xit "sets the current user to the signed in user"
  end

  describe "#sign_out" do
    xit "clears the current_user from the session"
  end

  describe "#signed_in?" do
    xit "returns false if user is not logged in"
    xit "returns true if user is logged in"
  end
end

The before do block contains the SessionsHelperKlass class which includes the SessionsHelper So the instance variable @session_helper can use any methods defined in session_helper.rb and the descibe blocks now contains the names of the method which is tested. I'm not testing the current_user=(user) because it is a setter method.

What we need to do now for our test is to to mock a request and set the user id of some of our test user in the session hash. To create a new session we will use Rack::Test::Session and mock the last_request method call:

# spec/app/helpers/sessions_helper_spec.rb

require 'spec_helper'

describe SessionsHelper do
  ...
  describe "#current_user" do
    it "returns the current user if user is set" do
      user = User.new
      @session_helper.current_user = user
      expect(User).to receive(:find_by_id).never
      expect(@session_helper.current_user).to eq user
    end

    it "returns the current user from session" do
      user = User.first
      browser = Rack::Test::Session.new(JobVacancy::App)
      browser.get '/', {}, 'rack.session' => { :current_user => user.id }
      expect(User).to receive(:find_by_id).and_return(user)
      expect(@session_helper).to receive(:session).and_return(user)
      expect(@session_helper.current_user).to eq user
    end
  end
  ...
end

Instead of writing JobVacancy::App you can also pass app in the line Rack::Test::Session.new(JobVacancy::App). The app is defined in the spec_helper:

# spec/spec_helper.rb
...

# You can use this method to custom specify a Rack app
# you want rack-test to invoke:
#
#   app JobVacancy::App
#   app JobVacancy::App.tap { |a| }
#   app(JobVacancy::App) do
#     set :foo, :bar
#   end
#
def app(app = nil, &blk)
  @app ||= block_given? ? app.instance_eval(&blk) : app
  @app ||= Padrino.application
end

You can write the other tests as an exercise on your own. In case you have problems with writing them, please check the spec on GitHub.

Finally, we need to provider the edit link in the header navigation:

# app/views/application.erb

<nav id="navigation">
  ...
  <% if signed_in? %>
    <div class="span2">
      <%= link_to 'Logout', url(:sessions, :destroy) %>
    </div>
    <div class="span2">
      <%= link_to 'Edit Profile', url(:users, :edit,
        :id => session[:current_user]) %>
      <%= link_to 'Edit Profile', url(:users, :edit, :id => session[:current_user]) %>
    </div>
  <% else %>
    <div class="span3">
      <%= link_to 'Login', url(:sessions, :new) %>
    </div>
  <% end %>
  ...
</nav>

Remember Me Function

\label{sec:remember_me_funcion}

We are currently using the sign_in method from the session helper to login a user. But this is only valid for a session. What we need is something permanent. Cookies are the perfect choice for this. We could use the user_id from the user as a unique token, but this can be changed too easily. Creating an unique long secure hash is more secure.

Let's create and run the migration for the authentity token:

$ padrino-gen migration AddAuthentityTokenToUsers authentity_token:string
     apply  orms/activerecord
    create  db/migrate/006_add_authentity_token_to_users.rb
$ padrino rake ar:migrate
=> Executing Rake ar:migrate ...
  DEBUG -   (0.1ms)  SELECT "schema_migrations"."version"
    FROM "schema_migrations" ...
   INFO -  Migrating to AddAuthentityTokenFieldToUsers (6)
  DEBUG -   (0.0ms)  select sqlite_version(*)
  DEBUG -   (0.0ms)  begin transaction
==  AddAuthentityTokenFieldToUsers: migrating =================================
-- change_table(:users)
  DEBUG -   (0.3ms)  ALTER TABLE "users" ADD "authentity_token" varchar(255)
   -> 0.0050s
==  AddAuthentityTokenFieldToUsers: migrated (0.0051s) ========================

  DEBUG -   (0.1ms)  INSERT INTO "schema_migrations" ("version") VALUES ('7')
  DEBUG -   (10.0ms)  commit transaction
  DEBUG -   (0.1ms)  SELECT "schema_migrations"."version"
    FROM "schema_migrations"

\begin{aside} \heading{Cookies}

HTTP is a stateless protocol and a cookie is a way to save information sent from a website and store them in the browser. Each time the user visits the site again, the browser sends the information back to server and notifies the server about the identity of the user. A cookies can consists of the following components: name, value, expiry date, path (scope of the cookie), domain (valid for which domain), needs the cookie be used for a secure connection or if (or not) the cookie can be accessed by other.ways (like JavaScript to steal the cookie). \end{aside}

A way to create random strings in Ruby is to use the SecureRandom class. By using the before_create callback, we create a token for each registered userregistered_user_note.

registered_user_note. If you are in a situation where you already have a bunch of users and you now decide to create hashs for them, you have to create a migration script and migrate the existing user base.
# models/user.rb

class User < ActiveRecord::Base
  ...
  before_create :generate_authentity_token

  private
  def generate_authentity_token
    require 'securerandom'
    self.authentity_token = SecureRandom.base64(64)
    SecureRandom
  end
end

To test the private callback, we can use the send method to create our generate_authentity_token callback:

# spec/models/user_spec.rb

require 'spec_helper'


RSpec.describe "User Model" do
  ...
  describe "#generate_authentity_token" do
    let(:user_confirmation) { build(:user) }

    it 'generates the authentity_token before user is saved' do
      expect(user).to receive(:save).and_return(true)
      user.send(:generate_authentity_token)
      user.save
      expect(user.authentity_token).not_to be_empty
    end
  end
end

Next it's time to create the checkbox on the login page with help of the check_box_tag:

<%# views/sessions/new.erb %>

<h1>Login</h1>

  ...
  <label class="checkbox">
    <%= check_box_tag :remember_me, :val %> Remember me
  </label>

If the user click on the Remember me checkbox, it's time for our session controller to create a cookie:

# app/controllers/sessions.rb
JobVacancy::App.controllers :sessions do
  ...


  post :create do
    @user = User.find_by_email(params[:email])

    if @user && @user.confirmation && @user.password == params[:password]
      if (params[:remember_me] == "true")
        require 'securerandom'
        token = SecureRandom.hex
        @user.authentity_token = token
        thirty_days_in_seconds = 30*24*60*60
        response.set_cookie('permanent_cookie',
                            :value => { :domain => 'jobvacancy.de',
                                        :path => '/'} ,
                                        :max_age => "#{thirty_days_in_seconds}")
        @user.save
      end

      flash[:notice] = "You have successfully logged in!"
      sign_in(@user)
      redirect '/'
    else
      render 'new', :locals => { :error => true }
    end
  end
  ...
end

First, we create a secure random hex value and assign to the authentity_token attribute of the user. We then use the set_cookie method to generate a cookie which is valid for thirty days.

When you login the next time into the application, click the remember me you checkboxw. Stop and start the application again, you will be logged in automatically for the next thirty days.

Figure 2-2. Start page of the app

If you want to see the cookie in your browser, you can install Web Developer extension for Mozilla Firefox and open the View cookie information pane in the Cookies tab. The specs for the post :create action:

# spec/app/controllers/sessions_controller_spec.rb

describe "POST :create" do
  let(:user) { build(:user)}
  let(:params) { attributes_for(:user)}

  it "stays on login page if user is not found" do
    expect(User).to receive(:find_by_email) {false}
    post 'sessions/create'
    expect(last_response).to be_ok
  end

  it "stays on login page if user is not confirmed" do
    user.confirmation = false
    expect(User).to receive(:find_by_email) {user}
    post 'sessions/create'
    expect(last_response).to be_ok
  end

  it "stays on login page if user has wrong password" do
    user.confirmation = true
    user.password = 'correct'
    expect(User).to receive(:find_by_email) {user}
    post 'sessions/create', {:password => 'wrong'}
    expect(last_response).to be_ok
  end

  it "redirects to home for confirmed user and correct password" do
    user.confirmation = true
    user.password = 'correct'
    expect(User).to receive(:find_by_email) {user}
    post 'sessions/create', {:password => 'correct', :remember_me => false}
    expect(last_response).to be_redirect
  end

  it "redirects if user is correct and has remember_me" do
    token = 'real'
    user = double("User")
    expect(user).to receive(:id) {1}
    expect(user).to receive(:password) {'real'}
    expect(user).to receive(:confirmation) {true}
    expect(user).to receive(:authentity_token=) {token}
    expect(user).to receive(:save)
    expect(User).to receive(:find_by_email) {user}
    expect(SecureRandom).to receive(:hex).at_least(:once) {token}

    post 'sessions/create', {:password => 'real', :remember_me => true}
    expect(last_response).to be_redirect
    cookie = last_response['Set-Cookie']
    expect(cookie).to include('permanent_cookie')
    expect(cookie).to include('path=/')
    expect(cookie).to include('domain%3D%3E%22jobvacancy.de')
    expect(cookie).to include('max-age=2592000')
  end
end

Reset Password

This chapter will be a combination of all the things we have learned so far. Until now you should be familiar with the commands of creating new controllers, edit views as well as create migration and new mail templates. Because repetition is good, we will go through the whole procedure again.

We are going to create a new controller for the password forget feature:

$ padrino-gen controller PasswordForget get:new post:create get:edit post:update
    create  app/controllers/password_forget.rb
    create  app/views/password_forget
     apply  tests/rspec
    create  spec/app/controllers/password_forget_controller_spec.rb
    create  app/helpers/password_forget_helper.rb
     apply  tests/rspec
    create  spec/app/helpers/password_forget_helper_spec.rb

We have to create a GET :action and map it to /password_forget:

# app/controllers/password_forget.rb

JobVacancy::App.controllers :password_forget do
  get :new, :map => 'password_forget'  do
    render 'new'
  end
  ...
end

In the new action’s view we’ll create the form with the form_tag without a model that allows a user to enter their email address and request that their password is reset. The form looks like this:

<%# app/views/password_forget/new.erb %>

<h2>Forgot Password</h2>


<% form_tag url(:password_forget, :create) do %>
  <%= label_tag :email %>
  <%= text_field_tag :email %>

  <p>
    <%= submit_tag "Reset password", :class => "btn btn-primary" %>
  </p>
<% end %>

The idea behind the POST :create action is the following: We need to process the password-forget email and email instructions on how to reset password to the supplied email address. We don't validate if the email address is correct, we don't want to have malicious user to check if a user exists or not.

# app/controllers/password_forget.rb

JobVacancy::App.controllers :password_forget do
  ...
  post :create do
    @user = User.find_by_email(params[:email])

    if @user
      @user.save_forget_password_token
      link = "http://localhost:3000" + url(:password_forget, :edit,
        :token => @user.password_reset_token)
      deliver(:password_reset, :password_reset_email, @user, link)
    end

    render 'success'
  end
  ...
end

The save_forget_password_token method will generate a security token for the given user. The token should only valid for around one hour, we need to save the password_reset_sent_date as well as the password_reset_token. Before going on we need to add token and the method in the User model:

$ padrino-gen migration AddPasswordResetTokenToUsers
  password_reset_token:string password_reset_sent_date:datetime
       apply  orms/activerecord
      create  db/migrate/007_add_password_reset_for_users.rb
$ padrino rake ar:migrate
=> Executing Rake ar:migrate ...
   INFO -  Migrating to CreateUsers (1)
   INFO -  Migrating to CreateJobOffers (2)
   INFO -  Migrating to AddUserIdToJobOffers (3)
   INFO -  Migrating to AddRegistrationFieldsToUsers (4)
   INFO -  Migrating to AddConfirmationCodeAndConfirmationToUsers (5)
   INFO -  Migrating to AddAuthentityTokenFieldToUsers (6)
   INFO -  Migrating to AddPasswordResetTokenToUsers (7)
  DEBUG -   (0.0ms)  select sqlite_version(*)
  DEBUG -   (0.1ms)  SELECT "schema_migrations"."version"
    FROM "schema_migrations"

Due to this point it is not enough have only this migration, we need to set default value and say that the password_reset_token as well as the password_reset_sent_date can be null:

# db/migrate/007_add_password_reset_token_to_users.rb

class AddPasswordResetTokenToUsers < ActiveRecord::Migration
  def self.up
    change_table :users do |t|
      t.string :password_reset_token, default: 0, null: true
      t.datetime :password_reset_sent_date, default: 0, null: true
    end
  end

  def self.down
    change_table :users do |t|
      t.remove :password_reset_token
      t.remove :password_reset_sent_date
    end
  end
end

The stage for the save_forget_password_token method is set: It takes our generate_authentity_token method from chapter ~\ref{sec:remember_me_funcion} and use the Time.now method to set the send date from the password reset function:

# app/models/user.rb

class User < ActiveRecord::Base
  ...
  def save_forget_password_token
    self.password_reset_token = generate_authentity_token
    self.password_reset_sent_date = Time.now
    self.save
  end
  ...
end

But the token that gets generated can be of the form B4+KPW145dG9qjfsBuDhuNLVCG/32etcnEo+j5eAFz4M6/i98KRaZGIJ1K77n/HqePEbD2KFdI3ldIcbiOoazQ==. The slash (/) and plus (+) is bad for routing. We already used the normalize_confirmation_code from section ~\ref{sec:controller_method_and_action_for_password_confirmation} to remove such backslashes, and we could easily the same method again. But we want to apply DRY. For this purpose we will create a lib folder, which acts as a place for sharing code which can be used by models, controllers, and other components. Inside the directory we create a normalize_token.rb file:

# lib/StringNormalizer/normalize_token.rb

module StringNormalizer
  def normalize(token)
    token.gsub("/", "")
  end
end

And the test the StringNormalizer is similar to the helper tests in section ~\ref{sec:authorization}:

# spec/lib/normalize_token_spec.rb

require 'spec_helper'

RSpec.describe "StringNormalizer" do
  before do
    class StringNormalizerClass
      include StringNormalizer
    end

    @string_normalizer = StringNormalizerClass.new
  end

  it "replaces slashes and + signs in strings" do
    token = 'B4+KPW145dG9qjfsBuDhuNLVCG/32etcnEo+j5eAFz4M6/i98KRaZGIJ1K77n/H
      qePEbD2KFdI3ldIcbiOoazQ=='
    expected_token = 'B4KPW145dG9qjfsBuDhuNLVCG32etcnEoj5eAFz4M6i98KRaZGIJ1K
      77nHqePEbD2KFdI3ldIcbiOoazQ=='
    expect(@string_normalizer.normalize(token)).to eq expected_token
  end
end

And use the method in the users_observer.rb

# app/models/user_observer.rb

class UserObserver < ActiveRecord::Observer
  include StringNormalizer
  ...

  private

  def set_confirmation_code(user)
    require 'bcrypt'
    salt = BCrypt::Engine.generate_salt
    confirmation_code = BCrypt::Engine.hash_secret(user.password, salt)
    normalize(confirmation_code)
  end
end

as well as in user.rb:

# app/models/user.rb

class User < ActiveRecord::Base
  include StringNormalizer
  ...

  private
  def generate_authentity_token
    require 'securerandom'
    self.authentity_token = normalize(SecureRandom.base64(64))
  end
end

We are now ready to create our mailer:

$ padrino-gen mailer PasswordReset password_forget_email
    create  app/mailers/password_reset.rb
    create  app/views/mailers/password_forget

In the mailer we take the user to create the password reset token:

# app/mailers/password_reset.rb

JobVacancy::App.mailer :password_reset do
  email :password_reset_email do |user, link|
    from "admin@job-vacancy.de"
    subject "Password reset"
    to user.email
    locals :name => user.name, :link => link
    render 'password_reset/password_reset_email'
  end
end

The email template contains information and the link for reseting the password:

# app/views/mailers/password_reset/password_reset_email.plain.erb

Hi <%= name %>,

to reset your password, click on the link below

<%= link %>

If you do not requested a new password, you can ignore this message.

Your Job Vacancy!

When the email was send we need to write the edit action to handle the link action. The action will take the reset token and check if it still valid. If not, it will redirect us to the forget password route.

# app/controllers/password_forget.rb

JobVacancy::App.controllers :password_forget do
  ...
  get :edit, :map => "/password-reset/:token/edit" do
    @user = User.find_by_password_reset_token(params[:token])

    if @user && Time.now < @user.password_reset_sent_date + 60 * 60
      render 'edit'
    elsif @user && Time.now >= @user.password_reset_sent_date + 60 * 60
      @user.update_attributes({:password_reset_token => 0,
        :password_reset_sent_date => 0})
      redirect url(:sessions, :new), flash[:error] = 'Password reset token has expired.'
    else
      redirect url(:password_forget, :new)
    end
  end
end

The line with @user.password_reset_sent_date + (60 * 60) is not very readable. Rails has the functionality of using words like 1.hour.ago with the help of ActiveSupport module. Since Padrino is not using ActiveSupport, we use the Timerizer gem:

# Gemfile
gem 'timerizer', '0.1.4'

And now we can use the new syntax for describing time in a better way:

# app/controllers/password_forget.rb

require 'timerizer'

JobVacancy::App.controllers :password_forget do
  ...
  get :edit, :map => "/password-reset/:token/edit" do
    @user = User.find_by_password_reset_token(params[:token])

    if @user && Time.now < 1.hour.after(@user.password_reset_sent_date)
      ...
    elsif @user && Time.now >= 1.hour.after(@user.password_reset_sent_date)
      ...
    else
      ...
    end
  end
end

In the associated edit view we use the form_for and pass in the user model to have access to all validations. Besides we are using then method: hash to say which method we want to use for the action:

# app/views/password_forget/edit.erb

<h2>Reset Password</h2>

<% form_for @user, "/password-reset/#{@user.password_reset_token}",
  method: :post do |f| %>
  <%= f.label :password %>
  <%= f.password_field :password %>
  <%= error_message_on @user, :password, :class => "text-error",
    :prepend => "The password "%>

  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation %>
  <%= error_message_on @user, :password_confirmation, :class => "text-error" %>

  <p>
    <%= f.submit "Reset password", :class => "btn btn-primary" %>
  </p>
<% end %>

Next we add the update action. First it checks, if the user can be found by the passed token and then we use the password field validations from the user model:

# app/controllers/password_forget.rb

JobVacancy::App.controllers :password_forget do
  ...

  post :update, :map => "password-reset/:token" do
    @user = User.find_by_password_reset_token(params[:token])

    if @user && @user.update_attributes(params[:user])
      @user.update_attributes({:password_reset_token => 0,
        :password_reset_sent_date => 0})
      redirect url(:sessions, :new), flash[:notice] = "Password has been reseted.
        Please login with your new password."
    else
      render 'edit'
    end
  end
end

Since the routes are now defined, we can add the password forget link on the login page:

# app/views/sessions/new.erb

...
<label class="checkbox">
  <%= check_box_tag :remember_me %> Remember me
</label>

<p>
  <%= link_to 'forget password?', url(:password_forget, :new) %>
</p>
...
  • Box: Calling mailers in Padrino and where to put them