plutonium 0.26.1 → 0.26.2

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.
@@ -25,9 +25,11 @@ The Generator module is located in `lib/generators/pu/`.
25
25
  Sets up the base requirements for a Plutonium application.
26
26
 
27
27
  ::: code-group
28
+
28
29
  ```bash [Command]
29
30
  rails generate pu:core:install
30
31
  ```
32
+
31
33
  ```text [Generated Structure]
32
34
  config/
33
35
  ├── packages.rb # Package loading configuration
@@ -49,6 +51,7 @@ app/views/
49
51
  └── layouts/
50
52
  └── resource.html.erb # Ejected layout for customization
51
53
  ```
54
+
52
55
  :::
53
56
 
54
57
  ### Package Generators
@@ -58,6 +61,7 @@ app/views/
58
61
  Creates a complete portal package, which acts as a user-facing entry point to your application, often with its own authentication.
59
62
 
60
63
  ::: code-group
64
+
61
65
  ```bash [Command]
62
66
  # Creates an "admin" portal with authentication
63
67
  rails generate pu:pkg:portal admin
@@ -65,6 +69,7 @@ rails generate pu:pkg:portal admin
65
69
  # Creates a "customer" portal with public access
66
70
  rails generate pu:pkg:portal customer --public
67
71
  ```
72
+
68
73
  ```text [Generated Structure]
69
74
  packages/admin_portal/
70
75
  ├── lib/
@@ -83,6 +88,7 @@ packages/admin_portal/
83
88
  └── definitions/
84
89
  └── admin_portal/
85
90
  ```
91
+
86
92
  ```ruby [Authentication Integration]
87
93
  # Automatic Rodauth integration is added to the controller concern
88
94
  # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
@@ -98,6 +104,7 @@ module AdminPortal
98
104
  end
99
105
  end
100
106
  ```
107
+
101
108
  :::
102
109
 
103
110
  #### Package Generator (`pu:pkg:package`)
@@ -105,9 +112,11 @@ end
105
112
  Creates a standard feature package for encapsulating domain logic.
106
113
 
107
114
  ::: code-group
115
+
108
116
  ```bash [Command]
109
117
  rails generate pu:pkg:package blogging
110
118
  ```
119
+
111
120
  ```text [Generated Structure]
112
121
  packages/blogging/
113
122
  ├── lib/
@@ -124,6 +133,7 @@ packages/blogging/
124
133
  └── interactions/
125
134
  └── blogging/
126
135
  ```
136
+
127
137
  :::
128
138
 
129
139
  ### Resource Generators
@@ -133,10 +143,12 @@ packages/blogging/
133
143
  Creates a complete resource with a model, controller, policy, and definition, including full CRUD operations.
134
144
 
135
145
  ::: code-group
146
+
136
147
  ```bash [Command]
137
148
  # Generate a new resource with attributes, placing it in the 'blogging' package
138
149
  rails generate pu:res:scaffold Post title:string content:text author:references published:boolean --dest=blogging
139
150
  ```
151
+
140
152
  ```ruby [Generated Model]
141
153
  # packages/blogging/app/models/blogging/post.rb
142
154
  class Blogging::Post < Blogging::ResourceRecord
@@ -146,6 +158,7 @@ class Blogging::Post < Blogging::ResourceRecord
146
158
  validates :content, presence: true
147
159
  end
148
160
  ```
161
+
149
162
  ```ruby [Generated Policy]
150
163
  # packages/blogging/app/policies/blogging/post_policy.rb
151
164
  class Blogging::PostPolicy < Blogging B::ResourcePolicy
@@ -158,6 +171,7 @@ class Blogging::PostPolicy < Blogging B::ResourcePolicy
158
171
  end
159
172
  end
160
173
  ```
174
+
161
175
  ```ruby [Generated Definition]
162
176
  # packages/blogging/app/definitions/blogging/post_definition.rb
163
177
  class Blogging::PostDefinition < Blogging::ResourceDefinition
@@ -172,19 +186,80 @@ class Blogging::PostDefinition < Blogging::ResourceDefinition
172
186
  filter :author, with: :select
173
187
  end
174
188
  ```
189
+
175
190
  :::
176
191
 
177
192
  ### Authentication Generators
178
193
 
