rodauth-tools 0.3.0
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 +7 -0
- data/.gitignore +93 -0
- data/.gitlint +9 -0
- data/.markdownlint-cli2.jsonc +26 -0
- data/.pre-commit-config.yaml +46 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +243 -0
- data/CHANGELOG.md +81 -0
- data/CLAUDE.md +262 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +111 -0
- data/Gemfile +35 -0
- data/Gemfile.lock +356 -0
- data/LICENSE.txt +21 -0
- data/README.md +339 -0
- data/Rakefile +8 -0
- data/lib/rodauth/features/external_identity.rb +946 -0
- data/lib/rodauth/features/hmac_secret_guard.rb +119 -0
- data/lib/rodauth/features/jwt_secret_guard.rb +120 -0
- data/lib/rodauth/features/table_guard.rb +937 -0
- data/lib/rodauth/sequel_generator.rb +531 -0
- data/lib/rodauth/table_inspector.rb +124 -0
- data/lib/rodauth/template_inspector.rb +134 -0
- data/lib/rodauth/tools/console_helpers.rb +158 -0
- data/lib/rodauth/tools/migration/sequel/account_expiration.erb +9 -0
- data/lib/rodauth/tools/migration/sequel/active_sessions.erb +10 -0
- data/lib/rodauth/tools/migration/sequel/audit_logging.erb +12 -0
- data/lib/rodauth/tools/migration/sequel/base.erb +41 -0
- data/lib/rodauth/tools/migration/sequel/disallow_password_reuse.erb +8 -0
- data/lib/rodauth/tools/migration/sequel/email_auth.erb +17 -0
- data/lib/rodauth/tools/migration/sequel/jwt_refresh.erb +18 -0
- data/lib/rodauth/tools/migration/sequel/lockout.erb +21 -0
- data/lib/rodauth/tools/migration/sequel/otp.erb +9 -0
- data/lib/rodauth/tools/migration/sequel/otp_unlock.erb +8 -0
- data/lib/rodauth/tools/migration/sequel/password_expiration.erb +7 -0
- data/lib/rodauth/tools/migration/sequel/recovery_codes.erb +8 -0
- data/lib/rodauth/tools/migration/sequel/remember.erb +16 -0
- data/lib/rodauth/tools/migration/sequel/reset_password.erb +17 -0
- data/lib/rodauth/tools/migration/sequel/single_session.erb +7 -0
- data/lib/rodauth/tools/migration/sequel/sms_codes.erb +10 -0
- data/lib/rodauth/tools/migration/sequel/verify_account.erb +9 -0
- data/lib/rodauth/tools/migration/sequel/verify_login_change.erb +17 -0
- data/lib/rodauth/tools/migration/sequel/webauthn.erb +15 -0
- data/lib/rodauth/tools/migration.rb +188 -0
- data/lib/rodauth/tools/version.rb +9 -0
- data/lib/rodauth/tools.rb +29 -0
- data/package-lock.json +500 -0
- data/package.json +11 -0
- data/rodauth-tools.gemspec +40 -0
- metadata +136 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
# lib/rodauth/features/external_identity.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Enable with:
|
|
7
|
+
# enable :external_identity
|
|
8
|
+
#
|
|
9
|
+
# Configuration (Layer 1 - Basic):
|
|
10
|
+
# external_identity_column :stripe_customer_id
|
|
11
|
+
# external_identity_column :redis_uuid, method_name: :redis_session_key
|
|
12
|
+
# external_identity_on_conflict :warn # :error, :warn, :skip
|
|
13
|
+
# external_identity_check_columns true # true (default), false, or :autocreate
|
|
14
|
+
#
|
|
15
|
+
# Configuration (Layer 2 - Extended Features):
|
|
16
|
+
# external_identity_column :stripe_customer_id,
|
|
17
|
+
# before_create_account: -> { Stripe::Customer.create(email: account[:email]).id },
|
|
18
|
+
# formatter: -> (v) { v.to_s.strip.downcase },
|
|
19
|
+
# validator: -> (v) { v.start_with?('cus_') },
|
|
20
|
+
# verifier: -> (id) { Stripe::Customer.retrieve(id) && !customer.deleted? },
|
|
21
|
+
# handshake: -> (id, token) { session[:oauth_state] == token }
|
|
22
|
+
#
|
|
23
|
+
# Usage:
|
|
24
|
+
# rodauth.stripe_customer_id # Auto-generated helper method
|
|
25
|
+
# rodauth.redis_uuid # Auto-generated helper method
|
|
26
|
+
#
|
|
27
|
+
# Introspection:
|
|
28
|
+
# rodauth.external_identity_column_list # [:stripe_customer_id, :redis_uuid]
|
|
29
|
+
# rodauth.external_identity_column_config(:stripe_customer_id) # {...}
|
|
30
|
+
# rodauth.external_identity_status # Complete debug info
|
|
31
|
+
#
|
|
32
|
+
# Example:
|
|
33
|
+
#
|
|
34
|
+
# plugin :rodauth do
|
|
35
|
+
# enable :login, :logout, :external_identity
|
|
36
|
+
#
|
|
37
|
+
# # Declare external identity columns
|
|
38
|
+
# external_identity_column :stripe_customer_id
|
|
39
|
+
# external_identity_column :redis_uuid
|
|
40
|
+
#
|
|
41
|
+
# # Custom method name
|
|
42
|
+
# external_identity_column :redis_key, method_name: :redis_session_key
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# # In your app
|
|
46
|
+
# rodauth.stripe_customer_id # => "cus_abc123"
|
|
47
|
+
# rodauth.redis_uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
48
|
+
|
|
49
|
+
module Rodauth
|
|
50
|
+
Feature.define(:external_identity, :ExternalIdentity) do
|
|
51
|
+
# Configuration methods
|
|
52
|
+
auth_value_method :external_identity_on_conflict, :error
|
|
53
|
+
auth_value_method :external_identity_check_columns, true
|
|
54
|
+
|
|
55
|
+
# Public API methods for introspection
|
|
56
|
+
auth_methods(
|
|
57
|
+
:external_identity_column_list,
|
|
58
|
+
:external_identity_column_config,
|
|
59
|
+
:external_identity_helper_methods,
|
|
60
|
+
:external_identity_column?,
|
|
61
|
+
:external_identity_status
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Layer 2: Validation methods
|
|
65
|
+
auth_methods(
|
|
66
|
+
:validate_external_identity,
|
|
67
|
+
:validate_all_external_identities
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Layer 2: Verification methods
|
|
71
|
+
auth_methods(
|
|
72
|
+
:verify_external_identity,
|
|
73
|
+
:verify_all_external_identities
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Layer 2: Handshake methods
|
|
77
|
+
auth_methods(
|
|
78
|
+
:verify_handshake
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Use auth_cached_method for column configuration
|
|
82
|
+
# This ensures it works in both post_configure and runtime
|
|
83
|
+
auth_cached_method :external_identity_columns_config
|
|
84
|
+
|
|
85
|
+
# Add external_identity_column method to Configuration class
|
|
86
|
+
# This makes it available during the configuration block
|
|
87
|
+
configuration_module_eval do
|
|
88
|
+
# Declare an external identity column with optional lifecycle callbacks and database options
|
|
89
|
+
#
|
|
90
|
+
# Creates a helper method to access the column value and automatically includes
|
|
91
|
+
# it in account_select (unless disabled). Supports validation, formatting,
|
|
92
|
+
# verification, and lifecycle hooks.
|
|
93
|
+
#
|
|
94
|
+
# @param column [Symbol] Database column name (required)
|
|
95
|
+
# @param options [Hash] Configuration options
|
|
96
|
+
#
|
|
97
|
+
# @option options [Symbol] :method_name
|
|
98
|
+
# Custom method name for accessing the column (default: same as column name)
|
|
99
|
+
#
|
|
100
|
+
# @option options [Boolean] :include_in_select (true)
|
|
101
|
+
# Whether to automatically include in account_select
|
|
102
|
+
#
|
|
103
|
+
# @option options [Proc] :before_create_account
|
|
104
|
+
# Callback to generate value before account creation. Runs during
|
|
105
|
+
# before_create_account hook. Value will be formatted and validated
|
|
106
|
+
# if those callbacks are provided. Block is evaluated in Rodauth instance context.
|
|
107
|
+
#
|
|
108
|
+
# @option options [Proc] :after_create_account
|
|
109
|
+
# Callback to execute after account creation. Receives the column value
|
|
110
|
+
# as argument. Block is evaluated in Rodauth instance context.
|
|
111
|
+
# Useful for provisioning resources or sending notifications.
|
|
112
|
+
#
|
|
113
|
+
# @option options [Proc] :formatter
|
|
114
|
+
# Transform values (e.g., strip whitespace, normalize case).
|
|
115
|
+
# Block receives value and should return formatted value.
|
|
116
|
+
#
|
|
117
|
+
# @option options [Proc] :validator
|
|
118
|
+
# Validate values (must return truthy for valid). Block receives value
|
|
119
|
+
# and should return true/false. Raises ArgumentError if validation fails.
|
|
120
|
+
#
|
|
121
|
+
# @option options [Proc] :verifier
|
|
122
|
+
# Health check callback to verify external identity is still valid.
|
|
123
|
+
# Non-critical - returns false on failure rather than raising.
|
|
124
|
+
#
|
|
125
|
+
# @option options [Proc] :handshake
|
|
126
|
+
# Security-critical verification callback (e.g., OAuth state verification).
|
|
127
|
+
# Must return truthy. Raises on failure. Use for callbacks where failure
|
|
128
|
+
# indicates a security issue.
|
|
129
|
+
#
|
|
130
|
+
# @option options [Symbol, Class] :type (:String)
|
|
131
|
+
# Sequel column type for migration generation. Valid values:
|
|
132
|
+
# :String, :Integer, :Bignum, :Boolean, :Date, :DateTime, :Time, :Text
|
|
133
|
+
#
|
|
134
|
+
# @option options [Boolean] :null (true)
|
|
135
|
+
# Whether column allows NULL values
|
|
136
|
+
#
|
|
137
|
+
# @option options [Object] :default (nil)
|
|
138
|
+
# Default value for column. Can be a literal value or Proc for dynamic defaults.
|
|
139
|
+
#
|
|
140
|
+
# @option options [Boolean] :unique (false)
|
|
141
|
+
# Whether column has unique constraint
|
|
142
|
+
#
|
|
143
|
+
# @option options [Integer] :size
|
|
144
|
+
# Maximum size for String/varchar columns (e.g., 255)
|
|
145
|
+
#
|
|
146
|
+
# @option options [Boolean, Hash] :index (false)
|
|
147
|
+
# Whether to create index. Use true for simple index,
|
|
148
|
+
# or Hash for index options like { unique: true, name: :custom_name }
|
|
149
|
+
#
|
|
150
|
+
# @option options [Hash] :sequel
|
|
151
|
+
# Nested hash for Sequel-specific options (alternative to flat options).
|
|
152
|
+
# Keys: :type, :null, :default, :unique, :size, :index
|
|
153
|
+
#
|
|
154
|
+
# @example Basic usage
|
|
155
|
+
# external_identity_column :stripe_customer_id
|
|
156
|
+
#
|
|
157
|
+
# @example With lifecycle callbacks
|
|
158
|
+
# external_identity_column :stripe_customer_id,
|
|
159
|
+
# before_create_account: -> { Stripe::Customer.create(email: account[:email]).id },
|
|
160
|
+
# after_create_account: -> (id) { StripeMailer.welcome(id).deliver }
|
|
161
|
+
#
|
|
162
|
+
# @example With validation and formatting
|
|
163
|
+
# external_identity_column :api_key,
|
|
164
|
+
# formatter: -> (v) { v.to_s.strip.downcase },
|
|
165
|
+
# validator: -> (v) { v =~ /^[a-z0-9]{32}$/ }
|
|
166
|
+
#
|
|
167
|
+
# @example With database constraints
|
|
168
|
+
# external_identity_column :token,
|
|
169
|
+
# type: String,
|
|
170
|
+
# null: false,
|
|
171
|
+
# unique: true,
|
|
172
|
+
# size: 64,
|
|
173
|
+
# index: true
|
|
174
|
+
#
|
|
175
|
+
# @example With nested Sequel options
|
|
176
|
+
# external_identity_column :session_id,
|
|
177
|
+
# sequel: {
|
|
178
|
+
# type: String,
|
|
179
|
+
# null: false,
|
|
180
|
+
# default: -> { SecureRandom.hex(32) },
|
|
181
|
+
# unique: true,
|
|
182
|
+
# index: { unique: true, name: :idx_session_id }
|
|
183
|
+
# }
|
|
184
|
+
#
|
|
185
|
+
# @example Complete example with all options
|
|
186
|
+
# external_identity_column :external_user_id,
|
|
187
|
+
# method_name: :external_id,
|
|
188
|
+
# include_in_select: true,
|
|
189
|
+
# type: String,
|
|
190
|
+
# null: false,
|
|
191
|
+
# unique: true,
|
|
192
|
+
# size: 100,
|
|
193
|
+
# index: true,
|
|
194
|
+
# before_create_account: -> { ExternalService.create_user(account[:email]) },
|
|
195
|
+
# after_create_account: -> (id) { Logger.info("Created external user: #{id}") },
|
|
196
|
+
# formatter: -> (v) { v.to_s.strip },
|
|
197
|
+
# validator: -> (v) { v.present? && v.length <= 100 }
|
|
198
|
+
#
|
|
199
|
+
# @raise [ArgumentError] If column is not a Symbol
|
|
200
|
+
# @raise [ArgumentError] If column is not a valid Ruby identifier
|
|
201
|
+
# @raise [ArgumentError] If column is already declared
|
|
202
|
+
# @raise [ArgumentError] If method_name is not a valid Ruby identifier
|
|
203
|
+
#
|
|
204
|
+
# @return [nil]
|
|
205
|
+
def external_identity_column(column, **options)
|
|
206
|
+
# Validate column is a symbol
|
|
207
|
+
unless column.is_a?(Symbol)
|
|
208
|
+
raise ArgumentError, "external_identity_column must be a Symbol, got #{column.class}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Validate column is a valid Ruby identifier
|
|
212
|
+
unless column.to_s =~ /^[a-z_][a-z0-9_]*$/i
|
|
213
|
+
raise ArgumentError, "external_identity_column must be a valid Ruby identifier: #{column}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Default method name is the column name itself
|
|
217
|
+
method_name = options[:method_name] || column
|
|
218
|
+
|
|
219
|
+
# Validate method name is valid Ruby identifier
|
|
220
|
+
unless method_name.to_s =~ /^[a-z_][a-z0-9_]*[?!=]?$/i
|
|
221
|
+
raise ArgumentError, "Method name must be a valid Ruby identifier: #{method_name}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Store configuration on the Auth class
|
|
225
|
+
# Use class instance variable to store per-class configuration
|
|
226
|
+
unless @auth.instance_variable_get(:@_external_identity_columns)
|
|
227
|
+
@auth.instance_variable_set(:@_external_identity_columns,
|
|
228
|
+
{})
|
|
229
|
+
end
|
|
230
|
+
columns = @auth.instance_variable_get(:@_external_identity_columns)
|
|
231
|
+
|
|
232
|
+
# Check for duplicate declarations using column name as key
|
|
233
|
+
raise ArgumentError, "external_identity_column :#{column} already declared" if columns.key?(column)
|
|
234
|
+
|
|
235
|
+
# Store configuration (using column as both key and value)
|
|
236
|
+
columns[column] = {
|
|
237
|
+
column: column,
|
|
238
|
+
method_name: method_name,
|
|
239
|
+
include_in_select: options.fetch(:include_in_select, true),
|
|
240
|
+
validate: options[:validate] || false,
|
|
241
|
+
# Layer 2: Lifecycle callbacks
|
|
242
|
+
before_create_account: options[:before_create_account],
|
|
243
|
+
after_create_account: options[:after_create_account],
|
|
244
|
+
formatter: options[:formatter],
|
|
245
|
+
validator: options[:validator],
|
|
246
|
+
verifier: options[:verifier],
|
|
247
|
+
handshake: options[:handshake],
|
|
248
|
+
options: options
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Define the helper method on the Auth class
|
|
252
|
+
@auth.send(:define_method, method_name) do
|
|
253
|
+
value = account ? account[column] : nil
|
|
254
|
+
|
|
255
|
+
# Apply formatter if present (Layer 2)
|
|
256
|
+
config = external_identity_columns_config[column]
|
|
257
|
+
if config && config[:formatter] && value
|
|
258
|
+
instance_exec(value, &config[:formatter])
|
|
259
|
+
else
|
|
260
|
+
value
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# NOTE: external_identity_column is defined in configuration_module_eval above
|
|
269
|
+
# It's available during configuration but not as an instance method
|
|
270
|
+
|
|
271
|
+
# Get list of all declared external identity column names
|
|
272
|
+
#
|
|
273
|
+
# @return [Array<Symbol>] List of column names
|
|
274
|
+
#
|
|
275
|
+
# @example
|
|
276
|
+
# external_identity_column_list # => [:stripe_customer_id, :redis_uuid]
|
|
277
|
+
def external_identity_column_list
|
|
278
|
+
external_identity_columns_config.keys
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Get configuration for a specific external identity column
|
|
282
|
+
#
|
|
283
|
+
# @param column [Symbol] Column name
|
|
284
|
+
# @return [Hash, nil] Configuration hash or nil if not found
|
|
285
|
+
#
|
|
286
|
+
# @example
|
|
287
|
+
# external_identity_column_config(:stripe_customer_id)
|
|
288
|
+
# # => {column: :stripe_customer_id, method_name: :stripe_customer_id, ...}
|
|
289
|
+
def external_identity_column_config(column)
|
|
290
|
+
external_identity_columns_config[column]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Get list of all generated helper method names
|
|
294
|
+
#
|
|
295
|
+
# @return [Array<Symbol>] List of method names
|
|
296
|
+
#
|
|
297
|
+
# @example
|
|
298
|
+
# external_identity_helper_methods # => [:stripe_customer_id, :redis_uuid]
|
|
299
|
+
def external_identity_helper_methods
|
|
300
|
+
external_identity_columns_config.values.map { |config| config[:method_name] }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Check if a column has been declared as an external identity
|
|
304
|
+
#
|
|
305
|
+
# @param column [Symbol] Column name
|
|
306
|
+
# @return [Boolean] True if declared
|
|
307
|
+
#
|
|
308
|
+
# @example
|
|
309
|
+
# external_identity_column?(:stripe_customer_id) # => true
|
|
310
|
+
# external_identity_column?(:redis_uuid) # => true
|
|
311
|
+
# external_identity_column?(:unknown) # => false
|
|
312
|
+
def external_identity_column?(column)
|
|
313
|
+
external_identity_columns_config.key?(column)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Get complete status information for all declared external identities
|
|
317
|
+
#
|
|
318
|
+
# Useful for debugging and introspection. Shows configuration,
|
|
319
|
+
# current values, and validation status.
|
|
320
|
+
#
|
|
321
|
+
# @return [Array<Hash>] Array of status hashes
|
|
322
|
+
#
|
|
323
|
+
# @example
|
|
324
|
+
# external_identity_status
|
|
325
|
+
# # => [
|
|
326
|
+
# # {
|
|
327
|
+
# # column: :stripe_customer_id,
|
|
328
|
+
# # method: :stripe_customer_id,
|
|
329
|
+
# # value: "cus_abc123",
|
|
330
|
+
# # present: true,
|
|
331
|
+
# # in_select: true,
|
|
332
|
+
# # in_account: true,
|
|
333
|
+
# # column_exists: true
|
|
334
|
+
# # },
|
|
335
|
+
# # ...
|
|
336
|
+
# # ]
|
|
337
|
+
def external_identity_status
|
|
338
|
+
current_select = account_select
|
|
339
|
+
|
|
340
|
+
external_identity_columns_config.map do |column, config|
|
|
341
|
+
method_name = config[:method_name]
|
|
342
|
+
|
|
343
|
+
# Safely get the value
|
|
344
|
+
value = begin
|
|
345
|
+
account ? account[column] : nil
|
|
346
|
+
rescue StandardError
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Check if column exists in database
|
|
351
|
+
column_exists = begin
|
|
352
|
+
db.schema(accounts_table).any? { |col| col[0] == column }
|
|
353
|
+
rescue StandardError
|
|
354
|
+
nil # Unknown if can't check
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Check if column is in select (nil means select all)
|
|
358
|
+
in_select = current_select.nil? || current_select.include?(column)
|
|
359
|
+
|
|
360
|
+
{
|
|
361
|
+
column: column,
|
|
362
|
+
method: method_name,
|
|
363
|
+
value: value,
|
|
364
|
+
present: !value.nil?,
|
|
365
|
+
in_select: in_select,
|
|
366
|
+
in_account: account&.key?(column) || false,
|
|
367
|
+
column_exists: column_exists
|
|
368
|
+
}
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Validate a specific external identity column value
|
|
373
|
+
#
|
|
374
|
+
# Applies formatter (if configured) then validator (if configured).
|
|
375
|
+
# Returns true if valid or no validator configured.
|
|
376
|
+
# Raises ArgumentError if validation fails.
|
|
377
|
+
#
|
|
378
|
+
# @param column [Symbol] Column name
|
|
379
|
+
# @param value [Object] Value to validate
|
|
380
|
+
# @return [Boolean] True if valid
|
|
381
|
+
# @raise [ArgumentError] If validation fails
|
|
382
|
+
#
|
|
383
|
+
# @example
|
|
384
|
+
# validate_external_identity(:stripe_customer_id, "cus_123") # => true
|
|
385
|
+
# validate_external_identity(:stripe_customer_id, "invalid") # => ArgumentError
|
|
386
|
+
def validate_external_identity(column, value)
|
|
387
|
+
config = external_identity_columns_config[column]
|
|
388
|
+
return true unless config
|
|
389
|
+
return true if value.nil? # nil values are not validated
|
|
390
|
+
|
|
391
|
+
# Apply formatter if present
|
|
392
|
+
formatted_value = if config[:formatter]
|
|
393
|
+
instance_exec(value, &config[:formatter])
|
|
394
|
+
else
|
|
395
|
+
value
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Apply validator if present
|
|
399
|
+
if config[:validator]
|
|
400
|
+
is_valid = instance_exec(formatted_value, &config[:validator])
|
|
401
|
+
raise ArgumentError, "Invalid format for #{column}: #{value.inspect}" unless is_valid
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
true
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Validate all configured external identity columns
|
|
408
|
+
#
|
|
409
|
+
# Checks all columns that have validators configured.
|
|
410
|
+
# Returns hash of column => validation result.
|
|
411
|
+
#
|
|
412
|
+
# @return [Hash<Symbol, Boolean>] Results per column
|
|
413
|
+
#
|
|
414
|
+
# @example
|
|
415
|
+
# validate_all_external_identities
|
|
416
|
+
# # => {stripe_customer_id: true, redis_uuid: true}
|
|
417
|
+
def validate_all_external_identities
|
|
418
|
+
results = {}
|
|
419
|
+
external_identity_columns_config.each do |column, config|
|
|
420
|
+
next unless config[:validator]
|
|
421
|
+
|
|
422
|
+
value = account ? account[column] : nil
|
|
423
|
+
next if value.nil? # Skip nil values
|
|
424
|
+
|
|
425
|
+
validate_external_identity(column, value)
|
|
426
|
+
results[column] = true
|
|
427
|
+
end
|
|
428
|
+
results
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Verify external identity by checking with external service
|
|
432
|
+
#
|
|
433
|
+
# Health check that verifies the external identity still exists
|
|
434
|
+
# and is valid. Non-critical - returns false on failure rather
|
|
435
|
+
# than raising (except for unhandled exceptions).
|
|
436
|
+
#
|
|
437
|
+
# @param column [Symbol] Column name
|
|
438
|
+
# @return [Boolean] True if verified, false if not or no verifier configured
|
|
439
|
+
#
|
|
440
|
+
# @example
|
|
441
|
+
# verify_external_identity(:stripe_customer_id) # => true
|
|
442
|
+
# verify_external_identity(:deleted_id) # => false
|
|
443
|
+
def verify_external_identity(column)
|
|
444
|
+
config = external_identity_columns_config[column]
|
|
445
|
+
return true unless config
|
|
446
|
+
return true unless config[:verifier]
|
|
447
|
+
|
|
448
|
+
value = account ? account[column] : nil
|
|
449
|
+
return true if value.nil? # Skip nil values
|
|
450
|
+
|
|
451
|
+
# Apply formatter if present
|
|
452
|
+
formatted_value = if config[:formatter]
|
|
453
|
+
instance_exec(value, &config[:formatter])
|
|
454
|
+
else
|
|
455
|
+
value
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Execute verifier callback
|
|
459
|
+
# Non-critical: catch errors and return false
|
|
460
|
+
begin
|
|
461
|
+
result = instance_exec(formatted_value, &config[:verifier])
|
|
462
|
+
!!result # Ensure boolean
|
|
463
|
+
rescue StandardError => e
|
|
464
|
+
# Log error but don't raise - verification is non-critical
|
|
465
|
+
warn "[external_identity] Verification failed for #{column}: #{e.class} - #{e.message}"
|
|
466
|
+
false
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Verify all external identities with verifier callbacks
|
|
471
|
+
#
|
|
472
|
+
# Performs health checks on all configured external identities.
|
|
473
|
+
# Skips columns without verifiers or with nil values.
|
|
474
|
+
# Non-critical - continues checking all columns even if some fail.
|
|
475
|
+
#
|
|
476
|
+
# @return [Hash<Symbol, Boolean>] Results per column with verifier
|
|
477
|
+
#
|
|
478
|
+
# @example
|
|
479
|
+
# verify_all_external_identities
|
|
480
|
+
# # => {stripe_customer_id: true, github_user_id: false}
|
|
481
|
+
def verify_all_external_identities
|
|
482
|
+
results = {}
|
|
483
|
+
external_identity_columns_config.each do |column, config|
|
|
484
|
+
next unless config[:verifier]
|
|
485
|
+
|
|
486
|
+
value = account ? account[column] : nil
|
|
487
|
+
next if value.nil? # Skip nil values
|
|
488
|
+
|
|
489
|
+
results[column] = verify_external_identity(column)
|
|
490
|
+
end
|
|
491
|
+
results
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Verify handshake between external identity and verification token
|
|
495
|
+
#
|
|
496
|
+
# Security-critical verification that checks the external identity
|
|
497
|
+
# value against a verification token (e.g., OAuth state, CSRF token).
|
|
498
|
+
# MUST raise on failure by default - this is security-critical.
|
|
499
|
+
#
|
|
500
|
+
# @param column [Symbol] Column name
|
|
501
|
+
# @param value [Object] External identity value to verify
|
|
502
|
+
# @param token [Object] Verification token (e.g., OAuth state)
|
|
503
|
+
# @return [Boolean] True if handshake verified
|
|
504
|
+
# @raise [RuntimeError] If handshake fails (security-critical)
|
|
505
|
+
#
|
|
506
|
+
# @example OAuth CSRF protection
|
|
507
|
+
# verify_handshake(:github_user_id, user_info['id'], params['state'])
|
|
508
|
+
#
|
|
509
|
+
# @example Team invite verification
|
|
510
|
+
# verify_handshake(:team_id, invite.team_id, invite.token)
|
|
511
|
+
def verify_handshake(column, value, token)
|
|
512
|
+
config = external_identity_columns_config[column]
|
|
513
|
+
|
|
514
|
+
# Return true if no handshake configured (pass-through)
|
|
515
|
+
return true unless config
|
|
516
|
+
return true unless config[:handshake]
|
|
517
|
+
|
|
518
|
+
# Apply formatter if present
|
|
519
|
+
formatted_value = if config[:formatter]
|
|
520
|
+
instance_exec(value, &config[:formatter])
|
|
521
|
+
else
|
|
522
|
+
value
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Execute handshake callback
|
|
526
|
+
# Security-critical: MUST raise on failure
|
|
527
|
+
result = instance_exec(formatted_value, token, &config[:handshake])
|
|
528
|
+
|
|
529
|
+
raise "Handshake verification failed for #{column}" unless result
|
|
530
|
+
|
|
531
|
+
true
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Generate external identities during account creation
|
|
535
|
+
#
|
|
536
|
+
# Runs in before_create_account hook. For each column with
|
|
537
|
+
# before_create_account callback configured:
|
|
538
|
+
# - Skip if value already set (manual override)
|
|
539
|
+
# - Execute callback to generate value
|
|
540
|
+
# - Apply formatter if configured
|
|
541
|
+
# - Apply validator if configured
|
|
542
|
+
# - Set account column
|
|
543
|
+
#
|
|
544
|
+
# Errors during generation will prevent account creation.
|
|
545
|
+
def before_create_account
|
|
546
|
+
super if defined?(super)
|
|
547
|
+
generate_external_identities
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Process external identity callbacks after account creation
|
|
551
|
+
#
|
|
552
|
+
# Runs in after_create_account hook. For each column with
|
|
553
|
+
# after_create_account callback configured:
|
|
554
|
+
# - Retrieve current column value
|
|
555
|
+
# - Execute callback with column value as argument
|
|
556
|
+
#
|
|
557
|
+
# Useful for:
|
|
558
|
+
# - Provisioning external resources
|
|
559
|
+
# - Sending notifications with external IDs
|
|
560
|
+
# - Triggering webhooks
|
|
561
|
+
# - Logging/auditing
|
|
562
|
+
def after_create_account
|
|
563
|
+
super if defined?(super)
|
|
564
|
+
process_after_create_callbacks
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Validation hook - runs after configuration is complete
|
|
568
|
+
#
|
|
569
|
+
# Validates configuration and provides helpful warnings/errors
|
|
570
|
+
def post_configure
|
|
571
|
+
super if defined?(super)
|
|
572
|
+
|
|
573
|
+
# Wrap account_select to add external identity columns
|
|
574
|
+
setup_account_select_wrapper
|
|
575
|
+
|
|
576
|
+
# Check columns based on configuration
|
|
577
|
+
case external_identity_check_columns
|
|
578
|
+
when true
|
|
579
|
+
# Check existence and raise if missing
|
|
580
|
+
check_columns_exist!
|
|
581
|
+
when :autocreate
|
|
582
|
+
# Check existence and inform table_guard if missing
|
|
583
|
+
check_and_autocreate_columns!
|
|
584
|
+
when false
|
|
585
|
+
# Skip checking entirely
|
|
586
|
+
else
|
|
587
|
+
raise ArgumentError,
|
|
588
|
+
"external_identity_check_columns must be true, false, or :autocreate, got: #{external_identity_check_columns.inspect}"
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Always check that columns are included in account_select if they should be
|
|
592
|
+
validate_account_select_inclusion!
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
private
|
|
596
|
+
|
|
597
|
+
# Wrap account_select method to add external identity columns
|
|
598
|
+
#
|
|
599
|
+
# This runs in post_configure after all configuration is complete.
|
|
600
|
+
# It wraps the existing account_select method (whether default nil or
|
|
601
|
+
# user-configured) to add our columns.
|
|
602
|
+
def setup_account_select_wrapper
|
|
603
|
+
# Get current account_select value at configuration time
|
|
604
|
+
# (calling the method gets the configured value)
|
|
605
|
+
current_select = account_select
|
|
606
|
+
|
|
607
|
+
# Get columns to add
|
|
608
|
+
columns_to_add = external_identity_columns_config.select do |_name, config|
|
|
609
|
+
config[:include_in_select]
|
|
610
|
+
end.keys
|
|
611
|
+
|
|
612
|
+
# Return early if no columns to add
|
|
613
|
+
return if columns_to_add.empty?
|
|
614
|
+
|
|
615
|
+
# Redefine account_select on the Auth subclass to wrap it
|
|
616
|
+
#
|
|
617
|
+
# NOTE: We use self.class.send(:define_method) rather than define_singleton_method
|
|
618
|
+
# because in Rodauth's architecture, each configuration gets its own Rodauth::Auth
|
|
619
|
+
# SUBCLASS (not just instance). Defining on self.class ensures the method is available
|
|
620
|
+
# on all runtime instances of this particular Auth subclass, which is what we need.
|
|
621
|
+
# Using define_singleton_method would only affect the temporary configuration instance.
|
|
622
|
+
#
|
|
623
|
+
# Capturing current_select at configuration time (rather than calling account_select
|
|
624
|
+
# dynamically) is intentional - it preserves the configured state from post_configure,
|
|
625
|
+
# which runs after all user configuration is complete. This prevents infinite recursion
|
|
626
|
+
# and ensures we build on the base configuration's account_select value.
|
|
627
|
+
self.class.send(:define_method, :account_select) do
|
|
628
|
+
# Use the captured value from configuration time
|
|
629
|
+
cols = current_select
|
|
630
|
+
|
|
631
|
+
# If configured value is nil, preserve that (select all columns)
|
|
632
|
+
return nil if cols.nil?
|
|
633
|
+
|
|
634
|
+
# Normalize to array
|
|
635
|
+
cols = case cols
|
|
636
|
+
when Array then cols.dup
|
|
637
|
+
else [cols]
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Add external identity columns (idempotent via include? check)
|
|
641
|
+
columns_to_add.each do |column|
|
|
642
|
+
cols << column unless cols.include?(column)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
cols
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Generate external identities for columns with before_create_account callbacks
|
|
650
|
+
#
|
|
651
|
+
# Called during before_create_account hook
|
|
652
|
+
def generate_external_identities
|
|
653
|
+
external_identity_columns_config.each do |column, config|
|
|
654
|
+
next unless config[:before_create_account]
|
|
655
|
+
|
|
656
|
+
# Skip if value already set (manual override)
|
|
657
|
+
next if account[column]
|
|
658
|
+
|
|
659
|
+
# Execute generator callback
|
|
660
|
+
generated_value = instance_exec(&config[:before_create_account])
|
|
661
|
+
|
|
662
|
+
# Skip if generator returned nil (intentionally not setting)
|
|
663
|
+
next if generated_value.nil?
|
|
664
|
+
|
|
665
|
+
# Apply formatter if configured
|
|
666
|
+
value = if config[:formatter]
|
|
667
|
+
instance_exec(generated_value, &config[:formatter])
|
|
668
|
+
else
|
|
669
|
+
generated_value
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# Apply validator if configured
|
|
673
|
+
if config[:validator]
|
|
674
|
+
is_valid = instance_exec(value, &config[:validator])
|
|
675
|
+
raise ArgumentError, "Generated value for #{column} failed validation: #{value.inspect}" unless is_valid
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Set the account column
|
|
679
|
+
account[column] = value
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Process after_create_account callbacks for external identity columns
|
|
684
|
+
#
|
|
685
|
+
# Called during after_create_account hook. For each column with
|
|
686
|
+
# after_create_account callback configured, execute the callback
|
|
687
|
+
# with the current column value as argument.
|
|
688
|
+
def process_after_create_callbacks
|
|
689
|
+
external_identity_columns_config.each do |column, config|
|
|
690
|
+
next unless config[:after_create_account]
|
|
691
|
+
|
|
692
|
+
# Get current value from account
|
|
693
|
+
current_value = account[column]
|
|
694
|
+
|
|
695
|
+
# Execute callback with current value
|
|
696
|
+
instance_exec(current_value, &config[:after_create_account])
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Internal method called by auth_cached_method
|
|
701
|
+
#
|
|
702
|
+
# Retrieves the storage hash for external identity configurations from the Auth class
|
|
703
|
+
# This is called lazily per-instance and cached
|
|
704
|
+
#
|
|
705
|
+
# @return [Hash] Configuration hash from the Auth class
|
|
706
|
+
def _external_identity_columns_config
|
|
707
|
+
# Get from class instance variable set during configuration
|
|
708
|
+
self.class.instance_variable_get(:@_external_identity_columns) || {}
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Check that declared columns exist in the database
|
|
712
|
+
#
|
|
713
|
+
# Runs when external_identity_check_columns is true
|
|
714
|
+
#
|
|
715
|
+
# @raise [ArgumentError] If columns are missing
|
|
716
|
+
def check_columns_exist!
|
|
717
|
+
missing = find_missing_columns
|
|
718
|
+
return if missing.empty?
|
|
719
|
+
|
|
720
|
+
column_list = missing.map { |col| ":#{col}" }.join(', ')
|
|
721
|
+
raise ArgumentError, "External identity columns not found in #{accounts_table} table: #{column_list}. " \
|
|
722
|
+
'Add columns to database, set external_identity_check_columns to false, or use :autocreate mode.'
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Check columns and inform table_guard if any are missing
|
|
726
|
+
#
|
|
727
|
+
# Runs when external_identity_check_columns is :autocreate
|
|
728
|
+
def check_and_autocreate_columns!
|
|
729
|
+
missing = find_missing_columns
|
|
730
|
+
return if missing.empty?
|
|
731
|
+
|
|
732
|
+
# If table_guard is enabled, inform it about missing columns
|
|
733
|
+
raise ArgumentError, build_external_identity_columns_error(missing) unless respond_to?(:table_guard_mode)
|
|
734
|
+
|
|
735
|
+
# Register missing external identity columns with table_guard
|
|
736
|
+
register_external_columns_with_table_guard(missing)
|
|
737
|
+
|
|
738
|
+
# No table_guard available - provide helpful error with migration code
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Find columns that don't exist in the database
|
|
742
|
+
#
|
|
743
|
+
# @return [Array<Symbol>] Array of missing column names
|
|
744
|
+
def find_missing_columns
|
|
745
|
+
return [] unless db # Skip if no database available
|
|
746
|
+
|
|
747
|
+
schema = begin
|
|
748
|
+
db.schema(accounts_table)
|
|
749
|
+
rescue StandardError
|
|
750
|
+
# Can't check - database might not exist yet
|
|
751
|
+
return []
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
column_names = schema.map { |col| col[0] }
|
|
755
|
+
|
|
756
|
+
missing = []
|
|
757
|
+
external_identity_columns_config.each do |column, _config|
|
|
758
|
+
missing << column unless column_names.include?(column)
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
missing
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Register missing external identity columns with table_guard
|
|
765
|
+
#
|
|
766
|
+
# @param missing [Array<Symbol>] Array of missing column names
|
|
767
|
+
def register_external_columns_with_table_guard(missing)
|
|
768
|
+
# Add missing columns to table_guard's configuration
|
|
769
|
+
# This allows table_guard to generate migrations or create columns
|
|
770
|
+
# based on its sequel_mode setting
|
|
771
|
+
|
|
772
|
+
missing.each do |column|
|
|
773
|
+
# Get column configuration
|
|
774
|
+
config = external_identity_columns_config[column]
|
|
775
|
+
|
|
776
|
+
# Build column definition with Sequel options
|
|
777
|
+
column_def = {
|
|
778
|
+
name: column,
|
|
779
|
+
type: extract_column_type(config),
|
|
780
|
+
null: extract_column_null(config),
|
|
781
|
+
default: extract_column_default(config),
|
|
782
|
+
unique: extract_column_unique(config),
|
|
783
|
+
size: extract_column_size(config),
|
|
784
|
+
index: extract_column_index(config),
|
|
785
|
+
feature: :external_identity
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
# Register column with table_guard
|
|
789
|
+
# table_guard will validate and optionally create based on its configuration
|
|
790
|
+
register_required_column(accounts_table, column_def)
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Trigger table_guard's validation which will handle creation/logging
|
|
794
|
+
# based on table_guard_mode and table_guard_sequel_mode
|
|
795
|
+
#
|
|
796
|
+
# Since external_identity's post_configure runs after table_guard's,
|
|
797
|
+
# we need to explicitly trigger the checking again now that we've
|
|
798
|
+
# registered our columns
|
|
799
|
+
#
|
|
800
|
+
# Check if we should handle columns: either validation is enabled OR sequel_mode is set
|
|
801
|
+
# (sequel_mode works even in silent mode since it indicates intent to create/generate)
|
|
802
|
+
return unless should_check_tables? || table_guard_sequel_mode
|
|
803
|
+
|
|
804
|
+
# Get the missing columns we just registered
|
|
805
|
+
missing_cols = missing_columns
|
|
806
|
+
return if missing_cols.empty?
|
|
807
|
+
|
|
808
|
+
# Handle columns according to table_guard's configuration
|
|
809
|
+
# (skip validation in silent mode, but allow sequel operations)
|
|
810
|
+
handle_column_guard_mode(missing_cols) if should_check_tables?
|
|
811
|
+
|
|
812
|
+
# Generate/create columns if sequel mode is configured
|
|
813
|
+
handle_sequel_generation([], missing_cols) if table_guard_sequel_mode
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Extract column type from configuration
|
|
817
|
+
#
|
|
818
|
+
# Checks in order: nested sequel hash, flat options, default to String
|
|
819
|
+
#
|
|
820
|
+
# @param config [Hash] Column configuration
|
|
821
|
+
# @return [Class] Column type (Integer, String, etc.)
|
|
822
|
+
def extract_column_type(config)
|
|
823
|
+
config.dig(:options, :sequel, :type) ||
|
|
824
|
+
config.dig(:options, :type) ||
|
|
825
|
+
String
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Extract column null setting from configuration
|
|
829
|
+
#
|
|
830
|
+
# @param config [Hash] Column configuration
|
|
831
|
+
# @return [Boolean] Whether column allows NULL
|
|
832
|
+
def extract_column_null(config)
|
|
833
|
+
# Check nested sequel hash first, then flat options
|
|
834
|
+
if config.dig(:options, :sequel)&.key?(:null)
|
|
835
|
+
config.dig(:options, :sequel, :null)
|
|
836
|
+
elsif config.dig(:options)&.key?(:null)
|
|
837
|
+
config.dig(:options, :null)
|
|
838
|
+
else
|
|
839
|
+
true # External IDs are optional by default
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# Extract column default value from configuration
|
|
844
|
+
#
|
|
845
|
+
# @param config [Hash] Column configuration
|
|
846
|
+
# @return [Object, nil] Default value or nil
|
|
847
|
+
def extract_column_default(config)
|
|
848
|
+
config.dig(:options, :sequel, :default) ||
|
|
849
|
+
config.dig(:options, :default)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# Extract column unique setting from configuration
|
|
853
|
+
#
|
|
854
|
+
# @param config [Hash] Column configuration
|
|
855
|
+
# @return [Boolean] Whether column has unique constraint
|
|
856
|
+
def extract_column_unique(config)
|
|
857
|
+
config.dig(:options, :sequel, :unique) ||
|
|
858
|
+
config.dig(:options, :unique) ||
|
|
859
|
+
false
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Extract column size from configuration
|
|
863
|
+
#
|
|
864
|
+
# @param config [Hash] Column configuration
|
|
865
|
+
# @return [Integer, nil] Column size or nil
|
|
866
|
+
def extract_column_size(config)
|
|
867
|
+
config.dig(:options, :sequel, :size) ||
|
|
868
|
+
config.dig(:options, :size)
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
# Extract column index setting from configuration
|
|
872
|
+
#
|
|
873
|
+
# @param config [Hash] Column configuration
|
|
874
|
+
# @return [Boolean, Hash] Index configuration
|
|
875
|
+
def extract_column_index(config)
|
|
876
|
+
config.dig(:options, :sequel, :index) ||
|
|
877
|
+
config.dig(:options, :index) ||
|
|
878
|
+
false
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Build error message for missing external identity columns with migration example
|
|
882
|
+
#
|
|
883
|
+
# @param missing [Array<Symbol>] Array of missing column names
|
|
884
|
+
# @return [String] Error message with migration code
|
|
885
|
+
def build_external_identity_columns_error(missing)
|
|
886
|
+
column_list = missing.map { |col| ":#{col}" }.join(', ')
|
|
887
|
+
|
|
888
|
+
migration_code = generate_external_identity_migration_code(missing)
|
|
889
|
+
|
|
890
|
+
<<~ERROR
|
|
891
|
+
External identity columns not found in #{accounts_table} table: #{column_list}
|
|
892
|
+
|
|
893
|
+
With external_identity_check_columns set to :autocreate but table_guard not enabled.
|
|
894
|
+
|
|
895
|
+
Either:
|
|
896
|
+
1. Enable table_guard feature with sequel_mode to auto-create
|
|
897
|
+
2. Create migration manually:
|
|
898
|
+
|
|
899
|
+
#{migration_code}
|
|
900
|
+
|
|
901
|
+
3. Set external_identity_check_columns to false to skip checking
|
|
902
|
+
ERROR
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# Generate migration code for missing external identity columns
|
|
906
|
+
#
|
|
907
|
+
# @param missing [Array<Symbol>] Array of missing column names
|
|
908
|
+
# @return [String] Sequel migration code
|
|
909
|
+
def generate_external_identity_migration_code(missing)
|
|
910
|
+
lines = ['Sequel.migration do', ' up do', " alter_table :#{accounts_table} do"]
|
|
911
|
+
|
|
912
|
+
missing.each do |column|
|
|
913
|
+
lines << " add_column :#{column}, String"
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
lines += [' end', ' end', '', ' down do', " alter_table :#{accounts_table} do"]
|
|
917
|
+
|
|
918
|
+
missing.each do |column|
|
|
919
|
+
lines << " drop_column :#{column}"
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
lines += [' end', ' end', 'end']
|
|
923
|
+
|
|
924
|
+
lines.join("\n")
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Validate that columns are included in account_select if they should be
|
|
928
|
+
#
|
|
929
|
+
# Provides helpful warnings if configuration might not work as expected
|
|
930
|
+
def validate_account_select_inclusion!
|
|
931
|
+
current_select = account_select
|
|
932
|
+
return unless current_select # Skip if account_select not defined (e.g., no login feature)
|
|
933
|
+
|
|
934
|
+
missing = []
|
|
935
|
+
external_identity_columns_config.each do |column, config|
|
|
936
|
+
missing << ":#{column}" if config[:include_in_select] && !current_select.include?(column)
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
return if missing.empty?
|
|
940
|
+
|
|
941
|
+
warn "[external_identity] WARNING: Columns #{missing.join(", ")} marked for inclusion " \
|
|
942
|
+
'but not in account_select. This may indicate a configuration order issue. ' \
|
|
943
|
+
'The feature should have added them automatically.'
|
|
944
|
+
end
|
|
945
|
+
end
|
|
946
|
+
end
|