I am trying to set up devise_invitable with two user models; User and Exechost. Everything works fine with the User model, the User code was implemented first and then the second model, Exechost, was added.

With the second model, no email invitation is created or send and we are redirected to the devise sign-up form with the following errors:

1 error prohibited this exechost from being saved:

  • Password can't be blank

I am also using rolify and pundit.

below is the terminal output:

Started POST "/exechosts" for ::1 at 2022-04-07 10:17:33 -0400
10:17:33 web.1  | Processing by Exechosts::RegistrationsController#create as HTML
10:17:33 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]", "exechost"=>{"username"=>"test20", "email"=>"[email protected]"}, "commit"=>"Add Moderator"}
10:17:33 web.1  |   TRANSACTION (0.1ms)  BEGIN
10:17:33 web.1  |   ↳ app/controllers/exechosts/registrations_controller.rb:18:in `create'
10:17:33 web.1  |   Exechost Exists? (0.3ms)  SELECT 1 AS one FROM "exechosts" WHERE "exechosts"."email" = $1 LIMIT $2  [["email", "[email protected]"], ["LIMIT", 1]]
10:17:33 web.1  |   ↳ app/controllers/exechosts/registrations_controller.rb:18:in `create'
10:17:33 web.1  |   TRANSACTION (0.1ms)  ROLLBACK
10:17:33 web.1  |   ↳ app/controllers/exechosts/registrations_controller.rb:18:in `create'
10:17:33 web.1  |   Rendering layout layouts/application.html.erb
10:17:33 web.1  |   Rendering exechosts/registrations/new.html.erb within layouts/application
10:17:33 web.1  |   Rendered exechosts/shared/_error_messages.html.erb (Duration: 0.9ms | Allocations: 432)
10:17:33 web.1  |   Rendered exechosts/shared/_links.html.erb (Duration: 0.1ms | Allocations: 72)
10:17:33 web.1  |   Rendered exechosts/registrations/new.html.erb within layouts/application (Duration: 3.3ms | Allocations: 2073)
10:17:33 web.1  |   Exechost Load (0.3ms)  SELECT "exechosts".* FROM "exechosts" WHERE "exechosts"."id" = $1 ORDER BY "exechosts"."created_at" ASC, "exechosts"."id" ASC LIMIT $2  [["id", "9335e908-f90a-455c-9eb4-f2f8a7ab29ec"], ["LIMIT", 1]]
10:17:33 web.1  |   ↳ app/views/pages/_navBar.html.erb:25
10:17:33 web.1  |   ExecRole Load (0.4ms)  SELECT "exec_roles".* FROM "exec_roles" INNER JOIN "exechosts_exec_roles" ON "exec_roles"."id" = "exechosts_exec_roles"."exec_role_id" WHERE "exechosts_exec_roles"."exechost_id" = $1 AND (((exec_roles.name = 'owner') AND (exec_roles.resource_type IS NULL) AND (exec_roles.resource_id IS NULL)))  [["exechost_id", "9335e908-f90a-455c-9eb4-f2f8a7ab29ec"]]
10:17:33 web.1  |   ↳ app/views/pages/_navBar.html.erb:27
10:17:33 web.1  |   ExecRole Load (0.6ms)  SELECT "exec_roles".* FROM "exec_roles" INNER JOIN "exechosts_exec_roles" ON "exec_roles"."id" = "exechosts_exec_roles"."exec_role_id" WHERE "exechosts_exec_roles"."exechost_id" = $1 AND (((exec_roles.name = 'superowner') AND (exec_roles.resource_type IS NULL) AND (exec_roles.resource_id IS NULL)))  [["exechost_id", "9335e908-f90a-455c-9eb4-f2f8a7ab29ec"]]

The following is the successful output when inviting with the User model:



10:22:01 web.1  | Started POST "/account/users" for ::1 at 2022-04-07 10:22:01 -0400
10:22:01 web.1  | Processing by UsersController#create as HTML
10:22:01 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"first_name"=>"test2", "last_name"=>"test with User", "phone"=>"111-111-1111", "email"=>"[email protected]"}, "commit"=>"Add Team Member"}
10:22:01 web.1  |   User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."customer_num" = $1 LIMIT $2  [["customer_num", 86384], ["LIMIT", 1]]
10:22:01 web.1  |   ↳ app/models/user.rb:39:in `block in assign_unique_customer_num'
10:22:01 web.1  |   User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."created_at" ASC, "users"."id" ASC LIMIT $2  [["id", "e22018b6-61a9-401e-9d15-f02924debcfd"], ["LIMIT", 1]]
10:22:01 web.1  |   ↳ app/controllers/application_controller.rb:20:in `current_account'
10:22:01 web.1  |   Account Load (0.2ms)  SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2  [["id", "eaa76bc4-078f-432f-be4f-e1730f0c7274"], ["LIMIT", 1]]
10:22:01 web.1  |   ↳ app/controllers/application_controller.rb:21:in `current_account'
10:22:02 web.1  |   User Exists? (0.4ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[email protected]"], ["LIMIT", 1]]
10:22:02 web.1  |   ↳ app/controllers/users_controller.rb:90:in `block in create'
10:22:02 web.1  |   User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."invitation_token" = $1 ORDER BY "users"."created_at" ASC, "users"."id" ASC LIMIT $2  [["invitation_token", "[FILTERED]"], ["LIMIT", 1]]
10:22:02 web.1  |   ↳ app/controllers/users_controller.rb:90:in `block in create'
10:22:02 web.1  |   TRANSACTION (0.3ms)  BEGIN
10:22:02 web.1  |   ↳ app/controllers/users_controller.rb:90:in `block in create'
10:22:02 web.1  |   User Create (42.3ms)  INSERT INTO "users" ("first_name", "last_name", "phone", "status", "customer_num", "expiration_date", "created_at", "updated_at", "email", "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at", "sign_in_count", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip", "account_id", "invitation_token", "invitation_created_at", "invitation_sent_at", "invitation_accepted_at", "invitation_limit", "invited_by_type", "invited_by_id", "invitations_count") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27) RETURNING "id"  [["first_name", "test2"], ["last_name", "test with User"], ["phone", "111-111-1111"], ["status", "acitve"], ["customer_num", 86384], ["expiration_date", nil], ["created_at", "2022-04-07 14:22:02.102863"], ["updated_at", "2022-04-07 14:22:02.102863"], ["email", "[email protected]"], ["encrypted_password", "[FILTERED]"], ["reset_password_token", "[FILTERED]"], ["reset_password_sent_at", "[FILTERED]"], ["remember_created_at", nil], ["sign_in_count", 0], ["current_sign_in_at", nil], ["last_sign_in_at", nil], ["current_sign_in_ip", nil], ["last_sign_in_ip", nil], ["account_id", "eaa76bc4-078f-432f-be4f-e1730f0c7274"], ["invitation_token", "[FILTERED]"], ["invitation_created_at", "2022-04-07 14:22:02.089182"], ["invitation_sent_at", "2022-04-07 14:22:02.089182"], ["invitation_accepted_at", nil], ["invitation_limit", nil], ["invited_by_type", "User"], ["invited_by_id", 0], ["invitations_count", 0]]
10:22:02 web.1  |   ↳ app/controllers/users_controller.rb:90:in `block in create'
10:22:02 web.1  |   TRANSACTION (3.5ms)  COMMIT
10:22:02 web.1  |   ↳ app/controllers/users_controller.rb:90:in `block in create'
10:22:02 web.1  |   Rendering devise/mailer/invitation_instructions.html.erb
10:22:02 web.1  |   Rendered devise/mailer/invitation_instructions.html.erb (Duration: 2.2ms | Allocations: 1376)
10:22:02 web.1  |   Rendering devise/mailer/invitation_instructions.text.erb
10:22:02 web.1  |   Rendered devise/mailer/invitation_instructions.text.erb (Duration: 1.7ms | Allocations: 698)
10:22:02 web.1  | Devise::Mailer#invitation_instructions: processed outbound mail in 12.1ms
10:22:03 web.1  | Delivered mail [email protected] (852.5ms)
10:22:03 web.1  | Date: Thu, 07 Apr 2022 10:22:02 -0400
10:22:03 web.1  | From: [email protected]
10:22:03 web.1  | Reply-To: [email protected]
10:22:03 web.1  | To: [email protected]
10:22:03 web.1  | Message-ID: <[email protected]>
10:22:03 web.1  | Subject: Invitation instructions
10:22:03 web.1  | Mime-Version: 1.0
10:22:03 web.1  | Content-Type: multipart/alternative;
10:22:03 web.1  |  boundary="--==_mimepart_624ef38a28bcc_9d004ed411214";
10:22:03 web.1  |  charset=UTF-8
10:22:03 web.1  | Content-Transfer-Encoding: 7bit
10:22:03 web.1  | 
10:22:03 web.1  | 
10:22:03 web.1  | ----==_mimepart_624ef38a28bcc_9d004ed411214
10:22:03 web.1  | Content-Type: text/plain;
10:22:03 web.1  |  charset=UTF-8
10:22:03 web.1  | Content-Transfer-Encoding: 7bit
10:22:03 web.1  | 
10:22:03 web.1  | Hello [email protected]
10:22:03 web.1  | 
10:22:03 web.1  | Someone has invited you to http://localhost:3000/, you can accept it through the link below.
10:22:03 web.1  | 
10:22:03 web.1  | http://localhost:3000/users/invitation/accept?invitation_token=4TL5Vpyjfv5CFwCLGx9y
10:22:03 web.1  | 
10:22:03 web.1  |   This invitation will be due in April 21, 2022 02:22 PM.
10:22:03 web.1  | 
10:22:03 web.1  | If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password.
10:22:03 web.1  | 
10:22:03 web.1  | ----==_mimepart_624ef38a28bcc_9d004ed411214
10:22:03 web.1  | Content-Type: text/html;
10:22:03 web.1  |  charset=UTF-8
10:22:03 web.1  | Content-Transfer-Encoding: 7bit
10:22:03 web.1  | 
10:22:03 web.1  | <p>Hello [email protected]</p>
10:22:03 web.1  | 
10:22:03 web.1  | <p>Someone has invited you to http://localhost:3000/, you can accept it through the link below.</p>
10:22:03 web.1  | 
10:22:03 web.1  | <p>testing here what to say</p>
10:22:03 web.1  | <p><span class="translation_missing" title="translation missing: en.testing here what to say">Testing Here What To Say</span> </p>
10:22:03 web.1  | 
10:22:03 web.1  | 
10:22:03 web.1  | 
10:22:03 web.1  | 
10:22:03 web.1  | <p><a href="http://localhost:3000/users/invitation/accept?invitation_token=4TL5Vpyjfv5CFwCLGx9y">Accept invitation</a></p>
10:22:03 web.1  | 
10:22:03 web.1  |   <p>This invitation will be due in April 21, 2022 02:22 PM.</p>
10:22:03 web.1  | 
10:22:03 web.1  | <p>If you don&#39;t want to accept the invitation, please ignore this email. Your account won&#39;t be created until you access the link above and set your password.</p>
10:22:03 web.1  | 
10:22:03 web.1  | ----==_mimepart_624ef38a28bcc_9d004ed411214--
10:22:03 web.1  | 

Below are the routes:


  get 'dashboard/show'
  get 'execdashboard/show'

  devise_for :users, controllers: { registrations: "registrations" }
  devise_for :exechosts, controllers: { registrations: "exechosts/registrations", invitations: "exechosts/invitations" }

The Exechost model:

class Exechost < ApplicationRecord

  rolify :role_cname => 'ExecRole'

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

end

In the Exechost controller:

class ExechostsController < ApplicationController

  before_action :set_exechost, only: [:show, :edit, :update, :edit_roles ,:update_roles] 
  before_action :set_exechosts, only: [:index]

  def new
    @exechost = Exechost.new
    set_exechost_choices
  end

  def create
    @exechost = Exechost.unscoped.new(exechost_params.except("role"))
    @exechost.password = "password123"

    respond_to do |format|
      begin
        if @exechost.valid? && @exechost.invite!(current_exechost)
          @exechost.add_role :moderator
          format.html {
            redirect_to exechosts_path,
            notice: 'Moderator was successfully invited.'
          }
        else
          set_exechost_choices
          format.html { render :new }
        end
      rescue ActiveRecord::RecordNotUnique
        flash[:alert]= 'Email must be unique'
        format.html { render :new}
      end
    end
  end


  private

   def pundit_user
     user = current_exechost
   end

   def set_exechosts
     @exechosts = Exechost.all
   end

   def set_exechost
     @exechost = Exechost.find(params[:id])
   end

   def exechost_params
     params.require(:exechost).permit(:username, :email, :role)
   end

end

In the invitations controller:

class Exechosts::InvitationsController < Devise::InvitationsController

  before_action :update_sanitized_params, only: :update

  def create

    @exechost = Exechost.invite!(exechost_params[:exechost], current_exechost) do |u| 
      u.skip_invitation = true
    end

    ExechostInvitationNotificationMailer.invite_message(@exechost).deliver if @exechost.errors.empty?
    @exechost.invitation_sent_at = Time.now.utc # mark invitation as delivered

    if @exechost.errors.empty?
      flash[:notice] = "successfully sent invite to #{@exechost.email}"
      respond_with @exechost, :location => exechosts_path
    else
      render :new
    end
  end


  private

  def exechost_params
    params.require(:exechost).permit(:username, :email, :role)
  end
end


The registration controller:



class Exechosts::RegistrationsController < Devise::RegistrationsController
  protect_from_forgery with: :exception, prepend: true

  prepend_before_action :require_no_authentication, only: [:cancel]

  before_action :configure_sign_up_params

  

  

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [[:username, :email]])
  end

  # The path used after sign up.
  def after_sign_up_path_for(resource)
    exechosts_path
  end

  
end

The devise controller:


class Exechosts::DeviseController < ApplicationController
  class Responder < ActionController::Responder
    def to_turbo_stream
      controller.render(options.merge(formats: :html))
    rescue ActionView::MissingTemplate => error
      if get?
        raise error
      elsif has_errors? && default_action
        render rendering_options.merge(formats: :html, status: :unprocessable_entity)
      else
        redirect_to navigation_location
      end
    end
  end

  self.responder = Responder
  respond_to :html, :turbo_stream
end

And the config/initializers/devise.rb; only the inevitable portion, the only switch on is a two week limit.



  # ==> Configuration for :invitable
  # The period the generated invitation token is valid.
  # After this period, the invited resource won't be able to accept the invitation.
  # When invite_for is 0 (the default), the invitation won't expire.
  config.invite_for = 2.weeks

  # Number of invitations users can send.
  # - If invitation_limit is nil, there is no limit for invitations, users can
  # send unlimited invitations, invitation_limit column is not used.
  # - If invitation_limit is 0, users can't send invitations by default.
  # - If invitation_limit n > 0, users can send n invitations.
  # You can change invitation_limit column for some users so they can send more
  # or less invitations, even with global invitation_limit = 0
  # Default: nil
  # config.invitation_limit = 5

  # The key to be used to check existing users when sending an invitation
  # and the regexp used to test it when validate_on_invite is not set.
  # config.invite_key = { email: /\A[^@]+@[^@]+\z/ }
  # config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil }

  # Ensure that invited record is valid.
  # The invitation won't be sent if this check fails.
  # Default: false
  # config.validate_on_invite = true

  # Resend invitation if user with invited status is invited again
  # Default: true
  # config.resend_invitation = false

  # The class name of the inviting model. If this is nil,
  # the #invited_by association is declared to be polymorphic.
  # Default: nil
  #config.invited_by_class_name = 'User'


  # The foreign key to the inviting model (if invited_by_class_name is set)
  # Default: :invited_by_id
  # config.invited_by_foreign_key = :invited_by_id

  # The column name used for counter_cache column. If this is nil,
  # the #invited_by association is declared without counter_cache.
  # Default: nil
  # config.invited_by_counter_cache = :invitations_count

  # Auto-login after the user accepts the invite. If this is false,
  # the user will need to manually log in after accepting the invite.
  # Default: true
  # config.allow_insecure_sign_in_after_accept = false

The code from the User controller for create; the code is the same with the addition of referencing the user account.

As noted above the create action does create a new user and the email invitation.

def create
    @user = User.unscoped.new(user_params.except("role"))
    @user.account = current_account
    @user.password = "password123"

    respond_to do |format|
      begin
        if @user.valid? && @user.invite!(current_user)
          @user.add_role :member, current_account
          format.html {
            redirect_to account_users_path,
            notice: 'User was successfully invited.'
          }
        else
          set_choices
          format.html { render :new }
        end
      rescue ActiveRecord::RecordNotUnique
        flash[:alert]= 'Email must be unique'
        format.html { render :new}
      end
    end
  end

1

There are 1 best solutions below

0
Sergio Cambra On

/exechosts goes to Exechosts::RegistrationsController, it isn't ExechostsController, and create action is normal create action from Devise, which requires password param. However, /account/users goes to UsersController request, which has your custom code.

Code for routes doesn't include anything related to UsersController or ExechostsController. Also, if you have some custom code which calls invite for user and exechost, you may not need routes to invitations controller and you may want to use skip option to not generate routes for invitations.