Adding invite only users

Brandon Brown
Hi,

I'm building an app where users must be invited. I added the Devise Invitable gem, and added :invitable to the user model. I created an invitations controller and then generated custom views for the inviting and accepting. I can invite users just fine, but I am getting some errors when submitting the acceptance form:

```
NoMethodError (undefined method `update' for nil:NilClass):
  
app/models/concerns/user_accounts.rb:39:in `sync_personal_account_name'
app/controllers/users/invitations_controller.rb:14:in `accept_resource'
```
Here is my invitations_controller:
```
class Users::InvitationsController < Devise::InvitationsController
  private

  # This is called when creating invitation.
  # It should return an instance of resource class.
  def invite_resource
    super
  end

  # This is called when accepting invitation.
  # It should return an instance of resource class.
  def accept_resource
    resource_class.accept_invitation!(update_resource_params)
    resource
  end

end
```
I suspect that I am missing something during the creation that is not linking users to accounts appropriately, even though the logs look like they are trying to create the associated account. I also noticed the the invite logic is already being used for account invites, so I'm wondering if that is conflicting as well. Or it's possible I'm overcomplicating this. Any ideas?

I'm trying to figure out something similar, but without devise_invitable since there is already an invitation structure in place. But I'd like to require an invitation in order to create an account, without linking the invite to an existing account.... an "invitation only" user registration process during a beta testing phase. Becasue there is so much "baked-in magic" in JSP's invitations, I'll likely resort to adding some sort of CouponCode table, and disallow a new user registration without a valid coupon code. (I could but probably won't include an email address in the CouponCode to tie coupon X to invitee X.)

As for your approach...

First, if you are using the gem 'devise_invitable' I wonder  if there is a conflict between the "invited_by_id" the gem uses vs the polymorphic "invited_by" that JSP already uses*, which has both an invited_by_id and invited_by_type.

Second, in your customer invitation process, is an invite from (e.g., linked to) an Account, or a User? I ask because it looked to me like JSP widely assumes invite is linked to an Account:

class AccountInvitation ....    belongs_to :account    ....

* Out of the box, the JSP migrations include:

class DeviseInvitableAddToUsers < ActiveRecord::Migration[5.2]
  def up
    safety_assured {
      change_table :users do |t|
        t.string :invitation_token
        t.datetime :invitation_created_at
        t.datetime :invitation_sent_at
        t.datetime :invitation_accepted_at
        t.integer :invitation_limit
        t.references :invited_by, polymorphic: true
        t.integer :invitations_count, default: 0
        t.index :invitations_count
        t.index :invitation_token, unique: true # for invitable
        t.index :invited_by_id
      end
    }
  end

  def down
    safety_assured {
      change_table :users do |t|
        t.remove_references :invited_by, polymorphic: true
        t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at
      end
    }
  end
end

Brandon Brown
Right now, my invites are for a user, but JSP seems to have the logic to auto create a linked default account when you create a new user (same flow as a normal sign up really). In fact, I can see JSP doing this in the rails console output, but it eventually breaks:

User Update (10.7ms)  UPDATE "users" SET "encrypted_password" = $1, "first_name" = $2, "last_name" = $3, "time_zone" = $4, "updated_at" = $5, "invitation_token" = $6, "invitation_accepted_at" = $7 WHERE "users"."id" = $8  [["encrypted_password", "$2a$11$aHPD8rLSpBKugVY1h7TFu.f/qJCHNYtj.WEEL7r2/.S5ITaHUtO2W"], ["first_name", "james"], ["last_name", "stiller"], ["time_zone", "Eastern Time (US & Canada)"], ["updated_at", "2021-06-07 00:06:51.858210"], ["invitation_token", nil], ["invitation_accepted_at", "2021-06-07 00:06:51.857320"], ["id", 28]]
  ↳ app/controllers/users/invitations_controller.rb:16:in `accept_resource'
  Account Load (0.3ms)  SELECT "accounts".* FROM "accounts" WHERE "accounts"."owner_id" = $1 AND "accounts"."personal" = $2 LIMIT $3  [["owner_id", 28], ["personal", true], ["LIMIT", 1]]
  ↳ app/models/concerns/user_accounts.rb:36:in `sync_personal_account_name'
  Account Exists? (0.8ms)  SELECT 1 AS one FROM "accounts" INNER JOIN "account_users" ON "accounts"."id" = "account_users"."account_id" WHERE "account_users"."user_id" = $1 LIMIT $2  [["user_id", 28], ["LIMIT", 1]]
  ↳ app/models/concerns/user_accounts.rb:25:in `create_default_account'
  AccountUser Exists? (0.6ms)  SELECT 1 AS one FROM "account_users" WHERE "account_users"."user_id" = $1 AND "account_users"."account_id" IS NULL LIMIT $2  [["user_id", 28], ["LIMIT", 1]]
  ↳ app/models/concerns/user_accounts.rb:29:in `create_default_account'
  Account Create (9.3ms)  INSERT INTO "accounts" ("name", "owner_id", "personal", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["name", "james stiller"], ["owner_id", 28], ["personal", true], ["created_at", "2021-06-07 00:06:51.913156"], ["updated_at", "2021-06-07 00:06:51.913156"]]
  ↳ app/models/concerns/user_accounts.rb:29:in `create_default_account'
  AccountUser Exists? (0.7ms)  SELECT 1 AS one FROM "account_users" WHERE "account_users"."user_id" = $1 AND "account_users"."account_id" = $2 LIMIT $3  [["user_id", 28], ["account_id", 38], ["LIMIT", 1]]
  ↳ app/models/concerns/user_accounts.rb:29:in `create_default_account'
  AccountUser Create (8.9ms)  INSERT INTO "account_users" ("account_id", "user_id", "roles", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["account_id", 38], ["user_id", 28], ["roles", "{\"admin\":true}"], ["created_at", "2021-06-07 00:06:51.925467"], ["updated_at", "2021-06-07 00:06:51.925467"]]
  ↳ app/models/concerns/user_accounts.rb:29:in `create_default_account'
  TRANSACTION (0.6ms)  ROLLBACK
  ↳ app/controllers/users/invitations_controller.rb:16:in `accept_resource'
Completed 500 Internal Server Error in 253ms (ActiveRecord: 47.6ms | Allocations: 34512)
NoMethodError (undefined method `update' for nil:NilClass):
  
