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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +95 -0
- data/app/assets/builds/easy_admin.base.js.map +3 -3
- data/app/assets/builds/easy_admin.css +226 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
- data/app/components/easy_admin/form_layout_component.rb +553 -0
- data/app/components/easy_admin/navbar_component.rb +19 -4
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
- data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
- data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
- data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
- data/app/components/easy_admin/profile/settings_component.rb +103 -0
- data/app/components/easy_admin/show_layout_component.rb +694 -24
- data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
- data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
- data/app/components/easy_admin/two_factor/status_component.rb +92 -0
- data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
- data/app/controllers/easy_admin/application_controller.rb +10 -5
- data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
- data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
- data/app/controllers/easy_admin/profile_controller.rb +25 -0
- data/app/controllers/easy_admin/resources_controller.rb +1 -5
- data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
- data/app/controllers/easy_admin/sessions_controller.rb +107 -1
- data/app/helpers/easy_admin/fields_helper.rb +8 -22
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
- data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
- data/app/javascript/easy_admin/controllers.js +3 -1
- data/app/models/easy_admin/admin_user.rb +3 -0
- data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/change_password.html.erb +24 -0
- data/app/views/easy_admin/profile/index.html.erb +1 -0
- data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
- data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
- data/app/views/easy_admin/resources/edit.html.erb +2 -2
- data/app/views/easy_admin/resources/new.html.erb +2 -2
- data/app/views/easy_admin/resources/show.html.erb +3 -1
- data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
- data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
- data/config/routes.rb +20 -1
- data/lib/easy-admin-rails.rb +1 -0
- data/lib/easy_admin/field.rb +3 -2
- data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
- data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
- data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
- data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
- data/lib/easy_admin/layouts/dsl.rb +200 -0
- data/lib/easy_admin/layouts/layout_context.rb +189 -0
- data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
- data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
- data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
- data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
- data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
- data/lib/easy_admin/layouts/nodes/root.rb +25 -0
- data/lib/easy_admin/layouts/nodes/section.rb +46 -0
- data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
- data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
- data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
- data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
- data/lib/easy_admin/layouts.rb +28 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
- data/lib/easy_admin/resource/base.rb +2 -2
- data/lib/easy_admin/resource/dsl.rb +2 -11
- data/lib/easy_admin/resource/field_registry.rb +58 -2
- data/lib/easy_admin/resource.rb +0 -9
- data/lib/easy_admin/resource_modules.rb +21 -4
- data/lib/easy_admin/two_factor_authentication.rb +156 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
- data/lib/generators/easy_admin/two_factor/templates/README +29 -0
- data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
- data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
- metadata +49 -9
- data/lib/easy_admin/resource/form_builder.rb +0 -123
- data/lib/easy_admin/resource/layout_builder.rb +0 -249
- data/lib/easy_admin/resource/show_builder.rb +0 -359
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
- 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
|
|
@@ -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
|
91
|
-
|
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
|
|
@@ -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
|