plutonium 0.42.0 → 0.43.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/.claude/skills/plutonium-controller/SKILL.md +38 -1
- data/.claude/skills/plutonium-definition/SKILL.md +14 -0
- data/.claude/skills/plutonium-forms/SKILL.md +16 -1
- data/.claude/skills/plutonium-profile/SKILL.md +276 -0
- data/.claude/skills/plutonium-views/SKILL.md +23 -1
- data/CHANGELOG.md +36 -0
- data/app/assets/plutonium.css +1 -1
- data/app/views/plutonium/_resource_header.html.erb +6 -27
- data/app/views/plutonium/_resource_sidebar.html.erb +1 -2
- data/app/views/resource/_resource_details.rabl +3 -2
- data/app/views/resource/index.rabl +3 -2
- data/app/views/resource/show.rabl +3 -2
- data/docs/guides/user-profile.md +322 -0
- data/docs/reference/controller/index.md +38 -1
- data/docs/reference/definition/index.md +16 -0
- data/docs/reference/views/forms.md +15 -0
- data/docs/reference/views/index.md +23 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +12 -0
- data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +11 -0
- data/lib/generators/pu/core/typespec/templates/common.tsp.tt +95 -0
- data/lib/generators/pu/core/typespec/templates/main.tsp.tt +27 -0
- data/lib/generators/pu/core/typespec/templates/main_multi.tsp.tt +25 -0
- data/lib/generators/pu/core/typespec/templates/model.tsp.tt +226 -0
- data/lib/generators/pu/core/typespec/typespec_generator.rb +342 -0
- data/lib/generators/pu/invites/USAGE +0 -1
- data/lib/generators/pu/invites/install_generator.rb +62 -15
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +2 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +1 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +5 -5
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +4 -4
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +1 -1
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +29 -0
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +6 -23
- data/lib/generators/pu/pkg/portal/portal_generator.rb +5 -1
- data/lib/generators/pu/profile/USAGE +59 -0
- data/lib/generators/pu/profile/concerns/profile_arguments.rb +27 -0
- data/lib/generators/pu/profile/conn/USAGE +33 -0
- data/lib/generators/pu/profile/conn_generator.rb +167 -0
- data/lib/generators/pu/profile/install_generator.rb +119 -0
- data/lib/generators/pu/profile/setup/USAGE +42 -0
- data/lib/generators/pu/profile/setup_generator.rb +73 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -4
- data/lib/generators/pu/rodauth/install_generator.rb +2 -2
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
- data/lib/generators/pu/saas/api_client_generator.rb +0 -2
- data/lib/generators/pu/saas/membership_generator.rb +68 -19
- data/lib/generators/pu/saas/setup_generator.rb +7 -2
- data/lib/generators/pu/saas/user_generator.rb +0 -2
- data/lib/plutonium/auth/rodauth.rb +8 -0
- data/lib/plutonium/core/controller.rb +7 -4
- data/lib/plutonium/core/controllers/authorizable.rb +5 -1
- data/lib/plutonium/definition/base.rb +7 -0
- data/lib/plutonium/helpers/display_helper.rb +6 -0
- data/lib/plutonium/profile/security_section.rb +118 -0
- data/lib/plutonium/resource/controller.rb +17 -7
- data/lib/plutonium/resource/controllers/interactive_actions.rb +11 -25
- data/lib/plutonium/resource/controllers/presentable.rb +46 -3
- data/lib/plutonium/resource/record/associated_with.rb +7 -1
- data/lib/plutonium/routing/mapper_extensions.rb +18 -18
- data/lib/plutonium/routing/route_set_extensions.rb +23 -2
- data/lib/plutonium/ui/breadcrumbs.rb +111 -131
- data/lib/plutonium/ui/dyna_frame/content.rb +12 -2
- data/lib/plutonium/ui/form/resource.rb +26 -19
- data/lib/plutonium/ui/page/base.rb +14 -14
- data/lib/plutonium/ui/table/components/selection_column.rb +6 -2
- data/lib/plutonium/ui/table/resource.rb +3 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +17 -3
- data/lib/generators/pu/rodauth/concerns/gem_helpers.rb +0 -19
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "../lib/plutonium_generators"
|
|
5
|
+
require_relative "concerns/profile_arguments"
|
|
6
|
+
|
|
7
|
+
module Pu
|
|
8
|
+
module Profile
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
include PlutoniumGenerators::Generator
|
|
11
|
+
include Concerns::ProfileArguments
|
|
12
|
+
|
|
13
|
+
desc "Generate a Profile resource for managing Rodauth account settings"
|
|
14
|
+
|
|
15
|
+
class_option :user_model, type: :string, default: "User",
|
|
16
|
+
desc: "The Rodauth user model"
|
|
17
|
+
|
|
18
|
+
class_option :dest, type: :string,
|
|
19
|
+
desc: "Package where the Profile resource should be created"
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
normalize_arguments
|
|
23
|
+
generate_profile_scaffold
|
|
24
|
+
add_user_association
|
|
25
|
+
add_unique_index_to_migration
|
|
26
|
+
rescue => e
|
|
27
|
+
exception "#{self.class} failed:", e
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def generate_profile_scaffold
|
|
33
|
+
invoke "pu:res:scaffold", [@profile_name, *scaffold_attributes],
|
|
34
|
+
dest: selected_destination_feature,
|
|
35
|
+
force: options[:force],
|
|
36
|
+
skip: options[:skip]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_user_association
|
|
40
|
+
association = if dest_package?
|
|
41
|
+
" has_one :#{file_name}, class_name: \"#{namespaced_class_name}\", dependent: :destroy\n"
|
|
42
|
+
else
|
|
43
|
+
" has_one :#{file_name}, dependent: :destroy\n"
|
|
44
|
+
end
|
|
45
|
+
inject_into_file user_model_path, association,
|
|
46
|
+
before: /^\s*# add has_one associations above\.\n/
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add_unique_index_to_migration
|
|
50
|
+
migration_file = Dir[File.join(migration_dir, "*_create_#{table_name}.rb")].first
|
|
51
|
+
unless migration_file
|
|
52
|
+
say_status :warning, "Migration file not found in #{migration_dir}, skipping unique index", :yellow
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Add unique: true to the user reference for has_one relationship
|
|
57
|
+
gsub_file migration_file,
|
|
58
|
+
/t\.belongs_to :#{user_table}, null: false, foreign_key: true/,
|
|
59
|
+
"t.belongs_to :#{user_table}, null: false, foreign_key: true, index: {unique: true}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def class_name
|
|
63
|
+
@profile_name.camelize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def namespaced_class_name
|
|
67
|
+
if dest_package?
|
|
68
|
+
"#{dest_name.camelize}::#{class_name}"
|
|
69
|
+
else
|
|
70
|
+
class_name
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def file_name
|
|
75
|
+
@profile_name.underscore
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def user_table
|
|
79
|
+
options[:user_model].underscore
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def user_model_path
|
|
83
|
+
"app/models/#{user_table}.rb"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def dest_package?
|
|
87
|
+
selected_destination_feature != "main_app"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def dest_name
|
|
91
|
+
selected_destination_feature
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def migration_dir
|
|
95
|
+
if dest_package?
|
|
96
|
+
"packages/#{dest_name}/db/migrate"
|
|
97
|
+
else
|
|
98
|
+
"db/migrate"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def table_name
|
|
103
|
+
if dest_package?
|
|
104
|
+
"#{dest_name}_#{file_name.pluralize}"
|
|
105
|
+
else
|
|
106
|
+
file_name.pluralize
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def scaffold_attributes
|
|
111
|
+
["#{user_table}:belongs_to", *@profile_attributes.map(&:to_s)]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def selected_destination_feature
|
|
115
|
+
feature_option :dest, prompt: "Select destination feature"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate a complete Profile setup: creates the resource and connects it to a portal.
|
|
3
|
+
This combines pu:profile:install and pu:profile:conn into a single command.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
rails g pu:profile:setup [NAME] [field:type ...] --dest=PACKAGE --portal=PORTAL
|
|
7
|
+
|
|
8
|
+
Arguments:
|
|
9
|
+
NAME Profile resource name (default: Profile)
|
|
10
|
+
field:type Additional fields for the profile
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--dest=PACKAGE Package where Profile resource is created
|
|
14
|
+
--portal=PORTAL Portal to connect the Profile to
|
|
15
|
+
--user-model=NAME Rodauth user model (default: User)
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
# Complete setup in one command
|
|
19
|
+
rails g pu:profile:setup date_of_birth:date bio:text \
|
|
20
|
+
--dest=competition \
|
|
21
|
+
--portal=competition_portal
|
|
22
|
+
|
|
23
|
+
# With custom name
|
|
24
|
+
rails g pu:profile:setup AccountSettings bio:text avatar:attachment \
|
|
25
|
+
--dest=main_app \
|
|
26
|
+
--portal=customer_portal
|
|
27
|
+
|
|
28
|
+
# Main app profile connected to admin portal
|
|
29
|
+
rails g pu:profile:setup --dest=main_app --portal=admin_portal
|
|
30
|
+
|
|
31
|
+
What This Does:
|
|
32
|
+
1. Creates Profile resource (model, migration, controller, policy, definition)
|
|
33
|
+
2. Adds has_one :profile to User model
|
|
34
|
+
3. Adds unique index on user_id
|
|
35
|
+
4. Customizes policy for owner-only access
|
|
36
|
+
5. Adds SecuritySection to show page
|
|
37
|
+
6. Connects to portal as singular resource
|
|
38
|
+
7. Adds profile_url helper to portal
|
|
39
|
+
|
|
40
|
+
Equivalent to running:
|
|
41
|
+
rails g pu:profile:install [NAME] [fields] --dest=PACKAGE
|
|
42
|
+
rails g pu:profile:conn [RESOURCE] --dest=PORTAL
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "../lib/plutonium_generators"
|
|
5
|
+
require_relative "concerns/profile_arguments"
|
|
6
|
+
|
|
7
|
+
module Pu
|
|
8
|
+
module Profile
|
|
9
|
+
class SetupGenerator < ::Rails::Generators::Base
|
|
10
|
+
include PlutoniumGenerators::Generator
|
|
11
|
+
include Concerns::ProfileArguments
|
|
12
|
+
|
|
13
|
+
desc "Generate a complete Profile setup with resource and portal connection"
|
|
14
|
+
|
|
15
|
+
class_option :user_model, type: :string, default: "User",
|
|
16
|
+
desc: "The Rodauth user model"
|
|
17
|
+
|
|
18
|
+
class_option :dest, type: :string,
|
|
19
|
+
desc: "Package where the Profile resource should be created"
|
|
20
|
+
|
|
21
|
+
class_option :portal, type: :string,
|
|
22
|
+
desc: "Portal to connect the Profile to"
|
|
23
|
+
|
|
24
|
+
def start
|
|
25
|
+
normalize_arguments
|
|
26
|
+
generate_profile
|
|
27
|
+
connect_to_portal if options[:portal].present?
|
|
28
|
+
rescue => e
|
|
29
|
+
exception "#{self.class} failed:", e
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def generate_profile
|
|
35
|
+
klass = Rails::Generators.find_by_namespace("pu:profile:install")
|
|
36
|
+
klass.new(
|
|
37
|
+
[@profile_name, *@profile_attributes],
|
|
38
|
+
{
|
|
39
|
+
user_model: options[:user_model],
|
|
40
|
+
dest: selected_destination_feature,
|
|
41
|
+
force: options[:force],
|
|
42
|
+
skip: options[:skip]
|
|
43
|
+
}
|
|
44
|
+
).invoke_all
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def connect_to_portal
|
|
48
|
+
# Shell out to a new process so the newly created model file gets loaded
|
|
49
|
+
generate "pu:profile:conn", "#{resource_class_name} --dest=#{options[:portal]} --user-model=#{options[:user_model]}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resource_class_name
|
|
53
|
+
if dest_package?
|
|
54
|
+
"#{dest_name.camelize}::#{@profile_name.camelize}"
|
|
55
|
+
else
|
|
56
|
+
@profile_name.camelize
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dest_package?
|
|
61
|
+
selected_destination_feature != "main_app"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dest_name
|
|
65
|
+
selected_destination_feature
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def selected_destination_feature
|
|
69
|
+
@selected_destination_feature ||= feature_option :dest, prompt: "Select destination feature"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
return unless defined?(Rodauth::Rails)
|
|
2
|
-
|
|
3
1
|
require "rails/generators/base"
|
|
4
2
|
require "securerandom"
|
|
5
3
|
|
|
6
4
|
require "#{__dir__}/concerns/configuration"
|
|
7
5
|
require "#{__dir__}/concerns/account_selector"
|
|
8
6
|
require "#{__dir__}/concerns/feature_selector"
|
|
9
|
-
require "#{__dir__}/concerns/
|
|
7
|
+
require "#{__dir__}/../lib/plutonium_generators/concerns/actions"
|
|
10
8
|
|
|
11
9
|
module Pu
|
|
12
10
|
module Rodauth
|
|
13
11
|
class AccountGenerator < ::Rails::Generators::Base
|
|
14
12
|
include Concerns::AccountSelector
|
|
15
13
|
include Concerns::FeatureSelector
|
|
16
|
-
include Concerns::
|
|
14
|
+
include PlutoniumGenerators::Concerns::Actions
|
|
17
15
|
|
|
18
16
|
source_root "#{__dir__}/templates"
|
|
19
17
|
|
|
@@ -2,13 +2,13 @@ require "rails/generators/base"
|
|
|
2
2
|
require "rails/generators/active_record/migration"
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
|
-
require "#{__dir__}/concerns/
|
|
5
|
+
require "#{__dir__}/../lib/plutonium_generators/concerns/actions"
|
|
6
6
|
|
|
7
7
|
module Pu
|
|
8
8
|
module Rodauth
|
|
9
9
|
class InstallGenerator < ::Rails::Generators::Base
|
|
10
10
|
include ::ActiveRecord::Generators::Migration
|
|
11
|
-
include Concerns::
|
|
11
|
+
include PlutoniumGenerators::Concerns::Actions
|
|
12
12
|
|
|
13
13
|
source_root "#{__dir__}/templates"
|
|
14
14
|
|
|
@@ -290,6 +290,9 @@ class <%= account_path.classify %>RodauthPlugin < RodauthPlugin
|
|
|
290
290
|
<% end -%>
|
|
291
291
|
<% if reset_password? -%>
|
|
292
292
|
|
|
293
|
+
# Redirect to login page after requesting password reset.
|
|
294
|
+
reset_password_email_sent_redirect { login_path }
|
|
295
|
+
|
|
293
296
|
# Redirect to login page after password reset.
|
|
294
297
|
reset_password_redirect { login_path }
|
|
295
298
|
<% end -%>
|
|
@@ -39,14 +39,14 @@ module Pu
|
|
|
39
39
|
|
|
40
40
|
def validate_models_exist!
|
|
41
41
|
user_model_path = File.join("app", "models", "#{normalized_user_name}.rb")
|
|
42
|
-
entity_model_path = File.join("app", "models", "#{
|
|
42
|
+
entity_model_path = File.join(dest_root, "app", "models", "#{full_entity_path}.rb")
|
|
43
43
|
|
|
44
44
|
unless File.exist?(Rails.root.join(user_model_path))
|
|
45
45
|
raise "User model '#{normalized_user_name}' does not exist at #{user_model_path}. Please create it first with: rails g pu:saas:user #{options[:user]}"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
unless File.exist?(
|
|
49
|
-
raise "Entity model '#{
|
|
48
|
+
unless File.exist?(entity_model_path)
|
|
49
|
+
raise "Entity model '#{full_entity_path}' does not exist at #{entity_model_path}. Please create it first with: rails g pu:saas:entity #{options[:entity]}"
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -63,7 +63,7 @@ module Pu
|
|
|
63
63
|
return unless migration_file
|
|
64
64
|
|
|
65
65
|
insert_into_file migration_file,
|
|
66
|
-
indent("add_index :#{
|
|
66
|
+
indent("add_index :#{full_membership_table_name}, [:#{normalized_entity_name}_id, :#{normalized_user_name}_id], unique: true\n", 4),
|
|
67
67
|
before: /^ end\s*$/
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -78,18 +78,18 @@ module Pu
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def add_role_enum_to_model
|
|
81
|
-
model_file = File.join("app", "models", "#{
|
|
81
|
+
model_file = File.join(dest_root, "app", "models", "#{full_membership_path}.rb")
|
|
82
82
|
|
|
83
|
-
return unless File.exist?(
|
|
83
|
+
return unless File.exist?(model_file)
|
|
84
84
|
|
|
85
85
|
enum_definition = "enum :role, #{roles_enum}\n"
|
|
86
86
|
insert_into_file model_file, indent(enum_definition, 2), before: /^\s*# add enums above\./
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def add_unique_validation_to_model
|
|
90
|
-
model_file = File.join("app", "models", "#{
|
|
90
|
+
model_file = File.join(dest_root, "app", "models", "#{full_membership_path}.rb")
|
|
91
91
|
|
|
92
|
-
return unless File.exist?(
|
|
92
|
+
return unless File.exist?(model_file)
|
|
93
93
|
|
|
94
94
|
validation = "validates :#{normalized_user_name}, uniqueness: {scope: :#{normalized_entity_name}_id, message: \"is already a member of this #{normalized_entity_name.humanize.downcase}\"}\n"
|
|
95
95
|
insert_into_file model_file, indent(validation, 2), before: /^\s*# add validations above\./
|
|
@@ -101,9 +101,9 @@ module Pu
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def add_association_to_entity_model
|
|
104
|
-
entity_model_path = File.join("app", "models", "#{
|
|
104
|
+
entity_model_path = File.join(dest_root, "app", "models", "#{full_entity_path}.rb")
|
|
105
105
|
|
|
106
|
-
return unless File.exist?(
|
|
106
|
+
return unless File.exist?(entity_model_path)
|
|
107
107
|
|
|
108
108
|
associations = <<~RUBY
|
|
109
109
|
has_many :#{membership_table_name}, dependent: :destroy
|
|
@@ -117,17 +117,24 @@ module Pu
|
|
|
117
117
|
|
|
118
118
|
return unless File.exist?(Rails.root.join(user_model_path))
|
|
119
119
|
|
|
120
|
-
associations =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
associations = if dest_namespace
|
|
121
|
+
<<~RUBY
|
|
122
|
+
has_many :#{membership_table_name}, class_name: "#{full_membership_class_name}", dependent: :destroy
|
|
123
|
+
has_many :#{normalized_entity_name.pluralize}, through: :#{membership_table_name}, source: :#{normalized_entity_name}
|
|
124
|
+
RUBY
|
|
125
|
+
else
|
|
126
|
+
<<~RUBY
|
|
127
|
+
has_many :#{membership_table_name}, dependent: :destroy
|
|
128
|
+
has_many :#{normalized_entity_name.pluralize}, through: :#{membership_table_name}
|
|
129
|
+
RUBY
|
|
130
|
+
end
|
|
124
131
|
inject_into_file user_model_path, indent(associations, 2), before: /^\s*# add has_many associations above\.\n/
|
|
125
132
|
end
|
|
126
133
|
|
|
127
134
|
def add_associated_with_scope_to_entity
|
|
128
|
-
entity_model_path = File.join("app", "models", "#{
|
|
135
|
+
entity_model_path = File.join(dest_root, "app", "models", "#{full_entity_path}.rb")
|
|
129
136
|
|
|
130
|
-
return unless File.exist?(
|
|
137
|
+
return unless File.exist?(entity_model_path)
|
|
131
138
|
|
|
132
139
|
scope_code = <<~RUBY
|
|
133
140
|
scope :associated_with_#{normalized_user_name}, ->(#{normalized_user_name}) { joins(:#{membership_table_name}).where(#{membership_table_name}: {#{normalized_user_name}_id: #{normalized_user_name}.id}) }
|
|
@@ -136,8 +143,8 @@ module Pu
|
|
|
136
143
|
end
|
|
137
144
|
|
|
138
145
|
def find_migration_file
|
|
139
|
-
migration_dir = File.join("db", "migrate")
|
|
140
|
-
Dir[
|
|
146
|
+
migration_dir = File.join(dest_root, "db", "migrate")
|
|
147
|
+
Dir[File.join(migration_dir, "*_create_#{full_membership_table_name}.rb")].first
|
|
141
148
|
end
|
|
142
149
|
|
|
143
150
|
def membership_model_name
|
|
@@ -150,7 +157,7 @@ module Pu
|
|
|
150
157
|
|
|
151
158
|
def membership_attributes
|
|
152
159
|
[
|
|
153
|
-
"#{
|
|
160
|
+
"#{full_entity_name}:references",
|
|
154
161
|
"#{normalized_user_name}:references",
|
|
155
162
|
"role:integer",
|
|
156
163
|
*Array(options[:extra_attributes])
|
|
@@ -161,6 +168,48 @@ module Pu
|
|
|
161
168
|
|
|
162
169
|
def normalized_entity_name = options[:entity].underscore
|
|
163
170
|
|
|
171
|
+
def dest_namespace
|
|
172
|
+
return nil if main_app?
|
|
173
|
+
|
|
174
|
+
selected_destination_feature.underscore
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def main_app?
|
|
178
|
+
selected_destination_feature == "main_app"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def dest_root
|
|
182
|
+
if main_app?
|
|
183
|
+
Rails.root
|
|
184
|
+
else
|
|
185
|
+
Rails.root.join("packages", dest_namespace)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def full_entity_path
|
|
190
|
+
[dest_namespace, normalized_entity_name].compact.join("/")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def full_membership_path
|
|
194
|
+
[dest_namespace, membership_model_name].compact.join("/")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def full_entity_name
|
|
198
|
+
full_entity_path
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def full_membership_table_name
|
|
202
|
+
full_membership_path.tr("/", "_").pluralize
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def full_membership_class_name
|
|
206
|
+
full_membership_path.camelize
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def full_entity_class_name
|
|
210
|
+
full_entity_path.camelize
|
|
211
|
+
end
|
|
212
|
+
|
|
164
213
|
def roles
|
|
165
214
|
Array(options[:roles]).flat_map { |r| r.split(",") }.map(&:strip)
|
|
166
215
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
return unless defined?(Rodauth::Rails)
|
|
4
|
-
|
|
5
3
|
require "rails/generators/base"
|
|
6
4
|
require_relative "../lib/plutonium_generators"
|
|
7
5
|
|
|
@@ -46,6 +44,7 @@ module Pu
|
|
|
46
44
|
desc: "Available roles for API client memberships"
|
|
47
45
|
|
|
48
46
|
def start
|
|
47
|
+
ensure_rodauth_installed
|
|
49
48
|
generate_user
|
|
50
49
|
generate_entity unless options[:skip_entity]
|
|
51
50
|
generate_membership unless options[:skip_membership]
|
|
@@ -56,6 +55,12 @@ module Pu
|
|
|
56
55
|
|
|
57
56
|
private
|
|
58
57
|
|
|
58
|
+
def ensure_rodauth_installed
|
|
59
|
+
return if File.exist?(Rails.root.join("app/rodauth/rodauth_app.rb"))
|
|
60
|
+
|
|
61
|
+
invoke "pu:rodauth:install"
|
|
62
|
+
end
|
|
63
|
+
|
|
59
64
|
def generate_user
|
|
60
65
|
# Use class-based invocation to avoid Thor's invoke caching
|
|
61
66
|
klass = Rails::Generators.find_by_namespace("pu:saas:user")
|
|
@@ -9,6 +9,7 @@ module Plutonium
|
|
|
9
9
|
included do
|
|
10
10
|
helper_method :current_user
|
|
11
11
|
helper_method :logout_url
|
|
12
|
+
helper_method :profile_url
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
private
|
|
@@ -27,6 +28,13 @@ module Plutonium
|
|
|
27
28
|
rodauth.logout_path
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# Override this method to return your profile page URL.
|
|
32
|
+
# When defined, a "Profile" link will appear in the user menu.
|
|
33
|
+
# Example: rodauth.change_password_path or your custom profile_path
|
|
34
|
+
def profile_url
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
define_singleton_method(:to_s) { "Plutonium::Auth::Rodauth(:#{name})" }
|
|
31
39
|
define_singleton_method(:inspect) { "Plutonium::Auth::Rodauth(:#{name})" }
|
|
32
40
|
RUBY
|
|
@@ -207,7 +207,11 @@ module Plutonium
|
|
|
207
207
|
url_args[:action] ||= :index if index == args.length - 1
|
|
208
208
|
elsif element.is_a?(Class)
|
|
209
209
|
controller_chain << element.to_s.pluralize
|
|
210
|
-
|
|
210
|
+
if index == args.length - 1
|
|
211
|
+
# Singular resources have no index, default to show
|
|
212
|
+
is_singular = current_engine.routes.singular_resource_route?(element.model_name.plural)
|
|
213
|
+
url_args[:action] ||= is_singular ? :show : :index
|
|
214
|
+
end
|
|
211
215
|
else
|
|
212
216
|
model_class = element.class
|
|
213
217
|
if model_class.respond_to?(:base_class) && model_class != model_class.base_class
|
|
@@ -223,8 +227,7 @@ module Plutonium
|
|
|
223
227
|
else
|
|
224
228
|
model_class.model_name.plural
|
|
225
229
|
end
|
|
226
|
-
|
|
227
|
-
is_singular = resource_route_config&.dig(:route_type) == :resource
|
|
230
|
+
is_singular = current_engine.routes.singular_resource_route?(route_key)
|
|
228
231
|
url_args[:id] = element.to_param unless is_singular
|
|
229
232
|
url_args[:action] ||= :show
|
|
230
233
|
else
|
|
@@ -236,7 +239,7 @@ module Plutonium
|
|
|
236
239
|
|
|
237
240
|
url_args[:"#{parent.model_name.singular}_id"] = parent.to_param if parent.present?
|
|
238
241
|
if scoped_to_entity? && scoped_entity_strategy == :path
|
|
239
|
-
url_args[scoped_entity_param_key] = current_scoped_entity
|
|
242
|
+
url_args[scoped_entity_param_key] = current_scoped_entity.to_param
|
|
240
243
|
end
|
|
241
244
|
|
|
242
245
|
if !url_args.key?(:format) && request.present? && request.format.present? && !request.format.symbol.in?([:html, :turbo_stream])
|
|
@@ -30,7 +30,11 @@ module Plutonium
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def entity_scope_for_authorize
|
|
33
|
-
|
|
33
|
+
# Use the instance variable directly to avoid circular dependency.
|
|
34
|
+
# When authorizing the scoped entity itself (in fetch_current_scoped_entity),
|
|
35
|
+
# @current_scoped_entity is not yet set, so this returns nil, which is correct
|
|
36
|
+
# since we can't use the entity as its own authorization scope.
|
|
37
|
+
@current_scoped_entity if scoped_to_entity?
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def verify_authorized
|
|
@@ -75,6 +75,13 @@ module Plutonium
|
|
|
75
75
|
# global default
|
|
76
76
|
breadcrumbs true
|
|
77
77
|
|
|
78
|
+
# forms
|
|
79
|
+
# Controls the "Save and add another" / "Update and continue editing" buttons
|
|
80
|
+
# nil = auto-detect (hidden for singular resources, shown for plural)
|
|
81
|
+
# true = always show
|
|
82
|
+
# false = always hide
|
|
83
|
+
inheritable_config_attr :submit_and_continue
|
|
84
|
+
|
|
78
85
|
def initialize
|
|
79
86
|
super
|
|
80
87
|
end
|
|
@@ -14,6 +14,12 @@ module Plutonium
|
|
|
14
14
|
resource_name resource_class, 2
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Returns the appropriate label for a resource (singular for singular resources, plural otherwise)
|
|
18
|
+
def resource_label(resource_class)
|
|
19
|
+
is_singular = current_engine.routes.singular_resource_route?(resource_class.model_name.plural)
|
|
20
|
+
resource_name(resource_class, is_singular ? 1 : 2)
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
# Returns a human-readable name for a nested collection using the association name.
|
|
18
24
|
# Falls back to resource_name_plural if not in a nested context.
|
|
19
25
|
# Uses I18n via human_attribute_name for proper localization.
|