panda-core 0.11.0 → 0.12.5
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/README.md +15 -0
- data/app/assets/tailwind/application.css +196 -109
- data/app/builders/panda/core/form_builder.rb +9 -9
- data/app/components/panda/core/UI/button.rb +4 -4
- data/app/components/panda/core/admin/button_component.rb +2 -2
- data/app/components/panda/core/admin/form_input_component.rb +2 -2
- data/app/components/panda/core/admin/form_select_component.rb +2 -2
- data/app/components/panda/core/admin/panel_component.rb +1 -1
- data/app/components/panda/core/admin/statistics_component.rb +3 -3
- data/app/components/panda/core/admin/tab_bar_component.rb +3 -3
- data/app/components/panda/core/admin/table_component.rb +3 -3
- data/app/controllers/panda/core/admin/sessions_controller.rb +4 -1
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +1 -1
- data/app/models/panda/core/user.rb +23 -14
- data/app/views/layouts/panda/core/admin.html.erb +10 -1
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +3 -3
- data/app/views/panda/core/admin/sessions/new.html.erb +2 -3
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +5 -5
- data/config/brakeman.ignore +51 -0
- data/db/migrate/20250809000001_create_panda_core_users.rb +1 -1
- data/db/migrate/20251203100000_rename_is_admin_to_admin_in_panda_core_users.rb +18 -0
- data/lib/panda/core/asset_loader.rb +1 -1
- data/lib/panda/core/configuration.rb +4 -0
- data/lib/panda/core/engine/admin_controller_config.rb +6 -2
- data/lib/panda/core/engine/autoload_config.rb +9 -10
- data/lib/panda/core/engine/omniauth_config.rb +92 -34
- data/lib/panda/core/engine/route_config.rb +37 -0
- data/lib/panda/core/engine.rb +46 -41
- data/lib/panda/core/middleware.rb +146 -0
- data/lib/panda/core/module_registry.rb +21 -13
- data/lib/panda/core/testing/rails_helper.rb +17 -8
- data/lib/panda/core/testing/support/authentication_helpers.rb +6 -6
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +2 -12
- data/lib/panda/core/testing/support/system/browser_console_logger.rb +1 -2
- data/lib/panda/core/testing/support/system/chrome_path.rb +38 -0
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +79 -0
- data/lib/panda/core/testing/support/system/cuprite_setup.rb +61 -66
- data/lib/panda/core/testing/support/system/system_test_helpers.rb +11 -11
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +11 -0
- data/lib/tasks/panda/core/users.rake +3 -3
- data/lib/tasks/panda/shared.rake +31 -5
- data/public/panda-core-assets/favicons/browserconfig.xml +1 -1
- data/public/panda-core-assets/favicons/site.webmanifest +1 -1
- data/public/panda-core-assets/panda-core-0.11.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.12.2.css +2 -0
- data/public/panda-core-assets/panda-core-0.12.3.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- metadata +10 -8
- data/lib/panda/core/engine/middleware_config.rb +0 -17
- data/lib/panda/core/testing/support/system/better_system_tests.rb +0 -180
- data/lib/panda/core/testing/support/system/capybara_config.rb +0 -64
- data/lib/panda/core/testing/support/system/ci_capybara_config.rb +0 -77
- data/public/panda-core-assets/panda-core-0.10.6.css +0 -2
- data/public/panda-core-assets/panda-core-0.10.7.css +0 -2
|
@@ -8,9 +8,9 @@ module Panda
|
|
|
8
8
|
prop :value, _Nilable(_Union(String, Integer, Float))
|
|
9
9
|
|
|
10
10
|
def view_template
|
|
11
|
-
div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-
|
|
12
|
-
dt(class: "text-base font-medium truncate text-
|
|
13
|
-
dd(class: "mt-1 text-3xl font-medium tracking-tight text-
|
|
11
|
+
div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-primary-50/20 to-primary-50 border-primary-400") do
|
|
12
|
+
dt(class: "text-base font-medium truncate text-primary-900") { @metric }
|
|
13
|
+
dd(class: "mt-1 text-3xl font-medium tracking-tight text-primary-900") { @value }
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -21,7 +21,7 @@ module Panda
|
|
|
21
21
|
select(
|
|
22
22
|
id: "tabs",
|
|
23
23
|
name: "tabs",
|
|
24
|
-
class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-
|
|
24
|
+
class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-primary-400 focus:border-primary-600 focus:ring-primary-600"
|
|
25
25
|
) do
|
|
26
26
|
@tabs.each do |tab|
|
|
27
27
|
option { tab[:name] }
|
|
@@ -46,7 +46,7 @@ module Panda
|
|
|
46
46
|
def render_tab(tab, is_current = false)
|
|
47
47
|
classes = "py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 "
|
|
48
48
|
classes += if is_current || tab[:current]
|
|
49
|
-
"border-
|
|
49
|
+
"border-primary-600 text-primary-600"
|
|
50
50
|
else
|
|
51
51
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
52
52
|
end
|
|
@@ -66,7 +66,7 @@ module Panda
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def render_view_button(type, selected: false)
|
|
69
|
-
button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-
|
|
69
|
+
button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-primary-600"
|
|
70
70
|
button_class += if selected
|
|
71
71
|
" ml-0.5 bg-white shadow-sm"
|
|
72
72
|
else
|
|
@@ -33,7 +33,7 @@ module Panda
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def render_table_with_rows
|
|
36
|
-
div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-
|
|
36
|
+
div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-gray-700", style: "table-layout: fixed;") do
|
|
37
37
|
render_header
|
|
38
38
|
render_rows
|
|
39
39
|
end
|
|
@@ -49,7 +49,7 @@ module Panda
|
|
|
49
49
|
|
|
50
50
|
def render_header
|
|
51
51
|
div(class: "table-header-group") do
|
|
52
|
-
div(class: "table-row text-base font-medium text-white bg-
|
|
52
|
+
div(class: "table-row text-base font-medium text-white bg-gray-800") do
|
|
53
53
|
@columns.each_with_index do |column, i|
|
|
54
54
|
header_classes = "table-cell sticky top-0 z-10 p-4"
|
|
55
55
|
header_classes += " rounded-tl-md" if i.zero?
|
|
@@ -70,7 +70,7 @@ module Panda
|
|
|
70
70
|
data: {post_id: row.id}
|
|
71
71
|
) do
|
|
72
72
|
@columns.each do |column|
|
|
73
|
-
div(class: "table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-
|
|
73
|
+
div(class: "table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-gray-700/20") do
|
|
74
74
|
# Capture the cell content by calling the block with the row
|
|
75
75
|
render_cell_content(row, column.cell)
|
|
76
76
|
end
|
|
@@ -10,7 +10,10 @@ module Panda
|
|
|
10
10
|
skip_before_action :authenticate_admin_user!, only: [:new, :create, :destroy, :failure]
|
|
11
11
|
|
|
12
12
|
def new
|
|
13
|
-
|
|
13
|
+
# Only show providers that have valid credentials configured
|
|
14
|
+
@providers = Core.config.authentication_providers.select do |_key, config|
|
|
15
|
+
config[:client_id].present? && config[:client_secret].present?
|
|
16
|
+
end.keys
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def create
|
|
@@ -21,7 +21,7 @@ export default class extends Controller {
|
|
|
21
21
|
connect() {
|
|
22
22
|
// Ensure menu starts in correct state
|
|
23
23
|
// Check if this menu should be expanded by default (if a child is active)
|
|
24
|
-
const hasActiveChild = this.menuTarget.querySelector(".bg-
|
|
24
|
+
const hasActiveChild = this.menuTarget.querySelector(".bg-primary-500")
|
|
25
25
|
if (hasActiveChild) {
|
|
26
26
|
this.expand()
|
|
27
27
|
} else {
|
|
@@ -19,16 +19,15 @@ module Panda
|
|
|
19
19
|
|
|
20
20
|
before_save :downcase_email
|
|
21
21
|
|
|
22
|
+
# Determine which column stores admin flag (supports legacy `admin` and new `is_admin`)
|
|
23
|
+
def self.admin_column
|
|
24
|
+
# Prefer canonical `admin` if available, otherwise fall back to legacy `is_admin`
|
|
25
|
+
@admin_column ||= column_names.include?("admin") ? "admin" : "is_admin"
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
# Scopes
|
|
23
|
-
# Support both 'admin' (newer) and 'is_admin' (older) column names
|
|
24
29
|
scope :admins, -> {
|
|
25
|
-
|
|
26
|
-
where(admin: true)
|
|
27
|
-
elsif column_names.include?("is_admin")
|
|
28
|
-
where(is_admin: true)
|
|
29
|
-
else
|
|
30
|
-
none
|
|
31
|
-
end
|
|
30
|
+
where(admin_column => true)
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
def self.find_or_create_from_auth_hash(auth_hash)
|
|
@@ -45,10 +44,10 @@ module Panda
|
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
attributes = {
|
|
48
|
-
email
|
|
49
|
-
name
|
|
50
|
-
image_url
|
|
51
|
-
|
|
47
|
+
:email => auth_hash.info.email.downcase,
|
|
48
|
+
:name => auth_hash.info.name || "Unknown User",
|
|
49
|
+
:image_url => auth_hash.info.image,
|
|
50
|
+
admin_column => User.count.zero? # First user is admin
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
user = create!(attributes)
|
|
@@ -62,10 +61,20 @@ module Panda
|
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
# Admin status check
|
|
65
|
-
# Note: Column is named 'admin' in newer schemas, 'is_admin' in older ones
|
|
66
64
|
def admin?
|
|
67
|
-
|
|
65
|
+
ActiveRecord::Type::Boolean.new.cast(admin)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Support both legacy `admin` and new `is_admin` columns
|
|
69
|
+
def admin
|
|
70
|
+
self[self.class.admin_column]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def admin=(value)
|
|
74
|
+
self[self.class.admin_column] = ActiveRecord::Type::Boolean.new.cast(value)
|
|
68
75
|
end
|
|
76
|
+
alias_method :is_admin, :admin
|
|
77
|
+
alias_method :is_admin=, :admin=
|
|
69
78
|
|
|
70
79
|
def active_for_authentication?
|
|
71
80
|
true
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<%= render "panda/core/admin/shared/sidebar" %>
|
|
6
6
|
</div>
|
|
7
7
|
</div>
|
|
8
|
-
<div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc->
|
|
8
|
+
<div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc@window->toggle#hide keydown.escape@window->toggle#hide" tabindex="-1"<% end %>>
|
|
9
9
|
<section id="panda-main" class="flex flex-row h-full">
|
|
10
10
|
<div class="flex-1 h-full" id="panda-primary-content">
|
|
11
11
|
<%= render "panda/core/admin/shared/breadcrumbs" %>
|
|
@@ -60,6 +60,15 @@
|
|
|
60
60
|
<% end %>
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
|
+
<script>
|
|
64
|
+
window.addEventListener('keydown', function(e) {
|
|
65
|
+
if (e.key === 'Escape') {
|
|
66
|
+
document.querySelectorAll('[data-toggle-target="toggleable"]').forEach(function(el) {
|
|
67
|
+
el.classList.add('hidden');
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
63
72
|
<% end %>
|
|
64
73
|
</section>
|
|
65
74
|
</div>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<div class="ml-5 w-0 flex-1">
|
|
16
16
|
<dt class="text-sm font-medium text-gray-500 truncate">Content Management</dt>
|
|
17
17
|
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
18
|
-
<%= link_to "Manage CMS", panda_cms.admin_cms_dashboard_path, class: "text-
|
|
18
|
+
<%= link_to "Manage CMS", panda_cms.admin_cms_dashboard_path, class: "text-primary-600 hover:text-primary-800" %>
|
|
19
19
|
</dd>
|
|
20
20
|
</div>
|
|
21
21
|
</div>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<div class="ml-5 w-0 flex-1">
|
|
36
36
|
<dt class="text-sm font-medium text-gray-500 truncate">My Profile</dt>
|
|
37
37
|
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
38
|
-
<%= link_to "Edit Profile", edit_admin_my_profile_path, class: "text-
|
|
38
|
+
<%= link_to "Edit Profile", edit_admin_my_profile_path, class: "text-primary-600 hover:text-primary-800" %>
|
|
39
39
|
</dd>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
<div class="ml-5 w-0 flex-1">
|
|
59
59
|
<dt class="text-sm font-medium text-gray-500 truncate"><%= card[:title] %></dt>
|
|
60
60
|
<dd class="mt-1 text-lg font-semibold text-gray-900">
|
|
61
|
-
<%= link_to card[:link_text], card[:path], class: "text-
|
|
61
|
+
<%= link_to card[:link_text], card[:path], class: "text-primary-600 hover:text-primary-800" %>
|
|
62
62
|
</dd>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
<%= Panda::Core.config.login_page_title || "Sign in to your account" %>
|
|
8
8
|
</h2>
|
|
9
9
|
</div>
|
|
10
|
-
<% if @providers&.any?
|
|
11
|
-
<%
|
|
12
|
-
<% providers.each do |provider| %>
|
|
10
|
+
<% if @providers&.any? %>
|
|
11
|
+
<% @providers.each do |provider| %>
|
|
13
12
|
<% provider_config = Panda::Core.config.authentication_providers[provider] %>
|
|
14
13
|
<% provider_path = provider_config&.dig(:path_name) || provider %>
|
|
15
14
|
<div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
data-action="click->navigation-toggle#toggle"
|
|
43
43
|
aria-controls="sub-menu-<%= index %>"
|
|
44
44
|
aria-expanded="false"
|
|
45
|
-
class="<%= is_active ? 'bg-
|
|
45
|
+
class="<%= is_active ? 'bg-primary-500 text-white' : 'text-white hover:bg-primary-500/60' %> transition-all group flex items-center w-full gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal">
|
|
46
46
|
<span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
|
|
47
47
|
<span class="flex-1 text-left"><%= item[:label] %></span>
|
|
48
48
|
<i class="fa-solid fa-chevron-right text-xs transition-transform duration-150 ease-in-out"
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
<%
|
|
57
57
|
child_is_active = child[:path] && (request.path == child[:path] || request.path.starts_with?(child[:path] + "/"))
|
|
58
58
|
%>
|
|
59
|
-
<%= link_to child[:path], class: "#{child_is_active ? 'bg-
|
|
59
|
+
<%= link_to child[:path], class: "#{child_is_active ? 'bg-primary-500 text-white' : 'text-white hover:bg-primary-500/60'} group flex items-center w-full py-2 pr-2 pl-11 rounded-md text-sm font-normal transition-all" do %>
|
|
60
60
|
<%= child[:label] %>
|
|
61
61
|
<% end %>
|
|
62
62
|
<% end %>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
65
65
|
<% else %>
|
|
66
|
-
<%= link_to item[:path], class: "#{is_active ? "bg-
|
|
66
|
+
<%= link_to item[:path], class: "#{is_active ? "bg-primary-500 text-white relative flex items-center transition-all py-3 px-2 mb-2 rounded-md group gap-x-3 text-base leading-6 font-normal" : "text-white hover:bg-primary-500/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal"}" do %>
|
|
67
67
|
<span class="text-center w-6"><i class="<%= item[:icon] %> text-xl fa-fw"></i></span>
|
|
68
68
|
<span><%= item[:label] %></span>
|
|
69
69
|
<% end %>
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
</li>
|
|
72
72
|
<% end %>
|
|
73
73
|
<li>
|
|
74
|
-
<%= button_to panda_core.admin_logout_path, method: :delete, id: "logout-link", data: { turbo: false }, class: "text-white hover:bg-
|
|
74
|
+
<%= button_to panda_core.admin_logout_path, method: :delete, id: "logout-link", data: { turbo: false }, class: "text-white hover:bg-primary-500/60 transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full" do %>
|
|
75
75
|
<span class="text-center w-6"><i class="text-xl fa-solid fa-door-open fa-fw"></i></span>
|
|
76
76
|
<span>Logout</span>
|
|
77
77
|
<% end %>
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
# Check if we're in the my_profile section
|
|
82
82
|
is_my_profile_active = request.path.starts_with?("#{Panda::Core.config.admin_path}/my_profile")
|
|
83
83
|
%>
|
|
84
|
-
<%= link_to panda_core.admin_my_profile_path, class: "#{is_my_profile_active ? 'bg-
|
|
84
|
+
<%= link_to panda_core.admin_my_profile_path, class: "#{is_my_profile_active ? 'bg-primary-500 text-white' : 'text-white hover:bg-primary-500/60'} transition-all group flex items-center gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-normal w-full", title: "My Profile" do %>
|
|
85
85
|
<% if current_user.avatar.attached? %>
|
|
86
86
|
<span class="text-center w-6"><%= image_tag main_app.url_for(current_user.avatar), alt: current_user.name, class: "w-auto h-7 rounded-full object-cover" %></span>
|
|
87
87
|
<% elsif !current_user.image_url.to_s.empty? %>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ignored_warnings": [
|
|
3
|
+
{
|
|
4
|
+
"warning_type": "Command Injection",
|
|
5
|
+
"warning_code": 14,
|
|
6
|
+
"fingerprint": "29d5b60b583fc98e293f6ac47f153bbf5db48a03963f8c9a7711bd251ecf2d81",
|
|
7
|
+
"check_name": "Execute",
|
|
8
|
+
"message": "Possible command injection",
|
|
9
|
+
"file": "lib/panda/core/testing/support/system/cuprite_helpers.rb",
|
|
10
|
+
"line": 78,
|
|
11
|
+
"link": "https://brakemanscanner.org/docs/warning_types/command_injection/",
|
|
12
|
+
"code": "system(\"ffmpeg -y -framerate 8 -pattern_type glob -i '#{File.join(dir, \"frames\")}/*.png' -c:v libx264 -pix_fmt yuv420p '#{capybara_artifacts_dir.join(\"#{example.metadata[:full_description].parameterize}.mp4\")}'\\n\")",
|
|
13
|
+
"render_path": null,
|
|
14
|
+
"location": {
|
|
15
|
+
"type": "method",
|
|
16
|
+
"class": "Panda::Core::Testing::CupriteHelpers",
|
|
17
|
+
"method": "record_video!"
|
|
18
|
+
},
|
|
19
|
+
"user_input": "File.join(dir, \"frames\")",
|
|
20
|
+
"confidence": "Medium",
|
|
21
|
+
"cwe_id": [
|
|
22
|
+
77
|
|
23
|
+
],
|
|
24
|
+
"note": "This does not use user input and is based on environment variables and the current folder"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"warning_type": "Command Injection",
|
|
28
|
+
"warning_code": 14,
|
|
29
|
+
"fingerprint": "d3c339f054cbd511956e04eaf8763bc722bd2e4559a1c1fec58749abef9792cd",
|
|
30
|
+
"check_name": "Execute",
|
|
31
|
+
"message": "Possible command injection",
|
|
32
|
+
"file": "lib/panda/core/testing/support/system/chrome_verification.rb",
|
|
33
|
+
"line": 43,
|
|
34
|
+
"link": "https://brakemanscanner.org/docs/warning_types/command_injection/",
|
|
35
|
+
"code": "Process.spawn(*[BrowserPath.resolve, v.nil? ? (\"--#{k}\") : (\"--#{k}=#{v}\"), \"about:blank\"], :out => (File::NULL), :err => (File::NULL))",
|
|
36
|
+
"render_path": null,
|
|
37
|
+
"location": {
|
|
38
|
+
"type": "method",
|
|
39
|
+
"class": "Panda::Core::Testing::Support::System::ChromeVerification",
|
|
40
|
+
"method": "s(:self).verify!"
|
|
41
|
+
},
|
|
42
|
+
"user_input": "k",
|
|
43
|
+
"confidence": "Medium",
|
|
44
|
+
"cwe_id": [
|
|
45
|
+
77
|
|
46
|
+
],
|
|
47
|
+
"note": "Chrome verification uses a fixed set of browser flags from CHROME_FLAGS constant; no user input is accepted"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"brakeman_version": "7.1.1"
|
|
51
|
+
}
|
|
@@ -11,7 +11,7 @@ class CreatePandaCoreUsers < ActiveRecord::Migration[7.1]
|
|
|
11
11
|
t.string :name
|
|
12
12
|
t.string :email, null: false
|
|
13
13
|
t.string :image_url
|
|
14
|
-
t.boolean :
|
|
14
|
+
t.boolean :admin, default: false, null: false
|
|
15
15
|
t.string :current_theme
|
|
16
16
|
t.string :oauth_avatar_url
|
|
17
17
|
t.timestamps
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RenameIsAdminToAdminInPandaCoreUsers < ActiveRecord::Migration[7.1]
|
|
4
|
+
def up
|
|
5
|
+
# If apps created an `is_admin` column during the brief rename, move it back
|
|
6
|
+
return unless column_exists?(:panda_core_users, :is_admin)
|
|
7
|
+
return if column_exists?(:panda_core_users, :admin)
|
|
8
|
+
|
|
9
|
+
rename_column :panda_core_users, :is_admin, :admin
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def down
|
|
13
|
+
return unless column_exists?(:panda_core_users, :admin)
|
|
14
|
+
return if column_exists?(:panda_core_users, :is_admin)
|
|
15
|
+
|
|
16
|
+
rename_column :panda_core_users, :admin, :is_admin
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -45,7 +45,7 @@ module Panda
|
|
|
45
45
|
|
|
46
46
|
# In production, prefer local compiled assets over GitHub
|
|
47
47
|
# Only use GitHub assets when explicitly enabled or when local assets aren't available
|
|
48
|
-
if Rails.env.production?
|
|
48
|
+
if Rails.env.production? || Rails.env.staging?
|
|
49
49
|
# Check if compiled assets exist locally
|
|
50
50
|
return false if compiled_assets_available?
|
|
51
51
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "active_support/core_ext/numeric/bytes"
|
|
2
|
+
|
|
1
3
|
module Panda
|
|
2
4
|
module Core
|
|
3
5
|
class Configuration
|
|
@@ -7,6 +9,7 @@ module Panda
|
|
|
7
9
|
:cache_store,
|
|
8
10
|
:parent_controller,
|
|
9
11
|
:parent_mailer,
|
|
12
|
+
:auto_mount_engine,
|
|
10
13
|
:mailer_sender,
|
|
11
14
|
:mailer_default_url_options,
|
|
12
15
|
:session_token_cookie,
|
|
@@ -38,6 +41,7 @@ module Panda
|
|
|
38
41
|
@cache_store = :memory_store
|
|
39
42
|
@parent_controller = "ActionController::API"
|
|
40
43
|
@parent_mailer = "ActionMailer::Base"
|
|
44
|
+
@auto_mount_engine = true
|
|
41
45
|
@mailer_sender = "support@example.com"
|
|
42
46
|
@mailer_default_url_options = {host: "localhost:3000"}
|
|
43
47
|
@session_token_cookie = :panda_session
|
|
@@ -10,9 +10,13 @@ module Panda
|
|
|
10
10
|
included do
|
|
11
11
|
# Create AdminController alias after controllers are loaded
|
|
12
12
|
# This allows other gems to inherit from Panda::Core::AdminController
|
|
13
|
-
|
|
13
|
+
config.to_prepare do
|
|
14
|
+
# Use on_load to ensure ActionController is available
|
|
14
15
|
ActiveSupport.on_load(:action_controller_base) do
|
|
15
|
-
|
|
16
|
+
# Create the alias if it doesn't exist
|
|
17
|
+
unless Panda::Core.const_defined?(:AdminController)
|
|
18
|
+
Panda::Core.const_set(:AdminController, Panda::Core::Admin::BaseController)
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
module Panda
|
|
4
4
|
module Core
|
|
5
5
|
class Engine < ::Rails::Engine
|
|
6
|
-
# Autoload paths configuration
|
|
7
6
|
module AutoloadConfig
|
|
8
7
|
extend ActiveSupport::Concern
|
|
9
8
|
|
|
10
9
|
included do
|
|
11
|
-
|
|
10
|
+
# These must run BEFORE initialization, so this is allowed
|
|
11
|
+
config.autoload_paths << root.join("app/builders")
|
|
12
|
+
config.autoload_paths << root.join("app/components")
|
|
13
|
+
config.autoload_paths << root.join("app/services")
|
|
14
|
+
config.autoload_paths << root.join("app/models")
|
|
15
|
+
config.autoload_paths << root.join("app/helpers")
|
|
16
|
+
config.autoload_paths << root.join("app/constraints")
|
|
12
17
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
# Zeitwerk will automatically discover nested modules from these roots
|
|
16
|
-
config.autoload_paths += Dir[root.join("app", "models")]
|
|
17
|
-
config.autoload_paths += Dir[root.join("app", "controllers")]
|
|
18
|
-
config.autoload_paths += Dir[root.join("app", "builders")]
|
|
19
|
-
config.autoload_paths += Dir[root.join("app", "components")]
|
|
20
|
-
config.autoload_paths += Dir[root.join("app", "services")]
|
|
18
|
+
# Mirror eager-load as needed
|
|
19
|
+
config.eager_load_paths.concat(config.autoload_paths)
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
end
|
|
@@ -1,51 +1,109 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
3
5
|
module Panda
|
|
4
6
|
module Core
|
|
5
7
|
class Engine < ::Rails::Engine
|
|
6
|
-
# OmniAuth configuration
|
|
7
8
|
module OmniauthConfig
|
|
8
9
|
extend ActiveSupport::Concern
|
|
9
10
|
|
|
11
|
+
PROVIDER_REGISTRY = {
|
|
12
|
+
# Microsoft
|
|
13
|
+
"microsoft" => :microsoft_graph,
|
|
14
|
+
"microsoft_graph" => :microsoft_graph,
|
|
15
|
+
|
|
16
|
+
# Google
|
|
17
|
+
"google" => :google_oauth2,
|
|
18
|
+
"google_oauth2" => :google_oauth2,
|
|
19
|
+
"gmail" => :google_oauth2,
|
|
20
|
+
|
|
21
|
+
# GitHub
|
|
22
|
+
"github" => :github,
|
|
23
|
+
"gh" => :github,
|
|
24
|
+
|
|
25
|
+
# Developer
|
|
26
|
+
"developer" => :developer
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
10
29
|
included do
|
|
11
|
-
initializer
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Mount OmniAuth at configurable admin path
|
|
17
|
-
app.middleware.use OmniAuth::Builder do
|
|
18
|
-
# Configure OmniAuth to use the configured admin path
|
|
19
|
-
configure do |config|
|
|
20
|
-
config.path_prefix = "#{Panda::Core.config.admin_path}/auth"
|
|
21
|
-
# POST-only for CSRF protection (CVE-2015-9284)
|
|
22
|
-
# All login forms use POST via form_tag method: "post"
|
|
23
|
-
config.allowed_request_methods = [:post]
|
|
24
|
-
end
|
|
30
|
+
if respond_to?(:initializer)
|
|
31
|
+
initializer "panda_core.omniauth" do |app|
|
|
32
|
+
require_relative "../oauth_providers"
|
|
33
|
+
Panda::Core::OAuthProviders.setup
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
case provider_name.to_s
|
|
36
|
-
when "microsoft_graph"
|
|
37
|
-
provider :microsoft_graph, settings[:client_id], settings[:client_secret], provider_options
|
|
38
|
-
when "google_oauth2"
|
|
39
|
-
provider :google_oauth2, settings[:client_id], settings[:client_secret], provider_options
|
|
40
|
-
when "github"
|
|
41
|
-
provider :github, settings[:client_id], settings[:client_secret], provider_options
|
|
42
|
-
when "developer"
|
|
43
|
-
provider :developer if Rails.env.development?
|
|
44
|
-
end
|
|
35
|
+
load_yaml_provider_overrides!
|
|
36
|
+
mount_omniauth_middleware(app)
|
|
37
|
+
|
|
38
|
+
# Configure OmniAuth globals AFTER all initializers have run
|
|
39
|
+
# This ensures Panda::Core.config.admin_path has been set by the app
|
|
40
|
+
app.config.after_initialize do
|
|
41
|
+
configure_omniauth_globals
|
|
45
42
|
end
|
|
46
43
|
end
|
|
47
44
|
end
|
|
48
45
|
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# 1. YAML overrides
|
|
50
|
+
def load_yaml_provider_overrides!
|
|
51
|
+
path = Panda::Core::Engine.root.join("config/providers.yml")
|
|
52
|
+
return unless File.exist?(path)
|
|
53
|
+
|
|
54
|
+
yaml = YAML.load_file(path) || {}
|
|
55
|
+
(yaml["providers"] || {}).each do |name, settings|
|
|
56
|
+
Panda::Core.config.authentication_providers[name.to_s] ||= {}
|
|
57
|
+
Panda::Core.config.authentication_providers[name.to_s].deep_merge!(settings)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 2. Global settings
|
|
62
|
+
def configure_omniauth_globals
|
|
63
|
+
OmniAuth.configure do |c|
|
|
64
|
+
c.allowed_request_methods = [:post]
|
|
65
|
+
c.path_prefix = "#{Panda::Core.config.admin_path}/auth"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 3. Middleware insertion
|
|
70
|
+
def mount_omniauth_middleware(app)
|
|
71
|
+
ctx = self # Capture the Engine/Concern context
|
|
72
|
+
|
|
73
|
+
Panda::Core::Middleware.use(app, OmniAuth::Builder) do
|
|
74
|
+
Panda::Core.config.authentication_providers.each do |name, settings|
|
|
75
|
+
ctx.send(:configure_provider, self, name, settings)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 4. Provider builder
|
|
81
|
+
def configure_provider(builder, name, settings)
|
|
82
|
+
symbol = PROVIDER_REGISTRY[name.to_s]
|
|
83
|
+
|
|
84
|
+
unless symbol
|
|
85
|
+
Rails.logger.warn("[panda-core] Unknown OmniAuth provider: #{name.inspect}")
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return if symbol == :developer && !Rails.env.development?
|
|
90
|
+
|
|
91
|
+
# Skip providers without credentials (except developer which doesn't need them)
|
|
92
|
+
has_credentials = settings[:client_id].present? && settings[:client_secret].present?
|
|
93
|
+
if symbol != :developer && !has_credentials
|
|
94
|
+
Rails.logger.info("[panda-core] Skipping OmniAuth provider #{name.inspect}: missing client_id or client_secret")
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
options = (settings[:options] || {}).dup
|
|
99
|
+
options[:name] = settings[:path_name] if settings[:path_name].present?
|
|
100
|
+
|
|
101
|
+
if settings[:client_id] && settings[:client_secret]
|
|
102
|
+
builder.provider symbol, settings[:client_id], settings[:client_secret], options
|
|
103
|
+
else
|
|
104
|
+
builder.provider symbol, options
|
|
105
|
+
end
|
|
106
|
+
end
|
|
49
107
|
end
|
|
50
108
|
end
|
|
51
109
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
# Automatically mount the engine into host applications
|
|
7
|
+
module RouteConfig
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Append Core routes to the host app after initialization so
|
|
12
|
+
# engine routes are available without manual mounting.
|
|
13
|
+
config.after_initialize do |app|
|
|
14
|
+
next unless Panda::Core.config.auto_mount_engine
|
|
15
|
+
|
|
16
|
+
route_set = app.routes
|
|
17
|
+
already_mounted =
|
|
18
|
+
route_set.routes.any? do |route|
|
|
19
|
+
route.app == Panda::Core::Engine ||
|
|
20
|
+
(route.app.respond_to?(:app) && route.app.app == Panda::Core::Engine)
|
|
21
|
+
end
|
|
22
|
+
already_mounted ||= route_set.named_routes.key?(:panda_core)
|
|
23
|
+
|
|
24
|
+
next if already_mounted
|
|
25
|
+
|
|
26
|
+
route_set.append do
|
|
27
|
+
# Re-check inside the mapper to avoid duplicate mounts during reloads
|
|
28
|
+
next if route_set.named_routes.key?(:panda_core)
|
|
29
|
+
|
|
30
|
+
mount Panda::Core::Engine => "/", :as => "panda_core"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|