easy-admin-rails 0.2.5 → 0.2.7

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +95 -0
  3. data/app/assets/builds/easy_admin.base.js.map +3 -3
  4. data/app/assets/builds/easy_admin.css +226 -0
  5. data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
  6. data/app/components/easy_admin/form_layout_component.rb +553 -0
  7. data/app/components/easy_admin/navbar_component.rb +19 -4
  8. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
  9. data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
  10. data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
  11. data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
  12. data/app/components/easy_admin/profile/settings_component.rb +103 -0
  13. data/app/components/easy_admin/show_layout_component.rb +694 -24
  14. data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
  15. data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
  16. data/app/components/easy_admin/two_factor/status_component.rb +92 -0
  17. data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
  18. data/app/controllers/easy_admin/application_controller.rb +10 -5
  19. data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
  20. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
  21. data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
  22. data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
  23. data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
  24. data/app/controllers/easy_admin/profile_controller.rb +25 -0
  25. data/app/controllers/easy_admin/resources_controller.rb +1 -5
  26. data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
  27. data/app/controllers/easy_admin/sessions_controller.rb +107 -1
  28. data/app/helpers/easy_admin/fields_helper.rb +8 -22
  29. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
  30. data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
  31. data/app/javascript/easy_admin/controllers.js +3 -1
  32. data/app/models/easy_admin/admin_user.rb +3 -0
  33. data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
  34. data/app/views/easy_admin/profile/change_password.html.erb +24 -0
  35. data/app/views/easy_admin/profile/index.html.erb +1 -0
  36. data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
  37. data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
  38. data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
  39. data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
  40. data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
  41. data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
  42. data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
  43. data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
  44. data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
  45. data/app/views/easy_admin/resources/edit.html.erb +2 -2
  46. data/app/views/easy_admin/resources/new.html.erb +2 -2
  47. data/app/views/easy_admin/resources/show.html.erb +3 -1
  48. data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
  49. data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
  50. data/config/routes.rb +20 -1
  51. data/lib/easy-admin-rails.rb +1 -0
  52. data/lib/easy_admin/field.rb +3 -2
  53. data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
  54. data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
  55. data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
  56. data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
  57. data/lib/easy_admin/layouts/dsl.rb +200 -0
  58. data/lib/easy_admin/layouts/layout_context.rb +189 -0
  59. data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
  60. data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
  61. data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
  62. data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
  63. data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
  64. data/lib/easy_admin/layouts/nodes/root.rb +25 -0
  65. data/lib/easy_admin/layouts/nodes/section.rb +46 -0
  66. data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
  67. data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
  68. data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
  69. data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
  70. data/lib/easy_admin/layouts.rb +28 -0
  71. data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
  72. data/lib/easy_admin/resource/base.rb +2 -2
  73. data/lib/easy_admin/resource/dsl.rb +2 -11
  74. data/lib/easy_admin/resource/field_registry.rb +58 -2
  75. data/lib/easy_admin/resource.rb +0 -9
  76. data/lib/easy_admin/resource_modules.rb +21 -4
  77. data/lib/easy_admin/two_factor_authentication.rb +156 -0
  78. data/lib/easy_admin/version.rb +1 -1
  79. data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
  80. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
  81. data/lib/generators/easy_admin/two_factor/templates/README +29 -0
  82. data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
  83. data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
  84. metadata +49 -9
  85. data/lib/easy_admin/resource/form_builder.rb +0 -123
  86. data/lib/easy_admin/resource/layout_builder.rb +0 -249
  87. data/lib/easy_admin/resource/show_builder.rb +0 -359
  88. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
  89. data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
  90. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
  91. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -0,0 +1,118 @@