194
+ #### Rodauth Customer Generator (`pu:rodauth:customer`)
195
+
196
+ Easily add multitenancy and SaaS-ready authentication to your Plutonium app. This generator creates a customer-oriented Rodauth account, an entity model, and a membership join model, wiring up all necessary relationships for multi-tenant architectures.
197
+
198
+ > **Note:** If you omit the `--entity` parameter, the entity name will default to `Entity` and the join relation will be `EntityCustomer`. **It is strongly recommended to always provide a meaningful entity name using `--entity=YourEntityName` to ensure clarity and proper model relationships in your application.**
199
+
200
+ > **Option:** `--allow-signup` determines whether the customer user is allowed to sign up on the platform. If not allowed, new customer accounts will typically be created by platform admins and users notified. Use `--no-allow-signup` to restrict self-signup.
201
+
202
+ ::: code-group
203
+
204
+ ```bash [Command]
205
+ rails generate pu:rodauth:customer Customer --entity=Organization
206
+ ```
207
+
208
+ ```text [Generated Structure]
209
+ app/
210
+ ├── models/
211
+ │ ├── customer.rb
212
+ │ ├── organization.rb
213
+ │ └── organization_customer.rb
214
+ └── rodauth/
215
+ ├── customer_rodauth.rb
216
+ └── customer_rodauth_plugin.rb
217
+ db/
218
+ └── migrate/
219
+ ├── ..._create_customers.rb
220
+ ├── ..._create_organizations.rb
221
+ └── ..._create_organization_customers.rb
222
+ ```
223
+
224
+ ```ruby [Generated Models]
225
+ # app/models/organization.rb
226
+ class Organization < ::ResourceRecord
227
+ has_many :organization_customers
228
+ has_many :customers, through: :organization_customers
229
+ end
230
+
231
+ # app/models/customer.rb
232
+ class Customer < ResourceRecord
233
+ include Rodauth::Rails.model(:customer)
234
+
235
+ has_many :organization_customers
236
+ has_many :organizations, through: :organization_customers
237
+ end
238
+
239
+ # app/models/organization_customer.rb
240
+ class OrganizationCustomer < ::ResourceRecord
241
+
242
+ belongs_to :organization
243
+ belongs_to :customer
244
+ enum role: { member: 0, admin: 1 }
245
+ end
246
+ ```
247
+
248
+ :::
249
+
250
+ > **Note:** If you already have a customer user model and want to add an entity (for example, as your project evolves into a SaaS), use the Entity Resource Generator below to generate just the entity and membership join model.
251
+
179
252
  #### Rodauth Account Generator (`pu:rodauth:account`)
180
253
 
181
254
  Generates the necessary files for a Rodauth authentication setup for a given account type.
182
255
 
183
256
  ::: code-group
257
+
184
258
  ```bash [Command]
185
259
  # Generate a 'user' account with common features
186
260
  rails generate pu:rodauth:account user --features login logout create-account verify-account reset-password remember
187
261
  ```
262
+
188
263
  ```text [Generated Structure]
189
264
  app/
190
265
  ├── controllers/
@@ -201,6 +276,7 @@ db/
201
276
  └── migrate/
202
277
  └── ..._create_users.rb
203
278
  ```
279
+
204
280
  :::
205
281
 
206
282
  #### Rodauth Admin Generator (`pu:rodauth:admin`)
@@ -208,9 +284,11 @@ db/
208
284
  A specialized generator for creating a secure admin account with enhanced features like MFA and audit logging.
209
285
 
210
286
  ::: code-group
287
+
211
288
  ```bash [Command]
212
289
  rails generate pu:rodauth:admin admin
213
290
  ```
291
+
214
292
  ```ruby [Generated Plugin]
215
293
  # app/rodauth/admin_rodauth_plugin.rb
216
294
  class AdminRodauthPlugin < RodauthPlugin
@@ -224,6 +302,40 @@ class AdminRodauthPlugin < RodauthPlugin
224
302
  end
225
303
  end
226
304
  ```
305
+
306
+ :::
307
+
308
+ ### Entity Resource Generator (`pu:res:entity`)
309
+
310
+ Creates an entity model and a membership join model for associating customers with entities. Use this if you already have a customer model and want to add multitenancy or evolve your project into a SaaS platform.
311
+
312
+ ::: code-group
313
+
314
+ ```bash [Command]
315
+ rails generate pu:res:entity Organization --auth-account=Customer
316
+ ```
317
+
318
+ ```text [Generated Structure]
319
+ app/
320
+ ├── models/
321
+ │ ├── organization.rb
322
+ │ └── organization_customer.rb
323
+ db/
324
+ └── migrate/
325
+ ├── ..._create_organizations.rb
326
+ └── ..._create_organization_customers.rb
327
+ ```
328
+
329
+ ```ruby [Generated Membership Model]
330
+ # app/models/organization_customer.rb
331
+ class OrganizationCustomer < ResourceRecord
332
+ belongs_to :organization
333
+ belongs_to :customer
334
+
335
+ enum role: { member: 0, admin: 1 } # not added by default
336
+ end
337
+ ```
338
+
227
339
  :::
