plutonium 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +36 -0
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/controllers/crud_actions.rb +21 -18
- data/lib/plutonium/resource/controllers/interactive_actions.rb +21 -25
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Claude Code Guide for Plutonium Development
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<script setup>
|
|
6
|
-
import { withBase } from 'vitepress'
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
# Claude Code Guide for Plutonium Development
|
|
10
|
-
|
|
11
|
-
This page provides comprehensive development guidance for building Plutonium applications effectively. This guide is designed to help AI assistants and developers understand the framework's patterns and best practices.
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
**Download the CLAUDE.md File**: <a :href="withBase('/CLAUDE.md')" target="_blank">📄 CLAUDE.md</a>
|
|
16
|
-
|
|
17
|
-
**Or download directly from your terminal**:
|
|
18
|
-
|
|
19
|
-
::: code-group
|
|
20
|
-
|
|
21
|
-
```bash [Unix/Linux/macOS/WSL]
|
|
22
|
-
curl -o CLAUDE.md https://radioactive-labs.github.io/plutonium-core/CLAUDE.md
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
```cmd [Windows]
|
|
26
|
-
curl -o CLAUDE.md https://radioactive-labs.github.io/plutonium-core/CLAUDE.md
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
:::
|
|
30
|
-
|
|
31
|
-
## Using This Guide
|
|
32
|
-
|
|
33
|
-
Claude Code uses CLAUDE.md files for project-specific context:
|
|
34
|
-
|
|
35
|
-
1. **Download the guide**: Right-click the link above and "Save As" to download the `CLAUDE.md` file
|
|
36
|
-
2. **Place in your project root** as `CLAUDE.md`
|
|
37
|
-
3. **Claude Code automatically loads** this file for project context
|
|
38
|
-
4. **Enhance with your own instructions** by adding project-specific details
|
|
39
|
-
|
|
40
|
-
The guide provides comprehensive patterns and examples for building Plutonium applications with AI assistance.
|
|
41
|
-
|
|
42
|
-
## What's Included
|
|
43
|
-
|
|
44
|
-
The CLAUDE.md guide contains comprehensive guidelines for:
|
|
45
|
-
|
|
46
|
-
### 🏗️ **Framework Architecture**
|
|
47
|
-
- Resource-oriented development patterns
|
|
48
|
-
- Package architecture (Feature & Portal packages)
|
|
49
|
-
- Component-based UI with Phlex
|
|
50
|
-
- Business logic through Interactions
|
|
51
|
-
|
|
52
|
-
### 📝 **Resource Development**
|
|
53
|
-
- **Auto-detection philosophy** - Field types are automatically detected from models
|
|
54
|
-
- **Definition patterns** - Only override when needed
|
|
55
|
-
- **Policy-based authorization** - Fine-grained permissions
|
|
56
|
-
- **Interaction-driven business logic** - Encapsulated operations
|
|
57
|
-
|
|
58
|
-
### 🔧 **Development Patterns**
|
|
59
|
-
- **Generator commands** for scaffolding
|
|
60
|
-
- **Authentication setup** with Rodauth
|
|
61
|
-
- **Multi-tenancy** with entity scoping
|
|
62
|
-
- **Query objects** for filtering and search
|
|
63
|
-
|
|
64
|
-
### 🎨 **UI Customization**
|
|
65
|
-
- **Component architecture** with Phlex
|
|
66
|
-
- **Custom display blocks** with `phlexi_tag`
|
|
67
|
-
- **Conditional rendering** with context awareness
|
|
68
|
-
- **Layout customization** patterns
|
|
69
|
-
|
|
70
|
-
### ⚡ **Best Practices**
|
|
71
|
-
- **Performance optimization** techniques
|
|
72
|
-
- **Security guidelines** and defaults
|
|
73
|
-
- **Code organization** principles
|
|
74
|
-
- **Development workflow** recommendations
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
# Deep Dive: Authorization
|
|
2
|
-
|
|
3
|
-
Plutonium provides a robust authorization system built on top of [Action Policy](https://actionpolicy.evilmartians.io/). It's designed to be secure by default while offering fine-grained control over every aspect of your application's access control.
|
|
4
|
-
|
|
5
|
-
## Core Principles
|
|
6
|
-
|
|
7
|
-
Authorization in Plutonium is handled by **Policy** classes. Every resource should have a corresponding policy that inherits from `Plutonium::Resource::Policy`.
|
|
8
|
-
|
|
9
|
-
A policy's primary job is to answer the question: "Can the current `user` perform this `action` on this `record`?"
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
13
|
-
# Can the user see a list of posts?
|
|
14
|
-
def index?
|
|
15
|
-
true # Everyone can see the list
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Can the user update this specific post?
|
|
19
|
-
def update?
|
|
20
|
-
# `record` is the post instance.
|
|
21
|
-
# `user` is the current authenticated user.
|
|
22
|
-
record.author == user || user.admin?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# ... other permissions
|
|
26
|
-
end
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Secure by Default: The Permission Chain
|
|
30
|
-
|
|
31
|
-
Plutonium policies are secure by default. If a permission is not explicitly granted, it's denied. This is achieved through a clear inheritance chain.
|
|
32
|
-
|
|
33
|
-
::: code-group
|
|
34
|
-
```ruby [Core Permissions]
|
|
35
|
-
# These are the base permissions.
|
|
36
|
-
# They both default to `false`. You MUST override them.
|
|
37
|
-
def create?
|
|
38
|
-
false
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def read?
|
|
42
|
-
false
|
|
43
|
-
end
|
|
44
|
-
```
|
|
45
|
-
```ruby [Derived Permissions]
|
|
46
|
-
# These permissions inherit from the core ones.
|
|
47
|
-
# You can override them for more granular control.
|
|
48
|
-
def update?
|
|
49
|
-
create?
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def destroy?
|
|
53
|
-
create?
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def index?
|
|
57
|
-
read?
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def show?
|
|
61
|
-
read?
|
|
62
|
-
end
|
|
63
|
-
```
|
|
64
|
-
:::
|
|
65
|
-
|
|
66
|
-
::: danger Always Define Core Permissions
|
|
67
|
-
Because `create?` and `read?` default to `false`, you must define them in your policy to grant any access. If `create?` is `false`, then `update?` and `destroy?` will also be `false` unless you explicitly override them.
|
|
68
|
-
:::
|
|
69
|
-
|
|
70
|
-
## Attribute & Association Permissions
|
|
71
|
-
|
|
72
|
-
Beyond actions, policies also control access to a resource's data at a granular level.
|
|
73
|
-
|
|
74
|
-
### Attribute Permissions
|
|
75
|
-
|
|
76
|
-
Attribute permissions control which fields a user can see or submit in a form. They follow a similar inheritance chain.
|
|
77
|
-
|
|
78
|
-
::: code-group
|
|
79
|
-
```ruby [Read Attributes]
|
|
80
|
-
# Controls which fields are returned for `index` and `show` actions.
|
|
81
|
-
def permitted_attributes_for_read
|
|
82
|
-
# By default, auto-detects all columns in development,
|
|
83
|
-
# but MUST be overridden for production.
|
|
84
|
-
end
|
|
85
|
-
```
|
|
86
|
-
```ruby [Create/Update Attributes]
|
|
87
|
-
# Controls which fields are allowed in `create` and `update` actions.
|
|
88
|
-
def permitted_attributes_for_create
|
|
89
|
-
# By default, auto-detects columns (minus some system ones)
|
|
90
|
-
# in development, but MUST be overridden for production.
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def permitted_attributes_for_update
|
|
94
|
-
# Inherits from `permitted_attributes_for_create` by default.
|
|
95
|
-
permitted_attributes_for_create
|
|
96
|
-
end
|
|
97
|
-
```
|
|
98
|
-
:::
|
|
99
|
-
|
|
100
|
-
::: warning Override in Production
|
|
101
|
-
The default auto-detection for attributes only works in development to speed up initial scaffolding. You **must** override `permitted_attributes_for_create` and `permitted_attributes_for_read` in your policies for them to work in production.
|
|
102
|
-
:::
|
|
103
|
-
|
|
104
|
-
### Association Permissions
|
|
105
|
-
|
|
106
|
-
By default, no associations are permitted. You must explicitly list which related resources can be included.
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
110
|
-
def permitted_associations
|
|
111
|
-
[:comments, :author]
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Scoping: Filtering Collections
|
|
117
|
-
|
|
118
|
-
A policy's `relation_scope` is used to filter down a collection of records to only what the current user should see. This is applied automatically on `index` pages.
|
|
119
|
-
|
|
120
|
-
::: code-group
|
|
121
|
-
```ruby [Simple Scope]
|
|
122
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
123
|
-
relation_scope do |relation|
|
|
124
|
-
if user.admin?
|
|
125
|
-
relation # Admins see all posts
|
|
126
|
-
else
|
|
127
|
-
# Others only see their own posts or published posts
|
|
128
|
-
relation.where(author: user).or(relation.where(published: true))
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
```
|
|
133
|
-
```ruby [Multi-Tenant Scope]
|
|
134
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
135
|
-
relation_scope do |relation|
|
|
136
|
-
# `super` applies the portal's entity scoping first
|
|
137
|
-
# e.g., `relation.associated_with(current_organization)`
|
|
138
|
-
relation = super(relation)
|
|
139
|
-
|
|
140
|
-
# Then, apply additional logic
|
|
141
|
-
if user.admin?
|
|
142
|
-
relation
|
|
143
|
-
else
|
|
144
|
-
relation.where(published: true)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
```
|
|
149
|
-
:::
|
|
150
|
-
|
|
151
|
-
## Authorization Context
|
|
152
|
-
|
|
153
|
-
Policies have access to a `context` object. By default, Plutonium provides two:
|
|
154
|
-
|
|
155
|
-
- **`user`**: The current authenticated user. This is **required**.
|
|
156
|
-
- **`entity_scope`**: The current portal's multi-tenancy record (e.g., the current `Organization`). This is optional.
|
|
157
|
-
|
|
158
|
-
You can add your own custom context objects for more complex scenarios.
|
|
159
|
-
|
|
160
|
-
::: details Adding Custom Context
|
|
161
|
-
Imagine you have a separate `Ability` system that you also want to check.
|
|
162
|
-
|
|
163
|
-
**1. Define the context in the Policy:**
|
|
164
|
-
```ruby
|
|
165
|
-
class PostPolicy < ResourcePolicy
|
|
166
|
-
authorize :ability, allow_nil: true
|
|
167
|
-
|
|
168
|
-
def promote?
|
|
169
|
-
# You can now use `ability` in your permission checks
|
|
170
|
-
user.admin? && ability&.can?(:promote, record)
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
**2. Provide the context from the Controller:**
|
|
176
|
-
```ruby
|
|
177
|
-
class PostsController < ResourceController
|
|
178
|
-
# This tells the policy how to find the `ability` object.
|
|
179
|
-
authorize :ability, through: :current_ability
|
|
180
|
-
|
|
181
|
-
private
|
|
182
|
-
|
|
183
|
-
def current_ability
|
|
184
|
-
# Your custom logic to find the ability object
|
|
185
|
-
Ability.new(user)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
```
|
|
189
|
-
:::
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# Deep Dive: Multitenancy
|
|
2
|
-
|
|
3
|
-
Plutonium is designed to make building sophisticated, multi-tenant applications straightforward. Multitenancy allows you to serve distinct groups of users (like different companies or teams) from a single application instance, ensuring each group's data is kept private and isolated.
|
|
4
|
-
|
|
5
|
-
This is achieved through a powerful feature called **Entity Scoping**, which automates data isolation, URL generation, and authorization with minimal setup.
|
|
6
|
-
|
|
7
|
-
::: tip What You'll Learn
|
|
8
|
-
- The core concept of Entity Scoping
|
|
9
|
-
- A step-by-step guide to configuring multitenancy
|
|
10
|
-
- The practical benefits of automatic data isolation and routing
|
|
11
|
-
- Advanced patterns for complex multi-tenant scenarios
|
|
12
|
-
- Best practices for security, testing, and performance
|
|
13
|
-
:::
|
|
14
|
-
|
|
15
|
-
## How Entity Scoping Works
|
|
16
|
-
|
|
17
|
-
Entity Scoping is the heart of Plutonium's multitenancy. Instead of manually filtering data in every controller and model, you declare a **Scoping Entity** for a user-facing **Portal**. This entity is typically a model like `Organization`, `Account`, or `Workspace`.
|
|
18
|
-
|
|
19
|
-
Once configured, Plutonium handles the rest automatically.
|
|
20
|
-
|
|
21
|
-
::: code-group
|
|
22
|
-
```ruby [Without Plutonium Scoping]
|
|
23
|
-
# Manual filtering is required everywhere
|
|
24
|
-
class PostsController < ApplicationController
|
|
25
|
-
def index
|
|
26
|
-
# Manually find the organization and scope posts
|
|
27
|
-
@organization = Organization.find(params[:organization_id])
|
|
28
|
-
@posts = @organization.posts
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def create
|
|
32
|
-
# Manually associate new records with the organization
|
|
33
|
-
@organization = Organization.find(params[:organization_id])
|
|
34
|
-
@post = @organization.posts.build(post_params)
|
|
35
|
-
# ...
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
```ruby [With Plutonium Scoping]
|
|
41
|
-
# Scoping is automatic and declarative
|
|
42
|
-
class PostsController < ResourceController
|
|
43
|
-
def index
|
|
44
|
-
# current_authorized_scope automatically returns posts scoped to the current organization
|
|
45
|
-
# When using path strategy: URL -> /organizations/123/posts
|
|
46
|
-
# When using subdomain strategy: URL -> /posts (on acme.yourapp.com)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def create
|
|
50
|
-
# resource_params automatically includes the current organization association
|
|
51
|
-
# Scoping works regardless of the strategy used
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
```
|
|
55
|
-
:::
|
|
56
|
-
|
|
57
|
-
## Setting Up Your Multi-Tenant Portal
|
|
58
|
-
|
|
59
|
-
Configuring multitenancy involves three main steps: defining your strategy, implementing it, and ensuring your models are correctly associated.
|
|
60
|
-
|
|
61
|
-
### 1. Configure Your Portal Engine
|
|
62
|
-
|
|
63
|
-
First, you tell your portal which entity to scope to and which strategy to use. This is done in the portal's `engine.rb` file using `scope_to_entity`.
|
|
64
|
-
|
|
65
|
-
A **Scoping Strategy** tells Plutonium how to identify the current tenant for each request.
|
|
66
|
-
|
|
67
|
-
::: code-group
|
|
68
|
-
```ruby [Path Strategy (Most Common)]
|
|
69
|
-
# packages/admin_portal/lib/engine.rb
|
|
70
|
-
# Tenant is identified by a URL parameter, e.g., /organizations/:organization_id
|
|
71
|
-
scope_to_entity Organization, strategy: :path
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
```ruby [Custom Strategy (Subdomain)]
|
|
75
|
-
# packages/customer_portal/lib/engine.rb
|
|
76
|
-
# Tenant is identified by the a custom strategy that checks the subdomain, e.g., acme.yourapp.com
|
|
77
|
-
scope_to_entity Organization, strategy: :current_organization
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
```ruby [Custom Parameter Key]
|
|
81
|
-
# packages/client_portal/lib/engine.rb
|
|
82
|
-
# Same as :path, but with a custom parameter name.
|
|
83
|
-
scope_to_entity Client,
|
|
84
|
-
strategy: :path,
|
|
85
|
-
param_key: :client_slug # URL -> /clients/:client_slug
|
|
86
|
-
```
|
|
87
|
-
:::
|
|
88
|
-
|
|
89
|
-
You can also use any custom name for your strategy method (the method name becomes the strategy).
|
|
90
|
-
|
|
91
|
-
### 2. Implement the Strategy Method
|
|
92
|
-
|
|
93
|
-
If you use any strategy other than `:path`, you must implement a controller method that returns the current tenant object. The method name must exactly match the strategy name.
|
|
94
|
-
|
|
95
|
-
This logic typically lives in your portal's base controller concern.
|
|
96
|
-
|
|
97
|
-
::: code-group
|
|
98
|
-
```ruby [Subdomain Strategy]
|
|
99
|
-
# packages/customer_portal/app/controllers/customer_portal/concerns/controller.rb
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
# Method name :current_organization matches the strategy
|
|
103
|
-
def current_organization
|
|
104
|
-
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
|
|
105
|
-
rescue ActiveRecord::RecordNotFound
|
|
106
|
-
redirect_to root_path, error: "Invalid organization subdomain"
|
|
107
|
-
end
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
```ruby [Session-Based Strategy]
|
|
111
|
-
# packages/internal_portal/app/controllers/internal_portal/concerns/controller.rb
|
|
112
|
-
# In engine.rb: scope_to_entity Workspace, strategy: :current_workspace
|
|
113
|
-
private
|
|
114
|
-
|
|
115
|
-
def current_workspace
|
|
116
|
-
return @current_workspace if defined?(@current_workspace)
|
|
117
|
-
|
|
118
|
-
workspace_id = session[:workspace_id] || params[:workspace_id]
|
|
119
|
-
@current_workspace = current_user.workspaces.find(workspace_id)
|
|
120
|
-
|
|
121
|
-
session[:workspace_id] = @current_workspace.id # Remember for next request
|
|
122
|
-
@current_workspace
|
|
123
|
-
rescue ActiveRecord::RecordNotFound
|
|
124
|
-
redirect_to workspace_selection_path, error: "Please select a workspace"
|
|
125
|
-
end
|
|
126
|
-
```
|
|
127
|
-
:::
|
|
128
|
-
|
|
129
|
-
### 3. Connect Your Models
|
|
130
|
-
|
|
131
|
-
Plutonium needs to understand how your resources relate to the scoping entity. It automatically discovers these relationships in three ways:
|
|
132
|
-
|
|
133
|
-
::: code-group
|
|
134
|
-
```ruby [1. Direct Association (Preferred)]
|
|
135
|
-
# The model belongs directly to the scoping entity.
|
|
136
|
-
class Post < ApplicationRecord
|
|
137
|
-
belongs_to :organization # Direct link
|
|
138
|
-
end
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
```ruby [2. Indirect Association]
|
|
142
|
-
# The model belongs to another model that has the direct link.
|
|
143
|
-
# Plutonium automatically follows the chain: Comment -> Post -> Organization
|
|
144
|
-
class Comment < ApplicationRecord
|
|
145
|
-
belongs_to :post
|
|
146
|
-
has_one :organization, through: :post # Indirect link
|
|
147
|
-
end
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
```ruby [3. Custom Scope (For Complex Cases)]
|
|
151
|
-
# For complex relationships, you can define an explicit scope.
|
|
152
|
-
# The scope name must be `associated_with_#{scoping_entity_name}`.
|
|
153
|
-
class Invoice < ApplicationRecord
|
|
154
|
-
belongs_to :customer
|
|
155
|
-
|
|
156
|
-
scope :associated_with_organization, ->(organization) do
|
|
157
|
-
joins(customer: :organization_memberships)
|
|
158
|
-
.where(organization_memberships: { organization_id: organization.id })
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
```
|
|
162
|
-
:::
|
|
163
|
-
|
|
164
|
-
## The Benefits in Practice
|
|
165
|
-
|
|
166
|
-
With this setup complete, you gain several powerful features across your portal.
|
|
167
|
-
|
|
168
|
-
### Tenant-Aware Routing
|
|
169
|
-
|
|
170
|
-
Your application's URLs are automatically transformed to include the tenant context, and Plutonium's URL helpers adapt accordingly.
|
|
171
|
-
|
|
172
|
-
- **URL Transformation:** Routes like `/posts` and `/posts/123` become `/:organization_id/posts` and `/:organization_id/posts/123`.
|
|
173
|
-
- **Automatic URL Generation:** The `resource_url_for` helper automatically includes the current tenant in all generated URLs, so links and forms work without any changes.
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
# Both of these helpers are automatically aware of the current tenant.
|
|
177
|
-
resource_url_for(Post) # => "/organizations/456/posts"
|
|
178
|
-
form_with model: @post # action -> "/organizations/456/posts/123"
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Secure Data Scoping
|
|
182
|
-
|
|
183
|
-
All data access is automatically and securely filtered to the current tenant.
|
|
184
|
-
|
|
185
|
-
- **Query Scoping:** A query like `Post.all` is automatically converted to `current_scoped_entity.posts`. This prevents accidental data leaks.
|
|
186
|
-
- **Record Scoping:** When fetching a single record (e.g., for `show` or `edit`), Plutonium ensures it belongs to the current tenant. If not, it raises an `ActiveRecord::RecordNotFound` error, just as if the record didn't exist.
|
|
187
|
-
|
|
188
|
-
### Integrated Authorization
|
|
189
|
-
|
|
190
|
-
The current `entity_scope` is seamlessly passed to your authorization policies, allowing for fine-grained, tenant-aware rules.
|
|
191
|
-
|
|
192
|
-
```ruby
|
|
193
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
194
|
-
# `entity_scope` is automatically available in all policy methods.
|
|
195
|
-
authorize :entity_scope, allow_nil: true
|
|
196
|
-
|
|
197
|
-
def update?
|
|
198
|
-
# A user can only update a post if it belongs to their current tenant
|
|
199
|
-
# AND they are the author of the post.
|
|
200
|
-
record.organization == entity_scope && record.author == user
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
relation_scope do |relation|
|
|
204
|
-
# `super` automatically applies the base entity scoping.
|
|
205
|
-
relation = super(relation)
|
|
206
|
-
|
|
207
|
-
# Add more logic: Admins can see all posts within their organization,
|
|
208
|
-
# but others can only see published posts.
|
|
209
|
-
user.admin? ? relation : relation.where(published: true)
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## Security Best Practices
|
|
215
|
-
|
|
216
|
-
Securing a multi-tenant application is critical. While Plutonium provides strong defaults, you must ensure your implementation is secure.
|
|
217
|
-
|
|
218
|
-
::: warning Always Validate Tenant Access
|
|
219
|
-
A user might belong to multiple tenants. It's crucial to verify that the logged-in user has permission to access the tenant specified in the URL. Failure to do so could allow a user to see data from another organization they don't belong to.
|
|
220
|
-
:::
|
|
221
|
-
|
|
222
|
-
```ruby
|
|
223
|
-
# ✅ Good: Proper tenant validation
|
|
224
|
-
# In your custom strategy method or a before_action:
|
|
225
|
-
private
|
|
226
|
-
|
|
227
|
-
def current_organization
|
|
228
|
-
@current_organization ||= begin
|
|
229
|
-
# Find the organization from the URL
|
|
230
|
-
organization = Organization.find(params[:organization_id])
|
|
231
|
-
|
|
232
|
-
# CRITICAL: Verify the current user is a member of that organization
|
|
233
|
-
unless current_user.organizations.include?(organization)
|
|
234
|
-
raise ActionPolicy::Unauthorized, "Access denied to organization"
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
organization
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# ❌ Dangerous: No access validation
|
|
242
|
-
def current_organization
|
|
243
|
-
# This allows ANY authenticated user to access ANY organization's data
|
|
244
|
-
# simply by changing the ID in the URL.
|
|
245
|
-
Organization.find(params[:organization_id])
|
|
246
|
-
end
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## Advanced Patterns
|
|
250
|
-
|
|
251
|
-
Plutonium's scoping is flexible enough to handle more complex scenarios.
|
|
252
|
-
|
|
253
|
-
- **Multi-Level Tenancy:** For hierarchical tenancy (e.g., Company -> Department), you can apply the primary scope at the engine level and add secondary scoping logic inside your policies' `relation_scope`.
|
|
254
|
-
- **Cross-Tenant Data Access:** For resources that can be shared, define a custom `associated_with_...` scope that includes both shared records and records belonging to the current tenant.
|
|
255
|
-
- **Tenant Switching:** Build a controller that allows users to change their active tenant by updating a `session` key, then use a session-based scoping strategy to read it.
|
|
256
|
-
- **API Multitenancy:** Create a custom scoping strategy (e.g., `:api_tenant`) that authenticates and identifies the tenant based on an API key or JWT from the request headers.
|