Devise Profile Usernames

I have worked on several projects recently with user accounts managed by Devise, and I have been working at changing the way user profile URL's are set up and presented to the user. In this article I will address this task and use a few of my recent favorite gems.

TLTR: If you just want the code go grab it, and post questions or responses if you like.

Goals

Here are the project parameters:

name_of_person - We will use the name_of_person gem by Basecamp. This gem creates a pseudo-field for full name (requires first_name and last_name in the User table). It has many other abstractions, but this is the only feature we will use.

friendly_id - We will use the friendly_id gem, which created slugs that we can map to a predetermined route. This is a method you can use throughout an application, not just with the User models.

Basic set up

We will start by setting up a basic Rails app: rails new awesome_app, and set up a static controller for a home route: rails g controller static home.

Configure the routes to load the basic home.html.erb, in config/routes.rb:

Rails.application.routes.draw do

  root "static#home"

end

Add to follow to the application.html.erb, to add a rudimentary navbar we can use later:

<!DOCTYPE html>
<html>
<head>
  <title>ArticleDeviseUsernames</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>

  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body>
<div style="margin-top: 20px">
  <% if user_signed_in? %>
    <%= link_to "User Profile", user_path(current_user.slug) %>
    <%= link_to "Logout", destroy_user_session_path(current_user.slug), method: :delete %>
  <% else %>
    <%= link_to "Log in", new_user_session_path %>
  <% end %>
  <%= yield %>

</div>
</body>
</html>

Set up Devise

Add the Devise gem to the Gemfile and bundle install, then install Devise: rails generate devise:install. You will need to add to config/environments/development.rb the following line:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

We are going to generate a devise User model: rails generate devise User.

To use name_of_person gem, we need to add two columns to the created Devise migration. Add first_name and last_name somewhere within the database migration, then migrate with rails db:migrate:

class DeviseCreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ...

      t.string :first_name
      t.string :last_name

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

Devise will automatically add the appropriate routes, but it always a good idea to check, so make sure that in config/routes.rb the route is found: devise_for: users.

Add to the User model to use the name_of_person gem, by adding has_person_name:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_person_name
end

This is the basic configure for the gem, but we are going to have to tell Devise to allow the new name field. So, in app/controllers/application_controller.rb add the following:

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

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

We will need to add the name field to the sign-in and sign-up forms, so we need to generate the views: rails generate devise:views. In app/views/devise/registrations/new.html.erb, .../registrations/edit.html.erb, and .../sessions/new.html.erb add the following for the name field:

<h2>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="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: false, autocomplete: "email" %>
  </div>

  ...

Almost there ... We need to create a page to contain the User Profile, so we need to create a User controller:

rails g controller User show

Edit controller:

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

end

Lastly, update the routes, so for the route to the profile page:

Rails.application.routes.draw do
  devise_for :users

  resources :users, only: [:show]

  root "static#home"

end

All done with Devise. You can restart your rails server, open the development site, and create a User Account. When you are redirected to the root_path, click the "User Profile" link in the navbar. You will be redirected to a path, something like http://localhost:3000/users/1. This is not the goal, so lets move on.

Friendly ID

Add the friendly_id gem to the Gemfile and bundle install, then create a migration:

rails g migration AddSlugToUsers slug:uniq

This will create a unique slug. In our case we are going to use the first and last name fields, and it will create a unique username. So in my case /users/chuck-smith. If this is not unique, maybe there is another "Chuck Smith" in the user table, it will make it unique: /users/chuck-s.

Next we need to generate friendly_id: rails generate friendly_id, and migrate the database: rails db:migrate.

We use Friendly_Id by extending the User model, and define the :name column, from name_of_person as the slug field:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_person_name

  extend FriendlyId
  friendly_id :name, use: :slugged
end

Now, edit the show action in the UsersController to use the :slug param from Friendly:

class UsersController < ApplicationController

  def show
    @user = User.friendly.find(params[:slug])
  end

end

Lastly, update the routes, for the new param:

Rails.application.routes.draw do
  devise_for :users

  resources :users, only: [:show], param: :slug

  root "static#home"

end

If you already have Users created, from the rails console execute: User.find_each(&:save), which will update the new slug column.

Now, where you log in and browse to your User Profile the URL is friendlier (forgive the pun): /users/chuck-smith.

One additional optional configuration. If you want to change the user profile path you can edit the routes file:

Rails.application.routes.draw do
  devise_for :users

  resources :users, only: [:show], param: :slug, path: ""

  root "static#home"

end

Notice I have added a path key to the :users resource which is empty. So, now if you browse to the user profile page the path will be (in this example): /chuck-smith.

Footnote

This has been fun. Leave a comment or send me a DM on Twitter.

Shameless Plug: If you work at a great company, and you are in the market for a Software Developer with a varied skill set and life experiences, send me a message on Twitter and check out my LinkedIn.