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,46 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Layouts
|
3
|
+
module Nodes
|
4
|
+
# Section node for grouping content
|
5
|
+
class Section < BaseNode
|
6
|
+
def initialize(title = nil, attributes = {})
|
7
|
+
super(attributes)
|
8
|
+
@attributes[:title] = title
|
9
|
+
@attributes[:description] ||= nil
|
10
|
+
@attributes[:collapsible] ||= false
|
11
|
+
@attributes[:collapsed] ||= false
|
12
|
+
@attributes[:icon] ||= nil
|
13
|
+
@attributes[:css_class] ||= nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def title
|
17
|
+
@attributes[:title]
|
18
|
+
end
|
19
|
+
|
20
|
+
def description
|
21
|
+
@attributes[:description]
|
22
|
+
end
|
23
|
+
|
24
|
+
def collapsible?
|
25
|
+
@attributes[:collapsible]
|
26
|
+
end
|
27
|
+
|
28
|
+
def collapsed?
|
29
|
+
@attributes[:collapsed]
|
30
|
+
end
|
31
|
+
|
32
|
+
def icon
|
33
|
+
@attributes[:icon]
|
34
|
+
end
|
35
|
+
|
36
|
+
def css_class
|
37
|
+
@attributes[:css_class]
|
38
|
+
end
|
39
|
+
|
40
|
+
def section_id
|
41
|
+
"section-#{(title || 'untitled').parameterize}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Layouts
|
3
|
+
module Nodes
|
4
|
+
# Spacer node for adding vertical space
|
5
|
+
class Spacer < BaseNode
|
6
|
+
def initialize(attributes = {})
|
7
|
+
super
|
8
|
+
@attributes[:size] ||= 4 # Tailwind spacing size
|
9
|
+
end
|
10
|
+
|
11
|
+
def size
|
12
|
+
@attributes[:size]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Stub node classes - to be fully implemented
|
2
|
+
module EasyAdmin
|
3
|
+
module Layouts
|
4
|
+
module Nodes
|
5
|
+
# Show page nodes
|
6
|
+
class DescriptionList < BaseNode; end
|
7
|
+
class Card < BaseNode
|
8
|
+
def initialize(title: nil, **attributes)
|
9
|
+
super(attributes)
|
10
|
+
@attributes[:title] = title
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class MetricCard < BaseNode
|
14
|
+
def initialize(title: nil, label: nil, value: nil, **attributes)
|
15
|
+
super(attributes)
|
16
|
+
@attributes[:title] = title || label
|
17
|
+
@attributes[:value] = value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
class ActionBar < BaseNode; end
|
21
|
+
class Panel < BaseNode
|
22
|
+
def initialize(title, **attributes)
|
23
|
+
super(attributes)
|
24
|
+
@attributes[:title] = title
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class Badge < BaseNode
|
28
|
+
def initialize(text, **attributes)
|
29
|
+
super(attributes)
|
30
|
+
@attributes[:text] = text
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class RelatedResources < BaseNode
|
34
|
+
def initialize(resource_name, **attributes)
|
35
|
+
super(attributes)
|
36
|
+
@attributes[:resource_name] = resource_name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
class Action < BaseNode
|
40
|
+
def initialize(type, **attributes)
|
41
|
+
super(attributes)
|
42
|
+
@attributes[:type] = type
|
43
|
+
end
|
44
|
+
end
|
45
|
+
class Dropdown < BaseNode; end
|
46
|
+
|
47
|
+
# Form page nodes
|
48
|
+
class Fieldset < BaseNode
|
49
|
+
def initialize(legend = nil, **attributes)
|
50
|
+
super(attributes)
|
51
|
+
@attributes[:legend] = legend
|
52
|
+
end
|
53
|
+
end
|
54
|
+
class FormActions < BaseNode; end
|
55
|
+
class InlineFields < BaseNode; end
|
56
|
+
class ConditionalFields < BaseNode; end
|
57
|
+
class NestedFields < BaseNode
|
58
|
+
def initialize(association, **attributes)
|
59
|
+
super(attributes)
|
60
|
+
@attributes[:association] = association
|
61
|
+
end
|
62
|
+
end
|
63
|
+
class HelpText < BaseNode
|
64
|
+
def initialize(text, **attributes)
|
65
|
+
super(attributes)
|
66
|
+
@attributes[:text] = text
|
67
|
+
end
|
68
|
+
end
|
69
|
+
class ErrorsSummary < BaseNode; end
|
70
|
+
class FormAction < BaseNode
|
71
|
+
def initialize(type, **attributes)
|
72
|
+
super(attributes)
|
73
|
+
@attributes[:type] = type
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Additional nodes needed by DSL
|
78
|
+
class Content < BaseNode
|
79
|
+
def initialize(attributes = {}, block: nil)
|
80
|
+
@block = block
|
81
|
+
super(attributes)
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :block
|
85
|
+
end
|
86
|
+
|
87
|
+
class Row < BaseNode
|
88
|
+
def initialize(attributes = {})
|
89
|
+
super(attributes)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Column < BaseNode
|
94
|
+
def initialize(attributes = {})
|
95
|
+
super(attributes)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Heading < BaseNode
|
100
|
+
def initialize(text, attributes = {})
|
101
|
+
@text = text
|
102
|
+
super(attributes)
|
103
|
+
end
|
104
|
+
|
105
|
+
attr_reader :text
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Layouts
|
3
|
+
module Nodes
|
4
|
+
# Individual tab within a tabs container
|
5
|
+
class Tab < BaseNode
|
6
|
+
def initialize(name, attributes = {})
|
7
|
+
super(attributes)
|
8
|
+
@attributes[:name] = name.to_s
|
9
|
+
@attributes[:label] ||= name.to_s.humanize
|
10
|
+
@attributes[:icon] ||= nil
|
11
|
+
@attributes[:badge] ||= nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
@attributes[:name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def label
|
19
|
+
@attributes[:label]
|
20
|
+
end
|
21
|
+
|
22
|
+
def icon
|
23
|
+
@attributes[:icon]
|
24
|
+
end
|
25
|
+
|
26
|
+
def badge
|
27
|
+
@attributes[:badge]
|
28
|
+
end
|
29
|
+
|
30
|
+
def tab_id
|
31
|
+
"tab-#{name.parameterize}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def panel_id
|
35
|
+
"panel-#{name.parameterize}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Layouts
|
3
|
+
module Nodes
|
4
|
+
# Container for tab nodes
|
5
|
+
class Tabs < BaseNode
|
6
|
+
def initialize(attributes = {})
|
7
|
+
super
|
8
|
+
@attributes[:type] ||= :horizontal
|
9
|
+
@attributes[:id] ||= "tabs-#{SecureRandom.hex(4)}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def horizontal?
|
13
|
+
@attributes[:type] == :horizontal
|
14
|
+
end
|
15
|
+
|
16
|
+
def vertical?
|
17
|
+
@attributes[:type] == :vertical
|
18
|
+
end
|
19
|
+
|
20
|
+
def tabs_id
|
21
|
+
@attributes[:id]
|
22
|
+
end
|
23
|
+
|
24
|
+
def active_tab_index
|
25
|
+
@attributes[:active_tab] || 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get all tab children
|
29
|
+
def tabs
|
30
|
+
@children.select { |child| child.is_a?(Tab) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Find tab by name
|
34
|
+
def find_tab(name)
|
35
|
+
tabs.find { |tab| tab[:name] == name.to_s }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Layouts
|
3
|
+
# Registry for custom components
|
4
|
+
mattr_accessor :registered_components, default: {}
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Register a custom component for use in DSL
|
8
|
+
def register_component(name, component_class)
|
9
|
+
registered_components[name.to_sym] = component_class
|
10
|
+
end
|
11
|
+
|
12
|
+
# Unregister a component
|
13
|
+
def unregister_component(name)
|
14
|
+
registered_components.delete(name.to_sym)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Clear all registered components
|
18
|
+
def clear_registered_components!
|
19
|
+
registered_components.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if component is registered
|
23
|
+
def component_registered?(name)
|
24
|
+
registered_components.key?(name.to_sym)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -74,8 +74,6 @@ module EasyAdmin
|
|
74
74
|
|
75
75
|
# Seed permissions into database
|
76
76
|
def seed_permissions!
|
77
|
-
Rails.logger.info "🔐 Seeding EasyAdmin resource permissions..."
|
78
|
-
|
79
77
|
permissions_data = discover_all_permissions
|
80
78
|
created_count = 0
|
81
79
|
|
@@ -90,8 +88,6 @@ module EasyAdmin
|
|
90
88
|
|
91
89
|
created_count += 1 if permission.saved_change_to_id?
|
92
90
|
end
|
93
|
-
|
94
|
-
Rails.logger.info "✅ Seeded #{created_count} new permissions (#{permissions_data.size} total)"
|
95
91
|
permissions_data.size
|
96
92
|
end
|
97
93
|
|
@@ -228,4 +224,4 @@ module EasyAdmin
|
|
228
224
|
end
|
229
225
|
end
|
230
226
|
end
|
231
|
-
end
|
227
|
+
end
|
@@ -10,8 +10,8 @@ module EasyAdmin
|
|
10
10
|
include EasyAdmin::ResourceModules::Configuration
|
11
11
|
include EasyAdmin::ResourceModules::DSL
|
12
12
|
include EasyAdmin::ResourceModules::FieldRegistry
|
13
|
-
include EasyAdmin::ResourceModules::LayoutBuilder
|
14
13
|
include EasyAdmin::ResourceModules::ScopeManager
|
14
|
+
include EasyAdmin::Layouts::DSL
|
15
15
|
|
16
16
|
# Class attributes for core functionality
|
17
17
|
class_attribute :resource_name, :custom_title
|
@@ -28,8 +28,8 @@ module EasyAdmin
|
|
28
28
|
self.resource_name = name.demodulize.gsub(/Resource$/, '')
|
29
29
|
initialize_configurations
|
30
30
|
initialize_field_registry
|
31
|
-
initialize_layout_builder
|
32
31
|
initialize_scope_manager
|
32
|
+
# Layout initialization is now handled by EasyAdmin::Layouts::DSL
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -77,17 +77,8 @@ module EasyAdmin
|
|
77
77
|
**options.except(:confirmation_label))
|
78
78
|
end
|
79
79
|
|
80
|
-
#
|
81
|
-
|
82
|
-
form_builder = EasyAdmin::ResourceModules::FormBuilder.new(self)
|
83
|
-
form_builder.instance_eval(&block)
|
84
|
-
end
|
85
|
-
|
86
|
-
# Show layout building DSL
|
87
|
-
def show(&block)
|
88
|
-
show_builder = EasyAdmin::ResourceModules::ShowBuilder.new(self)
|
89
|
-
show_builder.instance_eval(&block)
|
90
|
-
end
|
80
|
+
# Layout DSL is now handled by EasyAdmin::Layouts::DSL module
|
81
|
+
# See lib/easy_admin/layouts/dsl.rb for implementation
|
91
82
|
|
92
83
|
# Batch Actions DSL
|
93
84
|
def enable_batch_actions
|
@@ -55,7 +55,13 @@ module EasyAdmin
|
|
55
55
|
|
56
56
|
# Field categorization methods
|
57
57
|
def index_fields
|
58
|
-
|
58
|
+
# If custom index layout is defined, extract fields from the AST
|
59
|
+
if has_custom_index_layout?
|
60
|
+
extract_index_fields_from_layout
|
61
|
+
else
|
62
|
+
# Default behavior: return fields excluding certain types
|
63
|
+
fields_config.select { |field| ![:text, :has_many, :file].include?(field[:type]) }
|
64
|
+
end
|
59
65
|
end
|
60
66
|
|
61
67
|
def sortable_fields
|
@@ -183,7 +189,57 @@ module EasyAdmin
|
|
183
189
|
def association_fields_count
|
184
190
|
association_fields.count
|
185
191
|
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
# Extract fields from custom index layout AST
|
196
|
+
def extract_index_fields_from_layout
|
197
|
+
return [] unless index_layout_content
|
198
|
+
|
199
|
+
# Extract field nodes from the index layout AST
|
200
|
+
extract_field_configs_from_ast_node(index_layout_content)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Helper method to recursively extract field configs from AST nodes
|
204
|
+
def extract_field_configs_from_ast_node(node)
|
205
|
+
field_configs = []
|
206
|
+
|
207
|
+
if node.is_a?(EasyAdmin::Layouts::Nodes::FieldNode)
|
208
|
+
# For index layouts, look up the field config from registered fields
|
209
|
+
base_field_config = find_field_config(node.field_name)
|
210
|
+
|
211
|
+
if base_field_config
|
212
|
+
# Use the registered field config with any node-specific overrides
|
213
|
+
# Only merge non-nil values from node attributes to preserve base config
|
214
|
+
field_config = base_field_config.dup
|
215
|
+
node.attributes.each do |key, value|
|
216
|
+
field_config[key] = value unless value.nil?
|
217
|
+
end
|
218
|
+
field_configs << field_config
|
219
|
+
else
|
220
|
+
field_config = {
|
221
|
+
name: node.field_name,
|
222
|
+
type: :string,
|
223
|
+
label: node.field_name.to_s.humanize,
|
224
|
+
sortable: true,
|
225
|
+
searchable: false,
|
226
|
+
filterable: false,
|
227
|
+
readonly: false
|
228
|
+
}.merge(node.attributes)
|
229
|
+
field_configs << field_config
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Recursively process child nodes
|
234
|
+
if node.respond_to?(:children) && node.children
|
235
|
+
node.children.each do |child_node|
|
236
|
+
field_configs += extract_field_configs_from_ast_node(child_node)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
field_configs
|
241
|
+
end
|
186
242
|
end
|
187
243
|
end
|
188
244
|
end
|
189
|
-
end
|
245
|
+
end
|
data/lib/easy_admin/resource.rb
CHANGED
@@ -1,14 +1,5 @@
|
|
1
1
|
module EasyAdmin
|
2
2
|
class Resource
|
3
3
|
include EasyAdmin::ResourceModules::Base
|
4
|
-
|
5
|
-
# Legacy methods - kept for backward compatibility
|
6
|
-
# These delegate to the appropriate modules
|
7
|
-
|
8
|
-
# Deprecated classes - kept for backward compatibility
|
9
|
-
# Use EasyAdmin::ResourceModules::FormBuilder instead
|
10
|
-
FormBuilder = EasyAdmin::ResourceModules::FormBuilder
|
11
|
-
ShowBuilder = EasyAdmin::ResourceModules::ShowBuilder
|
12
|
-
ChartBuilder = EasyAdmin::ResourceModules::ChartBuilder
|
13
4
|
end
|
14
5
|
end
|
@@ -4,8 +4,25 @@
|
|
4
4
|
require_relative 'resource/configuration'
|
5
5
|
require_relative 'resource/field_registry'
|
6
6
|
require_relative 'resource/scope_manager'
|
7
|
-
require_relative 'resource/layout_builder'
|
8
|
-
require_relative 'resource/form_builder'
|
9
|
-
require_relative 'resource/show_builder'
|
10
7
|
require_relative 'resource/dsl'
|
11
|
-
require_relative 'resource/base'
|
8
|
+
require_relative 'resource/base'
|
9
|
+
|
10
|
+
# Layout system modules
|
11
|
+
require_relative 'layouts'
|
12
|
+
require_relative 'layouts/nodes/base_node'
|
13
|
+
require_relative 'layouts/nodes/root'
|
14
|
+
require_relative 'layouts/nodes/tabs'
|
15
|
+
require_relative 'layouts/nodes/tab'
|
16
|
+
require_relative 'layouts/nodes/field_node'
|
17
|
+
require_relative 'layouts/nodes/render_node'
|
18
|
+
require_relative 'layouts/nodes/section'
|
19
|
+
require_relative 'layouts/nodes/grid'
|
20
|
+
require_relative 'layouts/nodes/divider'
|
21
|
+
require_relative 'layouts/nodes/spacer'
|
22
|
+
require_relative 'layouts/nodes/stubs'
|
23
|
+
require_relative 'layouts/layout_context'
|
24
|
+
require_relative 'layouts/builders/base_layout_builder'
|
25
|
+
require_relative 'layouts/builders/show_layout_builder'
|
26
|
+
require_relative 'layouts/builders/form_layout_builder'
|
27
|
+
require_relative 'layouts/builders/index_layout_builder'
|
28
|
+
require_relative 'layouts/dsl'
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module TwoFactorAuthentication
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Check if required gems are available
|
6
|
+
def self.available?
|
7
|
+
@available ||= begin
|
8
|
+
require 'rotp'
|
9
|
+
require 'rqrcode'
|
10
|
+
true
|
11
|
+
rescue LoadError
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
included do
|
17
|
+
# Only add validations and callbacks if 2FA gems are available
|
18
|
+
if EasyAdmin::TwoFactorAuthentication.available?
|
19
|
+
validates :otp_secret, presence: true, if: :otp_required_for_login?
|
20
|
+
validates :otp_secret, uniqueness: true, allow_blank: true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def two_factor_available?
|
25
|
+
EasyAdmin::TwoFactorAuthentication.available?
|
26
|
+
end
|
27
|
+
|
28
|
+
def two_factor_enabled?
|
29
|
+
two_factor_available? && otp_required_for_login? && otp_secret.present?
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_otp_secret!
|
33
|
+
return false unless two_factor_available?
|
34
|
+
|
35
|
+
require 'rotp'
|
36
|
+
self.otp_secret = ROTP::Base32.random
|
37
|
+
save!
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_otp
|
41
|
+
return nil unless two_factor_available? && otp_secret.present?
|
42
|
+
|
43
|
+
require 'rotp'
|
44
|
+
ROTP::TOTP.new(otp_secret, issuer: "EasyAdmin").now
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_and_consume_otp!(token)
|
48
|
+
return false unless two_factor_available? && otp_secret.present?
|
49
|
+
return false if token.blank?
|
50
|
+
|
51
|
+
require 'rotp'
|
52
|
+
|
53
|
+
totp = ROTP::TOTP.new(otp_secret, issuer: "EasyAdmin")
|
54
|
+
last_otp_at_timestamp = last_otp_at&.to_i
|
55
|
+
|
56
|
+
# Verify with 30-second drift tolerance and replay protection
|
57
|
+
if totp.verify(token.to_s, drift_behind: 30, drift_ahead: 30, after: last_otp_at_timestamp)
|
58
|
+
touch(:last_otp_at)
|
59
|
+
true
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_backup_code!(code)
|
66
|
+
return false unless two_factor_available?
|
67
|
+
return false if code.blank? || otp_backup_codes.blank?
|
68
|
+
|
69
|
+
normalized_code = code.to_s.upcase.strip
|
70
|
+
|
71
|
+
if otp_backup_codes.include?(normalized_code)
|
72
|
+
invalidate_backup_code!(normalized_code)
|
73
|
+
true
|
74
|
+
else
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_backup_codes!
|
80
|
+
return false unless two_factor_available?
|
81
|
+
|
82
|
+
# Generate 10 backup codes (8 characters each)
|
83
|
+
codes = 10.times.map { SecureRandom.hex(4).upcase }
|
84
|
+
self.otp_backup_codes = codes
|
85
|
+
save!
|
86
|
+
codes
|
87
|
+
end
|
88
|
+
|
89
|
+
def invalidate_backup_code!(code)
|
90
|
+
return false unless two_factor_available?
|
91
|
+
|
92
|
+
normalized_code = code.to_s.upcase.strip
|
93
|
+
self.otp_backup_codes = otp_backup_codes.reject { |c| c == normalized_code }
|
94
|
+
save!
|
95
|
+
end
|
96
|
+
|
97
|
+
def backup_codes_remaining
|
98
|
+
two_factor_available? ? (otp_backup_codes&.length || 0) : 0
|
99
|
+
end
|
100
|
+
|
101
|
+
def provisioning_uri
|
102
|
+
return nil unless two_factor_available? && otp_secret.present?
|
103
|
+
|
104
|
+
require 'rotp'
|
105
|
+
ROTP::TOTP.new(otp_secret, issuer: "EasyAdmin").provisioning_uri(email)
|
106
|
+
end
|
107
|
+
|
108
|
+
def qr_code_svg(size: 200)
|
109
|
+
return nil unless two_factor_available?
|
110
|
+
|
111
|
+
uri = provisioning_uri
|
112
|
+
return nil if uri.blank?
|
113
|
+
|
114
|
+
require 'rqrcode'
|
115
|
+
|
116
|
+
qr_code = RQRCode::QRCode.new(uri)
|
117
|
+
qr_code.as_svg(
|
118
|
+
viewbox: true,
|
119
|
+
module_size: 4,
|
120
|
+
standalone: true,
|
121
|
+
use_path: true
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
def enable_two_factor!
|
126
|
+
return false unless two_factor_available? && otp_secret.present?
|
127
|
+
|
128
|
+
update!(otp_required_for_login: true)
|
129
|
+
end
|
130
|
+
|
131
|
+
def disable_two_factor!
|
132
|
+
update!(
|
133
|
+
otp_required_for_login: false,
|
134
|
+
otp_secret: nil,
|
135
|
+
otp_backup_codes: nil,
|
136
|
+
last_otp_at: nil
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Check if user needs 2FA based on role requirements
|
141
|
+
def two_factor_required?
|
142
|
+
return false unless two_factor_available?
|
143
|
+
|
144
|
+
# Check if role requires 2FA (if role system exists)
|
145
|
+
if respond_to?(:role) && role.respond_to?(:require_two_factor?)
|
146
|
+
role.require_two_factor?
|
147
|
+
else
|
148
|
+
false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def should_enable_two_factor?
|
153
|
+
two_factor_required? && !two_factor_enabled?
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/easy_admin/version.rb
CHANGED
@@ -24,11 +24,6 @@ module EasyAdmin
|
|
24
24
|
def create_migrations
|
25
25
|
migration_template 'migrations/create_permission_tables.rb',
|
26
26
|
'db/migrate/create_easy_admin_permission_tables.rb'
|
27
|
-
|
28
|
-
if options[:user_model] != 'User' || has_user_model_changes?
|
29
|
-
migration_template 'migrations/update_users_for_permissions.rb',
|
30
|
-
'db/migrate/update_users_for_easy_admin_permissions.rb'
|
31
|
-
end
|
32
27
|
end
|
33
28
|
|
34
29
|
def create_initializer
|
@@ -79,11 +74,6 @@ module EasyAdmin
|
|
79
74
|
options[:contexts]
|
80
75
|
end
|
81
76
|
|
82
|
-
def has_user_model_changes?
|
83
|
-
# Check if we need to add permissions_cache column
|
84
|
-
!File.exist?("app/models/#{options[:user_model].underscore}.rb") ||
|
85
|
-
!File.read("app/models/#{options[:user_model].underscore}.rb").include?('permissions_cache')
|
86
|
-
end
|
87
77
|
end
|
88
78
|
end
|
89
79
|
end
|