Search code examples
ruby-on-railsrails-activerecordmulti-tenantuser-accounts

Ruby on Rails - Scoping resources by current_account


I’m new to ruby on rails and I’m a bit stuck with what the best next step is in a multi-tenancy application I’m building.

Basically I want to scope resources by account_id, so I have created a method and helper called current_account in my accounts base_controller.

However, the tutorial I’m following scopes current_account by subdomain which I do not want to do. So I need a way to identify the current user’s account_id so that that I can have a resource variable @contact = current_account.contacts."all".

Do I need to make a new association between the user and account model so that I can use the current_user helper to define the current account id or is there a better way? If so, what is the best way to do this?

Background The first user who signs up becomes the account owner. Account owners can then invite other users to the account. I'm using the devise gem. Resources are scoped by account so that only users linked to an account can see the records belonging to that account.

Base Controller

module Accounts
  class BaseController < ApplicationController
    def current_account
      @current_account ||= ?????
    end
    helper_method :current_account

    def owner?
      current_account.owner == current_user
    end
    helper_method :owner?
  end
end

Contacts (my resource) Controller

module Accounts
  class ContactsController < Accounts::BaseController
    def index
        @contact = current_account.contacts.all
    end
  end
end

Account Model

class Account < ActiveRecord::Base
  belongs_to :owner, class_name: "User"
  accepts_nested_attributes_for :owner

  validates :subdomain, presence: true, uniqueness: true

  has_many :contacts
  has_many :invitations
  has_many :memberships
  has_many :users, through: :memberships
end

Invitation Model

class Invitation < ActiveRecord::Base
  belongs_to :account
  validates :email, presence: true
end

Membership Model

class Membership < ActiveRecord::Base
  belongs_to :account
  belongs_to :user
end

User Model

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable
end

Routes

Rails.application.routes.draw do
  devise_for :users
  scope module: "accounts" do
  resources 'dashboard'
  resources 'contacts'
  resources :invitations, only: [:new, :create] do 
    member do
      get :accept
      patch :accepted
    end
  end
  resources :users, only: [:index, :destroy]
end

Schema

ActiveRecord::Schema.define(version: 20170124002015) do

  create_table "accounts", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "owner_id"
    t.string   "subdomain"
  end

  add_index "accounts", ["subdomain"], name: "index_accounts_on_subdomain"

  create_table "contacts", force: true do |t|
    t.string   "first_name"
    t.string   "last_name"
    t.string   "phone"
    t.string   "email"
    t.text     "comments"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "account_id"
  end

  add_index "contacts", ["account_id"], name: "index_contacts_on_account_id"

  create_table "invitations", force: true do |t|
    t.string   "email"
    t.integer  "account_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "token"
  end

  add_index "invitations", ["account_id"], name: "index_invitations_on_account_id"
  add_index "invitations", ["token"], name: "index_invitations_on_token"

  create_table "memberships", force: true do |t|
    t.integer  "account_id"
    t.integer  "user_id"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "memberships", ["account_id"], name: "index_memberships_on_account_id"
  add_index "memberships", ["user_id"], name: "index_memberships_on_user_id"

  create_table "users", force: true do |t|
    t.string   "email",                  default: "", null: false
    t.string   "encrypted_password",     default: "", null: false
    t.string   "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer  "sign_in_count",          default: 0,  null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string   "current_sign_in_ip"
    t.string   "last_sign_in_ip"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "users", ["email"], name: "index_users_on_email", unique: true
  add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

end

Solution

  • There are two possible associations between users and accounts:

    1. users have many accounts
    2. users belong to an account

    In the first case, the tenant cannot be set from the current_user, because it's unclear which account should be used as the current tenant. The membership table in the schema.rb indicates this is the approach taken by the tutorial you mentioned. Loading the account by subdomain helps specify which account shall be used as the current tenant.

    In the second case, every user has just one account. Users get an account_id, the membership table becomes obsolete, and you can load the current tenant like so:

    def current_account
      @current_account ||= current_user.account
    end
    

    Do I need to make a new association between the user and account model so that I can use the current_user helper to define the current account id or is there a better way? If so, what is the best way to do this?

    It seems to me that you want to take the second approach, which requires that an account has_many users and a user belongs_to an account.