Let’s Build: With Ruby on Rails – Extending Devise Series – Adding Custom Fields
Tags: ruby, rails, tutorial, video
Published: 2019-06-19 21:35:34

---
title: Let’s Build: With Ruby on Rails – Extending Devise Series – Adding Custom Fields
published: true
tags: ruby, rails, tutorial, video
canonical_url: https://web-crunch.com/extending-devise-custom-fields/
---

Devise is a ruby gem I use in nearly any Rails app. Authentication is a not so simple concept to master and as a result Devise has stood to be one of the most popular to help with this repetitive problem on Ruby on Rails applications.

This post is the first of many that will take a look at extending devise to work with you rather than against. My goal is to start off with the basics and then move into more advanced customizations like OmniAuth, inviting users, acting as users, and much more. I’ll be republishing this post as new installments get complete so in the end, there will be a larger collection.

To kick things off I’ll touch on adding custom fields to a given Devise install. Rather than adhering to the defaults that ship with the gem, we will extend it to include two new fields. One is a terms and conditions checkbox and the other being the location of a given user.

All of these fields are presented to the user when first signing up. I’ll extend the already generated devise view to include the new fields. As an added bonus, I’ll add in some geo-location (thanks to another gem) to pinpoint the user’s location during sign up.

{% youtube 3nFXKRF3GH8 %}

### Prerequisite

