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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +100 -27
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +59 -0
- data/app/components/easy_admin/navbar_component.rb +19 -4
- data/app/components/easy_admin/permissions/user_role_assignment_component.rb +43 -16
- 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/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 -0
- data/app/controllers/easy_admin/profile_controller.rb +25 -0
- data/app/controllers/easy_admin/sessions_controller.rb +107 -1
- data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +14 -7
- data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +2 -33
- 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/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/two_factor_authentication.rb +156 -0
- data/lib/easy_admin/version.rb +1 -1
- 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 +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.
|
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.
|
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.
|
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 =>
|