228
340
 
229
341
  ### Ejection Generators
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+ require_relative "../../lib/plutonium_generators"
5
+
6
+ module Pu
7
+ module Res
8
+ class EntityGenerator < ::Rails::Generators::NamedBase
9
+ include PlutoniumGenerators::Generator
10
+
11
+ desc "Generate an entity model for customer accounts"
12
+
13
+ class_option :model, type: :boolean, default: true
14
+ class_option :allow_signup, type: :boolean, default: true,
15
+ desc: "Whether to allow customer to sign up to the platform"
16
+ class_option :auth_account, type: :string,
17
+ desc: "Specify the authentication account name", required: true
18
+
19
+ def start
20
+ ensure_customer_model_exists! if behavior == :invoke
21
+ generate_entity_resource
22
+ generate_membership_resource
23
+ end
24
+
25
+ private
26
+
27
+ def ensure_customer_model_exists!
28
+ customer_model_path = File.join("app", "models", "#{normalized_auth_account_name}.rb")
29
+ unless File.exist?(customer_model_path)
30
+ raise "Customer model '#{normalized_auth_account_name}' does not exist. Please create it first."
31
+ end
32
+ rescue => e
33
+ exception "#{self.class} failed:", e
34
+ end
35
+
36
+ def generate_entity_resource
37
+ Rails::Generators.invoke(
38
+ "pu:res:scaffold",
39
+ [
40
+ normalized_name,
41
+ "name:string",
42
+ "--model",
43
+ ("--force" if options[:force]),
44
+ ("--skip" if options[:skip]),
45
+ "--dest=#{selected_destination_feature}"
46
+ ].compact,
47
+ behavior: behavior,
48
+ destination_root: destination_root
49
+ )
50
+
51
+ add_unique_index_to_migration(normalized_name, [:name])
52
+ end
53
+
54
+ def generate_membership_resource
55
+ Rails::Generators.invoke(
56
+ "pu:res:scaffold",
57
+ [
58
+ normalized_entity_membership_name,
59
+ *membership_attributes,
60
+ "--model",
61
+ ("--force" if options[:force]),
62
+ ("--skip" if options[:skip]),
63
+ "--dest=#{selected_destination_feature}"
64
+ ].compact,
65
+ behavior: behavior,
66
+ destination_root: destination_root
67
+ )
68
+
69
+ add_unique_index_to_migration(
70
+ normalized_entity_membership_name,
71
+ ["#{normalized_name}_id", "#{normalized_auth_account_name}_id"]
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ def selected_destination_feature = "main_app"
78
+
79
+ def normalized_name = name.underscore
80
+
81
+ def normalized_entity_membership_name
82
+ "#{normalized_name}_#{normalized_auth_account_name}"
83
+ end
84
+
85
+ def normalized_auth_account_name = options[:auth_account].underscore
86
+
87
+ def add_unique_index_to_migration(model_name, index_columns)
88
+ migration_dir = File.join("db", "migrate")
89
+ migration_file = Dir[File.join(migration_dir, "*_create_#{model_name.pluralize}.rb")].first
90
+
91
+ if migration_file && File.exist?(migration_file)
92
+ index_definition = build_index_definition(model_name, index_columns)
93
+ insert_into_file migration_file, indent(index_definition, 4), before: /^ end\s*$/
94
+ success "Added unique index to #{model_name.pluralize}"
95
+ end
96
+ end
97
+
98
+ def build_index_definition(model_name, index_columns)
99
+ table_name = model_name.pluralize
100
+
101
+ case index_columns
102
+ when Array
103
+ if index_columns.size == 1
104
+ "add_index :#{table_name}, :#{index_columns.first}, unique: true\n"
105
+ else
106
+ column_list = index_columns.map { |col| ":#{col}" }.join(", ")
107
+ "add_index :#{table_name}, [#{column_list}], unique: true\n"
108
+ end
109
+ else
110
+ "add_index :#{table_name}, :#{index_columns}, unique: true\n"
111
+ end
112
+ end
113
+
114
+ def membership_attributes
115
+ [
116
+ "#{normalized_name}:references",
117
+ "#{normalized_auth_account_name}:references",
118
+ "role:integer"
119
+ ]
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(Rodauth::Rails)
4
+
5
+ require "rails/generators/named_base"
6
+ require_relative "../lib/plutonium_generators"
7
+
8
+ module Pu
9
+ module Rodauth
10
+ class CustomerGenerator < ::Rails::Generators::NamedBase
11
+ include PlutoniumGenerators::Concerns::Logger
12
+
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ desc "Generate a rodauth-rails account optimized for customer-based tasks"
16
+
17
+ class_option :allow_signup, type: :boolean, default: true,
18
+ desc: "Whether to allow customer to sign up to the platform"
19
+
20
+ class_option :entity, default: "Entity",
21
+ desc: "Generate an entity model for customer accounts. Defaults to 'Entity'",
22
+ aliases: ["--entity", "-e"]
23
+
24
+ def start
25
+ create_customer_account
26
+ create_entity_model_and_membership
27
+ configure_model_relationships
28
+ rescue => e
29
+ exception "#{self.class} failed:", e
30
+ end
31
+
32
+ private
33
+
34
+ def create_customer_account
35
+ invoke "pu:rodauth:account", [name],
36
+ defaults: false,
37
+ **customer_features,
38
+ force: options[:force],
39
+ skip: options[:skip],
40
+ lint: true
41
+ end
42
+
43
+ def create_entity_model_and_membership
44
+ invoke "pu:res:entity", [normalized_entity_name],
45
+ auth_account: normalized_name,
46
+ force: options[:force],
47
+ skip: options[:skip]
48
+ end
49
+
50
+ def configure_model_relationships
51
+ entity_model_path = File.join("app", "models", "#{normalized_entity_name}.rb")
52
+ if File.exist?(entity_model_path)
53
+ insert_into_file entity_model_path, <<~RUBY + " ", before: /# add has_many associations above\.\n/
54
+ has_many :#{normalized_entity_membership_name.pluralize}
55
+ has_many :#{normalized_name.pluralize}, through: :#{normalized_entity_membership_name.pluralize}
56
+ RUBY
57
+ success "Added relationship to #{normalized_entity_name} model"
58
+ end
59
+
60
+ customer_model_path = File.join("app", "models", "#{normalized_name}.rb")
61
+ if File.exist?(customer_model_path)
62
+ insert_into_file customer_model_path, <<~RUBY + " ", before: /# add has_many associations above\.\n/
63
+ has_many :#{normalized_entity_membership_name.pluralize}
64
+ has_many :#{normalized_entity_name.pluralize}, through: :#{normalized_entity_membership_name.pluralize}
65
+ RUBY
66
+ success "Added relationship to #{normalized_name} model"
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def customer_features
73
+ features = %i[
74
+ login
75
+ remember
76
+ logout
77
+ create_account
78
+ verify_account
79
+ verify_account_grace_period
80
+ reset_password
81
+ reset_password_notify
82
+ change_login
83
+ verify_login_change
84
+ change_password
85
+ change_password_notify
86
+ case_insensitive_login
87
+ internal_request
88
+ ]
89
+
90
+ features.delete(:create_account) unless options[:allow_signup]
91
+ features.map { |feature| [feature, true] }.to_h
92
+ end
93
+
94
+ def normalized_name = name.underscore
95
+
96
+ def normalized_entity_name = options[:entity].underscore
97
+
98
+ def normalized_entity_membership_name
99
+ "#{normalized_entity_name.underscore}_#{normalized_name}"
100
+ end
101
+ end
102
+ end
103
+ end
@@ -12,16 +12,25 @@ module Plutonium
12
12
  # @param controller [ActionController::Base] The controller instance.
13
13
  # @return [void]
14
14
  def execute(controller)
15
+ # Capture the instance variables before entering instance_eval
16
+ redirect_args = @args
17
+ redirect_options = @options
18
+
15
19
  controller.instance_eval do
16
- url = url_for(*@args)
20
+ url = url_for(*redirect_args)
17
21
 
18
- format.any { redirect_to(url, **@options) }
19
- if helpers.current_turbo_frame == "remote_modal"
22
+ respond_to do |format|
20
23
  format.turbo_stream do
21
- render turbo_stream: [
22
- helpers.turbo_stream_redirect(url)
23
- ]
24
+ if helpers.current_turbo_frame == "remote_modal"
25
+ render turbo_stream: [
26
+ helpers.turbo_stream_redirect(url)
27
+ ]
28
+ else
29
+ redirect_to(url, **redirect_options)
30
+ end
24
31
  end
32
+
33
+ format.any { redirect_to(url, **redirect_options) }
25
34
  end
26
35
  end
27
36
  end
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.26.1"
2
+ VERSION = "0.26.2"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0