easy-admin-rails 0.2.4 → 0.2.6

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +100 -27
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +59 -0
  5. data/app/components/easy_admin/navbar_component.rb +19 -4
  6. data/app/components/easy_admin/permissions/user_role_assignment_component.rb +43 -16
  7. data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
  8. data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
  9. data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
  10. data/app/components/easy_admin/profile/settings_component.rb +103 -0
  11. data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
  12. data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
  13. data/app/components/easy_admin/two_factor/status_component.rb +92 -0
  14. data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
  15. data/app/controllers/easy_admin/application_controller.rb +10 -0
  16. data/app/controllers/easy_admin/profile_controller.rb +25 -0
  17. data/app/controllers/easy_admin/sessions_controller.rb +107 -1
  18. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +14 -7
  19. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +2 -33
  20. data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
  21. data/app/javascript/easy_admin/controllers.js +3 -1
  22. data/app/models/easy_admin/admin_user.rb +3 -0
  23. data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
  24. data/app/views/easy_admin/profile/change_password.html.erb +24 -0
  25. data/app/views/easy_admin/profile/index.html.erb +1 -0
  26. data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
  27. data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
  28. data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
  29. data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
  30. data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
  31. data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
  32. data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
  33. data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
  34. data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
  35. data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
  36. data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
  37. data/config/routes.rb +20 -1
  38. data/lib/easy-admin-rails.rb +1 -0
  39. data/lib/easy_admin/two_factor_authentication.rb +156 -0
  40. data/lib/easy_admin/version.rb +1 -1
  41. data/lib/generators/easy_admin/two_factor/templates/README +29 -0
  42. data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
  43. data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
  44. metadata +30 -2