1
+ module EasyAdmin
2
+ module TwoFactor
3
+ class BackupCodesComponent < BaseComponent
4
+ def initialize(user:, codes: nil)
5
+ @user = user
6
+ @codes = codes # Fresh codes if just generated
7
+ end
8
+
9
+ def view_template
10
+ return render_unavailable unless @user.two_factor_available?
11
+ return render_not_enabled unless @user.two_factor_enabled?
12
+
13
+ if @codes
14
+ render_fresh_codes
15
+ else
16
+ render_existing_codes
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def render_unavailable
23
+ div(class: "text-center") do
24
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "2FA Not Available" }
25
+ p(class: "text-gray-600") { "Two-factor authentication is not available." }
26
+ end
27
+ end
28
+
29
+ def render_not_enabled
30
+ div(class: "text-center") do
31
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "2FA Not Enabled" }
32
+ p(class: "text-gray-600 mb-4") { "Enable two-factor authentication first to generate backup codes." }
33
+ a(
34
+ href: EasyAdmin::Engine.routes.url_helpers.two_factor_setup_path,
35
+ class: "bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
36
+ ) { "Enable 2FA" }
37
+ end
38
+ end
39
+
40
+ def render_fresh_codes
41
+ div do
42
+ div(class: "bg-green-50 border border-green-200 rounded-lg p-4 mb-6") do
43
+ div(class: "flex items-center mb-2") do
44
+ div(class: "w-5 h-5 text-green-500 mr-2") do
45
+ 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>'
46
+ end
47
+ h3(class: "text-sm font-medium text-green-900") { "New Backup Codes Generated" }
48
+ end
49
+ p(class: "text-sm text-green-700") do
50
+ "Save these codes in a secure place. Each code can only be used once."
51
+ end
52
+ end
53
+
54
+ render_codes_list(@codes)
55
+ end
56
+ end
57
+
58
+ def render_existing_codes
59
+ div do
60
+ div(class: "mb-6") do
61
+ div(class: "flex items-center justify-between mb-4") do
62
+ span(class: "text-sm text-gray-500") do
63
+ "#{@user.backup_codes_remaining} codes remaining"
64
+ end
65
+ end
66
+
67
+ if @user.backup_codes_remaining == 0
68
+ div(class: "bg-red-50 border border-red-200 rounded-lg p-4 mb-4") do
69
+ div(class: "flex items-center mb-2") do
70
+ div(class: "w-5 h-5 text-red-500 mr-2") do
71
+ 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>'
72
+ end
73
+ h4(class: "text-sm font-medium text-red-900") { "No Backup Codes Remaining" }
74
+ end
75
+ p(class: "text-sm text-red-700") do
76
+ "You've used all your backup codes. Generate new ones to ensure you can access your account."
77
+ end
78
+ end
79
+ elsif @user.backup_codes_remaining <= 2
80
+ div(class: "bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4") do
81
+ div(class: "flex items-center mb-2") do
82
+ div(class: "w-5 h-5 text-yellow-500 mr-2") do
83
+ 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>'
84
+ end
85
+ h4(class: "text-sm font-medium text-yellow-900") { "Low on Backup Codes" }
86
+ end
87
+ p(class: "text-sm text-yellow-700") do
88
+ "Consider generating new backup codes soon."
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ render_codes_list(@user.otp_backup_codes || [])
95
+ end
96
+ end
97
+
98
+ def render_codes_list(codes)
99
+ if codes.any?
100
+ div(class: "bg-gray-50 p-4 rounded-lg mb-6") do
101
+ div(class: "grid grid-cols-2 gap-2") do
102
+ codes.each do |code|
103
+ div(class: "bg-white p-3 rounded border text-center font-mono text-lg tracking-wider") do
104
+ code
105
+ end
106
+ end
107
+ end
108
+ end
109
+ else
110
+ div(class: "bg-gray-50 p-4 rounded-lg mb-6 text-center") do
111
+ p(class: "text-gray-500 italic") { "No backup codes available" }
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -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
@@ -93,11 +103,6 @@ module EasyAdmin
93
103
  private
94
104
 
95
105
  def handle_authorization_failure(exception)
96
- Rails.logger.warn "🚫 Authorization failed: #{exception.message}"
97
- Rails.logger.warn "🚫 User: #{current_admin_user&.email} (Role: #{current_admin_user&.role&.name || 'None'})"
98
- Rails.logger.warn "🚫 Action: #{action_name} on #{params[:resource_name]}"
99
-
100
- # Extract meaningful error details
101
106
  action_name = extract_action_from_exception(exception)
102
107
  resource_name = extract_resource_from_exception(exception)
103
108
 
@@ -85,7 +85,6 @@ module EasyAdmin
85
85
  end
86
86
 
87
87
  rescue NameError => e
88
- Rails.logger.error e.backtrace
89
88
  respond_to do |format|
90
89
  format.turbo_stream do
