plutonium 0.43.1 → 0.44.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/.ruby-version +1 -1
- data/CHANGELOG.md +35 -0
- data/Rakefile +24 -6
- data/app/assets/plutonium.css +1 -1
- data/config/initializers/pagy.rb +1 -4
- data/gemfiles/rails_7.gemfile +1 -1
- data/gemfiles/rails_7.gemfile.lock +128 -103
- data/gemfiles/rails_8.0.gemfile +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +56 -46
- data/gemfiles/rails_8.1.gemfile +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +108 -98
- data/lib/generators/pu/invites/install_generator.rb +69 -11
- data/lib/generators/pu/invites/templates/INSTRUCTIONS +4 -1
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +1 -1
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +13 -8
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +17 -26
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +2 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +3 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +3 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +2 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -6
- data/lib/generators/pu/lib/plutonium_generators/concerns/rodauth_redirects.rb +41 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +0 -1
- data/lib/generators/pu/lib/plutonium_generators/non_interactive_prompt.rb +27 -0
- data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +4 -0
- data/lib/generators/pu/saas/entity_generator.rb +16 -0
- data/lib/generators/pu/saas/membership_generator.rb +4 -3
- data/lib/generators/pu/saas/portal/USAGE +15 -0
- data/lib/generators/pu/saas/portal_generator.rb +122 -0
- data/lib/generators/pu/saas/setup/USAGE +17 -22
- data/lib/generators/pu/saas/setup_generator.rb +62 -9
- data/lib/generators/pu/saas/welcome/USAGE +27 -0
- data/lib/generators/pu/saas/welcome/templates/app/controllers/authenticated_controller.rb.tt +18 -0
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +69 -0
- data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +33 -0
- data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +51 -0
- data/lib/generators/pu/saas/welcome/templates/app/views/welcome/select_entity.html.erb.tt +22 -0
- data/lib/generators/pu/saas/welcome_generator.rb +197 -0
- data/lib/plutonium/auth/sequel_adapter.rb +1 -2
- data/lib/plutonium/core/controller.rb +18 -8
- data/lib/plutonium/core/controllers/association_resolver.rb +19 -6
- data/lib/plutonium/core/controllers/authorizable.rb +11 -0
- data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -1
- data/lib/plutonium/invites/concerns/invite_token.rb +0 -8
- data/lib/plutonium/invites/concerns/invite_user.rb +1 -1
- data/lib/plutonium/resource/controller.rb +2 -3
- data/lib/plutonium/resource/controllers/authorizable.rb +4 -5
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/controllers/presentable.rb +2 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +1 -7
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +4 -6
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +2 -2
- metadata +18 -10
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AuthenticatedController < ApplicationController
|
|
4
|
+
<% if rodauth? -%>
|
|
5
|
+
include Plutonium::Auth::Rodauth(:<%= rodauth_config %>)
|
|
6
|
+
|
|
7
|
+
before_action { rodauth.require_authentication }
|
|
8
|
+
<% else -%>
|
|
9
|
+
before_action :require_authenticated
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def require_authenticated
|
|
14
|
+
# TODO: Implement based on your authentication system
|
|
15
|
+
redirect_to "/login" unless current_user
|
|
16
|
+
end
|
|
17
|
+
<% end -%>
|
|
18
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class WelcomeController < AuthenticatedController
|
|
4
|
+
layout "welcome"
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
entities = current_user.<%= entity_table.pluralize %>.to_a
|
|
8
|
+
if entities.none?
|
|
9
|
+
@<%= entity_table %> = <%= entity_model %>.new
|
|
10
|
+
<% if profile? -%>
|
|
11
|
+
@profile = current_user.profile || current_user.build_profile
|
|
12
|
+
<% end -%>
|
|
13
|
+
render :onboarding
|
|
14
|
+
elsif entities.one?
|
|
15
|
+
redirect_to portal_root_path(entities.first)
|
|
16
|
+
else
|
|
17
|
+
@entities = entities
|
|
18
|
+
render :select_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def new_entity
|
|
23
|
+
@<%= entity_table %> = <%= entity_model %>.new
|
|
24
|
+
<% if profile? -%>
|
|
25
|
+
@profile = current_user.profile || current_user.build_profile
|
|
26
|
+
<% end -%>
|
|
27
|
+
render :onboarding
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def onboard
|
|
31
|
+
@<%= entity_table %> = <%= entity_model %>.new(<%= entity_table %>_params)
|
|
32
|
+
<% if profile? -%>
|
|
33
|
+
@profile = current_user.profile || current_user.build_profile
|
|
34
|
+
@profile.assign_attributes(profile_params)
|
|
35
|
+
|
|
36
|
+
if @<%= entity_table %>.valid? && @profile.valid?
|
|
37
|
+
<% else -%>
|
|
38
|
+
if @<%= entity_table %>.valid?
|
|
39
|
+
<% end -%>
|
|
40
|
+
ActiveRecord::Base.transaction do
|
|
41
|
+
@<%= entity_table %>.save!
|
|
42
|
+
<%= membership_model %>.create!(<%= entity_table %>: @<%= entity_table %>, <%= user_table %>: current_user, role: :owner)
|
|
43
|
+
<% if profile? -%>
|
|
44
|
+
@profile.save!
|
|
45
|
+
<% end -%>
|
|
46
|
+
end
|
|
47
|
+
redirect_to portal_root_path(@<%= entity_table %>), notice: "Welcome! Your workspace is ready."
|
|
48
|
+
else
|
|
49
|
+
render :onboarding, status: :unprocessable_entity
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def <%= entity_table %>_params
|
|
56
|
+
params.require(:<%= entity_table %>).permit(:name)
|
|
57
|
+
end
|
|
58
|
+
<% if profile? -%>
|
|
59
|
+
|
|
60
|
+
def profile_params
|
|
61
|
+
params.require(:profile).permit(:name)
|
|
62
|
+
end
|
|
63
|
+
<% end -%>
|
|
64
|
+
|
|
65
|
+
def portal_root_path(<%= entity_table %>)
|
|
66
|
+
<%= portal_engine %>::Engine.routes.url_helpers.<%= entity_table %>_root_path(<%= entity_table %>: <%= entity_table %>)
|
|
67
|
+
end
|
|
68
|
+
helper_method :portal_root_path
|
|
69
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Welcome</title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
+
<meta name="csrf-param" content="authenticity_token" />
|
|
8
|
+
<meta name="csrf-token" content="<%%= form_authenticity_token %>" />
|
|
9
|
+
<%%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
10
|
+
<%%= javascript_importmap_tags %>
|
|
11
|
+
</head>
|
|
12
|
+
<body class="antialiased min-h-screen bg-[var(--pu-body)]">
|
|
13
|
+
<main class="p-4 min-h-screen flex flex-col items-center justify-center gap-2 px-6 py-8 mx-auto lg:py-0">
|
|
14
|
+
<%% if flash.any? %>
|
|
15
|
+
<%% flash.each do |type, message| %>
|
|
16
|
+
<div class="fixed z-50 top-16 inset-x-0 mx-auto flex items-center w-full max-w-md p-4 rounded-[var(--pu-radius-lg)]"
|
|
17
|
+
style="box-shadow: var(--pu-shadow-md); background: <%%= type == 'alert' ? 'var(--color-danger-50)' : 'var(--color-success-50)' %>; color: <%%= type == 'alert' ? 'var(--color-danger-600)' : 'var(--color-success-600)' %>"
|
|
18
|
+
role="alert">
|
|
19
|
+
<div class="ms-3 text-sm font-normal"><%%= message %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<%% end %>
|
|
22
|
+
<%% end %>
|
|
23
|
+
|
|
24
|
+
<!-- Main content card -->
|
|
25
|
+
<div class="w-full sm:max-w-md bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)]"
|
|
26
|
+
style="box-shadow: var(--pu-shadow-lg)">
|
|
27
|
+
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
|
28
|
+
<%%= yield %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</main>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<h1 class="text-lg font-bold leading-tight tracking-tight text-[var(--pu-text)] md:text-xl text-center">
|
|
2
|
+
Welcome! Let's get you set up.
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<%%= form_with(url: welcome_onboard_path, method: :post, class: "space-y-4 md:space-y-6") do |f| %>
|
|
6
|
+
<% if profile? -%>
|
|
7
|
+
<%% if @<%= entity_table %>.errors.any? || @profile.errors.any? %>
|
|
8
|
+
<% else -%>
|
|
9
|
+
<%% if @<%= entity_table %>.errors.any? %>
|
|
10
|
+
<% end -%>
|
|
11
|
+
<div class="p-4 rounded-[var(--pu-radius-lg)] bg-[color:var(--color-danger-50)] text-[color:var(--color-danger-600)]">
|
|
12
|
+
<ul class="list-disc list-inside text-sm">
|
|
13
|
+
<%% @<%= entity_table %>.errors.full_messages.each do |message| %>
|
|
14
|
+
<li><%%= message %></li>
|
|
15
|
+
<%% end %>
|
|
16
|
+
<% if profile? -%>
|
|
17
|
+
<%% @profile.errors.full_messages.each do |message| %>
|
|
18
|
+
<li><%%= message %></li>
|
|
19
|
+
<%% end %>
|
|
20
|
+
<% end -%>
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
23
|
+
<%% end %>
|
|
24
|
+
<% if profile? -%>
|
|
25
|
+
|
|
26
|
+
<%%= f.fields_for :profile, @profile do |pf| %>
|
|
27
|
+
<div>
|
|
28
|
+
<%%= pf.label :name, "Your Name", class: "block mb-2 text-sm font-medium text-[var(--pu-text)]" %>
|
|
29
|
+
<%%= pf.text_field :name,
|
|
30
|
+
class: "pu-input w-full",
|
|
31
|
+
placeholder: "e.g. Jane Smith",
|
|
32
|
+
autofocus: true %>
|
|
33
|
+
</div>
|
|
34
|
+
<%% end %>
|
|
35
|
+
<% end -%>
|
|
36
|
+
|
|
37
|
+
<%%= f.fields_for :<%= entity_table %>, @<%= entity_table %> do |ef| %>
|
|
38
|
+
<div>
|
|
39
|
+
<%%= ef.label :name, "Workspace Name", class: "block mb-2 text-sm font-medium text-[var(--pu-text)]" %>
|
|
40
|
+
<%%= ef.text_field :name,
|
|
41
|
+
class: "pu-input w-full",
|
|
42
|
+
placeholder: "e.g. Acme Corp",
|
|
43
|
+
<% unless profile? -%>
|
|
44
|
+
autofocus: true,
|
|
45
|
+
<% end -%>
|
|
46
|
+
required: true %>
|
|
47
|
+
</div>
|
|
48
|
+
<%% end %>
|
|
49
|
+
|
|
50
|
+
<%%= f.submit "Get Started", class: "w-full pu-btn pu-btn-md pu-btn-primary" %>
|
|
51
|
+
<%% end %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<h1 class="text-lg font-bold leading-tight tracking-tight text-[var(--pu-text)] md:text-xl text-center">
|
|
2
|
+
Select a Workspace
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<div class="space-y-2">
|
|
6
|
+
<%% @entities.each do |<%= entity_table %>| %>
|
|
7
|
+
<a href="<%%= portal_root_path(<%= entity_table %>) %>"
|
|
8
|
+
class="flex items-center gap-3 w-full p-4 text-left bg-[var(--pu-surface-alt)] border border-[var(--pu-border)] rounded-[var(--pu-radius-lg)] hover:border-[var(--color-primary-500)] hover:bg-[var(--pu-surface)] transition-colors">
|
|
9
|
+
<div class="flex-1">
|
|
10
|
+
<p class="font-medium text-[var(--pu-text)]"><%%= <%= entity_table %>.name %></p>
|
|
11
|
+
</div>
|
|
12
|
+
<%%= render Phlex::TablerIcons::ChevronRight.new(class: "w-5 h-5 text-[var(--pu-text-muted)]") %>
|
|
13
|
+
</a>
|
|
14
|
+
<%% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="pt-2">
|
|
18
|
+
<a href="<%%= welcome_onboard_path %>"
|
|
19
|
+
class="w-full block pu-btn pu-btn-md pu-btn-outline text-center">
|
|
20
|
+
Create New Workspace
|
|
21
|
+
</a>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "../lib/plutonium_generators"
|
|
5
|
+
|
|
6
|
+
module Pu
|
|
7
|
+
module Saas
|
|
8
|
+
class WelcomeGenerator < ::Rails::Generators::Base
|
|
9
|
+
include PlutoniumGenerators::Generator
|
|
10
|
+
include PlutoniumGenerators::Concerns::RodauthRedirects
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("welcome/templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Generate a post-login welcome flow with onboarding and entity selection"
|
|
15
|
+
|
|
16
|
+
class_option :user_model, type: :string, required: true,
|
|
17
|
+
desc: "The user model name (e.g., User)"
|
|
18
|
+
|
|
19
|
+
class_option :entity_model, type: :string, required: true,
|
|
20
|
+
desc: "The entity model name (e.g., Organization)"
|
|
21
|
+
|
|
22
|
+
class_option :portal, type: :string, required: true,
|
|
23
|
+
desc: "The portal engine name (e.g., CustomerPortal)"
|
|
24
|
+
|
|
25
|
+
class_option :membership_model, type: :string,
|
|
26
|
+
desc: "The membership model name (defaults to <Entity><User>)"
|
|
27
|
+
|
|
28
|
+
class_option :rodauth, type: :string, default: "user",
|
|
29
|
+
desc: "Rodauth configuration name"
|
|
30
|
+
|
|
31
|
+
class_option :profile, type: :boolean, default: false,
|
|
32
|
+
desc: "Include profile setup in onboarding"
|
|
33
|
+
|
|
34
|
+
def start
|
|
35
|
+
validate_requirements
|
|
36
|
+
create_authenticated_controller
|
|
37
|
+
create_welcome_controller
|
|
38
|
+
create_views
|
|
39
|
+
add_routes
|
|
40
|
+
configure_rodauth
|
|
41
|
+
show_instructions
|
|
42
|
+
rescue => e
|
|
43
|
+
exception "#{self.class} failed:", e
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def validate_requirements
|
|
49
|
+
errors = []
|
|
50
|
+
|
|
51
|
+
unless File.exist?(user_model_path)
|
|
52
|
+
errors << "User model not found: #{user_model_path.relative_path_from(Rails.root)}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless File.exist?(entity_model_path)
|
|
56
|
+
errors << "Entity model not found: #{entity_model_path.relative_path_from(Rails.root)}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if File.exist?(user_model_path)
|
|
60
|
+
user_content = File.read(user_model_path)
|
|
61
|
+
unless user_content.include?("has_many :#{entity_table.pluralize}")
|
|
62
|
+
errors << "User model missing 'has_many :#{entity_table.pluralize}' — run the membership generator first"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if errors.any?
|
|
67
|
+
errors.each { |e| say_status :error, e, :red }
|
|
68
|
+
raise Thor::Error, "Required files missing:\n - #{errors.join("\n - ")}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_authenticated_controller
|
|
73
|
+
template "app/controllers/authenticated_controller.rb",
|
|
74
|
+
"app/controllers/authenticated_controller.rb"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def create_welcome_controller
|
|
78
|
+
template "app/controllers/welcome_controller.rb",
|
|
79
|
+
"app/controllers/welcome_controller.rb"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def create_views
|
|
83
|
+
template "app/views/layouts/welcome.html.erb",
|
|
84
|
+
"app/views/layouts/welcome.html.erb"
|
|
85
|
+
|
|
86
|
+
template "app/views/welcome/select_entity.html.erb",
|
|
87
|
+
"app/views/welcome/select_entity.html.erb"
|
|
88
|
+
|
|
89
|
+
template "app/views/welcome/onboarding.html.erb",
|
|
90
|
+
"app/views/welcome/onboarding.html.erb"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_routes
|
|
94
|
+
routes_file = "config/routes.rb"
|
|
95
|
+
routes_content = File.read(Rails.root.join(routes_file))
|
|
96
|
+
|
|
97
|
+
if routes_content.include?("# Welcome & onboarding")
|
|
98
|
+
say_status :skip, "Welcome routes already present", :yellow
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
route_code = <<-RUBY
|
|
103
|
+
|
|
104
|
+
# Welcome & onboarding
|
|
105
|
+
get "welcome", to: "welcome#index"
|
|
106
|
+
get "welcome/onboard", to: "welcome#new_entity", as: :welcome_onboard
|
|
107
|
+
post "welcome/onboard", to: "welcome#onboard"
|
|
108
|
+
RUBY
|
|
109
|
+
|
|
110
|
+
# Remove the standalone invites welcome route if it exists,
|
|
111
|
+
# since the main WelcomeController now handles /welcome
|
|
112
|
+
if routes_content.include?('get "welcome", to: "invites/welcome#index"')
|
|
113
|
+
gsub_file routes_file,
|
|
114
|
+
/\n\s*# Welcome route \(handled by invites.*\n\s*get "welcome", to: "invites\/welcome#index"\n/,
|
|
115
|
+
"\n"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
inject_into_file routes_file,
|
|
119
|
+
route_code,
|
|
120
|
+
before: /^end\s*\z/
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def configure_rodauth
|
|
124
|
+
return unless rodauth?
|
|
125
|
+
|
|
126
|
+
update_rodauth_redirects("app/rodauth/#{rodauth_config}_rodauth_plugin.rb")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def show_instructions
|
|
130
|
+
say "\n"
|
|
131
|
+
say "=" * 79
|
|
132
|
+
say "\n"
|
|
133
|
+
say "Welcome flow installed successfully!"
|
|
134
|
+
say "\n"
|
|
135
|
+
say "Next steps:"
|
|
136
|
+
say "\n"
|
|
137
|
+
say "1. Run migrations (if you haven't already):"
|
|
138
|
+
say " rails db:migrate"
|
|
139
|
+
say "\n"
|
|
140
|
+
say "2. Customize the onboarding view to match your app:"
|
|
141
|
+
say " app/views/welcome/onboarding.html.erb"
|
|
142
|
+
say "\n"
|
|
143
|
+
if profile?
|
|
144
|
+
say "3. Ensure your User model has a `profile` association:"
|
|
145
|
+
say " has_one :profile"
|
|
146
|
+
say "\n"
|
|
147
|
+
end
|
|
148
|
+
say "=" * 79
|
|
149
|
+
say "\n"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def user_model
|
|
153
|
+
options[:user_model].camelize
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def user_table
|
|
157
|
+
options[:user_model].underscore
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def entity_model
|
|
161
|
+
options[:entity_model].camelize
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def entity_table
|
|
165
|
+
options[:entity_model].underscore
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def portal_engine
|
|
169
|
+
options[:portal].camelize
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def membership_model
|
|
173
|
+
options[:membership_model] || "#{entity_model}#{user_model}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def rodauth_config
|
|
177
|
+
options[:rodauth]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def rodauth?
|
|
181
|
+
rodauth_config.present?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def profile?
|
|
185
|
+
options[:profile]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def entity_model_path
|
|
189
|
+
Rails.root.join("app/models/#{entity_table}.rb")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def user_model_path
|
|
193
|
+
Rails.root.join("app/models/#{user_table}.rb")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sequel/core"
|
|
4
|
-
|
|
5
3
|
module Plutonium
|
|
6
4
|
module Auth
|
|
7
5
|
# Provides runtime detection of the database adapter for Sequel configuration.
|
|
@@ -26,6 +24,7 @@ module Plutonium
|
|
|
26
24
|
# @return [Sequel::Database] configured Sequel database connection
|
|
27
25
|
# @raise [RuntimeError] if the Sequel adapter initialization fails
|
|
28
26
|
def db
|
|
27
|
+
require "sequel/core"
|
|
29
28
|
adapter = sequel_adapter
|
|
30
29
|
begin
|
|
31
30
|
if RUBY_ENGINE == "jruby"
|
|
@@ -145,7 +145,10 @@ module Plutonium
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Build the named helper: e.g., "blogging_post_nested_post_metadata_path"
|
|
148
|
-
|
|
148
|
+
# For plural parent resources (resources :posts), nested routes use the singular member name (post_nested_...)
|
|
149
|
+
# For singular parent resources (resource :entities), nested routes use the route name as-is (entities_nested_...)
|
|
150
|
+
parent_is_singular_route = current_engine.routes.singular_resource_route?(parent.model_name.plural)
|
|
151
|
+
parent_prefix = parent_is_singular_route ? parent.model_name.plural : parent.model_name.singular
|
|
149
152
|
nested_resource_name = "#{prefix}#{association_name}"
|
|
150
153
|
|
|
151
154
|
# Determine if this is a collection action (no specific record)
|
|
@@ -158,12 +161,10 @@ module Plutonium
|
|
|
158
161
|
# - :new action uses singular (new_blogging_post_nested_comment)
|
|
159
162
|
# - member actions (show/edit/update/destroy) use singular (blogging_post_nested_comment)
|
|
160
163
|
is_collection_action = action == :index || action == :create || (no_record && action != :new)
|
|
161
|
-
helper_base = if is_singular
|
|
162
|
-
"#{
|
|
163
|
-
elsif is_collection_action
|
|
164
|
-
"#{parent_singular}_#{nested_resource_name}"
|
|
164
|
+
helper_base = if is_singular || is_collection_action
|
|
165
|
+
"#{parent_prefix}_#{nested_resource_name}"
|
|
165
166
|
else
|
|
166
|
-
"#{
|
|
167
|
+
"#{parent_prefix}_#{nested_resource_name.to_s.singularize}"
|
|
167
168
|
end
|
|
168
169
|
|
|
169
170
|
# Only add helper prefix for actions that have named route helpers (new, edit)
|
|
@@ -173,10 +174,19 @@ module Plutonium
|
|
|
173
174
|
else "#{action}_"
|
|
174
175
|
end
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
# Add entity scope prefix for path-based entity scoping
|
|
178
|
+
entity_prefix = if scoped_to_entity? && scoped_entity_strategy == :path
|
|
179
|
+
"#{scoped_entity_param_key}_"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
helper_name = :"#{helper_suffix}#{entity_prefix}#{helper_base}_path"
|
|
177
183
|
|
|
178
184
|
# Build the arguments for the helper
|
|
179
|
-
helper_args = [
|
|
185
|
+
helper_args = []
|
|
186
|
+
# Add entity scope param for path-based entity scoping
|
|
187
|
+
helper_args << current_scoped_entity.to_param if entity_prefix
|
|
188
|
+
# Singular parent resources (resource :entity) have no :id param in the route
|
|
189
|
+
helper_args << parent.to_param unless parent_is_singular_route
|
|
180
190
|
# Include element ID for plural routes (has_many) when we have a record instance
|
|
181
191
|
# Skip ID for collection actions (:index, :create) which don't need a member ID
|
|
182
192
|
unless is_singular || no_record || is_collection_action
|
|
@@ -61,21 +61,34 @@ module Plutonium
|
|
|
61
61
|
#
|
|
62
62
|
# For Blogging::Comment, returns [:blogging_comments, :comments]
|
|
63
63
|
# For Comment, returns [:comments]
|
|
64
|
+
# For Blogging::PostDetail, returns [:blogging_post_details, :post_details, :blogging_post_detail, :post_detail]
|
|
65
|
+
#
|
|
66
|
+
# Tries both plural (has_many) and singular (has_one) forms.
|
|
64
67
|
#
|
|
65
68
|
# @param klass [Class] The target class
|
|
66
69
|
# @return [Array<Symbol>] Candidate association names in priority order
|
|
67
70
|
def association_candidates_for(klass)
|
|
68
71
|
candidates = []
|
|
69
72
|
|
|
70
|
-
# Full namespaced name: Blogging::Comment => :blogging_comments
|
|
71
|
-
|
|
72
|
-
candidates <<
|
|
73
|
+
# Full namespaced name (plural): Blogging::Comment => :blogging_comments
|
|
74
|
+
full_plural = klass.model_name.plural.to_sym
|
|
75
|
+
candidates << full_plural
|
|
73
76
|
|
|
74
|
-
# Demodulized name: Blogging::Comment => :comments
|
|
77
|
+
# Demodulized name (plural): Blogging::Comment => :comments
|
|
75
78
|
demodulized = klass.name.demodulize
|
|
76
79
|
if demodulized != klass.name
|
|
77
|
-
|
|
78
|
-
candidates <<
|
|
80
|
+
short_plural = demodulized.underscore.pluralize.to_sym
|
|
81
|
+
candidates << short_plural unless candidates.include?(short_plural)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Full namespaced name (singular): Blogging::PostDetail => :blogging_post_detail
|
|
85
|
+
full_singular = klass.model_name.singular.to_sym
|
|
86
|
+
candidates << full_singular unless candidates.include?(full_singular)
|
|
87
|
+
|
|
88
|
+
# Demodulized name (singular): Blogging::PostDetail => :post_detail
|
|
89
|
+
if demodulized != klass.name
|
|
90
|
+
short_singular = demodulized.underscore.to_sym
|
|
91
|
+
candidates << short_singular unless candidates.include?(short_singular)
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
candidates
|
|
@@ -24,11 +24,22 @@ module Plutonium
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
options[:with] ||= ::ActionPolicy.lookup(resource, namespace: authorization_namespace)
|
|
27
|
+
options[:context] = (options[:context] || {}).deep_merge(current_policy_context)
|
|
27
28
|
relation ||= resource.all
|
|
28
29
|
|
|
29
30
|
authorized_scope(relation, **options)
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# Returns the base policy context available to all controllers.
|
|
34
|
+
# Resource controllers extend this with parent/association context.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash] context containing entity_scope
|
|
37
|
+
def current_policy_context
|
|
38
|
+
{
|
|
39
|
+
entity_scope: entity_scope_for_authorize
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
def entity_scope_for_authorize
|
|
33
44
|
# Use the instance variable directly to avoid circular dependency.
|
|
34
45
|
# When authorizing the scoped entity itself (in fetch_current_scoped_entity),
|
|
@@ -17,7 +17,9 @@ module Plutonium
|
|
|
17
17
|
extend ActiveSupport::Concern
|
|
18
18
|
|
|
19
19
|
included do
|
|
20
|
-
presents label: "Cancel Invitation", icon:
|
|
20
|
+
presents label: "Cancel Invitation", icon: Phlex::TablerIcons::CircleX
|
|
21
|
+
|
|
22
|
+
attribute :resource
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def execute
|
|
@@ -151,14 +151,6 @@ module Plutonium
|
|
|
151
151
|
raise NotImplementedError, "#{self.class}#create_membership_for must be implemented to create the membership record"
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
# Alias method for the entity association.
|
|
155
|
-
# Override if your entity association has a different name.
|
|
156
|
-
#
|
|
157
|
-
# @return [Object] the entity record
|
|
158
|
-
def entity
|
|
159
|
-
raise NotImplementedError, "#{self.class}#entity must be implemented or an entity association must exist"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
154
|
private
|
|
163
155
|
|
|
164
156
|
def extract_domain(email)
|
|
@@ -7,7 +7,7 @@ module Plutonium
|
|
|
7
7
|
# Controller module to handle resource actions and concerns
|
|
8
8
|
module Controller
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
|
-
include Pagy::
|
|
10
|
+
include Pagy::Method
|
|
11
11
|
include Plutonium::Core::Controller
|
|
12
12
|
include Plutonium::Resource::Controllers::Defineable
|
|
13
13
|
include Plutonium::Resource::Controllers::Authorizable
|
|
@@ -17,8 +17,7 @@ module Plutonium
|
|
|
17
17
|
include Plutonium::Resource::Controllers::InteractiveActions
|
|
18
18
|
|
|
19
19
|
included do
|
|
20
|
-
|
|
21
|
-
after_action { pagy_headers_merge(@pagy) if @pagy }
|
|
20
|
+
after_action { response.headers.merge!(@pagy.headers_hash) if @pagy }
|
|
22
21
|
|
|
23
22
|
helper_method :current_parent, :current_nested_association, :resource_record!, :resource_record?, :resource_param_key, :resource_class
|
|
24
23
|
|
|
@@ -109,15 +109,14 @@ module Plutonium
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
# Returns the policy context for the current resource
|
|
112
|
-
#
|
|
112
|
+
# Extends the base context with parent scoping from nested routes
|
|
113
113
|
#
|
|
114
114
|
# @return [Hash] context containing parent, parent_association, and entity_scope
|
|
115
115
|
def current_policy_context
|
|
116
|
-
|
|
116
|
+
super.merge(
|
|
117
117
|
parent: current_parent,
|
|
118
|
-
parent_association: current_nested_association
|
|
119
|
-
|
|
120
|
-
}
|
|
118
|
+
parent_association: current_nested_association
|
|
119
|
+
)
|
|
121
120
|
end
|
|
122
121
|
|
|
123
122
|
# Authorizes the current action for the given record of the current resource
|
|
@@ -59,6 +59,8 @@ module Plutonium
|
|
|
59
59
|
return @scoped_entity_association if defined?(@scoped_entity_association)
|
|
60
60
|
|
|
61
61
|
matching_assocs = resource_class.reflect_on_all_associations(:belongs_to).select do |assoc|
|
|
62
|
+
next false if assoc.polymorphic?
|
|
63
|
+
|
|
62
64
|
assoc.klass.name == scoped_entity_class.name
|
|
63
65
|
rescue NameError
|
|
64
66
|
false
|
|
@@ -5,8 +5,6 @@ module Plutonium
|
|
|
5
5
|
module Table
|
|
6
6
|
module Components
|
|
7
7
|
class PagyInfo < Plutonium::UI::Component::Base
|
|
8
|
-
include Pagy::Frontend
|
|
9
|
-
|
|
10
8
|
def initialize(pagy, per_page_options: [5, 10, 20, 50, 100])
|
|
11
9
|
@pagy = pagy
|
|
12
10
|
@per_page_options = (per_page_options + [@pagy.limit]).uniq.sort
|
|
@@ -57,11 +55,7 @@ module Plutonium
|
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
def page_url(limit)
|
|
60
|
-
|
|
61
|
-
@pagy.vars[:limit] = limit
|
|
62
|
-
pagy_url_for(@pagy, @pagy.page)
|
|
63
|
-
ensure
|
|
64
|
-
@pagy.vars[:limit] = original_limit
|
|
58
|
+
@pagy.page_url(@pagy.page, limit: limit, client_max_limit: limit)
|
|
65
59
|
end
|
|
66
60
|
end
|
|
67
61
|
end
|