@@ -0,0 +1,124 @@
1
+ module EasyAdmin
2
+ module TwoFactor
3
+ class SetupComponent < BaseComponent
4
+ def initialize(user:)
5
+ @user = user
6
+ end
7
+
8
+ def view_template
9
+ return render_unavailable unless @user.two_factor_available?
10
+
11
+ if @user.otp_secret.blank?
12
+ render_initial_setup
13
+ else
14
+ render_verification_step
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def render_unavailable
21
+ div(class: "max-w-md mx-auto bg-white p-6 rounded-lg shadow") do
22
+ div(class: "text-center") do
23
+ div(class: "w-12 h-12 mx-auto mb-4 text-gray-400") do
24
+ unsafe_raw '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.996-.833-2.764 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path></svg>'
25
+ end
26
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "2FA Not Available" }
27
+ p(class: "text-gray-600 mb-4") do
28
+ "To enable Two-Factor Authentication, please install the required gems:"
29
+ end
30
+ div(class: "bg-gray-100 p-3 rounded text-sm font-mono mb-4") do
31
+ p { "gem 'rotp', '~> 6.3'" }
32
+ p { "gem 'rqrcode', '~> 2.2'" }
33
+ end
34
+ p(class: "text-sm text-gray-500") { "Then restart your server to enable 2FA features." }
35
+ end
36
+ end
37
+ end
38
+
39
+ def render_initial_setup
40
+ div(class: "text-center mb-6") do
41
+ div(class: "w-16 h-16 mx-auto mb-4 text-blue-500") do
42
+ unsafe_raw '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>'
43
+ end
44
+ p(class: "text-gray-600") { "Secure your account with an additional layer of protection." }
45
+ end
46
+
47
+ a(
48
+ href: EasyAdmin::Engine.routes.url_helpers.two_factor_setup_path,
49
+ class: "w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors block text-center"
50
+ ) { "Continue Setup" }
51
+ end
52
+
53
+ def render_verification_step
54
+ div do
55
+ div(class: "text-center mb-6") do
56
+ h3(class: "text-lg font-medium mb-2") { "Scan QR Code" }
57
+ p(class: "text-sm text-gray-600 mb-4") do
58
+ "Use Google Authenticator, Authy, or any TOTP app to scan this code:"
59
+ end
60
+
61
+ # QR Code
62
+ div(class: "bg-white p-4 rounded-lg inline-block border") do
63
+ if @user.qr_code_svg.present?
64
+ div(class: "w-48 h-48", style: "svg { width: 100%; height: 100%; }") do
65
+ unsafe_raw(@user.qr_code_svg)
66
+ end
67
+ else
68
+ div(class: "w-48 h-48 bg-gray-100 rounded flex items-center justify-center") do
69
+ p(class: "text-gray-500 text-sm") { "QR code unavailable" }
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ div(class: "mb-6") do
76
+ h4(class: "text-sm font-medium mb-2") { "Manual Entry Key:" }
77
+ div(class: "bg-gray-100 p-3 rounded-lg") do
78
+ code(class: "text-sm font-mono break-all") { @user.otp_secret }
79
+ end
80
+ p(class: "text-xs text-gray-500 mt-1") do
81
+ "Enter this key manually if you can't scan the QR code"
82
+ end
83
+ end
84
+
85
+ render_verification_form
86
+ end
87
+ end
88
+
89
+ def render_verification_form
90
+ form(action: EasyAdmin::Engine.routes.url_helpers.two_factor_enable_path, method: :post) do
91
+ input(type: "hidden", name: "authenticity_token", value: helpers.form_authenticity_token) if helpers.respond_to?(:form_authenticity_token)
92
+
93
+ div(class: "mb-4") do
94
+ label(for: "otp_code", class: "block text-sm font-medium mb-2") { "Enter 6-digit code from your authenticator app:" }
95
+ input(
96
+ type: "text",
97
+ name: "otp_code",
98
+ id: "otp_code",
99
+ class: "w-full p-3 border border-gray-300 rounded-lg text-center text-2xl font-mono tracking-wider",
100
+ placeholder: "000000",
101
+ maxlength: 6,
102
+ pattern: "[0-9]{6}",
103
+ autocomplete: "one-time-code",
104
+ required: true
105
+ )
106
+ end
107
+
108
+ div(class: "flex space-x-3") do
109
+ input(
110
+ type: "submit",
111
+ value: "Enable 2FA",
112
+ class: "flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 transition-colors cursor-pointer"
113
+ )
114
+
115
+ a(
116
+ href: EasyAdmin::Engine.routes.url_helpers.profile_path,
117
+ class: "flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-400 transition-colors text-center"
118
+ ) { "Cancel" }
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,92 @@
1
+ module EasyAdmin
2
+ module TwoFactor
3
+ class StatusComponent < BaseComponent
4
+ def initialize(user:)
5
+ @user = user
6
+ end
7
+
8
+ def view_template
9
+ return render_unavailable unless @user.two_factor_available?
10
+
11
+ if @user.two_factor_enabled?
12
+ render_enabled_status
13
+ else
14
+ render_disabled_status
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def render_unavailable
21
+ div(class: "bg-gray-50 p-4 rounded-lg border") do
22
+ div(class: "flex items-center") do
23
+ div(class: "w-8 h-8 text-gray-400 mr-3") do
24
+ unsafe_raw '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.996-.833-2.764 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path></svg>'
25
+ end
26
+ div do
27
+ h4(class: "font-medium text-gray-900") { "2FA Unavailable" }
28
+ p(class: "text-sm text-gray-600") { "Contact System Administrator to enable 2FA" }
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def render_enabled_status
35
+ div(class: "bg-green-50 p-4 rounded-lg border border-green-200") do
36
+ div(class: "flex items-start justify-between") do
37
+ div(class: "flex items-center") do
38
+ div(class: "w-8 h-8 text-green-500 mr-3") do
39
+ unsafe_raw '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'
40
+ end
41
+ div do
42
+ h4(class: "font-medium text-green-900") { "Two-Factor Authentication Enabled" }
43
+ p(class: "text-sm text-green-700") do
44
+ "Your account is protected with 2FA"
45
+ end
46
+ p(class: "text-xs text-green-600 mt-1") do
47
+ "#{@user.backup_codes_remaining} backup codes remaining"
48
+ end
49
+ end
50
+ end
51
+
52
+ div do
53
+ a(
54
+ href: EasyAdmin::Engine.routes.url_helpers.two_factor_backup_codes_path,
55
+ class: "text-sm bg-green-100 text-green-800 px-3 py-1 rounded-md hover:bg-green-200 transition-colors",
56
+ data: { turbo_frame: "modal" }
57
+ ) { "Backup Codes" }
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def render_disabled_status
64
+ div(class: "bg-yellow-50 p-4 rounded-lg border border-yellow-200") do
65
+ div(class: "flex items-start justify-between") do
66
+ div(class: "flex items-center") do
67
+ div(class: "w-8 h-8 text-yellow-500 mr-3") do
68
+ unsafe_raw '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>'
69
+ end
70
+ div do
71
+ h4(class: "font-medium text-yellow-900") { "Two-Factor Authentication Disabled" }
72
+ p(class: "text-sm text-yellow-700") do
73
+ if @user.two_factor_required?
74
+ "Two-factor authentication is required for your role"
75
+ else
76
+ "Add an extra layer of security to your account"
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ a(
83
+ href: EasyAdmin::Engine.routes.url_helpers.two_factor_setup_path,
84
+ class: "bg-blue-600 text-white px-4 py-2 rounded-md text-sm hover:bg-blue-700 transition-colors",
85
+ data: { turbo_frame: "modal" }
86
+ ) { "Enable 2FA" }
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,110 @@
1
+ module EasyAdmin
2
+ module TwoFactor
3
+ extend ActiveSupport::Concern
4
+
5
+ def two_factor_setup
6
+ unless current_admin_user.two_factor_available?
7
+ respond_to do |format|
8
+ format.turbo_stream { render "easy_admin/profile/two_factor_unavailable" }
9
+ format.html { redirect_to profile_path, alert: "2FA is not available" }
10
+ end
11
+ return
12
+ end
13
+
14
+ # Generate secret if not already present
15
+ unless current_admin_user.otp_secret.present?
16
+ current_admin_user.generate_otp_secret!
17
+ current_admin_user.save!
18
+ end
19
+
20
+ respond_to do |format|
21
+ format.html { render "two_factor_setup", layout: !turbo_frame_request? }
22
+ end
23
+ end
24
+
25
+ def two_factor_enable
26
+ unless current_admin_user.two_factor_available?
27
+ respond_to do |format|
28
+ format.turbo_stream { render "easy_admin/profile/two_factor_unavailable" }
29
+ end
30
+ return
31
+ end
32
+
33
+ @otp_code = params[:otp_code]
34
+
35
+ if current_admin_user.validate_and_consume_otp!(@otp_code)
36
+ current_admin_user.update!(otp_required_for_login: true)
37
+ current_admin_user.generate_backup_codes!
38
+
39
+ respond_to do |format|
40
+ format.turbo_stream { render "easy_admin/profile/two_factor_enabled" }
41
+ end
42
+ else
43
+ respond_to do |format|
44
+ format.turbo_stream { render "easy_admin/profile/two_factor_invalid_code" }
45
+ end
46
+ end
47
+ end
48
+
49
+
50
+ def two_factor_backup_codes
51
+ unless current_admin_user.two_factor_enabled?
52
+ respond_to do |format|
53
+ format.turbo_stream { render "easy_admin/profile/two_factor_not_enabled" }
54
+ end
55
+ return
56
+ end
57
+
58
+ respond_to do |format|
59
+ format.html { render "two_factor_backup_codes", layout: !turbo_frame_request? }
60
+ end
61
+ end
62
+
63
+ def regenerate_backup_codes
64
+ unless current_admin_user.two_factor_enabled?
65
+ respond_to do |format|
66
+ format.turbo_stream { render "easy_admin/profile/two_factor_not_enabled" }
67
+ end
68
+ return
69
+ end
70
+
71
+ current_admin_user.generate_backup_codes!
72
+
73
+ respond_to do |format|
74
+ format.turbo_stream { render "easy_admin/profile/backup_codes_regenerated" }
75
+ end
76
+ end
77
+
78
+ def change_password
79
+ respond_to do |format|
80
+ format.html { render "change_password", layout: !turbo_frame_request? }
81
+ end
82
+ end
83
+
84
+ def update_password
85
+ if current_admin_user.valid_password?(params[:admin_user][:current_password])
86
+ if current_admin_user.update_with_password(password_params.merge(current_password: params[:admin_user][:current_password]))
87
+ # Bypass auto sign out by signing the user back in
88
+ bypass_sign_in(current_admin_user)
89
+ respond_to do |format|
90
+ format.turbo_stream { render "easy_admin/profile/password_updated" }
91
+ end
92
+ else
93
+ respond_to do |format|
94
+ format.turbo_stream { render "easy_admin/profile/password_error" }
95
+ end
96
+ end
97
+ else
98
+ respond_to do |format|
99
+ format.turbo_stream { render "easy_admin/profile/password_invalid_current" }
100
+ end
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def password_params
107
+ params.require(:admin_user).permit(:password, :password_confirmation)
108
+ end
109
+ end
110
+ end
@@ -17,6 +17,7 @@ module EasyAdmin
17
17
  helper EasyAdmin::PagyHelper
