plutonium 0.34.1 → 0.35.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.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/skill.md +53 -0
  3. data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
  4. data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
  5. data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
  6. data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
  7. data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
  8. data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
  9. data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
  10. data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
  11. data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
  12. data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
  13. data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
  14. data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
  15. data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
  16. data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
  17. data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
  18. data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
  19. data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
  20. data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
  21. data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
  22. data/.claude/skills/plutonium-theming/SKILL.md +424 -0
  23. data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
  24. data/CHANGELOG.md +52 -0
  25. data/CLAUDE.md +215 -0
  26. data/CONTRIBUTING.md +72 -18
  27. data/README.md +100 -19
  28. data/app/assets/plutonium.css +1 -11
  29. data/app/assets/plutonium.js +1685 -1146
  30. data/app/assets/plutonium.js.map +4 -4
  31. data/app/assets/plutonium.min.js +70 -70
  32. data/app/assets/plutonium.min.js.map +4 -4
  33. data/app/views/resource/interactive_bulk_action.html.erb +1 -5
  34. data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  35. data/app/views/rodauth/_login_form.html.erb +15 -55
  36. data/app/views/rodauth/_login_form_footer.html.erb +2 -2
  37. data/app/views/rodauth/_password_visibility.html.erb +2 -8
  38. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  39. data/app/views/rodauth/change_login.html.erb +36 -19
  40. data/app/views/rodauth/change_password.html.erb +34 -10
  41. data/app/views/rodauth/close_account.html.erb +12 -4
  42. data/app/views/rodauth/confirm_password.html.erb +19 -17
  43. data/app/views/rodauth/create_account.html.erb +30 -109
  44. data/app/views/rodauth/email_auth.html.erb +1 -1
  45. data/app/views/rodauth/logout.html.erb +4 -4
  46. data/app/views/rodauth/otp_auth.html.erb +13 -4
  47. data/app/views/rodauth/otp_disable.html.erb +12 -4
  48. data/app/views/rodauth/otp_setup.html.erb +29 -12
  49. data/app/views/rodauth/otp_unlock.html.erb +19 -10
  50. data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
  51. data/app/views/rodauth/recovery_auth.html.erb +12 -4
  52. data/app/views/rodauth/recovery_codes.html.erb +12 -4
  53. data/app/views/rodauth/remember.html.erb +7 -7
  54. data/app/views/rodauth/reset_password.html.erb +23 -7
  55. data/app/views/rodauth/reset_password_request.html.erb +14 -10
  56. data/app/views/rodauth/sms_auth.html.erb +13 -4
  57. data/app/views/rodauth/sms_confirm.html.erb +13 -4
  58. data/app/views/rodauth/sms_disable.html.erb +12 -4
  59. data/app/views/rodauth/sms_request.html.erb +1 -1
  60. data/app/views/rodauth/sms_setup.html.erb +23 -7
  61. data/app/views/rodauth/two_factor_auth.html.erb +2 -2
  62. data/app/views/rodauth/two_factor_disable.html.erb +12 -4
  63. data/app/views/rodauth/two_factor_manage.html.erb +7 -7
  64. data/app/views/rodauth/unlock_account.html.erb +13 -5
  65. data/app/views/rodauth/unlock_account_request.html.erb +2 -2
  66. data/app/views/rodauth/verify_account.html.erb +25 -7
  67. data/app/views/rodauth/verify_account_resend.html.erb +14 -10
  68. data/app/views/rodauth/verify_login_change.html.erb +1 -1
  69. data/app/views/rodauth/webauthn_auth.html.erb +1 -1
  70. data/app/views/rodauth/webauthn_remove.html.erb +18 -8
  71. data/app/views/rodauth/webauthn_setup.html.erb +12 -4
  72. data/docs/.vitepress/config.ts +15 -26
  73. data/docs/.vitepress/theme/custom.css +388 -29
  74. data/docs/getting-started/index.md +1 -1
  75. data/docs/getting-started/tutorial/02-first-resource.md +9 -0
  76. data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
  77. data/docs/getting-started/tutorial/07-author-portal.md +191 -0
  78. data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
  79. data/docs/getting-started/tutorial/index.md +5 -2
  80. data/docs/guides/authorization.md +33 -0
  81. data/docs/guides/creating-packages.md +12 -16
  82. data/docs/guides/custom-actions.md +36 -0
  83. data/docs/guides/search-filtering.md +121 -42
  84. data/docs/guides/theming.md +232 -36
  85. data/docs/index.md +203 -57
  86. data/docs/public/og-image.png +0 -0
  87. data/docs/reference/controller/index.md +14 -16
  88. data/docs/reference/definition/actions.md +38 -3
  89. data/docs/reference/definition/fields.md +3 -3
  90. data/docs/reference/definition/index.md +2 -2
  91. data/docs/reference/generators/index.md +0 -1
  92. data/docs/reference/interaction/index.md +14 -10
  93. data/docs/reference/model/index.md +0 -1
  94. data/docs/reference/portal/index.md +13 -27
  95. data/gemfiles/rails_7.gemfile.lock +1 -1
  96. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
  99. data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
  100. data/lib/plutonium/action/interactive.rb +2 -2
  101. data/lib/plutonium/core/controller.rb +2 -1
  102. data/lib/plutonium/definition/actions.rb +2 -2
  103. data/lib/plutonium/lib/deep_freezer.rb +3 -7
  104. data/lib/plutonium/query/filter.rb +14 -0
  105. data/lib/plutonium/query/filters/association.rb +49 -0
  106. data/lib/plutonium/query/filters/boolean.rb +35 -0
  107. data/lib/plutonium/query/filters/date.rb +97 -0
  108. data/lib/plutonium/query/filters/date_range.rb +58 -0
  109. data/lib/plutonium/query/filters/select.rb +55 -0
  110. data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
  111. data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
  112. data/lib/plutonium/resource/controllers/queryable.rb +4 -2
  113. data/lib/plutonium/resource/query_object.rb +1 -1
  114. data/lib/plutonium/ui/action_button.rb +23 -65
  115. data/lib/plutonium/ui/actions_dropdown.rb +103 -0
  116. data/lib/plutonium/ui/block.rb +1 -1
  117. data/lib/plutonium/ui/breadcrumbs.rb +12 -19
  118. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  119. data/lib/plutonium/ui/component/kit.rb +6 -0
  120. data/lib/plutonium/ui/component_classes.rb +102 -0
  121. data/lib/plutonium/ui/display/base.rb +15 -0
  122. data/lib/plutonium/ui/display/components/attachment.rb +6 -5
  123. data/lib/plutonium/ui/display/components/boolean.rb +23 -0
  124. data/lib/plutonium/ui/display/components/color.rb +23 -0
  125. data/lib/plutonium/ui/display/resource.rb +1 -1
  126. data/lib/plutonium/ui/display/theme.rb +29 -15
  127. data/lib/plutonium/ui/empty_card.rb +3 -3
  128. data/lib/plutonium/ui/form/base.rb +20 -0
  129. data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
  130. data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
  131. data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
  132. data/lib/plutonium/ui/form/components/uppy.rb +5 -4
  133. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
  134. data/lib/plutonium/ui/form/interaction.rb +17 -1
  135. data/lib/plutonium/ui/form/query.rb +133 -80
  136. data/lib/plutonium/ui/form/theme.rb +50 -35
  137. data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
  138. data/lib/plutonium/ui/layout/base.rb +1 -1
  139. data/lib/plutonium/ui/layout/header.rb +4 -7
  140. data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
  141. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  142. data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
  143. data/lib/plutonium/ui/nav_user.rb +9 -8
  144. data/lib/plutonium/ui/page/interactive_action.rb +5 -5
  145. data/lib/plutonium/ui/page_header.rb +29 -10
  146. data/lib/plutonium/ui/panel.rb +4 -4
  147. data/lib/plutonium/ui/sidebar_menu.rb +8 -8
  148. data/lib/plutonium/ui/skeleton_table.rb +7 -8
  149. data/lib/plutonium/ui/tab_list.rb +5 -5
  150. data/lib/plutonium/ui/table/base.rb +3 -0
  151. data/lib/plutonium/ui/table/components/attachment.rb +4 -3
  152. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
  153. data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
  154. data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
  155. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
  156. data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
  157. data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
  158. data/lib/plutonium/ui/table/display_theme.rb +6 -6
  159. data/lib/plutonium/ui/table/resource.rb +93 -52
  160. data/lib/plutonium/ui/table/theme.rb +28 -15
  161. data/lib/plutonium/version.rb +1 -1
  162. data/package.json +2 -2
  163. data/plutonium.gemspec +5 -4
  164. data/src/css/components.css +471 -0
  165. data/src/css/intl_tel_input.css +2 -2
  166. data/src/css/plutonium.css +2 -0
  167. data/src/css/tokens.css +149 -0
  168. data/src/js/controllers/bulk_actions_controller.js +109 -0
  169. data/src/js/controllers/filter_panel_controller.js +35 -0
  170. data/src/js/controllers/register_controllers.js +5 -1
  171. data/src/js/controllers/resource_drop_down_controller.js +25 -1
  172. data/src/js/controllers/slim_select_controller.js +6 -2
  173. data/src/js/turbo/turbo_actions.js +1 -1
  174. metadata +52 -39
  175. data/.claude/skills/definition-query/SKILL.md +0 -334
  176. data/docs/concepts/architecture.md +0 -226
  177. data/docs/concepts/auto-detection.md +0 -254
  178. data/docs/concepts/index.md +0 -61
  179. data/docs/concepts/packages-portals.md +0 -304
  180. data/docs/concepts/resources.md +0 -224
  181. data/docs/cookbook/blog.md +0 -411
  182. data/docs/cookbook/index.md +0 -289
  183. data/docs/cookbook/saas.md +0 -481
  184. data/docs/public/CLAUDE.md +0 -578
  185. data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
@@ -1,481 +0,0 @@
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 --dest=core
465
- rails generate pu:res:scaffold Membership role:string organization:belongs_to user:belongs_to --dest=core
466
- rails generate pu:res:scaffold Project name:string organization:belongs_to --dest=projects
467
-
468
- # Connect to portal
469
- rails generate pu:res:conn Projects::Project --dest=app_portal
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