Skip to content

Commit

Permalink
add mutitenancy
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianthedev committed Feb 23, 2024
1 parent 0d175ec commit b3506e6
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const config = {
{text: "Localization (I18n)", link: "/3.0/localization"},
{text: "Branding", link: "/3.0/branding"},
{text: "Routing", link: "/3.0/routing"},
{text: "Multitenancy", link: "/3.0/multitenancy"},
],
},
{
Expand Down
8 changes: 8 additions & 0 deletions docs/3.0/avo-application-controller.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# `Avo::ApplicationController`

:::tip
To safely extend Avo's `ApplicationController` please use the [`extend_controllers_with`](./customization#extend_controllers_with) configuration option.
:::

## On extending the `ApplicationController`

You may sometimes want to add functionality to Avo's `ApplicationController`. That functionality may be setting attributes to `Current` or multi-tenancy scenarios.
Expand Down Expand Up @@ -63,3 +67,7 @@ With this technique, the `multitenancy_detector` method and its `before_action`
:::info
If you'd like to add a `before_action` before all of Avo's before actions, use `prepend_before_action` instead. That will run that code first and enable you to set an account or do something early on.
:::

**Related:**
- [Multitenancy](./multitenancy)
- [`extend_controllers_with`](./customization#extend_controllers_with)
12 changes: 12 additions & 0 deletions docs/3.0/avo-current.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ view_context.link_to "Avo", "https://avohq.io"
The `locale` of the app.
:::

:::option `tenant_id`
You can set the `tenant_id` for the current request.
:::

:::option `tenant`
You can set the `tenant` for the current request.
:::

**Related:**
- [Multitenancy](./multitenancy)
- [`extend_controllers_with`](./customization#extend_controllers_with)

2 changes: 1 addition & 1 deletion docs/3.0/common/file_other_common.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden.
:::

Related:
**Related:**
- [Attachment pundit policies](./../authorization.html#attachments)

<!-- ## Deprecated options
Expand Down
32 changes: 32 additions & 0 deletions docs/3.0/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,35 @@ config.logger = -> {
file_logger
}
```

::::option `extend_controllers_with`
The `extend_controllers_with` configuration option enables you to add methods or hooks to Avo's `ApplicationController` in the main `avo` gem and all the other ones (`avo-pro`, `avo-dashboards`, etc.).

All you need to do is to pass an array with the concerns you need to have included. The concerns should be strings as we later constantize them for you.

::: code-group
```ruby [config/initializers/avo.rb]{2}
Avo.configure do |config|
config.extend_controllers_with = ["Multitenancy"]
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern

included do
prepend_before_action :set_tenant
end

def set_tenant
Avo::Current.tenant_id = params[:tenant_id]
Avo::Current.tenant = Account.find params[:tenant_id]
end
end
```
:::
::::

Related:
- [Multitenancy](./multitenancy)
- [`Avo::Current`](./avo-current#tenant_id)
3 changes: 1 addition & 2 deletions docs/3.0/field-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,7 @@ You can add this property on `Id`, `Text`, and `Gravatar` fields.

Optionally you can enable the global config `id_links_to_resource`. More on that on the [id links to resource docs page](./customization.html#id-links-to-resource).

Related:

**Related:**
- [ID links to resource](./customization#id-links-to-resource)
- [Resource controls on the left side](./customization#resource-controls-on-the-left-side)

Expand Down
3 changes: 1 addition & 2 deletions docs/3.0/fields/password.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ The `Password` field renders a `input[type="password"]` element for that field.
field :password, as: :password
```

Related:

**Related:**
- [Devise password optional](./../resources#devise-password-optional)

132 changes: 132 additions & 0 deletions docs/3.0/multitenancy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Multitenancy

Multitenancy is a very talked-about subject. We're not going to go very deep into how to achieve it on the database level, but will talk a little bit about how it's supported in Avo.

## Breakdown

Usually, with multitenancy you add a new layer just one level below authentication. You don't have just a user to think about, but now that user might act on the behalf of a tenant. That tenant can be an `Account` or a `Team`, or any other model you design in your database.

So now, the mission is to pinpoint which tenant is the user acting for. Because Avo has such an integrated experience and we use our own `ApplicationController`, you might think it's difficult to add that layer, when in fact it's really not. There are a couple of steps to do.

:::info
We'll use the `foo` tenant id from now on.
:::

## Route-based tenancy

There are a couple of strategies here, but the a common one is to use route-based tenancy. That means that your user uses a URL like `https://example.com/foo/` and the app should know to scope everything to that `foo` tenant.

We need to do a few things:

#### 1. Disable automatic Avo engine mounting

_Do this step only if you use other Avo gems (`avo-pro`, `avo-advanced`, etc.)_

Avo will automatically mount it's engines unless you tell it otherwise, which is what we'll do now.
:::code-group
```ruby [config/avo.rb]{3}
Avo.configure do |config|
# Disable automatic engine mounting
config.mount_avo_engines = false

# other configuration
end
```
:::

**Related:**
- [Avo's Engines](./routing#avo-s-engines)

#### 2. Set the proper routing pattern

:::code-group
```ruby [config/routes.rb]
# Mount Avo and it's engines under the `tenant_id` scope
scope "/:tenant_id" do
mount Avo::Engine, at: Avo.configuration.root_path

scope Avo.configuration.root_path do
instance_exec(&Avo.mount_engines)
end
```
:::


#### 3. Set the tenant for each request

:::code-group
```ruby [config/initializers/avo.rb]{2}
Avo.configure do |config|
config.extend_controllers_with = ["Multitenancy"]
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern

included do
prepend_before_action :set_tenant
end

def set_tenant
Avo::Current.tenant_id = params[:tenant_id]
Avo::Current.tenant = Account.find params[:tenant_id]
end
end
```
:::

## Session-based tenancy

Using a session-based tenancy strategy is a bit simpler as we don't meddle with the routing.

:::warning
The code below shows how it's possible to do session-based multitenancy but your use-case or model names may vary a bit.
:::

We need to do a few things:

#### 1. Set the tenant for each request
:::code-group
```ruby [config/initializers/avo.rb]{2}
Avo.configure do |config|
config.extend_controllers_with = ["Multitenancy"]
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern

included do
prepend_before_action :set_tenant
end

def set_tenant
Avo::Current.tenant = Account.find session[:tenant_id] || current_user.accounts.first
end
end
```
:::

#### 2. Add an account switcher

Somewhere in a view on a navbar or sidebar add an account switcher.

:::code-group
```erb [app/views/avo/session_switcher.html.erb]
<% current_user.accounts.each do |account| %>
<%= link_to account.name, switch_account_path(account.id), class: class_names({"underline": session[:tenant_id].to_s == account.id.to_s}), data: {turbo_method: :put} %>
<% end %>
```

```ruby [app/controllers/avo/switch_accounts_controller.rb]
class Avo::SwitchAccountsController < Avo::ApplicationController
def update
# set the new tenant in session
session[:tenant_id] = params[:id]

redirect_back fallback_location: root_path
end
end
```
:::
4 changes: 4 additions & 0 deletions docs/3.0/recipes/multitenancy.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Use route-level multitenancy

:::tip
We published a new [multitenancy guide](./../multitenancy).
:::

Multitenancy is not a far-fetched concept, and you might need it when you reach a certain level with your app. Avo is ready to handle that.

This guide will show you **one way** of achieving that, but if can be changed if you have different needs.
Expand Down

0 comments on commit b3506e6

Please sign in to comment.