app/models/concerns/user_accounts.rb:39:in `sync_personal_account_name'
app/controllers/users/invitations_controller.rb:14:in `accept_resource'

If I remove the `sync_personal_account_name` function, everything appears to work correctly, but I have no idea what the downstream effects of that are.
Donn Felker
I only allow invite only in my app, and the way that I did it was that I added a "allows_registration" field to the account. This is something each tenant (I have a multi-tenant app) can enable or disable themselves. The invite stuff still works with just a couple small changes.

In the registrations_controller.rb I added this: 

before_action :new_registration_allowed?, only: [:new, :create]
Which checks if you can register for a new account, and in that method I merely do this: 

def new_registration_allowed?
  # Send them back to the root path if no invite is present and sign_up is not allowed.
  redirect_to root_path unless policy(:account).sign_up? || account_invitation_present?
end

def account_invitation_present?
  AccountInvitation.find_by(token: params[:invite]).present?
end
The policy, is a  Pundit policy and that pundit policy looks like this: 

class AccountPolicy
  attr_reader :account_user, :account

  def initialize(account_user, account)
    @account_user = account_user
    @account = account
  end

  def sign_up?
    # turns off registration for all new tenants and customers 
    if Current.account
      Current.account.registration_allowed
    else
      # True in test because some jumpstart tests require the signup page to be visible
      # could be improved, but works for me
      Rails.env.test?
    end
  end
end

I then also use this policy to show the Sign Up links on the site in the views:

<% if policy(:account).sign_up? %> sign_up button <% end %>
Notifications
You’re not receiving notifications from this thread.
© 2022 GoRails, LLC