Devise Profile Usernames

I have recently worked on several projects with user accounts managed by Devise, and I have been changing how user profile URLs are set up and presented to users. In this article, I will address this task and use a few of my recent favorite gems.

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

Goals

Here are the project parameters:

  • Set up user accounts with Devise
  • Set up a URL for a User's profile page as: /users/:username
  • Generate a unique username name that is not requested on the sign-up form

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 creates 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 setup

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 Devise migration we created. 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 is 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 , 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 addition, 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 let's move on.

Friendly ID

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

rails g migration AddSlugToUsers slug:uniq

This will create a unique slug. In our case, we will use the first and last name fields to 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 a ,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, when 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.