easy-admin-rails 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +95 -0
  3. data/app/assets/builds/easy_admin.base.js.map +3 -3
  4. data/app/assets/builds/easy_admin.css +226 -0
  5. data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
  6. data/app/components/easy_admin/form_layout_component.rb +553 -0
  7. data/app/components/easy_admin/navbar_component.rb +19 -4
  8. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
  9. data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
  10. data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
  11. data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
  12. data/app/components/easy_admin/profile/settings_component.rb +103 -0
  13. data/app/components/easy_admin/show_layout_component.rb +694 -24
  14. data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
  15. data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
  16. data/app/components/easy_admin/two_factor/status_component.rb +92 -0
  17. data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
  18. data/app/controllers/easy_admin/application_controller.rb +10 -5
  19. data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
  20. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
  21. data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
  22. data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
  23. data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
  24. data/app/controllers/easy_admin/profile_controller.rb +25 -0
  25. data/app/controllers/easy_admin/resources_controller.rb +1 -5
  26. data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
  27. data/app/controllers/easy_admin/sessions_controller.rb +107 -1
  28. data/app/helpers/easy_admin/fields_helper.rb +8 -22
  29. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
  30. data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
  31. data/app/javascript/easy_admin/controllers.js +3 -1
  32. data/app/models/easy_admin/admin_user.rb +3 -0
  33. data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
  34. data/app/views/easy_admin/profile/change_password.html.erb +24 -0
  35. data/app/views/easy_admin/profile/index.html.erb +1 -0
  36. data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
  37. data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
  38. data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
  39. data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
  40. data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
  41. data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
  42. data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
  43. data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
  44. data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
  45. data/app/views/easy_admin/resources/edit.html.erb +2 -2
  46. data/app/views/easy_admin/resources/new.html.erb +2 -2
  47. data/app/views/easy_admin/resources/show.html.erb +3 -1
  48. data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
  49. data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
  50. data/config/routes.rb +20 -1
  51. data/lib/easy-admin-rails.rb +1 -0
  52. data/lib/easy_admin/field.rb +3 -2
  53. data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
  54. data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
  55. data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
  56. data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
  57. data/lib/easy_admin/layouts/dsl.rb +200 -0
  58. data/lib/easy_admin/layouts/layout_context.rb +189 -0
  59. data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
  60. data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
  61. data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
  62. data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
  63. data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
  64. data/lib/easy_admin/layouts/nodes/root.rb +25 -0
  65. data/lib/easy_admin/layouts/nodes/section.rb +46 -0
  66. data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
  67. data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
  68. data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
  69. data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
  70. data/lib/easy_admin/layouts.rb +28 -0
  71. data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
  72. data/lib/easy_admin/resource/base.rb +2 -2
  73. data/lib/easy_admin/resource/dsl.rb +2 -11
  74. data/lib/easy_admin/resource/field_registry.rb +58 -2
  75. data/lib/easy_admin/resource.rb +0 -9
  76. data/lib/easy_admin/resource_modules.rb +21 -4
  77. data/lib/easy_admin/two_factor_authentication.rb +156 -0
  78. data/lib/easy_admin/version.rb +1 -1
  79. data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
  80. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
  81. data/lib/generators/easy_admin/two_factor/templates/README +29 -0
  82. data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
  83. data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
  84. metadata +49 -9
  85. data/lib/easy_admin/resource/form_builder.rb +0 -123
  86. data/lib/easy_admin/resource/layout_builder.rb +0 -249
  87. data/lib/easy_admin/resource/show_builder.rb +0 -359
  88. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
  89. data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
  90. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
  91. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -0,0 +1,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
- # Form building DSL
81
- def form(&block)
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
- fields_config.select { |field| ![:text, :has_many, :file].include?(field[:type]) }
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module EasyAdmin
2
- VERSION = "0.2.5"
2
+ VERSION = "0.2.7"
3
3
  end
@@ -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