In this tutorial I’m using a template I created previously called `kickoff_tailwind`. You can [download it here](https://github.com/justalever/kickoff_tailwind). It comes with some general configuration taken care of as well as installing the Devise gem itself. If you are brand new to devise I recommend [installing it on your own](https://github.com/plataformatec/devise) first to understand how it hooks into a given model within your Rails app.

### Let’s Begin

When I ran my installation I pass a template which I mentioned prior that comes from my [kickoff\_tailwind Github repo](https://github.com/justalever/kickoff_tailwind). If you’re following along step-by-step you should download this to your system before proceeding.

After a successful install, we pinpointed our `User` model to target with Devise. In doing so new fields are generated by default. We need to extend this to add two new fields `terms` and `location`.

### Create a new migration

```bash
$ rails g migration addFieldsToUsers terms:boolean location:string

```

Then run `rails db:migrate` to update the database schema.

#### Update the Application Controller

Next, we need to allow those new fields to enter the database once a user submits the sign-up form. Rails uses this concept of permitted parameters for forms. You need to classify what fields you want to accept during the submission otherwise they will not enter the database. This is a great security feature.

To pass the new fields as valid we need to update our `ApplicationController` to the following:

```ruby
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :name, :terms, :location])
      devise_parameter_sanitizer.permit(:account_update, keys: [:username, :name, :terms, :location])
    end
end

```

We had `:username` and `:name` here by default thanks to the `kickoff_tailwind` Rails template. Your mileage may vary if you’re not using the template.

#### Update views to reflect new fields

Our views need to reflect the new fields added. There are two in particular that need attention:

```erb
<!-- app/views/devise/registrations/edit.html.erb -->
<% content_for :devise_form do %>
  <h2 class="heading text-4xl font-bold pt-4 mb-8">Edit <%= resource_name.to_s.humanize %></h2>

  <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>

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

    <div class="mb-6">
      <%= f.label :username, class:"label" %>
      <%= f.text_field :username, autofocus: true, class:"input" %>
    </div>

    <div class="mb-6">
      <%= f.label :name, class:"label" %>
      <%= f.text_field :name, class:"input" %>
    </div>

    <div class="mb-6">
      <%= f.label :email, class:"label" %>
      <%= f.email_field :email, autocomplete: "email", class:"input" %>
    </div>

    <div class="mb-6">
      <%= f.label :location, class:"label" %>
      <%= f.text_field :location, class:"input", value: city %>
    </div>

    <div class="mb-6">
      <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
        <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
      <% end %>
    </div>

    <div class="mb-6">
      <%= f.label :password, class:"label" %>
      <%= f.password_field :password, autocomplete: "new-password", class:"input" %>
      <p class="text-sm text-grey-dark pt-1 italic"> <% if @minimum_password_length %>
        <%= @minimum_password_length %> characters minimum <% end %> (leave blank if you don't want to change it) </p>

    </div>

    <div class="mb-6">
      <%= f.label :password_confirmation, class: "label" %>
      <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "input" %>
    </div>

    <div class="mb-6">
      <%= f.label :current_password, class: "label" %>
      <%= f.password_field :current_password, autocomplete: "current-password", class: "input" %>
      <p class="text-sm text-grey-dark pt-2 italic">(we need your current password to confirm your changes)</p>
    </div>

    <div class="mb-6">
      <%= f.submit "Update", class: "btn btn-default" %>
    </div>
    <% end %>

    <hr class="border mt-6 mb-3" />

    <h3 class="heading text-xl font-bold mb-4">Cancel my account</h3>

    <div class="flex justify-between items-center">
      <div class="flex-1"><p class="py-4">Unhappy?</p></div>

      <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete, class: "btn btn-default" %>
    </div>

<% end %>

<%= render 'devise/shared/form_wrap' %>

```

Notice that this has been highly customized once again thanks to the `kickoff_tailwind` Rails template of mine.

```erb
<!-- app/views/devise/registrations/new.html.erb -->
<% content_for :devise_form do %>

  <h2 class="heading text-4xl font-bold pt-4 mb-8">Sign up</h2>

    <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
    <%= render "devise/shared/error_messages", resource: resource %>

    <div class="mb-6">
      <%= f.label :username, class:"label" %>
      <%= f.text_field :username, autofocus: true, class:"input" %>
    </div>

    <div class="mb-6">
      <%= f.label :name, class:"label" %>
      <%= f.text_field :name, class:"input" %>
    </div>

    <div class="mb-6">
      <%= f.label :email, class:"label" %>
      <%= f.email_field :email, autocomplete: "email", class:"input" %>
    </div>

    <div class="mb-6">
      <%= f.label :location, class:"label" %>
      <%= f.text_field :location, class:"input", value: city %>
    </div>

    <div class="mb-6">
      <div class="flex">
        <%= f.label :password, class: "label" %>
        <% if @minimum_password_length %>
        <span class="text-xs pl-1 text-grey-dark"><em>(<%= @minimum_password_length %> characters minimum)</em></span>
        <% end %>
      </div>
      <%= f.password_field :password, autocomplete: "new-password", class: "input" %>
    </div>

    <div class="mb-6">
      <%= f.label :password_confirmation, class:"label" %>
      <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "input" %>
    </div>

    <div class="mb-6">
      <%= f.check_box :terms, class: "mr-2" %>
      <%= f.label :terms, "I agree to terms and conditions" %>
    </div>

    <div class="mb-6">
      <%= f.submit "Sign up", class: "btn btn-default block w-full text-center" %>
    </div>

    <hr class="border mt-6" />

  <% end %>

  <%= render "devise/shared/links" %>

<% end %>

<%= render "devise/shared/form_wrap" %>

```

#### Bonus: Geo Location

Unfortunately, it’s not quite trivial to add geo location to an app. Most attempts at this require some kind of service you’ll need to subscribe to. How it works is relative to the given requests IP address. From that IP address more data can be discovered and mapped to cities, countries, system info and more. We simply want the city name to display in the location field by default when a user goes to sign up for an account. In doing so I’ll reach for a service call [https://ipinfo.io/](https://ipinfo.io/). They happen to have a Rails gem we can utilize to get going quickly – [Check that out here](https://github.com/ipinfo/rails).

You’ll need to add that gem to your `Gemfile` and run `bundle install`.

```ruby
 # Gemfile

gem 'ipinfo-rails'

```

You also will need an account on [https://ipinfo.io/](https://ipinfo.io/) to gain access to the API. They require an access token to use the gem.

Open your `config/environment.rb` file or your preferred file in the `config/environment` directory. Add the following code to your chosen configuration file. I chose `config/environment/development.rb` by example. I recommend just running this in production or on a live server though since your local environment won’t have the same data we need. To work around this we’ll install another tool called `ngrok` coming up. First, we need to configure the `ipinfo` gem.

```ruby
# config/environment/development.rb
require 'ipinfo-rails'

Rails.application.configure do
  ...
  config.middleware.use(IPinfoMiddleware, {
     token: Rails.application.credentials.dig(:ipinfo_token) 
  })
  ...
end

```

Note: if editing `config/environment.rb`, this needs to come before `Rails.application.initialize!` and with `Rails.application.` prepended to `config`, otherwise, you’ll get runtime errors.

Make sure you add your access token to your encrypted credentials.

```bash
$ rails credentials:edit

```

Restart your development server.

#### Testing Locally

To test locally we need to forward our port out to [ngrok](https://ngrok.com/download). You’ll need an auth token (create a free account to grab one).

Install ngrok by clicking the bash script. It will install in your user level account. You can create an alias to it via `zsh` or `bash_profile`. I chose to install it via yarn globally by running `yarn add ngrok`. This adds a zip file within a `.ngrok` folder on your user account within your machine. You’ll need to unzip that zip file.

I created an alias to that directory for quick access using `.zsh`. (optional)

Run `./ngrok http 3000` while having `rails server` running on `localhost:3000` in a new terminal tab.

Pass the ngrok url as a permitted host in `development.rb`

```ruby
 config.hosts << "7c8b6b27.ngrok.io" # example url
 config.middleware.use(IPinfoMiddleware, {
   token: Rails.application.credentials.dig(:ipinfo)
 })

```

#### Create a helper around the logic

Our helper with make use of the new `request.env['ipinfo']` logic we get from the new gem. We can wrap this up so our views won’t be cluttered.

Add a new `city` helper in `application_helper.rb`

```ruby
# app/helpers/application_helper.rb

 def city
  request.env['ipinfo'].city if request.env['ipinfo'].city
 end

```

Finally we can update form fields to have a predefined value using the new helper

```ruby
<!-- update in both app/views/registrations/edit.html.erb and app/views/registrations/new.html.erb -->
 <div class="mb-6">
   <%= f.label :location, class:"label" %>
   <%= f.text_field :location, class:"input", value: city %>
</div>

```


### Shameless plug

[![](https://i1.wp.com/web-crunch.com/wp-content/uploads/2019/01/YouTube-Promo3.jpg?resize=3840%2C2160&ssl=1)](http://hellorails.io)

I have a new course called [Hello Rails](https://hellorails.io). Hello Rails is modern course designed to help you start using and understanding Ruby on Rails fast. If you’re a novice when it comes to Ruby or Ruby on Rails I invite you to check out the site. The course will be much like these builds but a super more in-depth version with more realistic goals and deliverables. [Take me to the course](https://hellorails.io/)!

Follow [@hello\_rails](https://twitter.com/hello_rails) and [myself @justalever](https://twitter.com/justalever) on Twitter.

The post [Let’s Build: With Ruby on Rails – Extending Devise Series – Adding Custom Fields](https://web-crunch.com/extending-devise-custom-fields/) appeared first on [Web-Crunch](https://web-crunch.com).