18
18
 
19
19
  before_action :authenticate_easy_admin_admin_user!
20
+ before_action :check_pending_2fa!
20
21
  before_action :set_active_storage_url_options
21
22
  before_action :set_feature_toggles
22
23
  before_action :set_paper_trail_whodunnit
@@ -54,6 +55,15 @@ module EasyAdmin
54
55
  redirect_to new_admin_user_session_path, alert: 'You need to sign in to access this page.'
55
56
  end
56
57
 
58
+ def check_pending_2fa!
59
+ # Skip check for sessions controller
60
+ return if controller_name == 'sessions'
61
+ return unless session[:pending_2fa_user_id]
62
+
63
+ # If user has pending 2FA, redirect them to complete it
64
+ redirect_to two_factor_verification_path, alert: 'Please complete two-factor authentication to continue.'
65
+ end
66
+
57
67
  def set_active_storage_url_options
58
68
  ActiveStorage::Current.url_options = { host: request.base_url } if defined?(ActiveStorage::Current)
59
69
  end
@@ -0,0 +1,25 @@
1
+ module EasyAdmin
2
+ class ProfileController < ApplicationController
3
+ include TwoFactor
4
+
5
+ def index
6
+ @user = current_admin_user
7
+ end
8
+
9
+ def update
10
+ @user = current_admin_user
11
+
12
+ if @user.update(profile_params)
13
+ redirect_to profile_path, notice: 'Profile updated successfully'
14
+ else
15
+ redirect_to profile_path, alert: 'Failed to update profile'
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def profile_params
22
+ params.require(:admin_user).permit(:first_name, :last_name, :email)
23
+ end
24
+ end
25
+ end
@@ -4,16 +4,116 @@ module EasyAdmin
4
4
 
