Supporting Guest Users in Rails 5 App

Starter Todo List App

Create a new Rails 5 app.

rails new clist

Create a user model.

rails g model user email handle password_digest

Create a task model.

rails g model task name complete:boolean user:references 

Open the migration file and make the default value for complete field as false.

class CreateTasks < ActiveRecord::Migration[5.0]
  def change
    create_table :tasks do |t|
      t.string :name
      t.boolean :complete, default: false
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

Run the migration.

rails db:migrate

Add sample data in seeds.rb:

u = User.create! handle: "bbunny", email: "bugs@example.com", password: "secret"

u.tasks.create! name: "Meet Mr. Miyagi", complete: true
u.tasks.create! name: "Paint the fence", complete: true
u.tasks.create! name: "Wax the car"
u.tasks.create! name: "Sand the deck"

Populate the database:

rails db:seed

You will get the error:

ActiveModel::UnknownAttributeError: unknown attribute 'password' for User.

Add the has_secure_password to user model.

class User < ApplicationRecord
  has_secure_password
end

Uncomment the bcrypt gem in Gemfile.

gem 'bcrypt', '~> 3.1.7'

Run bundle. Populate the databse.

rails db:seed

You will get the error:

NoMethodError: undefined method `tasks' for #<User:0x007fa08>

Add the ActiveRecord declaration to user model:

has_many :tasks

Add the ActiveRecord declaration to task model:

belongs_to :user

Define the routes in routes.rb:

Rails.application.routes.draw do
  resources :users
  resources :tasks

  root to: 'tasks#index'
end

Create a users controller.

rails g controller users new

The implementation is straightforward:

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(allowed_params)
    if @user.save
      session[:user_id] = @user.id
      redirect_to root_url
    else
      render :new
    end
  end

  private

  def allowed_params
    params.require(:user).permit(:email, :handle, :password, :password_confirmation)
  end
end

Add current_user method to application controller.

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end

  helper_method :current_user
end

Create a tasks controller.

rails g controller tasks index

Implement the index, create, update and destroy.

class TasksController < ApplicationController
  def index
    if current_user
      @incomplete_tasks = current_user.tasks.where(complete: false)
      @complete_tasks = current_user.tasks.where(complete: true)
    else
      @incomplete_tasks = []
      @complete_tasks = []
    end
  end

  def create
    @task = current_user.tasks.create!(allowed_params)
    redirect_to tasks_url
  end

  def update
    @task = current_user.tasks.find(params[:id])
    @task.update_attributes!(allowed_params)
    respond_to do |f|
      f.html { redirect_to tasks_url }
      f.js
    end
  end

  def destroy
    @task = current_user.tasks.find(params[:id])
    @task.destroy
    respond_to do |f|
      f.html { redirect_to tasks_url }
      f.js
    end
  end

  private

  def allowed_params
    params.require(:task).permit(:name, :complete)
  end
end

Update the routes.rb:

Rails.application.routes.draw do
  get 'signup', to: 'users#new', as: 'signup'
  get 'login', to: 'sessions#new', as: 'login'
  get 'logout', to: 'sessions#destroy', as: 'logout'

  resources :users
  resources :sessions
  resources :tasks

  root to: 'tasks#index'
end

Generate the sessions controller to login and logout the user.

rails g controller sessions new 

Implement the create and destroy actions.

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(handle: params[:handle])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_url, notice: 'Logged in!'
    else
      flash.now.alert = 'Name or password is invalid'
      render :new
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: 'Logged out!'
  end
end

The sessions/new.html.erb looks like this:

<h1>Log In</h1>
<%= form_tag sessions_path do %>
  <div class="field">
    <%= label_tag :username %><br />
    <%= text_field_tag :handle, params[:handle] %>
  </div>
  <div class="field">
    <%= label_tag :password %><br />
    <%= password_field_tag :password %>
  </div>
  <div class="actions"><%= submit_tag "Log In" %></div>
<% end %>

The task partial in tasks/_task.html.erb looks like this:

<%= form_for task, remote: true do |f| %>
  <%= f.check_box :complete %>
  <%= f.label :complete, task.name %>
  <%= link_to "(remove)", task, method: :delete, data: {confirm: "Are you sure?"}, remote: true %>
<% end %>

You can now use the basic task management functionality, login, logout and signup.

Guest Record

In app/controllers/users_controller.rb, change the create action to add:

@user = params[:user] ? User.new(allowed_params) : User.new_guest
current_user.move_to(@user) if current_user && current_user.guest?

It will now look like this:

def create
  @user = params[:user] ? User.new(allowed_params) : User.new_guest
  if @user.save
    current_user.move_to(@user) if current_user && current_user.guest?
    session[:user_id] = @user.id
    redirect_to root_url
  else
    render :new
  end
end

In user model add the validation, guest record creation and moving data from guest to a new user:

class User < ApplicationRecord
  has_many :tasks, dependent: :destroy

  validates_presence_of :handle, :email, :password_digest
  validates_uniqueness_of :handle
  validates_confirmation_of :password

  has_secure_password

  def self.new_guest
    new(email: "#{Time.now}@bogus.com", 
        handle: Time.now,
        password_digest: 'bogus')
  end

  def move_to(user)
    tasks.update_all(user_id: user.id)
  end

  def guest?
    email.include?('bogus.com')
  end
end

The bogus guest records can be cleaned up by running a rake task that will delete old guest records. In app/views/layouts/application.html.erb display the links:

<% if current_user.guest? %>
  <%= link_to 'Become a member', signup_path %>
<% else %>
   Logged in as <strong><%= current_user.handle %></strong>
  <%= link_to "Log Out", logout_path %>
<% end %>

In app/views/tasks/index.html.erb change the code to:

<p><%= button_to "Try it for free!", users_path %></p>

You will now be able to try the app without creating a new account and become a member later. All the data will be transferred to the new account. You can download the source code from clist

Summary

In this article, you learned how to allow a visitor to try your web app without creating an account and how to transfer any data created for the guest user to the new account.


Related Articles