plutonium 0.33.1 → 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 +32 -1
- 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/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
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# Recipe: SaaS Application
|
|
2
|
+
|
|
3
|
+
Build a multi-tenant SaaS application with organizations, team management, and role-based access.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This recipe covers:
|
|
8
|
+
- Multi-tenant data isolation
|
|
9
|
+
- Organization and team management
|
|
10
|
+
- Role-based permissions
|
|
11
|
+
- Invitation system
|
|
12
|
+
- Subscription tiers
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
packages/
|
|
18
|
+
├── core/ # Shared: users, organizations
|
|
19
|
+
├── projects/ # Feature: project management
|
|
20
|
+
├── billing/ # Feature: subscriptions
|
|
21
|
+
├── app_portal/ # Main application interface
|
|
22
|
+
└── admin_portal/ # Super admin interface
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Core Models
|
|
26
|
+
|
|
27
|
+
### Organization
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
module Core
|
|
31
|
+
class Organization < Core::ResourceRecord
|
|
32
|
+
has_many :memberships, dependent: :destroy
|
|
33
|
+
has_many :users, through: :memberships
|
|
34
|
+
has_many :projects, class_name: 'Projects::Project'
|
|
35
|
+
|
|
36
|
+
validates :name, presence: true
|
|
37
|
+
validates :slug, presence: true, uniqueness: true
|
|
38
|
+
|
|
39
|
+
before_validation :generate_slug, on: :create
|
|
40
|
+
|
|
41
|
+
def owner
|
|
42
|
+
memberships.find_by(role: 'owner')&.user
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def generate_slug
|
|
48
|
+
self.slug ||= name&.parameterize
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Membership
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
module Core
|
|
58
|
+
class Membership < Core::ResourceRecord
|
|
59
|
+
belongs_to :organization
|
|
60
|
+
belongs_to :user
|
|
61
|
+
|
|
62
|
+
ROLES = %w[owner admin member viewer].freeze
|
|
63
|
+
|
|
64
|
+
validates :role, presence: true, inclusion: { in: ROLES }
|
|
65
|
+
validates :user_id, uniqueness: { scope: :organization_id }
|
|
66
|
+
|
|
67
|
+
scope :admins, -> { where(role: %w[owner admin]) }
|
|
68
|
+
|
|
69
|
+
def admin?
|
|
70
|
+
role.in?(%w[owner admin])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def owner?
|
|
74
|
+
role == 'owner'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### User Extensions
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class User < ApplicationRecord
|
|
84
|
+
has_many :memberships, class_name: 'Core::Membership'
|
|
85
|
+
has_many :organizations, through: :memberships
|
|
86
|
+
|
|
87
|
+
def role_in(organization)
|
|
88
|
+
memberships.find_by(organization: organization)&.role
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def admin_of?(organization)
|
|
92
|
+
memberships.admins.exists?(organization: organization)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def member_of?(organization)
|
|
96
|
+
memberships.exists?(organization: organization)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Multi-Tenant Setup
|
|
102
|
+
|
|
103
|
+
### Project Model
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
module Projects
|
|
107
|
+
class Project < Core::ResourceRecord
|
|
108
|
+
belongs_to :organization, class_name: 'Core::Organization'
|
|
109
|
+
belongs_to :creator, class_name: 'User'
|
|
110
|
+
has_many :tasks, dependent: :destroy
|
|
111
|
+
|
|
112
|
+
validates :name, presence: true
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Portal Engine
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
module AppPortal
|
|
121
|
+
class Engine < Rails::Engine
|
|
122
|
+
include Plutonium::Portal::Engine
|
|
123
|
+
|
|
124
|
+
config.after_initialize do
|
|
125
|
+
# Scope all data to current organization
|
|
126
|
+
scope_to_entity Core::Organization
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Portal Authentication
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# packages/app_portal/app/controllers/app_portal/concerns/controller.rb
|
|
136
|
+
module AppPortal
|
|
137
|
+
module Concerns
|
|
138
|
+
module Controller
|
|
139
|
+
extend ActiveSupport::Concern
|
|
140
|
+
include Plutonium::Portal::Controller
|
|
141
|
+
include Plutonium::Auth::Rodauth(:user)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Organization Context
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
module AppPortal
|
|
151
|
+
class ResourceController < Plutonium::Portal::ResourceController
|
|
152
|
+
before_action :set_current_organization
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def set_current_organization
|
|
157
|
+
@current_organization = current_user.organizations.find_by!(
|
|
158
|
+
slug: params[:org_slug]
|
|
159
|
+
)
|
|
160
|
+
rescue ActiveRecord::RecordNotFound
|
|
161
|
+
redirect_to select_organization_path
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def current_organization
|
|
165
|
+
@current_organization
|
|
166
|
+
end
|
|
167
|
+
helper_method :current_organization
|
|
168
|
+
|
|
169
|
+
def current_membership
|
|
170
|
+
@current_membership ||= current_user.memberships.find_by(
|
|
171
|
+
organization: current_organization
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
helper_method :current_membership
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Role-Based Policies
|
|
180
|
+
|
|
181
|
+
### Project Policy
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
module Projects
|
|
185
|
+
class ProjectPolicy < Plutonium::Resource::Policy
|
|
186
|
+
def read?
|
|
187
|
+
member?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def create?
|
|
191
|
+
admin?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def update?
|
|
195
|
+
admin? || creator?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def destroy?
|
|
199
|
+
owner?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def permitted_attributes_for_create
|
|
203
|
+
[:name, :description]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def permitted_attributes_for_update
|
|
207
|
+
if admin?
|
|
208
|
+
[:name, :description, :status, :archived]
|
|
209
|
+
else
|
|
210
|
+
[:description]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Entity scope handles organization filtering automatically
|
|
215
|
+
# Add role-based filtering here
|
|
216
|
+
def relation_scope(relation)
|
|
217
|
+
if viewer?
|
|
218
|
+
relation.where(archived: false)
|
|
219
|
+
else
|
|
220
|
+
relation
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def membership
|
|
227
|
+
@membership ||= context[:membership] ||
|
|
228
|
+
user.memberships.find_by(organization: record.organization)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def member?
|
|
232
|
+
membership.present?
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def viewer?
|
|
236
|
+
membership&.role == 'viewer'
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def admin?
|
|
240
|
+
membership&.admin?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def owner?
|
|
244
|
+
membership&.owner?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def creator?
|
|
248
|
+
record.creator_id == user.id
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Invitation System
|
|
255
|
+
|
|
256
|
+
### Invitation Model
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
module Core
|
|
260
|
+
class Invitation < Core::ResourceRecord
|
|
261
|
+
belongs_to :organization
|
|
262
|
+
belongs_to :inviter, class_name: 'User'
|
|
263
|
+
belongs_to :user, optional: true # Set when accepted
|
|
264
|
+
|
|
265
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
266
|
+
validates :role, presence: true, inclusion: { in: Membership::ROLES - ['owner'] }
|
|
267
|
+
validates :token, presence: true, uniqueness: true
|
|
268
|
+
|
|
269
|
+
before_validation :generate_token, on: :create
|
|
270
|
+
|
|
271
|
+
scope :pending, -> { where(accepted_at: nil, expired_at: nil) }
|
|
272
|
+
|
|
273
|
+
def pending?
|
|
274
|
+
accepted_at.nil? && !expired?
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def expired?
|
|
278
|
+
expired_at.present? || created_at < 7.days.ago
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def accept!(user)
|
|
282
|
+
transaction do
|
|
283
|
+
update!(accepted_at: Time.current, user: user)
|
|
284
|
+
organization.memberships.create!(user: user, role: role)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def generate_token
|
|
291
|
+
self.token ||= SecureRandom.urlsafe_base64(32)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Invite User Interaction
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
module Core
|
|
301
|
+
class InviteUser < Plutonium::Interaction::Base
|
|
302
|
+
presents model_class: Organization
|
|
303
|
+
presents label: "Invite Team Member"
|
|
304
|
+
|
|
305
|
+
attribute :email, :string
|
|
306
|
+
attribute :role, :string
|
|
307
|
+
|
|
308
|
+
validates :email, presence: true
|
|
309
|
+
validates :role, presence: true, inclusion: { in: Membership::ROLES - ['owner'] }
|
|
310
|
+
validate :not_already_member
|
|
311
|
+
|
|
312
|
+
def execute
|
|
313
|
+
invitation = resource.invitations.create!(
|
|
314
|
+
email: email,
|
|
315
|
+
role: role,
|
|
316
|
+
inviter: context[:user]
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
InvitationMailer.invite(invitation).deliver_later
|
|
320
|
+
|
|
321
|
+
succeed(resource)
|
|
322
|
+
.with_message("Invitation sent to #{email}")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
def not_already_member
|
|
328
|
+
if resource.users.exists?(email: email)
|
|
329
|
+
errors.add(:email, "is already a member")
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Organization Switcher
|
|
337
|
+
|
|
338
|
+
### Routes
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# config/routes.rb
|
|
342
|
+
Rails.application.routes.draw do
|
|
343
|
+
get '/org/select', to: 'organizations#select', as: :select_organization
|
|
344
|
+
post '/org/switch/:id', to: 'organizations#switch', as: :switch_organization
|
|
345
|
+
|
|
346
|
+
scope '/:org_slug' do
|
|
347
|
+
mount AppPortal::Engine, at: '/app'
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Controller
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
class OrganizationsController < ApplicationController
|
|
356
|
+
before_action :authenticate_user!
|
|
357
|
+
|
|
358
|
+
def select
|
|
359
|
+
@organizations = current_user.organizations
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def switch
|
|
363
|
+
organization = current_user.organizations.find(params[:id])
|
|
364
|
+
session[:current_organization_id] = organization.id
|
|
365
|
+
redirect_to app_root_path(org_slug: organization.slug)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Definitions
|
|
371
|
+
|
|
372
|
+
### Organization Definition
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
module Core
|
|
376
|
+
class OrganizationDefinition < Plutonium::Resource::Definition
|
|
377
|
+
field :name
|
|
378
|
+
field :slug, readonly: true
|
|
379
|
+
field :logo, as: :file, accept: "image/*"
|
|
380
|
+
|
|
381
|
+
column :name, sortable: true
|
|
382
|
+
column :members_count do |org|
|
|
383
|
+
org.memberships.count
|
|
384
|
+
end
|
|
385
|
+
column :created_at
|
|
386
|
+
|
|
387
|
+
association :memberships, fields: [:user, :role, :created_at]
|
|
388
|
+
|
|
389
|
+
action :invite, interaction: InviteUser
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Membership Definition
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
module Core
|
|
398
|
+
class MembershipDefinition < Plutonium::Resource::Definition
|
|
399
|
+
field :user
|
|
400
|
+
field :role, as: :select, collection: Membership::ROLES
|
|
401
|
+
|
|
402
|
+
column :user
|
|
403
|
+
column :role
|
|
404
|
+
column :created_at
|
|
405
|
+
|
|
406
|
+
action :change_role, interaction: ChangeMemberRole
|
|
407
|
+
action :remove, interaction: RemoveMember, color: :danger
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Subscription Integration
|
|
413
|
+
|
|
414
|
+
### Plan Model
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
module Billing
|
|
418
|
+
class Plan < Core::ResourceRecord
|
|
419
|
+
has_many :subscriptions
|
|
420
|
+
|
|
421
|
+
validates :name, presence: true
|
|
422
|
+
validates :price_cents, presence: true
|
|
423
|
+
validates :max_projects, presence: true
|
|
424
|
+
validates :max_members, presence: true
|
|
425
|
+
|
|
426
|
+
has_cents :price
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Subscription Checks
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
module Core
|
|
435
|
+
class Organization < Core::ResourceRecord
|
|
436
|
+
has_one :subscription, class_name: 'Billing::Subscription'
|
|
437
|
+
|
|
438
|
+
def can_add_project?
|
|
439
|
+
projects.count < subscription.plan.max_projects
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def can_add_member?
|
|
443
|
+
memberships.count < subscription.plan.max_members
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# In policy
|
|
449
|
+
def create?
|
|
450
|
+
admin? && record.organization.can_add_project?
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Usage
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
# Create packages
|
|
458
|
+
rails generate pu:pkg:package core
|
|
459
|
+
rails generate pu:pkg:package projects
|
|
460
|
+
rails generate pu:pkg:package billing
|
|
461
|
+
rails generate pu:pkg:portal app
|
|
462
|
+
|
|
463
|
+
# Generate models
|
|
464
|
+
rails generate pu:res:scaffold Organization name:string slug:string --package core
|
|
465
|
+
rails generate pu:res:scaffold Membership role:string organization:belongs_to user:belongs_to --package core
|
|
466
|
+
rails generate pu:res:scaffold Project name:string organization:belongs_to --package projects
|
|
467
|
+
|
|
468
|
+
# Connect to portal
|
|
469
|
+
rails generate pu:res:conn Project --package projects --portal app
|
|
470
|
+
|
|
471
|
+
rails db:migrate
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Security Checklist
|
|
475
|
+
|
|
476
|
+
- [ ] All models scoped to organization
|
|
477
|
+
- [ ] Policies check membership
|
|
478
|
+
- [ ] Invitations expire
|
|
479
|
+
- [ ] Role changes audited
|
|
480
|
+
- [ ] Owner cannot be removed
|
|
481
|
+
- [ ] Data isolated in queries
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
Welcome to Plutonium! This guide will help you get up and running quickly.
|
|
4
|
+
|
|
5
|
+
## What You'll Learn
|
|
6
|
+
|
|
7
|
+
- How to install Plutonium in a new or existing Rails application
|
|
8
|
+
- The basic concepts behind Plutonium's architecture
|
|
9
|
+
- How to create your first resource and connect it to a portal
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before you begin, make sure you have:
|
|
14
|
+
|
|
15
|
+
- **Ruby 3.2+** installed
|
|
16
|
+
- **Rails 7.1+** (Rails 8 recommended)
|
|
17
|
+
- **Node.js 18+** (for asset compilation)
|
|
18
|
+
- Basic familiarity with Ruby on Rails
|
|
19
|
+
|
|
20
|
+
## Choose Your Path
|
|
21
|
+
|
|
22
|
+
### New Application
|
|
23
|
+
|
|
24
|
+
If you're starting fresh, use our application template:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
rails new myapp -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This creates a fully configured Plutonium application with authentication ready to go.
|
|
31
|
+
|
|
32
|
+
[Continue to Installation →](./installation)
|
|
33
|
+
|
|
34
|
+
### Existing Application
|
|
35
|
+
|
|
36
|
+
Adding Plutonium to an existing Rails app requires a few more steps but is fully supported.
|
|
37
|
+
|
|
38
|
+
[Continue to Installation →](./installation#existing-application)
|
|
39
|
+
|
|
40
|
+
### Tutorial
|
|
41
|
+
|
|
42
|
+
Want to learn by building? Follow our step-by-step tutorial to create a complete blog application.
|
|
43
|
+
|
|
44
|
+
[Start the Tutorial →](./tutorial/)
|
|
45
|
+
|
|
46
|
+
## Next Steps
|
|
47
|
+
|
|
48
|
+
After installation, you'll typically:
|
|
49
|
+
|
|
50
|
+
1. **Create a Feature Package** - Organize your business logic
|
|
51
|
+
2. **Generate Resources** - Create your models and scaffolds
|
|
52
|
+
3. **Create a Portal** - Set up the web interface
|
|
53
|
+
4. **Connect Resources** - Make resources accessible through the portal
|
|
54
|
+
5. **Customize** - Override defaults as needed
|
|
55
|
+
|
|
56
|
+
Each of these steps is covered in detail in the [Tutorial](./tutorial/).
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
This guide covers installing Plutonium in both new and existing Rails applications.
|
|
4
|
+
|
|
5
|
+
## New Application
|
|
6
|
+
|
|
7
|
+
The fastest way to get started is with our application template:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
rails new myapp -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This template:
|
|
14
|
+
- Adds the Plutonium gem
|
|
15
|
+
- Configures TailwindCSS 4 with Plutonium's theme
|
|
16
|
+
- Sets up Rodauth for authentication
|
|
17
|
+
- Creates initial migrations
|
|
18
|
+
- Configures the asset pipeline
|
|
19
|
+
|
|
20
|
+
After the template completes:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd myapp
|
|
24
|
+
rails db:migrate
|
|
25
|
+
bin/dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Visit `http://localhost:3000` to see your new application.
|
|
29
|
+
|
|
30
|
+
## Existing Application
|
|
31
|
+
|
|
32
|
+
### Step 1: Add the Gem
|
|
33
|
+
|
|
34
|
+
Add Plutonium to your Gemfile:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
gem "plutonium"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then install:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Step 2: Run the Installer
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
rails generate pu:core:install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This generator:
|
|
53
|
+
- Creates the Plutonium initializer
|
|
54
|
+
- Adds required configurations
|
|
55
|
+
- Sets up the asset pipeline integration
|
|
56
|
+
|
|
57
|
+
### Step 3: Install Rodauth (Optional)
|
|
58
|
+
|
|
59
|
+
If you want Plutonium's built-in authentication:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
rails generate pu:rodauth:install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This creates:
|
|
66
|
+
- Rodauth configuration files
|
|
67
|
+
- Account model and migrations
|
|
68
|
+
- Email templates for authentication flows
|
|
69
|
+
|
|
70
|
+
### Step 4: Run Migrations
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
rails db:migrate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Step 5: Configure Assets
|
|
77
|
+
|
|
78
|
+
Run the assets generator to set up TailwindCSS and Plutonium styles:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
rails generate pu:core:assets
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This configures PostCSS, TailwindCSS, and imports Plutonium's styles into your application.
|
|
85
|
+
|
|
86
|
+
## Verifying Installation
|
|
87
|
+
|
|
88
|
+
After installation, verify everything is working:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
rails runner "puts Plutonium::VERSION"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You should see the installed version number.
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
Plutonium is configured in `config/initializers/plutonium.rb`:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
Plutonium.configure do |config|
|
|
102
|
+
# Load default settings for version 1.0
|
|
103
|
+
config.load_defaults 1.0
|
|
104
|
+
|
|
105
|
+
# Development mode (auto-detected from PLUTONIUM_DEV env var)
|
|
106
|
+
# config.development = true
|
|
107
|
+
|
|
108
|
+
# Cache discovery (defaults to true in production, false in development)
|
|
109
|
+
# config.cache_discovery = false
|
|
110
|
+
|
|
111
|
+
# Hot reloading (defaults to true in development)
|
|
112
|
+
# config.enable_hotreload = true
|
|
113
|
+
|
|
114
|
+
# Asset configuration
|
|
115
|
+
# config.assets.logo = "custom_logo.png"
|
|
116
|
+
# config.assets.favicon = "custom_favicon.ico"
|
|
117
|
+
# config.assets.stylesheet = "plutonium.css"
|
|
118
|
+
# config.assets.script = "plutonium.min.js"
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Development Setup
|
|
123
|
+
|
|
124
|
+
For the best development experience:
|
|
125
|
+
|
|
126
|
+
### 1. Use bin/dev
|
|
127
|
+
|
|
128
|
+
Plutonium includes a Procfile for `foreman`:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
bin/dev
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This starts Rails and the CSS watcher together.
|
|
135
|
+
|
|
136
|
+
### 2. Enable Reloading
|
|
137
|
+
|
|
138
|
+
In development, Plutonium automatically reloads definitions and policies when files change. This is controlled by `config.enable_hotreload` (enabled by default in development).
|
|
139
|
+
|
|
140
|
+
## Next Steps
|
|
141
|
+
|
|
142
|
+
Now that Plutonium is installed:
|
|
143
|
+
|
|
144
|
+
- [Create your first Feature Package](/guides/creating-packages)
|
|
145
|
+
- [Generate a Resource](/guides/adding-resources)
|
|
146
|
+
- [Follow the Tutorial](/getting-started/tutorial/)
|