5
5
  # GET /easy_admin/sign_in
6
6
  def new
7
+ # Clear any pending 2FA session
8
+ session.delete(:pending_2fa_user_id)
7
9
  super
8
10
  end
9
11
 
10
12
  # POST /easy_admin/sign_in
11
13
  def create
14
+ # First, try to authenticate with email/password
15
+ self.resource = warden.authenticate!(auth_options)
16
+
17
+ if resource
18
+ # Check if 2FA is required for this user
19
+ if resource.two_factor_enabled?
20
+ # Store user ID in session for 2FA verification
21
+ session[:pending_2fa_user_id] = resource.id
22
+
23
+ # Don't sign in yet - redirect to 2FA verification page
24
+ redirect_to two_factor_verification_path
25
+ else
26
+ # No 2FA required, proceed with normal sign in
27
+ set_flash_message!(:notice, :signed_in)
28
+ sign_in(resource_name, resource)
29
+ yield resource if block_given?
30
+ respond_with resource, location: after_sign_in_path_for(resource)
31
+ end
32
+ else
33
+ # Authentication failed
34
+ super
35
+ end
36
+ rescue => e
37
+ # Handle authentication errors
12
38
  super
13
39
  end
40
+
41
+ # GET /easy_admin/two_factor_verification
42
+ def two_factor_verification
43
+ user_id = session[:pending_2fa_user_id]
44
+
45
+ unless user_id
46
+ redirect_to new_admin_user_session_path, alert: "Session expired. Please sign in again."
47
+ return
48
+ end
49
+
50
+ @user = EasyAdmin::AdminUser.find_by(id: user_id)
51
+
52
+ unless @user&.two_factor_enabled?
53
+ session.delete(:pending_2fa_user_id)
54
+ redirect_to new_admin_user_session_path, alert: "Invalid session. Please sign in again."
55
+ return
56
+ end
57
+ end
58
+
59
+ # GET /easy_admin/cancel_2fa
60
+ def cancel_2fa
61
+ # Clear the pending 2FA session
62
+ session.delete(:pending_2fa_user_id)
63
+
64
+ # Sign out the user completely
65
+ sign_out(current_admin_user) if current_admin_user
66
+
67
+ # Redirect to sign in with a message
68
+ redirect_to new_admin_user_session_path, notice: "2FA verification cancelled. Please sign in again."
69
+ end
70
+
71
+ # POST /easy_admin/verify_2fa
72
+ def verify_2fa
73
+ user_id = session[:pending_2fa_user_id]
74
+
75
+ unless user_id
76
+ redirect_to new_admin_user_session_path, alert: "Session expired. Please sign in again."
77
+ return
78
+ end
79
+
80
+ user = EasyAdmin::AdminUser.find_by(id: user_id)
81
+
82
+ unless user&.two_factor_enabled?
83
+ session.delete(:pending_2fa_user_id)
84
+ redirect_to new_admin_user_session_path, alert: "Invalid session. Please sign in again."
85
+ return
86
+ end
87
+
88
+ otp_code = params[:otp_code]&.strip
89
+
90
+ if otp_code.present? && user.validate_and_consume_otp!(otp_code)
91
+ # 2FA verification successful
92
+ session.delete(:pending_2fa_user_id)
93
+ set_flash_message!(:notice, :signed_in)
94
+ sign_in(resource_name, user)
95
+
96
+ redirect_to after_sign_in_path_for(user)
97
+ else
98
+ # 2FA verification failed
99
+ @user = user # Make sure @user is available for the view
100
+
101
+ respond_to do |format|
102
+ format.html do
103
+ flash.now[:alert] = "Invalid authentication code. Please try again."
104
+ render :two_factor_verification
105
+ end
106
+ format.turbo_stream do
107
+ render "verify_2fa_error"
108
+ end
109
+ end
110
+ end
111
+ end
14
112
 
