better_auth-rails 0.2.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc3d5bd26fdfd519d462f2634d4187d3a52a7334864cd2fcddf43c22b88c0975
4
- data.tar.gz: 9b3f4ff8abd83a3264706fbb6e8406835168d781f6a247eba896f4736b4041a6
3
+ metadata.gz: d3bbbf52a2438b83d1668fc4b3d15eb28f162e9cc81cd4e88c37008407fcb5ff
4
+ data.tar.gz: d2b18b69ffcfb01601aa9a48e01883d0b1a6cc032e087b1fa92c5efa9d2eba08
5
5
  SHA512:
6
- metadata.gz: 78edf35b13082e9ab5ab0a964978dcf9fce608abcee9c080268a9868e1e7e9a24ce73033e36c3547e7b5eaacbbdf1573b695c95e2ac3805ce04f9048291f7e24
7
- data.tar.gz: c9a6a504de5d9560d4223ce47a8c4f9b2b3c331b890f061bac1ae56d9ef7a9a0e30a57d4f7c683aff2a32d9869fe2dc307686aac72e453e4dd7412de070a5272
6
+ metadata.gz: 4b70675596c580f94602b8505fc0df556da325a614aaff02c19b9939ff9d959354405986c55c0737c1d7f6b10dfeabc0f3168a75eff9736607651d26dc020544
7
+ data.tar.gz: 2cfa4a247413b9caa48c21613873021ccde047d813f34adf311b83bfa58192f186375d215f9eb883c81debf1b7406e4e628a2d721811bf060fab5db9c1ca5aed
data/README.md CHANGED
@@ -80,48 +80,47 @@ BetterAuth::Rails.configure do |config|
80
80
 
81
81
  config.base_url = ENV["BETTER_AUTH_URL"]
82
82
  config.base_path = "/api/auth"
83
- config.database = ->(options) { BetterAuth::Rails::ActiveRecordAdapter.new(options) }
84
83
  config.trusted_origins = [
85
84
  ENV["BETTER_AUTH_URL"]
86
85
  ].compact
87
86
 
88
- config.session = {
89
- cookie_cache: {
90
- enabled: true,
91
- max_age: 5 * 60,
92
- strategy: "jwe"
93
- }
94
- }
95
-
96
- config.advanced = {
97
- ip_address: {
98
- ip_address_headers: ["x-forwarded-for"],
99
- disable_ip_tracking: false
100
- }
101
- }
102
-
103
- config.experimental = {
104
- joins: false
105
- }
106
-
107
- config.social_providers = {
108
- # github: BetterAuth::SocialProviders.github(
87
+ config.session do |session|
88
+ session.cookie_cache do |cookie|
89
+ cookie.enabled = true
90
+ cookie.max_age = 5 * 60
91
+ cookie.strategy = "jwe"
92
+ end
93
+ end
94
+
95
+ config.advanced do |advanced|
96
+ advanced.ip_address do |ip|
97
+ ip.ip_address_headers = ["x-forwarded-for"]
98
+ ip.disable_ip_tracking = false
99
+ end
100
+ end
101
+
102
+ config.experimental do |experimental|
103
+ experimental.joins = false
104
+ end
105
+
106
+ config.social_providers do |providers|
107
+ # providers.github = BetterAuth::SocialProviders.github(
109
108
  # client_id: ENV.fetch("GITHUB_CLIENT_ID"),
110
109
  # client_secret: ENV.fetch("GITHUB_CLIENT_SECRET")
111
110
  # )
112
- }
111
+ end
113
112
 
114
113
  config.plugins = []
115
- config.hooks = {
116
- before: [],
117
- after: []
118
- }
114
+ config.hooks do |hooks|
115
+ hooks.before = []
116
+ hooks.after = []
117
+ end
119
118
  end
