plutonium 0.39.1 → 0.40.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-connect-resource/SKILL.md +19 -1
- data/.claude/skills/plutonium-controller/SKILL.md +5 -9
- data/.claude/skills/plutonium-definition-query/SKILL.md +10 -2
- data/.claude/skills/plutonium-installation/SKILL.md +9 -7
- data/.claude/skills/plutonium-invites/SKILL.md +363 -0
- data/.claude/skills/plutonium-package/SKILL.md +2 -1
- data/.claude/skills/plutonium-portal/SKILL.md +30 -16
- data/.claude/skills/plutonium-rodauth/SKILL.md +111 -18
- data/CHANGELOG.md +48 -0
- data/app/assets/plutonium.css +1 -1
- data/config/initializers/sqlite_alias.rb +8 -8
- data/docs/.vitepress/config.ts +1 -0
- data/docs/getting-started/tutorial/07-author-portal.md +1 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +5 -2
- data/docs/guides/adding-resources.md +10 -0
- data/docs/guides/authentication.md +15 -8
- data/docs/guides/creating-packages.md +13 -8
- data/docs/guides/index.md +2 -0
- data/docs/guides/search-filtering.md +8 -3
- data/docs/guides/user-invites.md +497 -0
- data/docs/public/templates/base.rb +5 -1
- data/docs/public/templates/lite.rb +42 -0
- data/docs/public/templates/pluton8.rb +7 -2
- data/docs/reference/controller/index.md +12 -7
- data/docs/reference/definition/query.md +12 -3
- data/docs/reference/generators/index.md +70 -10
- data/docs/reference/portal/index.md +22 -11
- 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/gem/active_shrine/active_shrine_generator.rb +31 -0
- data/lib/generators/pu/gem/active_shrine/templates/config/initializers/shrine.rb.tt +58 -0
- data/lib/generators/pu/gem/annotated/templates/lib/tasks/auto_annotate_models.rake +6 -1
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -0
- data/lib/generators/pu/invites/USAGE +27 -0
- data/lib/generators/pu/invites/install_generator.rb +364 -0
- data/lib/generators/pu/invites/invitable/USAGE +31 -0
- data/lib/generators/pu/invites/invitable_generator.rb +143 -0
- data/lib/generators/pu/invites/templates/INSTRUCTIONS +22 -0
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +24 -0
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +26 -0
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +47 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +77 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +68 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/resend_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +34 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +41 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/error.html.erb.tt +24 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +40 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +39 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +49 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +33 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +23 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +130 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +72 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -2
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +7 -1
- data/lib/generators/pu/lite/litestream/litestream_generator.rb +105 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +88 -0
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +14 -0
- data/lib/generators/pu/lite/setup/setup_generator.rb +54 -0
- data/lib/generators/pu/lite/solid_cable/solid_cable_generator.rb +65 -0
- data/lib/generators/pu/lite/solid_cache/solid_cache_generator.rb +66 -0
- data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +61 -0
- data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +107 -0
- data/lib/generators/pu/pkg/portal/USAGE +8 -2
- data/lib/generators/pu/pkg/portal/portal_generator.rb +11 -1
- data/lib/generators/pu/pkg/portal/templates/app/controllers/concerns/controller.rb.tt +2 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +1 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +7 -0
- data/lib/generators/pu/pkg/portal/templates/lib/engine.rb.tt +3 -0
- data/lib/generators/pu/res/conn/USAGE +5 -0
- data/lib/generators/pu/res/conn/conn_generator.rb +30 -4
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +6 -3
- data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +6 -6
- data/lib/generators/pu/rodauth/account_generator.rb +36 -11
- data/lib/generators/pu/rodauth/admin_generator.rb +55 -0
- data/lib/generators/pu/rodauth/install_generator.rb +1 -8
- data/lib/generators/pu/rodauth/templates/app/interactions/invite_admin_interaction.rb.tt +25 -0
- data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +6 -2
- data/lib/generators/pu/saas/USAGE +22 -0
- data/lib/generators/pu/saas/entity/USAGE +19 -0
- data/lib/generators/pu/saas/entity_generator.rb +55 -0
- data/lib/generators/pu/saas/membership/USAGE +25 -0
- data/lib/generators/pu/saas/membership_generator.rb +165 -0
- data/lib/generators/pu/saas/setup/USAGE +27 -0
- data/lib/generators/pu/saas/setup_generator.rb +98 -0
- data/lib/generators/pu/saas/user/USAGE +21 -0
- data/lib/generators/pu/saas/user_generator.rb +66 -0
- data/lib/plutonium/core/controller.rb +9 -5
- data/lib/plutonium/definition/base.rb +3 -1
- data/lib/plutonium/definition/scoping.rb +20 -0
- data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
- data/lib/plutonium/invites/concerns/invitable.rb +98 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
- data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
- data/lib/plutonium/invites/controller.rb +226 -0
- data/lib/plutonium/invites/pending_invite_check.rb +76 -0
- data/lib/plutonium/invites.rb +6 -0
- data/lib/plutonium/resource/controllers/queryable.rb +4 -0
- data/lib/plutonium/resource/query_object.rb +3 -5
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +64 -7
- data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
- data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
- data/public/plutonium-assets/plutonium-logo-original.png +0 -0
- data/public/plutonium-assets/plutonium-logo-white.png +0 -0
- data/public/plutonium-assets/plutonium-logo.png +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class SolidErrorsGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::ConfiguresSqlite
|
|
10
|
+
include PlutoniumGenerators::Concerns::MountsEngines
|
|
11
|
+
|
|
12
|
+
desc "Set up Solid Errors for error tracking with SQLite"
|
|
13
|
+
|
|
14
|
+
class_option :database, type: :string, default: "errors",
|
|
15
|
+
desc: "Database name for Solid Errors"
|
|
16
|
+
class_option :route, type: :string, default: "/manage/errors",
|
|
17
|
+
desc: "Route path for Solid Errors UI"
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
@db_name = options[:database]
|
|
21
|
+
|
|
22
|
+
bundle "solid_errors"
|
|
23
|
+
add_sqlite_database(@db_name)
|
|
24
|
+
run_solid_errors_install
|
|
25
|
+
configure_application
|
|
26
|
+
prepare_database(@db_name)
|
|
27
|
+
mount_solid_errors_engine
|
|
28
|
+
rescue => e
|
|
29
|
+
exception "#{self.class} failed:", e
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def run_solid_errors_install
|
|
35
|
+
Bundler.with_unbundled_env do
|
|
36
|
+
run "bin/rails generate solid_errors:install", env: {"DATABASE" => @db_name}
|
|
37
|
+
end
|
|
38
|
+
run "git checkout -- config/environments/production.rb 2>/dev/null || true"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configure_application
|
|
42
|
+
create_file "config/initializers/solid_errors.rb", <<~RUBY
|
|
43
|
+
# frozen_string_literal: true
|
|
44
|
+
|
|
45
|
+
Rails.application.configure do
|
|
46
|
+
config.solid_errors.connects_to = {database: {writing: :#{@db_name}}}
|
|
47
|
+
config.solid_errors.send_emails = ENV["SOLID_ERRORS_SEND_EMAILS"].present?
|
|
48
|
+
config.solid_errors.email_from = ENV["SOLID_ERRORS_EMAIL_FROM"]
|
|
49
|
+
config.solid_errors.email_to = ENV["SOLID_ERRORS_EMAIL_TO"]
|
|
50
|
+
config.solid_errors.username = ENV.fetch("SOLID_ERRORS_USERNAME", nil)
|
|
51
|
+
config.solid_errors.password = ENV.fetch("SOLID_ERRORS_PASSWORD", nil)
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def mount_solid_errors_engine
|
|
57
|
+
mount_engine %(mount SolidErrors::Engine, at: "#{options[:route]}"), authenticated: true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class SolidQueueGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::ConfiguresSqlite
|
|
10
|
+
include PlutoniumGenerators::Concerns::MountsEngines
|
|
11
|
+
|
|
12
|
+
desc "Set up Solid Queue for background jobs with SQLite"
|
|
13
|
+
|
|
14
|
+
class_option :database, type: :string, default: "queue",
|
|
15
|
+
desc: "Database name for Solid Queue"
|
|
16
|
+
class_option :route, type: :string, default: "/manage/jobs",
|
|
17
|
+
desc: "Route path for Mission Control Jobs UI"
|
|
18
|
+
class_option :skip_mission_control, type: :boolean, default: false,
|
|
19
|
+
desc: "Skip Mission Control Jobs UI"
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
@db_name = options[:database]
|
|
23
|
+
|
|
24
|
+
bundle "solid_queue"
|
|
25
|
+
add_sqlite_database(@db_name)
|
|
26
|
+
run_solid_queue_install
|
|
27
|
+
configure_application
|
|
28
|
+
prepare_database(@db_name)
|
|
29
|
+
create_jobs_script
|
|
30
|
+
configure_procfile
|
|
31
|
+
configure_kamal
|
|
32
|
+
setup_mission_control unless options[:skip_mission_control]
|
|
33
|
+
rescue => e
|
|
34
|
+
exception "#{self.class} failed:", e
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def run_solid_queue_install
|
|
40
|
+
Bundler.with_unbundled_env do
|
|
41
|
+
run "bin/rails generate solid_queue:install", env: {"DATABASE" => @db_name}
|
|
42
|
+
end
|
|
43
|
+
# Restore files modified by solid_queue:install
|
|
44
|
+
run "git checkout -- config/environments/production.rb 2>/dev/null || true"
|
|
45
|
+
run "git checkout -- config/puma.rb 2>/dev/null || true"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configure_application
|
|
49
|
+
create_file "config/initializers/solid_queue.rb", <<~RUBY
|
|
50
|
+
# frozen_string_literal: true
|
|
51
|
+
|
|
52
|
+
Rails.application.configure do
|
|
53
|
+
config.active_job.queue_adapter = :solid_queue
|
|
54
|
+
config.solid_queue.connects_to = {database: {writing: :#{@db_name}}}
|
|
55
|
+
config.solid_queue.silence_polling = true
|
|
56
|
+
end
|
|
57
|
+
RUBY
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_jobs_script
|
|
61
|
+
jobs_script = "bin/jobs"
|
|
62
|
+
return if File.exist?(File.expand_path(jobs_script, destination_root))
|
|
63
|
+
|
|
64
|
+
create_file jobs_script, <<~BASH
|
|
65
|
+
#!/usr/bin/env bash
|
|
66
|
+
set -e
|
|
67
|
+
exec bundle exec rake solid_queue:start
|
|
68
|
+
BASH
|
|
69
|
+
chmod jobs_script, 0o755
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def configure_kamal
|
|
73
|
+
deploy_file = "config/deploy.yml"
|
|
74
|
+
return unless File.exist?(File.expand_path(deploy_file, destination_root))
|
|
75
|
+
return if file_includes?(deploy_file, "job:")
|
|
76
|
+
|
|
77
|
+
insert_into_file deploy_file, after: /^servers:.*\n/ do
|
|
78
|
+
<<~YAML
|
|
79
|
+
job:
|
|
80
|
+
hosts:
|
|
81
|
+
- <%= ENV['DEPLOY_HOST'] %>
|
|
82
|
+
cmd: bin/jobs
|
|
83
|
+
YAML
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def configure_procfile
|
|
88
|
+
procfile = "Procfile.dev"
|
|
89
|
+
return unless File.exist?(File.expand_path(procfile, destination_root))
|
|
90
|
+
return if file_includes?(procfile, "jobs:")
|
|
91
|
+
|
|
92
|
+
append_to_file procfile, "jobs: bin/jobs\n"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def setup_mission_control
|
|
96
|
+
bundle "mission_control-jobs"
|
|
97
|
+
configure_mission_control
|
|
98
|
+
mount_engine %(mount MissionControl::Jobs::Engine, at: "#{options[:route]}"), authenticated: true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def configure_mission_control
|
|
102
|
+
# Disable built-in HTTP Basic Auth - using route constraints instead
|
|
103
|
+
environment "config.mission_control.jobs.http_basic_auth_enabled = false"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -7,7 +7,8 @@ Usage:
|
|
|
7
7
|
Options:
|
|
8
8
|
--auth=NAME Rodauth account to authenticate with (e.g., --auth=user)
|
|
9
9
|
--public Grant public access (no authentication required)
|
|
10
|
-
--byo
|
|
10
|
+
--byo Bring your own authentication (configure manually)
|
|
11
|
+
--scope=CLASS Entity class to scope to for multi-tenancy (e.g., --scope=Organization)
|
|
11
12
|
|
|
12
13
|
Without options, the generator prompts interactively for authentication choice.
|
|
13
14
|
|
|
@@ -25,6 +26,10 @@ Examples:
|
|
|
25
26
|
# Custom authentication (implement your own)
|
|
26
27
|
rails g pu:pkg:portal custom --byo
|
|
27
28
|
|
|
29
|
+
# With entity scoping (multi-tenancy)
|
|
30
|
+
rails g pu:pkg:portal admin --auth=admin --scope=Organization
|
|
31
|
+
rails g pu:pkg:portal customer --auth=customer --scope=Account
|
|
32
|
+
|
|
28
33
|
Generated Structure:
|
|
29
34
|
packages/[name]_portal/
|
|
30
35
|
├── app/
|
|
@@ -61,5 +66,6 @@ After Generation:
|
|
|
61
66
|
2. Connect resources to the portal:
|
|
62
67
|
rails g pu:res:conn Post --dest=admin_portal
|
|
63
68
|
|
|
64
|
-
3.
|
|
69
|
+
3. If you used --scope, entity scoping is already configured in engine.rb.
|
|
70
|
+
Otherwise, you can manually add it:
|
|
65
71
|
scope_to_entity Organization, strategy: :path
|
|
@@ -16,10 +16,12 @@ module Pu
|
|
|
16
16
|
class_option :auth, type: :string, desc: "Rodauth account to authenticate with (e.g., --auth=user)"
|
|
17
17
|
class_option :public, type: :boolean, default: false, desc: "Grant public access (no authentication)"
|
|
18
18
|
class_option :byo, type: :boolean, default: false, desc: "Bring your own authentication"
|
|
19
|
+
class_option :scope, type: :string, desc: "Entity class to scope to (e.g., --scope=Organization)"
|
|
19
20
|
|
|
20
21
|
def start
|
|
21
22
|
validate_package_name name
|
|
22
23
|
configure_authentication
|
|
24
|
+
configure_entity_scoping
|
|
23
25
|
|
|
24
26
|
template "lib/engine.rb", "packages/#{package_namespace}/lib/engine.rb"
|
|
25
27
|
template "config/routes.rb", "packages/#{package_namespace}/config/routes.rb"
|
|
@@ -28,6 +30,8 @@ module Pu
|
|
|
28
30
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/concerns/controller.rb"
|
|
29
31
|
template "app/controllers/plutonium_controller.rb",
|
|
30
32
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/plutonium_controller.rb"
|
|
33
|
+
template "app/controllers/resource_controller.rb",
|
|
34
|
+
"packages/#{package_namespace}/app/controllers/#{package_namespace}/resource_controller.rb"
|
|
31
35
|
|
|
32
36
|
template "app/controllers/dashboard_controller.rb",
|
|
33
37
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/dashboard_controller.rb"
|
|
@@ -48,7 +52,7 @@ module Pu
|
|
|
48
52
|
|
|
49
53
|
private
|
|
50
54
|
|
|
51
|
-
attr_reader :rodauth_account
|
|
55
|
+
attr_reader :rodauth_account, :scoped_entity_class
|
|
52
56
|
|
|
53
57
|
def configure_authentication
|
|
54
58
|
if options[:auth].present?
|
|
@@ -82,6 +86,12 @@ module Pu
|
|
|
82
86
|
def public_access? = @public_access
|
|
83
87
|
|
|
84
88
|
def bring_your_own_auth? = @bring_your_own_auth
|
|
89
|
+
|
|
90
|
+
def configure_entity_scoping
|
|
91
|
+
@scoped_entity_class = options[:scope].camelize if options[:scope].present?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def scoped_to_entity? = scoped_entity_class.present?
|
|
85
95
|
end
|
|
86
96
|
end
|
|
87
97
|
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module <%= package_name %>
|
|
2
|
+
# Base controller for portal resources when no feature package controller exists.
|
|
3
|
+
# Add customizations to Concerns::Controller, not here.
|
|
4
|
+
class ResourceController < ::ResourceController
|
|
5
|
+
include <%= package_name %>::Concerns::Controller
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -10,6 +10,7 @@ Options:
|
|
|
10
10
|
--src=PACKAGE Source feature package (optional)
|
|
11
11
|
Use main_app for resources in the main application
|
|
12
12
|
Use package name for resources in a feature package
|
|
13
|
+
--singular Register the resource as singular (e.g., profile, dashboard)
|
|
13
14
|
|
|
14
15
|
Arguments:
|
|
15
16
|
RESOURCES Space-separated list of model names to connect.
|
|
@@ -26,6 +27,9 @@ Examples:
|
|
|
26
27
|
# Connect namespaced resources (from a feature package)
|
|
27
28
|
rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
|
|
28
29
|
|
|
30
|
+
# Connect a singular resource (e.g., profile, dashboard)
|
|
31
|
+
rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
32
|
+
|
|
29
33
|
# Interactive mode (prompts for everything)
|
|
30
34
|
rails g pu:res:conn
|
|
31
35
|
|
|
@@ -56,6 +60,7 @@ Generated Code:
|
|
|
56
60
|
|
|
57
61
|
Routes register the resource:
|
|
58
62
|
register_resource ::Blogging::Post
|
|
63
|
+
register_resource ::Profile, singular: true # With --singular
|
|
59
64
|
|
|
60
65
|
Workflow:
|
|
61
66
|
1. Create feature package: rails g pu:pkg:package blogging
|
|
@@ -12,20 +12,25 @@ module Pu
|
|
|
12
12
|
|
|
13
13
|
desc(
|
|
14
14
|
"Create a connection between a resource and a portal\n\n" \
|
|
15
|
-
"e.g. rails g pu:res:conn todo --dest=dashboard_portal"
|
|
15
|
+
"e.g. rails g pu:res:conn todo --dest=dashboard_portal\n" \
|
|
16
|
+
" rails g pu:res:conn profile --dest=customer_portal --singular"
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
class_option :singular, type: :boolean, default: false,
|
|
20
|
+
desc: "Register the resource as a singular resource (e.g., profile)"
|
|
19
21
|
|
|
20
22
|
def start
|
|
21
23
|
selected_resources = resources_selection
|
|
22
24
|
@app_namespace = portal_option(:dest, prompt: "Select destination portal").camelize
|
|
23
25
|
|
|
26
|
+
validate_resources!(selected_resources)
|
|
27
|
+
|
|
24
28
|
selected_resources.each do |resource|
|
|
25
29
|
@resource_class = resource
|
|
30
|
+
|
|
26
31
|
if app_namespace == "MainApp"
|
|
27
32
|
insert_into_file "config/routes.rb",
|
|
28
|
-
indent("register_resource ::#{resource}\n", 2),
|
|
33
|
+
indent("register_resource ::#{resource}#{singular_option}\n", 2),
|
|
29
34
|
after: /.*Rails\.application\.routes\.draw do.*\n/
|
|
30
35
|
else
|
|
31
36
|
unless expected_parent_policy
|
|
@@ -42,7 +47,7 @@ module Pu
|
|
|
42
47
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/#{resource.pluralize.underscore}_controller.rb"
|
|
43
48
|
|
|
44
49
|
insert_into_file "packages/#{package_namespace}/config/routes.rb",
|
|
45
|
-
indent("register_resource ::#{resource}\n", 2),
|
|
50
|
+
indent("register_resource ::#{resource}#{singular_option}\n", 2),
|
|
46
51
|
before: /.*# register resources above.*/
|
|
47
52
|
end
|
|
48
53
|
end
|
|
@@ -58,6 +63,10 @@ module Pu
|
|
|
58
63
|
app_namespace.underscore
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
def singular_option
|
|
67
|
+
options[:singular] ? ", singular: true" : ""
|
|
68
|
+
end
|
|
69
|
+
|
|
61
70
|
def resource_namespace
|
|
62
71
|
app_namespace.underscore
|
|
63
72
|
end
|
|
@@ -110,6 +119,23 @@ module Pu
|
|
|
110
119
|
def policy_attributes_for_read
|
|
111
120
|
default_policy_attributes
|
|
112
121
|
end
|
|
122
|
+
|
|
123
|
+
def validate_resources!(resources)
|
|
124
|
+
invalid = resources.reject { |r| resource_record?(r) }
|
|
125
|
+
return if invalid.empty?
|
|
126
|
+
|
|
127
|
+
invalid.each do |resource|
|
|
128
|
+
say_status :error, "#{resource} does not include Plutonium::Resource::Record", :red
|
|
129
|
+
end
|
|
130
|
+
error "All resources must include Plutonium::Resource::Record to be connected to a portal"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def resource_record?(resource)
|
|
134
|
+
klass = resource.safe_constantize
|
|
135
|
+
return false unless klass
|
|
136
|
+
|
|
137
|
+
klass.included_modules.any? { |mod| mod.to_s.include?("Plutonium::Resource::Record") }
|
|
138
|
+
end
|
|
113
139
|
end
|
|
114
140
|
end
|
|
115
141
|
end
|
|
@@ -14,15 +14,18 @@ module Pu
|
|
|
14
14
|
|
|
15
15
|
class_option :model, type: :boolean, default: true
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Skip collision check when scaffolding for existing model
|
|
18
|
+
def check_class_collision
|
|
19
|
+
super if options[:model]
|
|
20
|
+
end
|
|
19
21
|
|
|
22
|
+
def setup
|
|
20
23
|
model_class = class_name.safe_constantize
|
|
21
24
|
if model_class.present?
|
|
22
25
|
if attributes.empty?
|
|
23
26
|
attributes_str = model_class.content_columns.map { |col| "#{col.name}:#{col.type}" }
|
|
24
27
|
self.attributes = parse_attributes_internal!(attributes_str)
|
|
25
|
-
|
|
28
|
+
elsif options[:model]
|
|
26
29
|
warn("Overwriting existing resource. You can leave out the attributes to import an existing resource.")
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
class <%= class_name %>Policy < <%= [feature_package_name, "ResourcePolicy"].join "::" %>
|
|
3
3
|
# Core actions
|
|
4
4
|
|
|
5
|
-
def create?
|
|
6
|
-
|
|
7
|
-
end
|
|
5
|
+
# def create?
|
|
6
|
+
# true
|
|
7
|
+
# end
|
|
8
8
|
|
|
9
|
-
def read?
|
|
10
|
-
|
|
11
|
-
end
|
|
9
|
+
# def read?
|
|
10
|
+
# true
|
|
11
|
+
# end
|
|
12
12
|
|
|
13
13
|
# Core attributes
|
|
14
14
|
|
|
@@ -18,6 +18,9 @@ module Pu
|
|
|
18
18
|
desc "Generate a rodauth-rails account.\n\n" \
|
|
19
19
|
"Configures a basic set of features as well as migrations, a model, mailer and views."
|
|
20
20
|
|
|
21
|
+
class_option :extra_attributes, type: :array, default: [],
|
|
22
|
+
desc: "Additional attributes to add to the account model (e.g., role:integer)"
|
|
23
|
+
|
|
21
24
|
def install_dependencies
|
|
22
25
|
Bundler.with_unbundled_env do
|
|
23
26
|
run "bundle add jwt" if jwt? || jwt_refresh?
|
|
@@ -51,20 +54,27 @@ module Pu
|
|
|
51
54
|
|
|
52
55
|
def configure_rodauth_plugin_load_memory
|
|
53
56
|
in_root do
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
rodauth_app = File.read("app/rodauth/rodauth_app.rb")
|
|
58
|
+
load_memory_pattern = primary? ? /rodauth\.load_memory/ : /rodauth\(:#{table_prefix}\)\.load_memory/
|
|
59
|
+
|
|
60
|
+
return if rodauth_app.match?(load_memory_pattern)
|
|
61
|
+
|
|
62
|
+
plugin_config = if primary?
|
|
63
|
+
indent("rodauth.load_memory # autologin remembered #{table}\n", 4)
|
|
64
|
+
else
|
|
65
|
+
indent(<<~RUBY, 4)
|
|
66
|
+
if r.path.start_with?("/#{table_prefix}_dashboard")
|
|
67
|
+
rodauth(:#{table_prefix}).load_memory # autologin remembered #{table}
|
|
68
|
+
end
|
|
69
|
+
RUBY
|
|
70
|
+
end
|
|
58
71
|
|
|
59
72
|
if remember?
|
|
60
73
|
insert_into_file "app/rodauth/rodauth_app.rb", plugin_config, after: "# plugin route configuration\n"
|
|
61
74
|
else
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
insert_into_file "app/rodauth/rodauth_app.rb", indent("# rodauth.load_memory # autologin remembered users\n", 4),
|
|
66
|
-
after: "# plugin route configuration\n"
|
|
67
|
-
end
|
|
75
|
+
unless rodauth_app.match?(/\.load_memory # autologin/)
|
|
76
|
+
insert_into_file "app/rodauth/rodauth_app.rb", indent("# rodauth.load_memory # autologin remembered users\n", 4),
|
|
77
|
+
after: "# plugin route configuration\n"
|
|
68
78
|
end
|
|
69
79
|
end
|
|
70
80
|
end
|
|
@@ -83,13 +93,28 @@ module Pu
|
|
|
83
93
|
migration_name: options[:migration_name],
|
|
84
94
|
force: options[:force],
|
|
85
95
|
skip: options[:skip]
|
|
96
|
+
|
|
97
|
+
add_extra_columns_to_migration
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_extra_columns_to_migration
|
|
101
|
+
return if options[:extra_attributes].blank?
|
|
102
|
+
|
|
103
|
+
migration_file = Dir[File.join(destination_root, "db/migrate/*_create_rodauth_#{table_prefix}_*.rb")].first
|
|
104
|
+
return unless migration_file
|
|
105
|
+
|
|
106
|
+
attributes = options[:extra_attributes].map { |attr| PlutoniumGenerators::ModelGeneratorBase::GeneratedAttribute.parse(table, attr) }
|
|
107
|
+
columns = attributes.map { |a| " t.#{a.type} :#{a.name}#{a.inject_options}" }.join("\n")
|
|
108
|
+
|
|
109
|
+
inject_into_file migration_file, "#{columns}\n", after: /t\.string :password_hash\n/
|
|
86
110
|
end
|
|
87
111
|
|
|
88
112
|
def create_account_model
|
|
89
113
|
return unless base?
|
|
90
114
|
|
|
91
115
|
template "app/models/account.rb", "app/models/#{account_path}.rb"
|
|
92
|
-
|
|
116
|
+
scaffold_attrs = ["email:string", "status:integer"] + Array(options[:extra_attributes])
|
|
117
|
+
invoke "pu:res:scaffold", [table, *scaffold_attrs], dest: "main_app",
|
|
93
118
|
model: false,
|
|
94
119
|
force: true,
|
|
95
120
|
skip: options[:skip]
|
|
@@ -15,6 +15,12 @@ module Pu
|
|
|
15
15
|
|
|
16
16
|
argument :name
|
|
17
17
|
|
|
18
|
+
class_option :roles, type: :array, default: %w[super_admin admin],
|
|
19
|
+
desc: "Available roles for admin accounts"
|
|
20
|
+
|
|
21
|
+
class_option :extra_attributes, type: :array, default: [],
|
|
22
|
+
desc: "Additional attributes to add to the account model (e.g., name:string)"
|
|
23
|
+
|
|
18
24
|
def start
|
|
19
25
|
generate_admin_account
|
|
20
26
|
configure_admin_account
|
|
@@ -28,11 +34,13 @@ module Pu
|
|
|
28
34
|
invoke "pu:rodauth:account", [name],
|
|
29
35
|
defaults: false,
|
|
30
36
|
**admin_features,
|
|
37
|
+
extra_attributes: options[:extra_attributes],
|
|
31
38
|
force: options[:force],
|
|
32
39
|
skip: options[:skip]
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def configure_admin_account
|
|
43
|
+
add_role_column_to_migration
|
|
36
44
|
# Prevent account creation from web
|
|
37
45
|
insert_into_file "app/rodauth/#{normalized_name}_rodauth_plugin.rb", indent(<<~EOT, 4), after: /# ==> Hooks\n/
|
|
38
46
|
|
|
@@ -66,6 +74,37 @@ module Pu
|
|
|
66
74
|
template "app/views/_login_form_footer.html.erb.tt", "app/views/rodauth/#{normalized_name}/_login_form_footer.html.erb"
|
|
67
75
|
|
|
68
76
|
template "lib/tasks/rodauth_admin.rake", "lib/tasks/rodauth_#{normalized_name}.rake"
|
|
77
|
+
|
|
78
|
+
add_role_enum
|
|
79
|
+
create_invite_interaction
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_role_column_to_migration
|
|
83
|
+
migration_file = Dir[File.join(destination_root, "db/migrate/*_create_rodauth_#{normalized_name}_*.rb")].first
|
|
84
|
+
return unless migration_file
|
|
85
|
+
|
|
86
|
+
inject_into_file migration_file,
|
|
87
|
+
" t.integer :role, null: false, default: 0\n",
|
|
88
|
+
after: /t\.string :password_hash\n/
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_role_enum
|
|
92
|
+
inject_into_file "app/models/#{normalized_name}.rb",
|
|
93
|
+
"enum :role, #{roles_enum}\n ",
|
|
94
|
+
before: "# add enums above.\n"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def create_invite_interaction
|
|
98
|
+
template "app/interactions/invite_admin_interaction.rb",
|
|
99
|
+
"app/interactions/#{normalized_name}/invite_interaction.rb"
|
|
100
|
+
|
|
101
|
+
inject_into_file "app/definitions/#{normalized_name}_definition.rb",
|
|
102
|
+
" action :invite, interaction: #{name.classify}::InviteInteraction, collection: true, category: :primary\n",
|
|
103
|
+
after: /class #{name.classify}Definition < .+\n/
|
|
104
|
+
|
|
105
|
+
inject_into_file "app/policies/#{normalized_name}_policy.rb",
|
|
106
|
+
"def invite?\n true\n end\n\n ",
|
|
107
|
+
before: "# Core attributes"
|
|
69
108
|
end
|
|
70
109
|
|
|
71
110
|
def admin_features
|
|
@@ -83,6 +122,22 @@ module Pu
|
|
|
83
122
|
def display_name = name.humanize.downcase
|
|
84
123
|
|
|
85
124
|
def normalized_name = name.underscore
|
|
125
|
+
|
|
126
|
+
def roles
|
|
127
|
+
Array(options[:roles]).flat_map { |r| r.split(",") }.map(&:strip)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def roles_enum
|
|
131
|
+
roles.each_with_index.map { |r, i| "#{r}: #{i}" }.join(", ")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def default_role
|
|
135
|
+
[1, roles.size - 1].min
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def default_role_name
|
|
139
|
+
roles[default_role]
|
|
140
|
+
end
|
|
86
141
|
end
|
|
87
142
|
end
|
|
88
143
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
require "rails/generators/base"
|
|
2
2
|
require "rails/generators/active_record/migration"
|
|
3
3
|
require "securerandom"
|
|
4
|
-
require "plutonium/auth/sequel_adapter"
|
|
5
4
|
|
|
6
5
|
module Pu
|
|
7
6
|
module Rodauth
|
|
@@ -54,13 +53,7 @@ module Pu
|
|
|
54
53
|
|
|
55
54
|
private
|
|
56
55
|
|
|
57
|
-
#
|
|
58
|
-
def sequel_adapter
|
|
59
|
-
Plutonium::Auth::SequelAdapter.sequel_adapter
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Delegates to the SequelAdapter module's internal ActiveRecord adapter detection.
|
|
63
|
-
# We still provide this method for use in create_install_migration.
|
|
56
|
+
# Detects the ActiveRecord adapter for migration generation.
|
|
64
57
|
def activerecord_adapter
|
|
65
58
|
if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
66
59
|
ActiveRecord::Base.connection_db_config&.adapter
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= name.classify %>::InviteInteraction < Plutonium::Resource::Interaction
|
|
4
|
+
presents label: "Invite <%= name.titleize %>", icon: Phlex::TablerIcons::Mail
|
|
5
|
+
|
|
6
|
+
attribute :email
|
|
7
|
+
attribute :role, default: :<%= default_role_name %>
|
|
8
|
+
|
|
9
|
+
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
10
|
+
validates :role, presence: true, inclusion: {in: <%= name.classify %>.roles.keys}
|
|
11
|
+
|
|
12
|
+
input :role, as: :select, choices: <%= name.classify %>.roles.keys
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
account = nil
|
|
16
|
+
<%= name.classify %>.transaction do
|
|
17
|
+
RodauthApp.rodauth(:<%= normalized_name %>).create_account(login: email)
|
|
18
|
+
account = <%= name.classify %>.find_by!(email: email)
|
|
19
|
+
account.update!(role: role)
|
|
20
|
+
end
|
|
21
|
+
succeed(account).with_message("Invitation sent to #{email}")
|
|
22
|
+
rescue ::Rodauth::InternalRequestError => e
|
|
23
|
+
failed(email: e.message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -2,6 +2,11 @@ class <%= account_path.classify %> < ResourceRecord
|
|
|
2
2
|
include Rodauth::Rails.model<%= "(:#{table_prefix})" unless primary? %>
|
|
3
3
|
# add concerns above.
|
|
4
4
|
|
|
5
|
+
# add constants above.
|
|
6
|
+
|
|
7
|
+
enum :status, unverified: 1, verified: 2, closed: 3
|
|
8
|
+
# add enums above.
|
|
9
|
+
|
|
5
10
|
<%- if account_path.include?("/") -%>
|
|
6
11
|
self.table_name = :<%= table_prefix.pluralize %>
|
|
7
12
|
<%- end -%>
|
|
@@ -24,8 +29,7 @@ class <%= account_path.classify %> < ResourceRecord
|
|
|
24
29
|
|
|
25
30
|
# add delegations above.
|
|
26
31
|
|
|
27
|
-
enum :status, unverified: 1, verified: 2, closed: 3
|
|
28
32
|
# add misc attribute macros above.
|
|
29
33
|
|
|
30
|
-
# add methods above.
|
|
34
|
+
# add methods above. add private methods below.
|
|
31
35
|
end
|