# Project DoList: User Authentication

This article will describe how to setup user authentication (sign up, sign in, forgot password) using Devise ([https://github.com/heartcombo/devise](https://github.com/heartcombo/devise)).

## Install Devise

To begin, perform the following steps:

1. Add `gem "devise"` to the `Gemfile`
    
2. Run `bundle install`
    
3. Run `bin/rails generate devise:install` to generate a config file
    

To enable emails to be sent from Devise (ex. password reset) perform the following steps:

1. In `config/environments/development.rb` add
    

```ruby
config.action_mailer.default_url_options = { host: 'http://localhost', port: 3000 }
```

2. In `config/application.rb` uncomment line 10: `require "action_mailer/railtie"`
    

## Create the User Model

Let's now create a `User` DB model that will store information about authenticated users.

Run the following commands:

1. `rails generate devise User`
    
2. `bin/rails db:migrate`
    

Note that running those commands adds the following line in `config/routes.rb`: `devise_for :users`. Adding that line creates all of the routes necessary for user authentication.

To view all defined routes visit `http://localhost:3000/rails/info/routes`. The page looks like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721008849032/b44c8410-00b8-4992-9a39-44bd91fa0db5.png align="center")

## Style the Authentication Views

Be sure to restart your local server before moving forward.

Let's add "Sign Up" and "Sign In" buttons to the homepage. Replace `app/views/pages/home.html.erb` with:

```erb
<div class="container mx-auto mt-28 px-5">
  <h1 class="font-bold text-8xl mb-3 tracking-tighter">DoList.</h1>
  <p class="text-gray-800 text-xl tracking-tight">
    A free and open-source Todoist alternative
  </p>

  <div class="mt-10 flex items-center gap-x-6">
    <%= link_to 'Sign Up',
        new_user_registration_path,
        class: 'rounded-md bg-indigo-600 px-3.5 py-2.5 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
    %>

    <%= link_to 'Sign In',
        new_user_session_path,
        class: 'rounded-md bg-white px-3.5 py-2.5 font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50'
    %>
  </div>
</div>
```

The homepage will now look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720999352706/caf7758f-af1a-46ed-810c-a5dedf777e00.png align="center")

You'll notice that if you visit either of those links you will see unstyled forms on each page. So that we can customize each of those pages let's run a command that will copy the templates from the Devise library into our app.

Run the following command: `bin/rails generate devise:views`. You'll now see a new directory: `app/views/devise`. This directory contains all the views for each authentication component (sign up, sign in, forgot password, etc).

Replace the following files with styled markup:

File: `app/views/devise/registrations/new.html.erb`

```erb
<div class="h-full bg-gray-50">
  <div class="flex flex-col justify-center py-12 sm:px-6 lg:px-8">
    <div class="sm:mx-auto sm:w-full sm:max-w-md">
      <% if !flash.empty? %>
        <div class="mb-6">
          <%= render 'layouts/flash_messages' %>
        </div>
      <% end %>

      <%= render "devise/shared/error_messages", resource: resource %>

      <h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
        Create a new account
      </h2>

      <p class="mt-2 text-center text-sm text-gray-600">
        Or
        <%= link_to 'login to your account', new_user_session_path, class: 'font-medium text-indigo-600 hover:text-indigo-500' %>
      </p>
    </div>

    <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
      <div class="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
        <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
          <div class="space-y-6">
            <div>
              <%= f.label :email, class: 'block text-sm font-medium leading-6 text-gray-900' %>
              <div class="mt-2">
                <%= f.email_field :email, required: true, autofocus: false, autocomplete: "email", class: 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6' %>
              </div>
            </div>

            <div>
              <%= f.label :password, class: 'block text-sm font-medium leading-6 text-gray-900' %>
              <div class="mt-2">
                <%= f.password_field :password, required: true, autocomplete: "current-password", class: 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6' %>
              </div>
            </div>

            <div>
              <%= f.label :password_confirmation, class: 'block text-sm font-medium leading-6 text-gray-900' %>
              <div class="mt-2">
                <%= f.password_field :password_confirmation, required: true, autocomplete: "current-password", class: 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6' %>
              </div>
            </div>

            <div>
              <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                Create account
              </button>
            </div>
          </div>
        <% end %>
      </div>
    </div>
  </div>
</div>
```