120
119
  ```
121
120
 
122
121
  Rails configuration is a thin option builder for the core Rack auth object. The same option concepts are available in core Ruby through `BetterAuth.auth(...)`; Rails places them in `config/initializers/better_auth.rb` so applications can rely on credentials, ActiveRecord, and Rails environment configuration.
123
122
 
124
- The ActiveRecord adapter uses whichever database adapter the Rails app is already configured with, including PostgreSQL and MySQL.
123
+ Rails uses `BetterAuth::Rails::ActiveRecordAdapter` by default. The adapter uses whichever database adapter the Rails app is already configured with, including PostgreSQL and MySQL. To be explicit, set `config.database_adapter = :active_record`; for custom adapters, assign `config.database` directly.
125
124
 
126
125
  ### JavaScript Client
127
126
 
@@ -20,12 +20,16 @@ module BetterAuth
20
20
 
21
21
  attr_reader :connection
22
22
 
23
- def initialize(options, connection: nil)
23
+ def initialize(options = nil, connection: nil)
24
24
  super(options)
25
- @connection = connection || ::ActiveRecord::Base
25
+ @connection = connection || (options ? ::ActiveRecord::Base : nil)
26
26
  @models = {}
27
27
  end
28
28
 
29
+ def call(options)
30
+ self.class.new(options, connection: connection)
31
+ end
32
+
29
33
  def create(model:, data:, force_allow_id: false)
30
34
  model = model.to_s
31
35
  input = transform_input(model, data, "create", force_allow_id)
@@ -219,36 +223,107 @@ module BetterAuth
219
223
  end
220
224
 
221
225
  def define_join_associations(model, klass)
222
- case model
223
- when "session", "account"
224
- return unless klass.respond_to?(:belongs_to)
225
-
226
- klass.belongs_to(
227
- :user,
228
- class_name: model_class("user").name,
229
- foreign_key: storage_field(model, "userId"),
230
- primary_key: storage_field("user", "id"),
231
- optional: true
232
- )
233
- when "user"
234
- return unless klass.respond_to?(:has_many)
235
-
236
- klass.has_many(
237
- :accounts,
238
- class_name: model_class("account").name,
239
- foreign_key: storage_field("account", "userId"),
240
- primary_key: storage_field("user", "id")
241
- )
226
+ schema_models.each_key do |join_model|
227
+ next if join_model == model
228
+
229
+ definition = safe_join_definition(model, join_model)
230
+ next unless definition
231
+
232
+ association = definition.fetch(:association)
233
+ next if association_defined?(klass, association)
234
+
235
+ if definition[:owner] == :base && definition[:collection]
236
+ next unless klass.respond_to?(:has_many)
237
+
238
+ klass.has_many(
239
+ association,
240
+ class_name: model_class(join_model).name,
241
+ foreign_key: storage_field(join_model, definition.fetch(:to)),
242
+ primary_key: storage_field(model, definition.fetch(:from))
243
+ )
244
+ elsif definition[:owner] == :base
245
+ next unless klass.respond_to?(:has_one)
246
+
247
+ klass.has_one(
248
+ association,
249
+ class_name: model_class(join_model).name,
250
+ foreign_key: storage_field(join_model, definition.fetch(:to)),
251
+ primary_key: storage_field(model, definition.fetch(:from))
252
+ )
253
+ else
254
+ next unless klass.respond_to?(:belongs_to)
255
+
256
+ klass.belongs_to(
257
+ association,
258
+ class_name: model_class(join_model).name,
259
+ foreign_key: storage_field(model, definition.fetch(:from)),
260
+ primary_key: storage_field(join_model, definition.fetch(:to)),
261
+ optional: true
262
+ )
263
+ end
242
264
  end
243
265
  end
244
266
 
245
267
  def join_definition(model, join_model)
246
- case [model.to_s, join_model.to_s]
247
- when ["session", "user"], ["account", "user"]
248
- {association: :user, collection: false}
249
- when ["user", "account"]
250
- {association: :accounts, collection: true}
268
+ inferred_join_config(model.to_s, join_model.to_s).merge(association: join_model.to_sym)
269
+ end
270
+
271
+ def safe_join_definition(model, join_model)
272
+ join_definition(model, join_model)
273
+ rescue BetterAuth::Error
274
+ nil
275
+ end
276
+
277
+ def inferred_join_config(model, join_model)
278
+ foreign_keys = schema_for(join_model).fetch(:fields).select do |_field, attributes|
279
+ reference_model_matches?(attributes, model)
251
280
  end
281
+
282
+ unless foreign_keys.empty?
283
+ raise BetterAuth::Error, "Multiple foreign keys found for model #{join_model} and base model #{model} while performing join operation. Only one foreign key is supported." if foreign_keys.length > 1
284
+
285
+ foreign_key, attributes = foreign_keys.first
286
+ reference = attributes.fetch(:references)
287
+ unique = attributes[:unique] == true
288
+ return {
289
+ from: reference.fetch(:field).to_s,
290
+ to: foreign_key,
291
+ collection: !unique,
292
+ owner: :base
293
+ }
294
+ end
295
+
296
+ foreign_keys = schema_for(model).fetch(:fields).select do |_field, attributes|
297
+ reference_model_matches?(attributes, join_model)
298
+ end
299
+
300
+ raise BetterAuth::Error, "No foreign key found for model #{join_model} and base model #{model} while performing join operation." if foreign_keys.empty?
301
+ raise BetterAuth::Error, "Multiple foreign keys found for model #{join_model} and base model #{model} while performing join operation. Only one foreign key is supported." if foreign_keys.length > 1
302
+
303
+ foreign_key, attributes = foreign_keys.first
304
+ reference = attributes.fetch(:references)
305
+ {
306
+ from: foreign_key,
307
+ to: reference.fetch(:field).to_s,
308
+ collection: false,
309
+ owner: :join
310
+ }
311
+ end
312
+
313
+ def reference_model_matches?(attributes, model)
314
+ reference = attributes[:references]
315
+ return false unless reference
316
+
317
+ reference_model = reference[:model] || reference["model"]
318
+ reference_model.to_s == model.to_s || reference_model.to_s == table_for(model)
319
+ end
320
+
321
+ def association_defined?(klass, association)
322
+ klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(association)
323
+ end
324
+
325
+ def schema_models
326
+ BetterAuth::Schema.auth_tables(options)
252
327
  end
253
328
 
254
329
  def model_namespace
@@ -29,9 +29,35 @@ module BetterAuth
29
29
  disabled_paths
30
30
  logger
31
31
  ].freeze
32
+ BLOCK_OPTION_NAMES = %i[
33
+ rate_limit
34
+ session
35
+ account
36
+ user
37
+ verification
38
+ advanced
39
+ email_and_password
40
+ email_verification
41
+ social_providers
42
+ experimental
43
+ database_hooks
44
+ hooks
45
+ on_api_error
46
+ ].freeze
32
47
 
33
48
  attr_accessor(*AUTH_OPTION_NAMES)
34
49
 
50
+ BLOCK_OPTION_NAMES.each do |name|
51
+ define_method(name) do |&block|
52
+ value = instance_variable_get(:"@#{name}")
53
+ return value unless block
54
+
55
+ builder = OptionBuilder.new(value.is_a?(Hash) ? value : {})
56
+ block.call(builder)
57
+ public_send(:"#{name}=", deep_merge(value.is_a?(Hash) ? value : {}, builder.to_h))
58
+ end
59
+ end
60
+
35
61
  def initialize
36
62
  @base_path = BetterAuth::Configuration::DEFAULT_BASE_PATH
37
63
  @plugins = []
@@ -39,6 +65,15 @@ module BetterAuth
39
65
  @database = ->(options) { ActiveRecordAdapter.new(options) }
40
66
  end
41
67
 
68
+ def database_adapter=(adapter)
69
+ case adapter&.to_sym
70
+ when :active_record
71
+ self.database = ->(options) { ActiveRecordAdapter.new(options) }
72
+ else
73
+ raise ArgumentError, "Unsupported database_adapter: #{adapter.inspect}. Use :active_record or assign a custom adapter with config.database = ..."
74
+ end
75
+ end
76
+
42
77
  def to_auth_options
43
78
  AUTH_OPTION_NAMES.each_with_object({}) do |name, options|
44
79
  value = public_send(name)
@@ -48,6 +83,18 @@ module BetterAuth
48
83
  options[name] = value
49
84
  end
50
85
  end
86
+
87
+ private
88
+
89
+ def deep_merge(base, override)
90
+ base.merge(override) do |_key, old_value, new_value|
91
+ if old_value.is_a?(Hash) && new_value.is_a?(Hash)
92
+ deep_merge(old_value, new_value)
93
+ else
94
+ new_value
95
+ end
96
+ end
97
+ end
51
98
  end
52
99
  end
53
100
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ class OptionBuilder
6
+ def initialize(values = {})
7
+ @values = deep_dup(symbolize_keys(values || {}))
8
+ end
9
+
10
+ def to_h
11
+ deep_dup(@values)
12
+ end
13
+
14
+ def method_missing(name, *args, &block)
15
+ method_name = name.to_s
16
+ if method_name.end_with?("=")
17
+ key = method_name.delete_suffix("=").to_sym
18
+ @values[key] = args.first
19
+ elsif block
20
+ key = method_name.to_sym
21
+ nested = self.class.new(@values[key].is_a?(Hash) ? @values[key] : {})
22
+ yield nested
23
+ @values[key] = nested.to_h
24
+ elsif args.empty?
25
+ @values[method_name.to_sym]
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def respond_to_missing?(_name, _include_private = false)
32
+ true
33
+ end
34
+
35
+ private
36
+
37
+ def symbolize_keys(value)
38
+ return value unless value.is_a?(Hash)
39
+
40
+ value.each_with_object({}) do |(key, object_value), result|
41
+ normalized_key = key.to_s
42
+ .gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
43
+ .tr("-", "_")
44
+ .downcase
45
+ .to_sym
46
+ result[normalized_key] = object_value.is_a?(Hash) ? symbolize_keys(object_value) : object_value
47
+ end
48
+ end
49
+
50
+ def deep_dup(value)
51
+ return value.transform_values { |entry| deep_dup(entry) } if value.is_a?(Hash)
52
+ return value.map { |entry| deep_dup(entry) } if value.is_a?(Array)
53
+
54
+ value
55
+ end
56
+ end
57
+ end
58
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Rails
5
- VERSION = "0.2.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "better_auth"
4
4
  require_relative "rails/version"
5
+ require_relative "rails/option_builder"
5
6
  require_relative "rails/configuration"
6
7
  require_relative "rails/migration"
7
8
  require_relative "rails/active_record_adapter"
@@ -8,43 +8,50 @@ BetterAuth::Rails.configure do |config|
8
8
 
9
9
  config.base_url = ENV["BETTER_AUTH_URL"]
10
10
  config.base_path = "/api/auth"
11
- config.database = ->(options) { BetterAuth::Rails::ActiveRecordAdapter.new(options) }
12
11
  config.trusted_origins = [
13
12
  ENV["BETTER_AUTH_URL"]
14
13
  ].compact
15
14
 
16
- config.session = {
17
- cookie_cache: {
18
- enabled: true,
19
- max_age: 5 * 60,
20
- strategy: "jwe"
21
- }
22
- }
23
-
24
- config.advanced = {
25
- ip_address: {
26
- ip_address_headers: ["x-forwarded-for"],
27
- disable_ip_tracking: false
28
- }
29
- }
30
-
31
- config.experimental = {
32
- joins: false
33
- }
34
-
35
- config.social_providers = {
36
- # github: BetterAuth::SocialProviders.github(
15
+ # Rails apps use BetterAuth::Rails::ActiveRecordAdapter by default.
16
+ # For a custom adapter, assign config.database directly.
17
+
18
+ # config.email_and_password do |auth|
19
+ # auth.enabled = true
20
+ # auth.require_email_verification = true
21
+ # end
22
+
23
+ config.session do |session|
24
+ session.cookie_cache do |cookie|
25
+ cookie.enabled = true
26
+ cookie.max_age = 5 * 60
27
+ cookie.strategy = "jwe"
28
+ end
29
+ end
30
+
31
+ config.advanced do |advanced|
32
+ advanced.ip_address do |ip|
33
+ ip.ip_address_headers = ["x-forwarded-for"]
34
+ ip.disable_ip_tracking = false
35
+ end
36
+ end
37
+
38
+ config.experimental do |experimental|
39
+ experimental.joins = false
40
+ end
41
+
42
+ config.social_providers do |providers|
43
+ # providers.github = BetterAuth::SocialProviders.github(
37
44
  # client_id: ENV.fetch("GITHUB_CLIENT_ID"),
38
45
  # client_secret: ENV.fetch("GITHUB_CLIENT_SECRET")
39
46
  # )
40
- }
47
+ end
41
48
 
42
49
  # Add Better Auth plugins here.
43
50
  config.plugins = []
44
51
 
45
52
  # Add Better Auth hooks here. Auth decisions still run through the core gem.
46
- config.hooks = {
47
- before: [],
48
- after: []
49
- }
53
+ config.hooks do |hooks|
54
+ hooks.before = []
55
+ hooks.after = []
56
+ end
50
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -167,6 +167,20 @@ dependencies:
167
167
  - - "~>"
168
168
  - !ruby/object:Gem::Version
169
169
  version: '1.5'
170
+ - !ruby/object:Gem::Dependency
171
+ name: mysql2
172
+ requirement: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - "~>"
175
+ - !ruby/object:Gem::Version
176
+ version: '0.5'
177
+ type: :development
178
+ prerelease: false
179
+ version_requirements: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - "~>"
182
+ - !ruby/object:Gem::Version
183
+ version: '0.5'
170
184
  description: Rails integration for Better Auth Ruby. Provides middleware, controller
171
185
  helpers, and generators.
172
186
  email:
@@ -184,6 +198,7 @@ files:
184
198
  - lib/better_auth/rails/controller_helpers.rb
185
199
  - lib/better_auth/rails/migration.rb
186
200
  - lib/better_auth/rails/mounted_app.rb
201
+ - lib/better_auth/rails/option_builder.rb
187
202
  - lib/better_auth/rails/railtie.rb
188
203
  - lib/better_auth/rails/routing.rb
189
204
  - lib/better_auth/rails/version.rb