Devise - thuy-econsys/rails_app GitHub Wiki

Install

Install Devise. The following command generates Devise configuration files config/initializers/devise.rb and config/locales/devise.en.yml.

possible unresolved issue with Spring when trying to install Devise as it hangs on Spring and never completes. hacky way was to remove Spring from Gemfile while installing Devise and then adding it back afterwards.

rails generate devise:install

Generate a Devise Migration file to create your User table with email and password attributes, as well as other columns for some optional Devise modules. Migrate the files after perusing the file and uncommenting the modules that you want to use:

rails generate devise user
rails db:migrate

Customize Views

To expose some views for authenticating your users, run rails generate devise:views which invokes Devise::Generators::SharedViewsGenerator and generates these view directories:

app/views/devise/
├── confirmations
│   └── new.html.erb
├── passwords
│   ├── edit.html.erb
│   └── new.html.erb
├── registrations
│   ├── edit.html.erb
│   └── new.html.erb
├── sessions
│   └── new.html.erb
├── shared
│   └── _links.html.erb
└── unlocks
    └── new.html.erb

But if you only want to customize certain views, you can pass them as options after the -v flag. For basic authentication, registrations/new.html.erb is the template for signing up users; registrations/edit.html.erb, the template for updating user registration data; sessions/new.html.erb, the template for signing in users.

rails generate devise:views -v registrations sessions

Customize Controllers

Similar to the views, there's also a generator for exposing the controllers for customization. Running rails generate devise:controllers invokes Devise::Generators::ControllersGenerator, which generates the following controllers:

app/controllers/
├── application_controller.rb
├── concerns
└── users
    ├── confirmations_controller.rb
    ├── omniauth_callbacks_controller.rb
    ├── passwords_controller.rb
    ├── registrations_controller.rb
    ├── sessions_controller.rb
    └── unlocks_controller.rb

If you only want to expose specific controllers, pass them after the -c flag, no comma separation

rails generate devise:controllers -c registrations

Methods are included but commented out. If at this point, you're scratching your head about what super is in all the controllers, perhaps understand how inheritance works in Ruby, first, before trying to override any Devise methods.

Make sure to also direct the router to use your custom controller:

devise_for :users, path: "account", :controllers => { registrations: 'users/registrations' }

Note that path is set to account allowing for there to be a separate User Controller route for Administrators to be able to update from a Dashboard page, to avoid any conflict with Users updating their own accounts.

At the top level of the controllers directory is ApplicationController, which Devise controllers inherit from. Here is where strong_params will be configured for the controllers. Note that there are default permitted parameters that do not need to be added to the whitelist, like email and password. And since :sign_in already has the permitted parameters that it needs, no need to adjust here:

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :configure_permitted_parameters, if: :devise_controller?
  
  protected
  
  def configure_permitted_parameters
    added_attrs = [:full_name, :phone, :account_type]
    devise_parameter_sanitizer.permit(:sign_up, keys: added_attrs)
    devise_parameter_sanitizer.permit(:account_update, keys: added_attrs)
  end
end

Customizing

Model

Run rails generate migration AddApprovedToUsers approved:boolean to generate a migration class that will add an approved attribute column. Inside the change method, append :default => false so that Users start out as unapproved, awaiting administrator approval.

db/migrate/YYYYMMDDHHMMSS_add_approved_to_users.rb

class AddApprovedToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :approved, :boolean, :default => false, :null => false
  end
end

Time to override Devise::Models::Authenticatable methods: active_for_authentication?, inactive_message:

app/models/user.rb

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

  def active_for_authentication? 
    super && approved? 
  end 
  
  def inactive_message 
    approved? ? super : :not_approved
  end
end

Note that super is being called as your methods are inheriting from the Authenticatable module and we're technically adding functionality and not completely overriding it. Calling super in active_for_authentication? returns a true. But we're also checking for the condition of approved?. The model is active, returns true, only if approved. If active_for_authentication? evaluates to false and therefore the model is inactive, inactive_message is invoked. Normally the message is :inactive but the ternary operator overrides super, as approved? returns false, so the message is :not_approved.

If at this point, your mind is blown because "How the hell is a module a superclass?", read their explanation.

If you're wondering why Devise::Models::Authenticatable is not included in the User class below like the other devise modules, read this explanation which breaks down include and extend in the context of Ruby. Might help to illuminate the purpose of super. A little background in understanding class and instance variables in Ruby as well as class and instance methods in Ruby might be helpful.

Controller

Extend Devise::RegistrationsController so that we can override the create method by running, rails generate devise:controllers users. It's possible to generate only specific controllers with the -c flag followed by each controller, no commas:

rails generate devise:controllers users -c registrations

Also possible to just create the file that you need like by creating app/controllers/users/registrations_controller.rb and defining like so:

class Users::RegistrationsController < Devise::RegistrationsController

  def create
    super

    if @user.errors.empty?
      AdminMailer.with(user: @user).admin_approval_email.deliver
    end
  end
end

The error check is to make sure email is only sent out for successful signups.

And then you will also need to override the route for registrations controller to route to your Users::RegistrationsController:

  devise_for :users, :controllers => { registrations: 'users/registrations' }