File: `app/views/devise/sessions/new.html.erb`

```erb
<div class="h-full bg-gray-50">
  <div class="flex flex-col justify-center py-12 sm:px-6 lg:px-8">
    <div class="sm:mx-auto sm:w-full sm:max-w-md">
      <%= render 'layouts/flash_messages' %>
      <%= render "devise/shared/error_messages", resource: resource %>

      <h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
        Sign in to your account
      </h2>

      <% if devise_mapping.registerable? %>
        <p class="mt-2 text-center text-sm text-gray-600">
          Or
          <%= link_to 'create a new account', new_user_registration_path, class: 'font-medium text-indigo-600 hover:text-indigo-500' %>
        </p>
      <% end %>
    </div>

    <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
      <div class="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
        <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
          <div class="space-y-6">
            <div>
              <%= f.label :email, class: 'block text-sm font-medium leading-6 text-gray-900' %>
              <div class="mt-2">
                <%= f.email_field :email, autofocus: false, autocomplete: "email", class: 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6' %>
              </div>
            </div>

            <div>
              <%= f.label :password, class: 'block text-sm font-medium leading-6 text-gray-900' %>
              <div class="mt-2">
                <%= f.password_field :password, autocomplete: "current-password", class: 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6' %>
              </div>
            </div>

            <div class="flex items-center justify-between">
              <div class="flex items-center">
                <%= f.check_box :remember_me, class: 'h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600' %>
                <%= f.label :remember_me, class: 'ml-2 block text-sm text-gray-900' %>
              </div>

              <div class="text-sm">
                <%= link_to 'Forgot your password?', new_user_password_path, class: 'font-medium text-indigo-600 hover:text-indigo-500' %>
              </div>
            </div>

            <div>
              <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                Sign in
              </button>
            </div>
          </div>
        <% end %>
      </div>
    </div>
  </div>
</div>
```

File: `app/views/devise/passwords/new.html.erb`

```erb
<div class="h-full bg-gray-50">
  <div class="flex flex-col justify-center py-12 sm:px-6 lg:px-8">
    <div class="sm:mx-auto sm:w-full sm:max-w-md">
      <% if !flash.empty? %>
        <div class="mb-6">
          <%= render 'layouts/flash_messages' %>
        </div>
      <% end %>

      <h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
        Reset Password
      </h2>
    </div>

    <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
      <div class="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
        <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
          <div class="space-y-6">
            <div>
              <%= f.label :email, class: 'block text-sm font-medium leading-6 text-gray-900' %>
              <div class="mt-2">
                <%= f.email_field :email, required: true, autofocus: false, autocomplete: "email", class: 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6' %>
              </div>
            </div>

            <div>
              <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                Submit
              </button>
            </div>
          </div>
        <% end %>

        <div class="mt-6 flex justify-center">
          <%= link_to 'Cancel', new_session_path(resource_name), class: 'font-medium text-indigo-600 hover:text-indigo-500' %>
        </div>
      </div>
    </div>
  </div>
</div>
```

Here is what the sign up page looks like now:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720999254892/70b0a31b-cb0b-42a6-ae3e-a554f0fc2456.png align="center")

Lastly lets add styling to the flash messages that appear when a validation has failed or an error has occurred. First, add the following file: `app/layouts/_flash_messages.html.erb` and add the contents:

```erb
<% if flash[:alert].present? %>
  <div class="rounded-md bg-red-50 p-4 mb-8 border border-red-300">
    <div class="flex">
      <div class="flex-shrink-0">
        <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
        </svg>
      </div>
      <div class="ml-3">
        <div class="text-sm text-red-700">
          <%= flash[:alert] %>
        </div>
      </div>
    </div>
  </div>
<% end %>

<% if flash[:notice].present? %>
  <div class="rounded-md bg-green-50 p-4 mb-8 border border-green-400">
    <div class="flex">
      <div class="flex-shrink-0">
        <svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
        </svg>
      </div>
      <div class="ml-3">
        <div class="text-sm text-green-700">
          <%= flash[:notice] %>
        </div>
      </div>
    </div>
  </div>
<% end %>
```

