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.
- checksums.yaml +4 -4
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +8 -1
- data/app/assets/plutonium.js.map +2 -2
- data/app/assets/plutonium.min.js +1 -1
- data/app/assets/plutonium.min.js.map +2 -2
- data/docs/modules/generator.md +112 -0
- data/lib/generators/pu/res/entity/entity_generator.rb +123 -0
- data/lib/generators/pu/rodauth/customer_generator.rb +103 -0
- data/lib/plutonium/interaction/response/redirect.rb +15 -6
- data/lib/plutonium/version.rb +1 -1
- data/src/css/flatpickr.css +797 -0
- data/src/css/plutonium.css +1 -0
- data/src/js/controllers/flatpickr_controller.js +25 -14
- metadata +5 -2
data/docs/modules/generator.md
CHANGED
@@ -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(
|
20
|
+
url = url_for(*redirect_args)
|
17
21
|
|
18
|
-
|
19
|
-
if helpers.current_turbo_frame == "remote_modal"
|
22
|
+
respond_to do |format|
|
20
23
|
format.turbo_stream do
|
21
|
-
|
22
|
-
|
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
|
data/lib/plutonium/version.rb
CHANGED