91
90
  render turbo_stream: turbo_stream.update("notifications",
@@ -56,9 +56,7 @@ module EasyAdmin
56
56
  if !field_config
57
57
  field_config = @resource_class.fields_config.find { |f| f[:association].to_s == params[:field] }
58
58
  end
59
-
60
- Rails.logger.debug "Suggest field_config for #{params[:field]}: #{field_config}"
61
-
59
+
62
60
  unless field_config && field_config[:suggest]
63
61
  render json: { error: "Field not found or suggest not configured" }, status: :not_found
64
62
  return
@@ -237,10 +235,7 @@ module EasyAdmin
237
235
  display_method = field_config[:display_method] || :name
238
236
  suggest_config = field_config[:suggest] || {}
239
237
  search_fields = suggest_config[:search_fields] || [display_method]
240
-
241
- Rails.logger.debug "HasMany suggest: association=#{association_name}, display_method=#{display_method}, search_fields=#{search_fields}"
242
-
243
- # Build search query
238
+
244
239
  records = association_class.all
245
240
 
246
241
  if search_term.present?
@@ -261,7 +256,6 @@ module EasyAdmin
261
256
  results = records.limit(limit).map do |record|
262
257
  label = if record.respond_to?(display_method)
263
258
  raw_label = record.public_send(display_method)
264
- Rails.logger.debug "HasMany record #{record.id}: raw_label=#{raw_label.class}:#{raw_label}"
265
259
  raw_label.to_s
266
260
  elsif record.respond_to?(:name)
267
261
  record.name.to_s
@@ -272,8 +266,7 @@ module EasyAdmin
272
266
  end
273
267
  [label, record.id]
274
268
  end
275
-
276
- Rails.logger.debug "HasMany suggest final results: #{results}"
269
+
277
270
  results
278
271
  end
279
272
 
@@ -294,4 +287,4 @@ module EasyAdmin
294
287
  end
295
288
  end
296
289
  end
297
- end
290
+ end
@@ -57,14 +57,12 @@ module EasyAdmin
57
57
  # Check both form fields and all fields (including hidden ones)
58
58
  all_fields = resource_class.form_fields + resource_class.fields_config
59
59
  unique_fields = all_fields.uniq { |f| f[:name] }
60
-
61
- Rails.logger.debug "🔍 [ResourceLoading] All fields for #{resource_class}:"
60
+
62
61
  unique_fields.each_with_index do |field_config, index|
63
- Rails.logger.debug "🔍 [ResourceLoading] Field #{index}: #{field_config[:name]} (type: #{field_config[:type]}, readonly: #{field_config[:readonly]})"
64
62
  next if field_config[:readonly]
65
63
 
66
64
  field_name = field_config[:name]
67
-
65
+
68
66
  # Handle different field types that might need special parameter handling
69
67
  case field_config[:type]
70
68
  when :has_many
@@ -87,8 +85,12 @@ module EasyAdmin
87
85
  permitted_attrs << field_name
88
86
  end
89
87
  when :json
90
- # For JSON fields, permit nested parameters as a hash
91
- permitted_attrs << { field_name => {} }
88
+ # For JSON fields, permit as string (JSONEditor submits as JSON string)
89
+ if field_name == :permissions_cache
90
+ permitted_attrs << { field_name => {} }
91
+ else
92
+ permitted_attrs << field_name
93
+ end
92
94
  else
93
95
  # For regular fields, check if it's the permissions_cache field which needs hash permission
94
96
  if field_name == :permissions_cache
@@ -98,8 +100,7 @@ module EasyAdmin
98
100
  end
99
101
  end
100
102
  end
101
-
102
- Rails.logger.debug "🔍 [ResourceLoading] Final permitted attributes: #{permitted_attrs.inspect}"
103
+
103
104
  permitted_attrs
104
105
  end
105
106
 
@@ -146,4 +147,4 @@ module EasyAdmin
146
147
  end
147
148
  end
148
149
  end
149
- end
150
+ end
@@ -114,6 +114,9 @@ module EasyAdmin
114
114
 
115
115
  # Check if pagination should reset due to filtering
116
116
  def should_reset_pagination?
117
+ # Don't reset pagination for turbo_stream requests (infinite scroll)
118
+ return false if request.format.turbo_stream?
119
+
117
120
  has_active_filters? && !first_page?
118
121
  end
119
122
 
@@ -29,7 +29,6 @@ module EasyAdmin
29
29
  format.json { render json: @card_data }
30
30
  end
31
31
  rescue => e
32
- Rails.logger.error "Dashboard card error: #{e.message}"
33
32
  @error_message = e.message
34
33
 
35
34
  respond_to do |format|
@@ -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
@@ -78,11 +78,7 @@ module EasyAdmin
78
78
  end
79
79
 
80
80
  def update
81
- Rails.logger.debug "Update called with params: #{record_params}"
82
- Rails.logger.debug "Record before update: #{@record.inspect}"
83
-
84
81
  if @record.update(record_params)
85
- Rails.logger.debug "Record after update: #{@record.reload.inspect}"
86
82
  respond_to do |format|
87
83
  format.html {
88
84
  redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
@@ -183,4 +179,4 @@ module EasyAdmin
183
179
  end
184
180
  end
185
181
  end
186
- end
182
+ end
@@ -72,9 +72,6 @@ module EasyAdmin
72
72
  end
73
73
  end
74
74
  rescue => e
75
- Rails.logger.error "Row action execution failed: #{e.message}"
76
- Rails.logger.error e.backtrace.join("\n")
77
-
78
75
  respond_to do |format|
79
76
  format.turbo_stream do
80
77
  render turbo_stream: turbo_stream.update(
@@ -238,4 +235,4 @@ module EasyAdmin
238
235
  end
239
236
  end
240
237
  end
241
- end
238
+ end