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'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.</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
/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.