15
113
  # DELETE /easy_admin/sign_out
16
114
  def destroy
115
+ # Clear any pending 2FA session on sign out
116
+ session.delete(:pending_2fa_user_id)
17
117
  super
18
118
  end
19
119
 
@@ -28,5 +128,11 @@ module EasyAdmin
28
128
  def after_sign_out_path_for(resource_or_scope)
29
129
  new_admin_user_session_path
30
130
  end
131
+
132
+ private
133
+
134
+ def auth_options
135
+ { scope: resource_name, recall: "#{controller_path}#new" }
136
+ end
31
137
  end
32
- end
138
+ end
@@ -4,12 +4,15 @@ import { Controller } from "@hotwired/stimulus"
4
4
  export default class extends Controller {
5
5
  static targets = ["content", "icon"]
6
6
  static values = {
7
- expanded: { type: Boolean, default: true }
7
+ expanded: { type: Boolean, default: true },
8
+ persistState: { type: Boolean, default: true }
8
9
  }
9
10
 
10
11
  connect() {
11
- // Load saved state
12
- this.loadState()
12
+ // Load saved state only if persistence is enabled
13
+ if (this.persistStateValue) {
14
+ this.loadState()
15
+ }
13
16
 
14
17
  // Set initial state
15
18
  if (this.expandedValue) {
@@ -50,8 +53,10 @@ export default class extends Controller {
50
53
  // Update icon rotation
51
54
  this.updateIconRotation()
52
55
 
53
- // Save state
54
- this.saveState(true)
56
+ // Save state only if persistence is enabled
57
+ if (this.persistStateValue) {
58
+ this.saveState(true)
59
+ }
55
60
  }
56
61
 
57
62
  collapse(animate = true) {
@@ -78,8 +83,10 @@ export default class extends Controller {
78
83
  // Update icon rotation
79
84
  this.updateIconRotation()
80
85
 
81
- // Save state
82
- this.saveState(false)
86
+ // Save state only if persistence is enabled
87
+ if (this.persistStateValue) {
88
+ this.saveState(false)
89
+ }
83
90
  }
84
91
 
85
92
  saveState(expanded) {
@@ -7,31 +7,7 @@ export default class extends Controller {
7
7
  }
8
8
 
9
9
  connect() {
10
- console.log("🎯 PermissionToggleController connected")
11
- console.log("🎯 User ID:", this.userIdValue)
12
-
13
- // Debug: Log all permission cards found
14
- const cards = this.permissionCardTargets
15
- console.log("🎯 Found", cards.length, "permission cards")
16
-
17
- cards.forEach((card, index) => {
18
- console.log(`🎯 Card ${index}:`, {
19
- permission: card.dataset.permissionName,
20
- granted: card.dataset.granted,
21
- resourceType: card.dataset.resourceType
22
- })
23
- })
24
-
25
- // Debug: Log all hidden inputs found
26
10
  const hiddenInputs = this.element.querySelectorAll('input[type="hidden"][data-permission-toggle-target="hiddenInput"]')
27
- console.log("🎯 Found", hiddenInputs.length, "hidden inputs")
28
-
29
- hiddenInputs.forEach((input, index) => {
30
- console.log(`🎯 Hidden input ${index}:`, {
31
- name: input.name,
32
- value: input.value
33
- })
34
- })
35
11
  }
36
12
 
37
13
  togglePermission(event) {
@@ -42,16 +18,8 @@ export default class extends Controller {
42
18
  const currentlyGranted = card.dataset.granted === "true"
43
19
  const newGrantedState = !currentlyGranted
44
20
 
45
- console.log("🎯 Toggling permission:", {
46
- permission: permissionName,
47
- wasGranted: currentlyGranted,
48
- nowGranted: newGrantedState
49
- })
50
-
51
- // Update the card UI immediately
52
21
  this.updateCardUI(card, newGrantedState)
53
-
54
- // Update the hidden input field for form submission
22
+
55
23
  this.updateHiddenInput(card, newGrantedState)
56
24
 
57
25
  // Show notification for feedback
@@ -63,6 +31,7 @@ export default class extends Controller {
63
31
 
64
32
  toggleAllForResource(event) {
65
33
  event.preventDefault()
34
+ event.stopPropagation() // Prevent triggering parent collapsible toggle
66
35
 
67
36
  const resourceType = event.currentTarget.dataset.resourceType
68
37
  const resourceCards = this.permissionCardTargets.filter(card =>