plutonium 0.42.0 → 0.43.1
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 +42 -0
- data/app/assets/plutonium.css +2 -2
- 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/scopes_bar.rb +2 -74
- 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/lib/tasks/release.rake +6 -6
- data/package.json +1 -1
- metadata +17 -3
- data/lib/generators/pu/rodauth/concerns/gem_helpers.rb +0 -19
|
@@ -117,11 +117,7 @@ module PlutoniumGenerators
|
|
|
117
117
|
if name.include? "/"
|
|
118
118
|
attr_options[:to_table] = name.underscore.tr("/", "_").pluralize.to_sym
|
|
119
119
|
attr_options[:class_name] = name.classify
|
|
120
|
-
name = name
|
|
121
|
-
if (shared_namespace = find_shared_namespace(model_name, name, separator: "/"))
|
|
122
|
-
name = name.sub("#{shared_namespace}/", "")
|
|
123
|
-
end
|
|
124
|
-
name = name.tr("/", "_")
|
|
120
|
+
name = PlutoniumGenerators::Generator.derive_association_name(model_name, name)
|
|
125
121
|
end
|
|
126
122
|
end
|
|
127
123
|
|
|
@@ -259,27 +255,14 @@ module PlutoniumGenerators
|
|
|
259
255
|
end
|
|
260
256
|
end
|
|
261
257
|
|
|
262
|
-
def find_shared_namespace(model1, model2, separator: "::")
|
|
263
|
-
parts1 = model1.underscore.split(separator)
|
|
264
|
-
parts2 = model2.underscore.split(separator)
|
|
265
|
-
|
|
266
|
-
shared_namespace = []
|
|
267
|
-
[parts1.length, parts2.length].min.times do |i|
|
|
268
|
-
if parts1[i] == parts2[i]
|
|
269
|
-
shared_namespace << parts1[i]
|
|
270
|
-
else
|
|
271
|
-
break
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
shared_namespace.empty? ? nil : shared_namespace.join(separator)
|
|
276
|
-
end
|
|
277
258
|
end
|
|
278
259
|
|
|
279
260
|
def required?
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
261
|
+
if attr_options.key?(:null)
|
|
262
|
+
!attr_options[:null]
|
|
263
|
+
else
|
|
264
|
+
super
|
|
265
|
+
end
|
|
283
266
|
end
|
|
284
267
|
|
|
285
268
|
def cents?
|
|
@@ -88,7 +88,11 @@ module Pu
|
|
|
88
88
|
def bring_your_own_auth? = @bring_your_own_auth
|
|
89
89
|
|
|
90
90
|
def configure_entity_scoping
|
|
91
|
-
|
|
91
|
+
return unless options[:scope].present?
|
|
92
|
+
|
|
93
|
+
scope = options[:scope].camelize
|
|
94
|
+
# Prepend :: to ensure absolute constant reference (avoids NameError in engine.rb)
|
|
95
|
+
@scoped_entity_class = scope.start_with?("::") ? scope : "::#{scope}"
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
def scoped_to_entity? = scoped_entity_class.present?
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate a Profile resource for managing Rodauth account settings.
|
|
3
|
+
Creates a resource linked to the User model with a customized ShowPage
|
|
4
|
+
that displays security settings links (change password, 2FA, etc.).
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
rails g pu:profile:install [NAME] [field:type field:type] --dest=DESTINATION
|
|
8
|
+
|
|
9
|
+
Arguments:
|
|
10
|
+
NAME Profile resource name (default: Profile)
|
|
11
|
+
field:type Additional fields to add to the profile
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--dest=DESTINATION Target destination (required to avoid interactive prompts)
|
|
15
|
+
Use 'main_app' for main application
|
|
16
|
+
Use 'package_name' for feature package
|
|
17
|
+
--user-model=NAME Rodauth user model (default: User)
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
# Basic profile in main_app
|
|
21
|
+
rails g pu:profile:install --dest=main_app
|
|
22
|
+
|
|
23
|
+
# Profile with custom fields
|
|
24
|
+
rails g pu:profile:install bio:text avatar:attachment --dest=main_app
|
|
25
|
+
|
|
26
|
+
# Custom name with fields in a package
|
|
27
|
+
rails g pu:profile:install AccountSettings \
|
|
28
|
+
bio:text \
|
|
29
|
+
'timezone:string?' \
|
|
30
|
+
'notifications_enabled:boolean' \
|
|
31
|
+
--dest=customer
|
|
32
|
+
|
|
33
|
+
# With custom user model
|
|
34
|
+
rails g pu:profile:install --dest=main_app --user-model=Account
|
|
35
|
+
|
|
36
|
+
Generated Files:
|
|
37
|
+
Model: app/models/[package/]profile.rb
|
|
38
|
+
Migration: db/migrate/xxx_create_[package_]profiles.rb
|
|
39
|
+
Controller: [packages/package/]app/controllers/[package/]profiles_controller.rb
|
|
40
|
+
Policy: [packages/package/]app/policies/[package/]profile_policy.rb
|
|
41
|
+
Definition: [packages/package/]app/definitions/[package/]profile_definition.rb
|
|
42
|
+
|
|
43
|
+
What Gets Modified:
|
|
44
|
+
User Model: Adds `has_one :profile, dependent: :destroy`
|
|
45
|
+
Definition: Injects ShowPage with SecuritySection for Rodauth links
|
|
46
|
+
|
|
47
|
+
The SecuritySection automatically displays links for enabled Rodauth features:
|
|
48
|
+
- Change Password (change_password)
|
|
49
|
+
- Change Email (change_login)
|
|
50
|
+
- Two-Factor Authentication (otp)
|
|
51
|
+
- Recovery Codes (recovery_codes)
|
|
52
|
+
- Security Keys (webauthn)
|
|
53
|
+
- Active Sessions (active_sessions)
|
|
54
|
+
- Close Account (close_account)
|
|
55
|
+
|
|
56
|
+
After Generation:
|
|
57
|
+
1. Run migrations: rails db:migrate
|
|
58
|
+
2. Connect to portal: rails g pu:profile:conn --dest=my_portal
|
|
59
|
+
3. Customize the profile definition as needed
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pu
|
|
4
|
+
module Profile
|
|
5
|
+
module Concerns
|
|
6
|
+
module ProfileArguments
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
argument :name, type: :string, default: "Profile", required: false, banner: "NAME"
|
|
11
|
+
argument :attributes, type: :array, default: [], banner: "field[:type] field[:type]"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Normalize arguments: if name contains ":", treat it as an attribute
|
|
15
|
+
def normalize_arguments
|
|
16
|
+
if name.include?(":")
|
|
17
|
+
@profile_attributes = [name, *attributes]
|
|
18
|
+
@profile_name = "Profile"
|
|
19
|
+
else
|
|
20
|
+
@profile_name = name
|
|
21
|
+
@profile_attributes = attributes
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Connect a Profile resource to a portal with the profile_url helper configured.
|
|
3
|
+
This enables the "Profile" link in the user menu.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
rails g pu:profile:conn [RESOURCE] --dest=PORTAL
|
|
7
|
+
|
|
8
|
+
Arguments:
|
|
9
|
+
RESOURCE Profile resource class (default: Profile)
|
|
10
|
+
Use namespaced class for package resources (e.g., Competition::Profile)
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--dest=PORTAL Target portal (required to avoid interactive prompts)
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
# Connect namespaced Profile to portal
|
|
17
|
+
rails g pu:profile:conn Competition::Profile --dest=competition_portal
|
|
18
|
+
|
|
19
|
+
# Connect main_app Profile to portal
|
|
20
|
+
rails g pu:profile:conn Profile --dest=customer_portal
|
|
21
|
+
|
|
22
|
+
# Connect custom-named profile
|
|
23
|
+
rails g pu:profile:conn Competition::AccountSettings --dest=competition_portal
|
|
24
|
+
|
|
25
|
+
What Gets Created:
|
|
26
|
+
Controller: packages/[portal]/app/controllers/[portal]/profiles_controller.rb
|
|
27
|
+
|
|
28
|
+
What Gets Modified:
|
|
29
|
+
Routes: packages/[portal]/config/routes.rb (registers as singular resource)
|
|
30
|
+
Controller: packages/[portal]/app/controllers/[portal]/resource_controller.rb
|
|
31
|
+
(adds profile_url helper method)
|
|
32
|
+
|
|
33
|
+
The profile_url helper enables the "Profile" link in the user navigation menu.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "../lib/plutonium_generators"
|
|
5
|
+
|
|
6
|
+
module Pu
|
|
7
|
+
module Profile
|
|
8
|
+
class ConnGenerator < ::Rails::Generators::Base
|
|
9
|
+
include PlutoniumGenerators::Generator
|
|
10
|
+
|
|
11
|
+
desc "Connect a Profile resource to a portal and configure the profile_url helper"
|
|
12
|
+
|
|
13
|
+
argument :name, type: :string, default: "Profile", required: false, banner: "RESOURCE"
|
|
14
|
+
|
|
15
|
+
class_option :dest, type: :string,
|
|
16
|
+
desc: "Destination portal"
|
|
17
|
+
|
|
18
|
+
class_option :user_model, type: :string, default: "User",
|
|
19
|
+
desc: "The Rodauth user model"
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
validate_portal_destination!
|
|
23
|
+
connect_to_portal
|
|
24
|
+
customize_policy
|
|
25
|
+
customize_definition
|
|
26
|
+
customize_controller
|
|
27
|
+
add_profile_url_helper
|
|
28
|
+
rescue => e
|
|
29
|
+
exception "#{self.class} failed:", e
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def validate_portal_destination!
|
|
35
|
+
if selected_destination_portal == "main_app"
|
|
36
|
+
raise ArgumentError, <<~MSG.squish
|
|
37
|
+
pu:profile:conn is for portal packages only. For main_app, configure
|
|
38
|
+
profile_url directly in your ResourceController.
|
|
39
|
+
MSG
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def connect_to_portal
|
|
44
|
+
invoke "pu:res:conn", [resource_class_name],
|
|
45
|
+
dest: selected_destination_portal,
|
|
46
|
+
singular: true,
|
|
47
|
+
policy: true,
|
|
48
|
+
definition: true,
|
|
49
|
+
force: options[:force],
|
|
50
|
+
skip: options[:skip]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def customize_policy
|
|
54
|
+
content = <<-RUBY.chomp
|
|
55
|
+
|
|
56
|
+
# Profile is scoped to current user, not entity.
|
|
57
|
+
# Note: `user` here is the policy's user method (current authenticated user),
|
|
58
|
+
# while `#{user_table}` is the model's association name.
|
|
59
|
+
relation_scope do |relation|
|
|
60
|
+
skip_default_relation_scope!
|
|
61
|
+
relation.where(#{user_table}: user)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def create?
|
|
65
|
+
user.#{profile_association}.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def destroy?
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# User is set automatically from current_user, not via mass assignment
|
|
73
|
+
def permitted_attributes_for_create
|
|
74
|
+
super - [:#{user_table}]
|
|
75
|
+
end
|
|
76
|
+
RUBY
|
|
77
|
+
inject_into_file policy_path, content, after: /include #{dest_name.camelize}::ResourcePolicy\n/
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def customize_definition
|
|
81
|
+
# Add ShowPage with SecuritySection
|
|
82
|
+
content = indent(<<~RUBY, 2)
|
|
83
|
+
|
|
84
|
+
class ShowPage < ShowPage
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def render_after_content
|
|
88
|
+
render Plutonium::Profile::SecuritySection.new
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
RUBY
|
|
92
|
+
inject_into_file definition_path, content, after: /include #{dest_name.camelize}::ResourceDefinition\n/
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def customize_controller
|
|
96
|
+
# Set user automatically when creating profile
|
|
97
|
+
content = <<-RUBY.chomp
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def resource_params
|
|
102
|
+
super.merge(#{user_table}: current_user)
|
|
103
|
+
end
|
|
104
|
+
RUBY
|
|
105
|
+
inject_into_file controller_path, content, after: /include #{dest_name.camelize}::Concerns::Controller\n/
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_profile_url_helper
|
|
109
|
+
content = <<-RUBY.chomp
|
|
110
|
+
|
|
111
|
+
included do
|
|
112
|
+
helper_method :profile_url
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Returns the URL to the user's profile page.
|
|
118
|
+
def profile_url
|
|
119
|
+
profile = current_user.#{profile_association}
|
|
120
|
+
if profile
|
|
121
|
+
resource_url_for(profile)
|
|
122
|
+
else
|
|
123
|
+
resource_url_for(#{resource_class_name}, action: :new)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
RUBY
|
|
127
|
+
inject_into_file concerns_controller_path, content, after: /# add concerns above\.\n/
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def profile_association
|
|
131
|
+
resource_class_name.demodulize.underscore
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resource_class_name
|
|
135
|
+
name.camelize
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def user_table
|
|
139
|
+
options[:user_model].underscore
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def dest_name
|
|
143
|
+
selected_destination_portal
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def concerns_controller_path
|
|
147
|
+
"packages/#{dest_name}/app/controllers/#{dest_name}/concerns/controller.rb"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def controller_path
|
|
151
|
+
"packages/#{dest_name}/app/controllers/#{dest_name}/#{resource_class_name.underscore.pluralize}_controller.rb"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def policy_path
|
|
155
|
+
"packages/#{dest_name}/app/policies/#{dest_name}/#{resource_class_name.underscore}_policy.rb"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def definition_path
|
|
159
|
+
"packages/#{dest_name}/app/definitions/#{dest_name}/#{resource_class_name.underscore}_definition.rb"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def selected_destination_portal
|
|
163
|
+
@selected_destination_portal ||= portal_option :dest, prompt: "Select destination portal"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -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 -%>
|