Next replace the file `app/views/devise/shared/_error_messages.html.erb` with:

```erb
<% if resource.errors.any? %>
  <div class="rounded-md bg-red-50 p-4 mb-8 border border-red-300">
    <div class="flex">
      <div class="flex-shrink-0">
        <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
        </svg>
      </div>
      <div class="ml-3">
        <h3 class="text-sm font-medium text-red-800">
          <%= I18n.t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase) %>
        </h3>

        <div class="mt-2 text-sm text-red-700">
          <ul role="list" class="list-disc space-y-1 pl-5">
            <% resource.errors.full_messages.each do |message| %>
              <li><%= message %></li>
            <% end %>
          </ul>
        </div>
      </div>
    </div>
  </div>
<% end %>
```

Now error messages look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720999668350/011cd5a8-7fd9-42c5-864e-35b3868a418d.png align="center")

## Create an Authenticated Area

Next lets create a "Dashboard" section that only authenticated users can see. If an unauthenticated user attempts to view the dashboard they will be redirected to the login page.

First let's create a base controller that all other "user" controllers will inherit from. Create the file `app/controllers/users/application_controller.rb` with the following contents:

```ruby
class Users::ApplicationController < ApplicationController
  layout 'users'
  before_action :authenticate_user!
end
```

Note that `layout 'users`' instructs Rails to use the file `app/views/layouts/users.html.erb` as the base template for the controller views.

Also note that `before_action :authenticate_user!` ensures that the user is authenticated before serving any of the pages. If the user is not authenticated they will be redirected to the login page.

Let's create the layout file that will be used for all the user pages: `app/views/layouts/users.html.erb` with the following content:

```erb
<!DOCTYPE html>
<html class="h-full">
  <head>
    <title>DoList: A free and open source alternative to Todoist</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "application" %>
  </head>

  <body class="h-full p-10">
    <div class="flex gap-4 mb-10">
      <div>
        Welcome <%= current_user.email %>
      </div>

      <div>
        <%= link_to 'Sign Out', destroy_user_session_path %>
      </div>
    </div>

    <%= yield %>
  </body>
</html>
```

Let's make a quick change to allow "Sign Out" links to work with a `GET` request instead of the default `DELETE` request. In the file `config/initializers/devise.rb` uncomment line 269: `config.sign_out_via = :get`. Be sure to restart your local server.

Next let's create a placeholder dashboard page that will act as the authenticated area. Run the following generator command:

`bin/rails g controller user/dashboard index`

This creates two files:

* `app/controllers/users/dashboard_controller.rb`
    
* `app/views/users/dashboard/index.html.erb`
    

In `app/controllers/users/dashboard_controller.rb` be sure to change line 1 so that the file contents are:

```ruby
class Users::DashboardController < Users::ApplicationController
  def index
  end
end
```

We must inherit from `Users::ApplicationController` since that is the class that defines the layout and the authentication check.

Finally we must define where the user should be redirected to on sign in and sign out. Replace the contents of the file `app/controllers/application_controller.rb` with:

```ruby
class ApplicationController < ActionController::Base
  private

  def after_sign_in_path_for(resource)
    users_dashboard_index_path
  end

  def after_sign_out_path_for(resource)
    new_user_session_path
  end
end
```

That's it! Trying logging in and out and view the bare-bones dashboard page. It looks like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721008285863/ed5034ce-d431-432d-aa57-919496d8eb95.png align="center")

## Summary

In this article we accomplished the following tasks:

* Installed and configured the Devise gem
    
* Styled the authentication screens using Tailwind
    
* Created a protected dashboard page that only authenticated users can access
    

Next up we'll get to the fun bits that are the core of the business requirements (adding and completing tasks) and we'll start using Hotwire and Turbo to make a slick UI.
