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
@@ -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
|
@@ -17,43 +17,26 @@ module EasyAdmin
|
|
17
17
|
return ""
|
18
18
|
when :custom_content
|
19
19
|
if field[:block]
|
20
|
-
Rails.logger.debug "🔍 [FieldsHelper] Processing custom_content block"
|
21
|
-
Rails.logger.debug "🔍 [FieldsHelper] Form present: #{form.present?}"
|
22
|
-
Rails.logger.debug "🔍 [FieldsHelper] Record present: #{record.present?}"
|
23
|
-
|
24
|
-
# Create a context object with access to form, record, and helpers
|
25
20
|
context = OpenStruct.new(form: form, record: record, helpers: self)
|
26
|
-
|
27
|
-
# Call the block directly to get the result (don't use capture which converts to string)
|
21
|
+
|
28
22
|
result = field[:block].call(context)
|
29
|
-
|
30
|
-
Rails.logger.debug "🔍 [FieldsHelper] Block result class: #{result.class}"
|
31
|
-
Rails.logger.debug "🔍 [FieldsHelper] Result responds to call: #{result.respond_to?(:call)}"
|
32
|
-
Rails.logger.debug "🔍 [FieldsHelper] Result responds to view_template: #{result.respond_to?(:view_template)}"
|
33
|
-
|
34
|
-
# Handle different types of results
|
35
23
|
if result.respond_to?(:call) && result.respond_to?(:view_template)
|
36
|
-
Rails.logger.debug "🔍 [FieldsHelper] Rendering Phlex component"
|
37
|
-
# It's a Phlex component - use Rails render helper
|
38
24
|
render(result)
|
39
25
|
elsif result.is_a?(String)
|
40
|
-
Rails.logger.debug "🔍 [FieldsHelper] Rendering string result"
|
41
26
|
result.html_safe
|
42
27
|
elsif result.respond_to?(:to_s)
|
43
|
-
Rails.logger.debug "🔍 [FieldsHelper] Converting to string and rendering"
|
44
28
|
result.to_s.html_safe
|
45
29
|
else
|
46
|
-
Rails.logger.debug "🔍 [FieldsHelper] No valid result, returning empty string"
|
47
30
|
""
|
48
31
|
end
|
49
32
|
else
|
50
|
-
Rails.logger.debug "🔍 [FieldsHelper] No block provided for custom_content"
|
51
33
|
""
|
52
34
|
end
|
53
35
|
else
|
54
36
|
# Regular field rendering
|
37
|
+
field_type = field[:type] || field[:field_type] || :text
|
55
38
|
component = EasyAdmin::Field.render(
|
56
|
-
|
39
|
+
field_type,
|
57
40
|
action: action,
|
58
41
|
field: field,
|
59
42
|
value: value,
|
@@ -61,13 +44,16 @@ module EasyAdmin
|
|
61
44
|
form: form
|
62
45
|
)
|
63
46
|
|
64
|
-
component.call
|
47
|
+
result = component.call
|
48
|
+
|
49
|
+
result.html_safe
|
65
50
|
end
|
66
51
|
end
|
67
52
|
|
68
53
|
def field_component(field, action:, value: nil, record: nil, form: nil)
|
54
|
+
field_type = field[:type] || field[:field_type] || :text
|
69
55
|
EasyAdmin::Field.render(
|
70
|
-
|
56
|
+
field_type,
|
71
57
|
action: action,
|
72
58
|
field: field,
|
73
59
|
value: value,
|
@@ -19,14 +19,26 @@ export default class extends Controller {
|
|
19
19
|
// Called when the element is replaced by turbo stream
|
20
20
|
urlValueChanged() {
|
21
21
|
if (this.observer && this.sentinel) {
|
22
|
+
// Reset loading state when URL changes (e.g., when filters are applied)
|
23
|
+
this.isLoading = false
|
24
|
+
this.hideLoading()
|
25
|
+
|
22
26
|
// Re-setup observer with new URL
|
23
27
|
this.setupIntersectionObserver()
|
28
|
+
|
29
|
+
// Re-initialize UI state
|
30
|
+
this.initializeUI()
|
24
31
|
}
|
25
32
|
}
|
26
33
|
|
27
34
|
hasMoreValueChanged() {
|
28
35
|
if (!this.hasMoreValue) {
|
29
36
|
this.showEndMessage()
|
37
|
+
} else {
|
38
|
+
// Hide end message if we have more pages (e.g., after filters change)
|
39
|
+
if (this.hasEndTarget) {
|
40
|
+
this.endTarget.style.display = 'none'
|
41
|
+
}
|
30
42
|
}
|
31
43
|
}
|
32
44
|
|
@@ -0,0 +1,112 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["tab", "content"]
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
// Check for tab parameter in URL query string
|
8
|
+
const urlParams = new URLSearchParams(window.location.search)
|
9
|
+
const tabFromUrl = urlParams.get('tab')
|
10
|
+
|
11
|
+
// If tab parameter exists and matches a valid tab, use it
|
12
|
+
if (tabFromUrl && this.isValidTab(tabFromUrl)) {
|
13
|
+
this.activateTab(tabFromUrl)
|
14
|
+
} else {
|
15
|
+
// Set initial active tab (first tab by default)
|
16
|
+
const firstTab = this.tabTargets[0]
|
17
|
+
if (firstTab) {
|
18
|
+
this.activateTab(firstTab.dataset.tabId)
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
// Listen for popstate events (back/forward navigation)
|
23
|
+
this.handlePopState = this.handlePopState.bind(this)
|
24
|
+
window.addEventListener('popstate', this.handlePopState)
|
25
|
+
}
|
26
|
+
|
27
|
+
disconnect() {
|
28
|
+
window.removeEventListener('popstate', this.handlePopState)
|
29
|
+
}
|
30
|
+
|
31
|
+
switchTab(event) {
|
32
|
+
event.preventDefault()
|
33
|
+
const tabId = event.currentTarget.dataset.tabId
|
34
|
+
this.activateTab(tabId)
|
35
|
+
}
|
36
|
+
|
37
|
+
activateTab(tabId) {
|
38
|
+
// Update tab buttons
|
39
|
+
this.tabTargets.forEach(tab => {
|
40
|
+
const isActive = tab.dataset.tabId === tabId
|
41
|
+
|
42
|
+
if (isActive) {
|
43
|
+
tab.classList.remove("text-gray-700", "hover:bg-gray-100", "hover:text-gray-900")
|
44
|
+
tab.classList.add("bg-blue-50", "text-blue-700", "border-blue-200")
|
45
|
+
} else {
|
46
|
+
tab.classList.remove("bg-blue-50", "text-blue-700", "border-blue-200")
|
47
|
+
tab.classList.add("text-gray-700", "hover:bg-gray-100", "hover:text-gray-900")
|
48
|
+
}
|
49
|
+
})
|
50
|
+
|
51
|
+
// Update content areas
|
52
|
+
this.contentTargets.forEach(content => {
|
53
|
+
const isActive = content.dataset.tabId === tabId
|
54
|
+
|
55
|
+
if (isActive) {
|
56
|
+
content.classList.remove("hidden")
|
57
|
+
} else {
|
58
|
+
content.classList.add("hidden")
|
59
|
+
}
|
60
|
+
})
|
61
|
+
|
62
|
+
// Update URL hash for deep linking (optional)
|
63
|
+
if (history.replaceState) {
|
64
|
+
const url = new URL(window.location)
|
65
|
+
url.searchParams.set('tab', tabId)
|
66
|
+
history.replaceState(null, '', url)
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
isValidTab(tabId) {
|
71
|
+
return this.tabTargets.some(tab => tab.dataset.tabId === tabId)
|
72
|
+
}
|
73
|
+
|
74
|
+
handlePopState() {
|
75
|
+
const urlParams = new URLSearchParams(window.location.search)
|
76
|
+
const tabFromUrl = urlParams.get('tab')
|
77
|
+
|
78
|
+
if (tabFromUrl && this.isValidTab(tabFromUrl)) {
|
79
|
+
this.activateTabSilently(tabFromUrl)
|
80
|
+
} else {
|
81
|
+
const firstTab = this.tabTargets[0]
|
82
|
+
if (firstTab) {
|
83
|
+
this.activateTabSilently(firstTab.dataset.tabId)
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
activateTabSilently(tabId) {
|
89
|
+
// Activate tab without updating URL (for popstate handling)
|
90
|
+
this.tabTargets.forEach(tab => {
|
91
|
+
const isActive = tab.dataset.tabId === tabId
|
92
|
+
|
93
|
+
if (isActive) {
|
94
|
+
tab.classList.remove("text-gray-700", "hover:bg-gray-100", "hover:text-gray-900")
|
95
|
+
tab.classList.add("bg-blue-50", "text-blue-700", "border-blue-200")
|
96
|
+
} else {
|
97
|
+
tab.classList.remove("bg-blue-50", "text-blue-700", "border-blue-200")
|
98
|
+
tab.classList.add("text-gray-700", "hover:bg-gray-100", "hover:text-gray-900")
|
99
|
+
}
|
100
|
+
})
|
101
|
+
|
102
|
+
this.contentTargets.forEach(content => {
|
103
|
+
const isActive = content.dataset.tabId === tabId
|
104
|
+
|
105
|
+
if (isActive) {
|
106
|
+
content.classList.remove("hidden")
|
107
|
+
} else {
|
108
|
+
content.classList.add("hidden")
|
109
|
+
}
|
110
|
+
})
|
111
|
+
}
|
112
|
+
}
|
@@ -28,6 +28,7 @@ import JsoneditorController from './controllers/jsoneditor_controller'
|
|
28
28
|
import VersionRevertController from './controllers/version_revert_controller'
|
29
29
|
import RolePreviewController from './controllers/role_preview_controller'
|
30
30
|
import PermissionToggleController from './controllers/permission_toggle_controller'
|
31
|
+
import VerticalTabsController from './controllers/vertical_tabs_controller'
|
31
32
|
|
32
33
|
// Register controllers
|
33
34
|
application.register('sidebar', SidebarController)
|
@@ -57,4 +58,5 @@ application.register('row-action', RowActionController)
|
|
57
58
|
application.register('jsoneditor', JsoneditorController)
|
58
59
|
application.register('version-revert', VersionRevertController)
|
59
60
|
application.register('role-preview', RolePreviewController)
|
60
|
-
application.register('permission-toggle', PermissionToggleController)
|
61
|
+
application.register('permission-toggle', PermissionToggleController)
|
62
|
+
application.register('vertical-tabs', VerticalTabsController)
|
@@ -10,6 +10,9 @@ module EasyAdmin
|
|
10
10
|
# EasyAdmin Permissions
|
11
11
|
include EasyAdmin::Permissions::UserExtensions
|
12
12
|
|
13
|
+
# Two-Factor Authentication (optional)
|
14
|
+
include EasyAdmin::TwoFactorAuthentication
|
15
|
+
|
13
16
|
# Direct role association (each admin user has one role)
|
14
17
|
belongs_to :role, class_name: 'EasyAdmin::Permissions::Role', optional: true
|
15
18
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<%= turbo_stream.replace "backup_codes_section" do %>
|
2
|
+
<div id="backup_codes_section">
|
3
|
+
<%== EasyAdmin::TwoFactor::BackupCodesComponent.new(user: current_admin_user).call %>
|
4
|
+
</div>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<%= turbo_stream.replace "notifications" do %>
|
8
|
+
<%== EasyAdmin::NotificationComponent.new(
|
9
|
+
message: "New backup codes have been generated",
|
10
|
+
type: :success
|
11
|
+
).call %>
|
12
|
+
<% end %>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<% if turbo_frame_request? %>
|
2
|
+
<turbo-frame id="modal">
|
3
|
+
<div id="modal-backdrop" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 opacity-100 transition-opacity duration-300" data-controller="modal" data-action="click->modal#closeOnBackdrop">
|
4
|
+
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 xl:w-2/5 shadow-lg rounded-md bg-white">
|
5
|
+
<div class="mt-3">
|
6
|
+
<!-- Modal header -->
|
7
|
+
<div class="flex items-center justify-between mb-4">
|
8
|
+
<h3 class="text-lg font-medium text-gray-900">Change Password</h3>
|
9
|
+
<button class="text-gray-400 hover:text-gray-600 focus:outline-none" data-action="click->modal#close">
|
10
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
11
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
12
|
+
</svg>
|
13
|
+
</button>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<!-- Modal content -->
|
17
|
+
<%== EasyAdmin::Profile::ChangePasswordModalComponent.new(user: current_admin_user).call %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</turbo-frame>
|
22
|
+
<% else %>
|
23
|
+
<%== EasyAdmin::Profile::ChangePasswordModalComponent.new(user: current_admin_user).call %>
|
24
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%== EasyAdmin::Profile::SettingsComponent.new(user: @user).call %>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<% if turbo_frame_request? %>
|
2
|
+
<turbo-frame id="modal">
|
3
|
+
<div id="modal-backdrop" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 opacity-100 transition-opacity duration-300" data-controller="modal" data-action="click->modal#closeOnBackdrop">
|
4
|
+
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 xl:w-2/5 shadow-lg rounded-md bg-white">
|
5
|
+
<div class="mt-3">
|
6
|
+
<!-- Modal header -->
|
7
|
+
<div class="flex items-center justify-between mb-4">
|
8
|
+
<h3 class="text-lg font-medium text-gray-900">Backup Codes</h3>
|
9
|
+
<button class="text-gray-400 hover:text-gray-600 focus:outline-none" data-action="click->modal#close">
|
10
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
11
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
12
|
+
</svg>
|
13
|
+
</button>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<!-- Modal content -->
|
17
|
+
<%== EasyAdmin::TwoFactor::BackupCodesComponent.new(user: current_admin_user).call %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</turbo-frame>
|
22
|
+
<% else %>
|
23
|
+
<%== EasyAdmin::TwoFactor::BackupCodesComponent.new(user: current_admin_user).call %>
|
24
|
+
<% end %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<%= turbo_stream.replace "two_factor_section" do %>
|
2
|
+
<div id="two_factor_section">
|
3
|
+
<%== EasyAdmin::TwoFactor::StatusComponent.new(user: current_admin_user).call %>
|
4
|
+
</div>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<%= turbo_stream.replace "notifications" do %>
|
8
|
+
<%== EasyAdmin::NotificationComponent.new(
|
9
|
+
message: "Two-factor authentication has been enabled successfully!",
|
10
|
+
type: :success
|
11
|
+
).call %>
|
12
|
+
<% end %>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<% if turbo_frame_request? %>
|
2
|
+
<turbo-frame id="modal">
|
3
|
+
<div id="modal-backdrop" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 opacity-100 transition-opacity duration-300" data-controller="modal" data-action="click->modal#closeOnBackdrop">
|
4
|
+
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 xl:w-2/5 shadow-lg rounded-md bg-white">
|
5
|
+
<div class="mt-3">
|
6
|
+
<!-- Modal header -->
|
7
|
+
<div class="flex items-center justify-between mb-4">
|
8
|
+
<h3 class="text-lg font-medium text-gray-900">Set up Two-Factor Authentication</h3>
|
9
|
+
<button class="text-gray-400 hover:text-gray-600 focus:outline-none" data-action="click->modal#close">
|
10
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
11
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
12
|
+
</svg>
|
13
|
+
</button>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<!-- Modal content -->
|
17
|
+
<%== EasyAdmin::TwoFactor::SetupComponent.new(user: current_admin_user).call %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</turbo-frame>
|
22
|
+
<% else %>
|
23
|
+
<%== EasyAdmin::TwoFactor::SetupComponent.new(user: current_admin_user).call %>
|
24
|
+
<% end %>
|
@@ -116,8 +116,8 @@
|
|
116
116
|
<% end %>
|
117
117
|
|
118
118
|
<!-- Form Fields -->
|
119
|
-
<% if @resource_class.
|
120
|
-
|
119
|
+
<% if @resource_class.has_custom_form_layout? %>
|
120
|
+
<%== EasyAdmin::FormLayoutComponent.new(resource_class: @resource_class, form: form, record: @record).call %>
|
121
121
|
<% else %>
|
122
122
|
<!-- Default Single Card Layout -->
|
123
123
|
<div class="bg-white shadow-sm rounded-lg border border-gray-200">
|
@@ -65,8 +65,8 @@
|
|
65
65
|
<% end %>
|
66
66
|
|
67
67
|
<!-- Form Fields -->
|
68
|
-
<% if @resource_class.
|
69
|
-
|
68
|
+
<% if @resource_class.has_custom_form_layout? %>
|
69
|
+
<%== EasyAdmin::FormLayoutComponent.new(resource_class: @resource_class, form: form).call %>
|
70
70
|
<% else %>
|
71
71
|
<!-- Default Single Card Layout -->
|
72
72
|
<div class="bg-white shadow-sm rounded-lg border border-gray-200">
|
@@ -27,5 +27,7 @@
|
|
27
27
|
|
28
28
|
<!-- Show Layout Content -->
|
29
29
|
<div class="show-content">
|
30
|
-
|
30
|
+
<div class="container-fluid">
|
31
|
+
<%== EasyAdmin::ShowLayoutComponent.new(resource_class: @resource_class, record: @record).call %>
|
32
|
+
</div>
|
31
33
|
</div>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<!-- Form -->
|
2
|
+
<div class="p-10">
|
3
|
+
<%= form_with url: verify_2fa_path, method: :post, local: true, html: { class: "space-y-8" } do |f| %>
|
4
|
+
|
5
|
+
<!-- Header -->
|
6
|
+
<div class="text-center mb-6">
|
7
|
+
<h2 class="text-xl font-bold text-gray-900 mb-2">Two-Factor Authentication</h2>
|
8
|
+
<p class="text-gray-600">Enter the 6-digit code from your authenticator app</p>
|
9
|
+
<% if @user %>
|
10
|
+
<p class="text-sm text-gray-500 mt-2">Signing in as <%= @user.email %></p>
|
11
|
+
<% end %>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<!-- Input Fields Container -->
|
15
|
+
<div class="space-y-4">
|
16
|
+
<!-- OTP Code Field -->
|
17
|
+
<div class="relative">
|
18
|
+
<%= text_field_tag :otp_code, '', autofocus: true, autocomplete: "one-time-code",
|
19
|
+
maxlength: 6, pattern: "[0-9]{6}", id: "otp_code",
|
20
|
+
class: "w-full px-6 py-5 bg-gray-50/80 border-0 text-gray-900 placeholder-gray-400 focus:bg-gray-100/80 focus:ring-0 focus:outline-none text-base rounded-2xl transition-colors duration-200 font-medium text-center tracking-widest",
|
21
|
+
placeholder: "000000" %>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<!-- Submit Button -->
|
26
|
+
<div class="pt-4">
|
27
|
+
<%= f.submit "Verify Code",
|
28
|
+
class: "w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-bold py-5 px-6 rounded-2xl transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-500/30 text-base shadow-xl shadow-blue-500/20 active:scale-[0.98]" %>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<% end %>
|
32
|
+
|
33
|
+
<!-- Help Text -->
|
34
|
+
<div class="text-center pt-6">
|
35
|
+
<p class="text-sm text-gray-500 mb-2">
|
36
|
+
Can't access your authenticator app?
|
37
|
+
</p>
|
38
|
+
<p class="text-sm text-gray-400">
|
39
|
+
Contact your administrator for help with account recovery.
|
40
|
+
</p>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<!-- Back to Sign In -->
|
44
|
+
<div class="text-center pt-4">
|
45
|
+
<%= link_to "← Back to Sign In", cancel_2fa_path,
|
46
|
+
class: "text-sm text-blue-500 hover:text-blue-600 font-medium transition-colors duration-200" %>
|
47
|
+
</div>
|
48
|
+
</div>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<%= turbo_stream.replace "notifications" do %>
|
2
|
+
<%== EasyAdmin::NotificationComponent.new(
|
3
|
+
message: "Invalid authentication code. Please try again.",
|
4
|
+
type: :error
|
5
|
+
).call %>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<%= turbo_stream.replace "otp_code" do %>
|
9
|
+
<%= text_field_tag :otp_code, '', autofocus: true, autocomplete: "one-time-code",
|
10
|
+
maxlength: 6, pattern: "[0-9]{6}",
|
11
|
+
class: "w-full px-6 py-5 bg-gray-50/80 border-0 text-gray-900 placeholder-gray-400 focus:bg-gray-100/80 focus:ring-0 focus:outline-none text-base rounded-2xl transition-colors duration-200 font-medium text-center tracking-widest",
|
12
|
+
placeholder: "000000" %>
|
13
|
+
<% end %>
|
data/config/routes.rb
CHANGED
@@ -13,13 +13,32 @@ EasyAdmin::Engine.routes.draw do
|
|
13
13
|
sign_out: 'sign_out',
|
14
14
|
sign_up: 'sign_up'
|
15
15
|
}
|
16
|
+
|
17
|
+
# 2FA verification routes (wrapped in devise_scope)
|
18
|
+
devise_scope :admin_user do
|
19
|
+
get 'two_factor_verification', to: 'sessions#two_factor_verification', as: 'two_factor_verification'
|
20
|
+
post 'verify_2fa', to: 'sessions#verify_2fa', as: 'verify_2fa'
|
21
|
+
get 'cancel_2fa', to: 'sessions#cancel_2fa', as: 'cancel_2fa'
|
22
|
+
end
|
16
23
|
|
17
24
|
root 'dashboard#index'
|
18
25
|
|
19
|
-
#
|
26
|
+
# Global settings routes (Flipper)
|
20
27
|
get 'settings', to: 'settings#index'
|
21
28
|
patch 'settings', to: 'settings#update'
|
22
29
|
|
30
|
+
# User profile settings routes
|
31
|
+
get 'profile', to: 'profile#index', as: 'profile'
|
32
|
+
patch 'profile', to: 'profile#update', as: 'update_profile'
|
33
|
+
get 'profile/change_password', to: 'profile#change_password', as: 'change_password'
|
34
|
+
patch 'profile/change_password', to: 'profile#update_password', as: 'update_password'
|
35
|
+
|
36
|
+
# Two-factor authentication routes (via profile controller)
|
37
|
+
get 'profile/two_factor/setup', to: 'profile#two_factor_setup', as: 'two_factor_setup'
|
38
|
+
post 'profile/two_factor/enable', to: 'profile#two_factor_enable', as: 'two_factor_enable'
|
39
|
+
get 'profile/two_factor/backup_codes', to: 'profile#two_factor_backup_codes', as: 'two_factor_backup_codes'
|
40
|
+
post 'profile/two_factor/regenerate_backup_codes', to: 'profile#regenerate_backup_codes', as: 'two_factor_regenerate_backup_codes'
|
41
|
+
|
23
42
|
# Confirmation modal
|
24
43
|
get 'confirmation_modal', to: 'confirmation_modal#show'
|
25
44
|
|
data/lib/easy-admin-rails.rb
CHANGED