better_auth 0.2.0 → 0.4.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +5 -3
  4. data/lib/better_auth/adapters/internal_adapter.rb +173 -20
  5. data/lib/better_auth/adapters/memory.rb +61 -12
  6. data/lib/better_auth/adapters/mongodb.rb +5 -365
  7. data/lib/better_auth/adapters/sql.rb +44 -3
  8. data/lib/better_auth/api.rb +7 -2
  9. data/lib/better_auth/async.rb +70 -0
  10. data/lib/better_auth/context.rb +2 -1
  11. data/lib/better_auth/database_hooks.rb +3 -3
  12. data/lib/better_auth/deprecate.rb +28 -0
  13. data/lib/better_auth/endpoint.rb +5 -2
  14. data/lib/better_auth/host.rb +166 -0
  15. data/lib/better_auth/instrumentation.rb +74 -0
  16. data/lib/better_auth/logger.rb +31 -0
  17. data/lib/better_auth/middleware/origin_check.rb +2 -2
  18. data/lib/better_auth/oauth2.rb +94 -0
  19. data/lib/better_auth/plugin.rb +14 -1
  20. data/lib/better_auth/plugins/email_otp.rb +16 -5
  21. data/lib/better_auth/plugins/generic_oauth.rb +14 -28
  22. data/lib/better_auth/plugins/oauth_protocol.rb +553 -64
  23. data/lib/better_auth/plugins/organization/schema.rb +6 -0
  24. data/lib/better_auth/plugins/organization.rb +56 -20
  25. data/lib/better_auth/plugins/two_factor.rb +53 -18
  26. data/lib/better_auth/rate_limiter.rb +37 -2
  27. data/lib/better_auth/request_state.rb +44 -0
  28. data/lib/better_auth/router.rb +14 -1
  29. data/lib/better_auth/routes/account.rb +16 -4
  30. data/lib/better_auth/routes/email_verification.rb +5 -2
  31. data/lib/better_auth/routes/password.rb +21 -1
  32. data/lib/better_auth/routes/session.rb +27 -4
  33. data/lib/better_auth/routes/sign_in.rb +3 -1
  34. data/lib/better_auth/routes/sign_up.rb +60 -1
  35. data/lib/better_auth/routes/social.rb +231 -22
  36. data/lib/better_auth/routes/user.rb +23 -5
  37. data/lib/better_auth/schema/sql.rb +11 -0
  38. data/lib/better_auth/schema.rb +16 -0
  39. data/lib/better_auth/session.rb +12 -1
  40. data/lib/better_auth/social_providers/apple.rb +44 -8
  41. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  42. data/lib/better_auth/social_providers/base.rb +262 -4
  43. data/lib/better_auth/social_providers/cognito.rb +32 -0
  44. data/lib/better_auth/social_providers/discord.rb +27 -5
  45. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  46. data/lib/better_auth/social_providers/facebook.rb +35 -0
  47. data/lib/better_auth/social_providers/figma.rb +31 -0
  48. data/lib/better_auth/social_providers/github.rb +21 -6
  49. data/lib/better_auth/social_providers/gitlab.rb +16 -3
  50. data/lib/better_auth/social_providers/google.rb +38 -13
  51. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  52. data/lib/better_auth/social_providers/kakao.rb +32 -0
  53. data/lib/better_auth/social_providers/kick.rb +32 -0
  54. data/lib/better_auth/social_providers/line.rb +33 -0
  55. data/lib/better_auth/social_providers/linear.rb +44 -0
  56. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  57. data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
  58. data/lib/better_auth/social_providers/naver.rb +31 -0
  59. data/lib/better_auth/social_providers/notion.rb +33 -0
  60. data/lib/better_auth/social_providers/paybin.rb +31 -0
  61. data/lib/better_auth/social_providers/paypal.rb +36 -0
  62. data/lib/better_auth/social_providers/polar.rb +31 -0
  63. data/lib/better_auth/social_providers/railway.rb +49 -0
  64. data/lib/better_auth/social_providers/reddit.rb +32 -0
  65. data/lib/better_auth/social_providers/roblox.rb +31 -0
  66. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  67. data/lib/better_auth/social_providers/slack.rb +30 -0
  68. data/lib/better_auth/social_providers/spotify.rb +31 -0
  69. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  70. data/lib/better_auth/social_providers/twitch.rb +39 -0
  71. data/lib/better_auth/social_providers/twitter.rb +32 -0
  72. data/lib/better_auth/social_providers/vercel.rb +47 -0
  73. data/lib/better_auth/social_providers/vk.rb +34 -0
  74. data/lib/better_auth/social_providers/wechat.rb +104 -0
  75. data/lib/better_auth/social_providers/zoom.rb +31 -0
  76. data/lib/better_auth/social_providers.rb +29 -0
  77. data/lib/better_auth/url_helpers.rb +195 -0
  78. data/lib/better_auth/version.rb +1 -1
  79. data/lib/better_auth.rb +8 -1
  80. metadata +38 -15
@@ -1,369 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securerandom"
4
- require "time"
3
+ begin
4
+ require "better_auth/mongo_adapter"
5
+ rescue LoadError => error
6
+ raise if error.path && error.path != "better_auth/mongo_adapter"
5
7
 
6
- module BetterAuth
7
- module Adapters
8
- class MongoDB < Base
9
- attr_reader :database, :client, :use_plural
10
-
11
- def initialize(options = nil, database:, client: nil, transaction: nil, use_plural: false)
12
- require "mongo" unless database
13
-
14
- super(options || Configuration.new(secret: Configuration::DEFAULT_SECRET, database: :memory))
15
- @database = database
16
- @client = client
17
- @transaction_enabled = transaction.nil? ? !client.nil? : !!transaction
18
- @use_plural = !!use_plural
19
- @session = nil
20
- end
21
-
22
- def create(model:, data:, force_allow_id: false)
23
- model = model.to_s
24
- record = transform_input(model, data, "create", force_allow_id)
25
- document = to_document(model, record)
26
- collection_for(model).insert_one(document, session_options)
27
- from_document(model, document)
28
- end
29
-
30
- def find_one(model:, where: [], select: nil, join: nil)
31
- find_many(model: model, where: where, select: select, join: join, limit: 1).first
32
- end
33
-
34
- def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
35
- model = model.to_s
36
- records = documents_for(model)
37
- .select { |document| matches_where?(model, document, where || []) }
38
- .map { |document| from_document(model, document) }
39
- records = records.map { |record| apply_join(model, record, join) } if join
40
- records = sort_records(records, sort_by) if sort_by
41
- records = records.drop(offset.to_i) if offset
42
- records = records.first(limit.to_i) if limit
43
- records = records.map { |record| select_fields(record, select, join) } if select && !select.empty?
44
- records
45
- end
46
-
47
- def update(model:, where:, update:)
48
- model = model.to_s
49
- records = update_matching(model, where || [], update, first_only: true)
50
- records.first
51
- end
52
-
53
- def update_many(model:, where:, update:)
54
- update_matching(model.to_s, where || [], update, first_only: false).length
55
- end
56
-
57
- def delete(model:, where:)
58
- delete_many(model: model, where: where, first_only: true)
59
- nil
60
- end
61
-
62
- def delete_many(model:, where:, first_only: false)
63
- model = model.to_s
64
- documents = documents_for(model)
65
- matches = documents.select { |document| matches_where?(model, document, where || []) }
66
- matches = matches.first(1) if first_only
67
- ids = matches.map { |document| document["_id"] }
68
- remaining = documents.reject { |document| ids.include?(document["_id"]) }
69
- replace_documents(model, remaining)
70
- ids.length
71
- end
72
-
73
- def count(model:, where: nil)
74
- find_many(model: model, where: where || []).length
75
- end
76
-
77
- def transaction
78
- return yield self unless client && @transaction_enabled && client.respond_to?(:start_session)
79
-
80
- session = client.start_session
81
- begin
82
- session.start_transaction
83
- @session = session
84
- result = yield self
85
- session.commit_transaction
86
- result
87
- rescue
88
- session.abort_transaction
89
- raise
90
- ensure
91
- @session = nil
92
- session.end_session
93
- end
94
- end
95
-
96
- private
97
-
98
- def transform_input(model, data, action, force_allow_id)
99
- fields = schema_for(model).fetch(:fields)
100
- input = stringify_keys(data)
101
- output = {}
102
-
103
- fields.each do |field, attributes|
104
- next if field == "id" && input.key?(field) && !force_allow_id
105
-
106
- value_provided = input.key?(field)
107
- value = input[field]
108
- if value_provided && attributes[:input] == false && value && !force_allow_id
109
- raise APIError.new("BAD_REQUEST", message: "#{field} is not allowed to be set")
110
- end
111
-
112
- if !value_provided && action == "create" && attributes.key?(:default_value)
113
- value = resolve_default(attributes[:default_value])
114
- value_provided = true
115
- elsif !value_provided && action == "update" && attributes[:on_update]
116
- value = resolve_default(attributes[:on_update])
117
- value_provided = true
118
- end
119
-
120
- if !value_provided && action == "create" && attributes[:required]
121
- raise APIError.new("BAD_REQUEST", message: "#{field} is required") unless field == "id"
122
- end
123
- output[field] = coerce_value(value, attributes) if value_provided
124
- end
125
-
126
- output["id"] = generated_id if action == "create" && !output.key?("id")
127
- output
128
- end
129
-
130
- def update_matching(model, where, update, first_only:)
131
- data = transform_input(model, update, "update", true)
132
- documents = documents_for(model)
133
- matches = documents.select { |document| matches_where?(model, document, where) }
134
- matches = matches.first(1) if first_only
135
- updates = to_document(model, data)
136
- ids = matches.map { |document| document["_id"] }
137
- updated = documents.map do |document|
138
- ids.include?(document["_id"]) ? document.merge(updates) : document
139
- end
140
- replace_documents(model, updated)
141
- updated.select { |document| ids.include?(document["_id"]) }.map { |document| from_document(model, document) }
142
- end
143
-
144
- def documents_for(model)
145
- collection = collection_for(model)
146
- if collection.respond_to?(:all_documents)
147
- collection.all_documents
148
- else
149
- collection.find({}, session_options).to_a.map { |document| stringify_document(document) }
150
- end
151
- end
152
-
153
- def replace_documents(model, documents)
154
- collection = collection_for(model)
155
- if collection.respond_to?(:replace_documents)
156
- collection.replace_documents(documents)
157
- else
158
- collection.delete_many({}, session_options)
159
- documents.each { |document| collection.insert_one(document, session_options) }
160
- end
161
- end
162
-
163
- def collection_for(model)
164
- database.collection(collection_name(model))
165
- end
166
-
167
- def collection_name(model)
168
- return schema_for(model).fetch(:model_name) if use_plural
169
-
170
- model.to_s
171
- end
172
-
173
- def to_document(model, record)
174
- schema_for(model).fetch(:fields).each_with_object({}) do |(field, attributes), document|
175
- next unless record.key?(field)
176
-
177
- key = (field == "id") ? "_id" : storage_field(model, field)
178
- document[key] = store_value(field, record[field], attributes)
179
- end
180
- end
181
-
182
- def from_document(model, document)
183
- fields = schema_for(model).fetch(:fields)
184
- fields.each_with_object({}) do |(field, attributes), record|
185
- key = (field == "id") ? "_id" : storage_field(model, field)
186
- record[field] = output_value(field, fetch_document(document, key), attributes) if document_key?(document, key)
187
- end
188
- end
189
-
190
- def stringify_document(document)
191
- document.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
192
- end
193
-
194
- def matches_where?(model, document, where)
195
- clauses = Array(where)
196
- return true if clauses.empty?
197
-
198
- result = evaluate_clause(model, document, clauses.first)
199
- clauses.drop(1).each do |clause|
200
- clause_result = evaluate_clause(model, document, clause)
201
- if fetch_key(clause, :connector).to_s.upcase == "OR"
202
- result ||= clause_result
203
- else
204
- result &&= clause_result
205
- end
206
- end
207
- result
208
- end
209
-
210
- def evaluate_clause(model, document, clause)
211
- field = Schema.storage_key(fetch_key(clause, :field))
212
- attributes = schema_for(model).fetch(:fields).fetch(field)
213
- key = (field == "id") ? "_id" : storage_field(model, field)
214
- expected = store_value(field, fetch_key(clause, :value), attributes)
215
- current = fetch_document(document, key)
216
- operator = (fetch_key(clause, :operator) || "eq").to_s
217
-
218
- case operator
219
- when "in"
220
- Array(expected).any? { |value| same_value?(current, value) }
221
- when "not_in"
222
- Array(expected).none? { |value| same_value?(current, value) }
223
- when "contains"
224
- current.to_s.include?(expected.to_s)
225
- when "starts_with"
226
- current.to_s.start_with?(expected.to_s)
227
- when "ends_with"
228
- current.to_s.end_with?(expected.to_s)
229
- when "ne"
230
- !same_value?(current, expected)
231
- when "gt"
232
- !expected.nil? && current > expected
233
- when "gte"
234
- !expected.nil? && current >= expected
235
- when "lt"
236
- !expected.nil? && current < expected
237
- when "lte"
238
- !expected.nil? && current <= expected
239
- else
240
- same_value?(current, expected)
241
- end
242
- end
243
-
244
- def same_value?(left, right)
245
- left == right || left.to_s == right.to_s
246
- end
247
-
248
- def apply_join(model, record, join)
249
- joined = record.dup
250
- join.each_key do |join_model|
251
- join_model = join_model.to_s
252
- joined[join_model] = case [model, join_model]
253
- when ["session", "user"], ["account", "user"]
254
- find_one(model: "user", where: [{field: "id", value: record["userId"]}])
255
- when ["user", "account"]
256
- find_many(model: "account", where: [{field: "userId", value: record["id"]}])
257
- end
258
- end
259
- joined
260
- end
261
-
262
- def sort_records(records, sort_by)
263
- field = Schema.storage_key(fetch_key(sort_by, :field))
264
- direction = fetch_key(sort_by, :direction).to_s
265
- records.sort_by { |record| record[field].nil? ? "" : record[field] }.then do |sorted|
266
- (direction == "desc") ? sorted.reverse : sorted
267
- end
268
- end
269
-
270
- def select_fields(record, select, join)
271
- fields = Array(select).map { |field| Schema.storage_key(field) }
272
- selected = record.slice(*fields)
273
- join&.each_key { |join_model| selected[join_model.to_s] = record[join_model.to_s] if record.key?(join_model.to_s) }
274
- selected
275
- end
276
-
277
- def store_value(field, value, attributes)
278
- return nil if value.nil?
279
- return Array(value).map { |entry| store_value(field, entry, attributes) } if value.is_a?(Array)
280
-
281
- if field == "id" || attributes.dig(:references, :field) == "id"
282
- return value if custom_id_generator?
283
- return bson_id(value)
284
- end
285
-
286
- coerce_value(value, attributes)
287
- end
288
-
289
- def output_value(field, value, attributes)
290
- return nil if value.nil?
291
- return value.to_s if field == "id" || attributes.dig(:references, :field) == "id"
292
-
293
- coerce_value(value, attributes)
294
- end
295
-
296
- def bson_id(value)
297
- return value unless defined?(BSON::ObjectId)
298
- return value if value.is_a?(BSON::ObjectId)
299
-
300
- BSON::ObjectId.from_string(value.to_s)
301
- rescue
302
- value
303
- end
304
-
305
- def generated_id
306
- generator = options.advanced.dig(:database, :generate_id)
307
- return generator.call.to_s if generator.respond_to?(:call)
308
- return SecureRandom.uuid if generator == "uuid"
309
- return BSON::ObjectId.new.to_s if defined?(BSON::ObjectId)
310
-
311
- SecureRandom.hex(12)
312
- end
313
-
314
- def custom_id_generator?
315
- options.advanced.dig(:database, :generate_id).respond_to?(:call)
316
- end
317
-
318
- def resolve_default(default)
319
- default.respond_to?(:call) ? default.call : default
320
- end
321
-
322
- def coerce_value(value, attributes)
323
- return value if value.nil?
324
- return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
325
-
326
- value
327
- end
328
-
329
- def session_options
330
- @session ? {session: @session} : {}
331
- end
332
-
333
- def document_key?(document, key)
334
- document.key?(key) || document.key?(key.to_sym)
335
- end
336
-
337
- def fetch_document(document, key)
338
- return document[key] if document.key?(key)
339
-
340
- document[key.to_sym]
341
- end
342
-
343
- def stringify_keys(data)
344
- data.each_with_object({}) do |(key, value), result|
345
- result[Schema.storage_key(key)] = value
346
- end
347
- end
348
-
349
- def fetch_key(hash, key)
350
- hash[key] || hash[key.to_s] || hash[Schema.storage_key(key)] || hash[Schema.storage_key(key).to_sym]
351
- end
352
-
353
- def schema_for(model)
354
- Schema.auth_tables(options).fetch(model.to_s)
355
- end
356
-
357
- def storage_field(model, field)
358
- schema_for(model).fetch(:fields).fetch(field.to_s).fetch(:field_name, physical_name(field))
359
- end
360
-
361
- def physical_name(value)
362
- value.to_s
363
- .gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
364
- .tr("-", "_")
365
- .downcase
366
- end
367
- end
368
- end
8
+ raise LoadError, "BetterAuth::Adapters::MongoDB requires the better_auth-mongo-adapter gem. Add `gem \"better_auth-mongo-adapter\"` and `require \"better_auth/mongo_adapter\"`."
369
9
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
+ require "json"
4
5
  require "time"
5
6
 
6
7
  module BetterAuth
@@ -203,10 +204,11 @@ module BetterAuth
203
204
  column = "#{quote(table_for(model))}.#{quote(storage_field(model, field))}"
204
205
  operator = (fetch_key(clause, :operator) || "eq").to_s
205
206
  value = fetch_key(clause, :value)
207
+ attributes = schema_for(model).fetch(:fields).fetch(field)
206
208
 
207
209
  expression = case operator
208
210
  when "in", "not_in"
209
- values = Array(value)
211
+ values = Array(value).map { |entry| coerce_where_value(entry, attributes) }
210
212
  placeholders = values.map do |entry|
211
213
  params << entry
212
214
  placeholder(params.length)
@@ -222,7 +224,7 @@ module BetterAuth
222
224
  params << pattern
223
225
  "#{column} LIKE #{placeholder(params.length)}"
224
226
  else
225
- params << value
227
+ params << coerce_where_value(value, attributes)
226
228
  "#{column} #{sql_operator(operator)} #{placeholder(params.length)}"
227
229
  end
228
230
 
@@ -379,18 +381,46 @@ module BetterAuth
379
381
  return value ? 1 : 0 if dialect == :sqlite && attributes[:type] == "boolean"
380
382
  return value.iso8601(6) if dialect == :sqlite && attributes[:type] == "date" && value.respond_to?(:iso8601)
381
383
  return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
384
+ return JSON.generate(value) if json_like?(attributes) && !value.is_a?(String)
382
385
 
383
386
  value
384
387
  end
385
388
 
389
+ def coerce_where_value(value, attributes)
390
+ return value if value.nil?
391
+
392
+ case attributes[:type]
393
+ when "boolean"
394
+ return coerce_value(false, attributes) if value == false || value == 0 || value.to_s.downcase == "false" || value.to_s == "0"
395
+ return coerce_value(true, attributes) if value == true || value == 1 || value.to_s.downcase == "true" || value.to_s == "1"
396
+ when "number"
397
+ return coerce_number(value)
398
+ when "date"
399
+ return Time.parse(value) if value.is_a?(String)
400
+ end
401
+
402
+ coerce_value(value, attributes)
403
+ end
404
+
386
405
  def coerce_output_value(value, attributes)
387
406
  return value if value.nil?
388
407
  return coerce_boolean(value) if attributes[:type] == "boolean"
389
408
  return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
409
+ return parse_json_value(value) if json_like?(attributes) && value.is_a?(String)
390
410
 
391
411
  value
392
412
  end
393
413
 
414
+ def json_like?(attributes)
415
+ %w[json string[] number[]].include?(attributes[:type])
416
+ end
417
+
418
+ def parse_json_value(value)
419
+ JSON.parse(value)
420
+ rescue JSON::ParserError
421
+ value
422
+ end
423
+
394
424
  def coerce_boolean(value)
395
425
  return value if value == true || value == false
396
426
  return false if value == 0 || value.to_s == "0" || value.to_s.downcase == "f" || value.to_s.downcase == "false"
@@ -399,6 +429,14 @@ module BetterAuth
399
429
  value
400
430
  end
401
431
 
432
+ def coerce_number(value)
433
+ return value unless value.is_a?(String)
434
+ return value.to_i if /\A-?\d+\z/.match?(value)
435
+ return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
436
+
437
+ value
438
+ end
439
+
402
440
  def stringify_keys(data)
403
441
  data.each_with_object({}) do |(key, value), result|
404
442
  result[storage_key(key)] = value
@@ -406,7 +444,10 @@ module BetterAuth
406
444
  end
407
445
 
408
446
  def fetch_key(hash, key)
409
- hash[key] || hash[key.to_s] || hash[storage_key(key)] || hash[storage_key(key).to_sym]
447
+ [key, key.to_s, storage_key(key), storage_key(key).to_sym].each do |candidate|
448
+ return hash[candidate] if hash.key?(candidate)
449
+ end
450
+ nil
410
451
  end
411
452
 
412
453
  def storage_key(value)
@@ -16,7 +16,7 @@ module BetterAuth
16
16
  input = symbolize_keys(input || {})
17
17
  endpoint_context = Endpoint::Context.new(
18
18
  path: endpoint.path,
19
- method: Array(endpoint.methods).first,
19
+ method: input[:method] || Array(endpoint.methods).first,
20
20
  query: input[:query] || {},
21
21
  body: input[:body] || {},
22
22
  params: input[:params] || {},
@@ -211,7 +211,12 @@ module BetterAuth
211
211
  return value unless value.is_a?(Hash)
212
212
 
213
213
  value.each_with_object({}) do |(key, object_value), result|
214
- result[normalize_key(key)] = object_value.is_a?(Hash) ? symbolize_keys(object_value) : object_value
214
+ normalized_key = normalize_key(key)
215
+ result[normalized_key] = if normalized_key == :metadata
216
+ object_value
217
+ else
218
+ object_value.is_a?(Hash) ? symbolize_keys(object_value) : object_value
219
+ end
215
220
  end
216
221
  end
217
222
 
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Async
5
+ module_function
6
+
7
+ def map_concurrent(items, concurrency:, &mapper)
8
+ list = items.to_a
9
+ return [] if list.empty?
10
+
11
+ width = normalized_concurrency(concurrency, list.length)
12
+ results = Array.new(list.length)
13
+ next_index = 0
14
+ first_error = nil
15
+ mutex = Mutex.new
16
+ status = Queue.new
17
+
18
+ workers = Array.new(width) do
19
+ Thread.new do
20
+ loop do
21
+ index = mutex.synchronize do
22
+ break if first_error || next_index >= list.length
23
+
24
+ current = next_index
25
+ next_index += 1
26
+ current
27
+ end
28
+ break unless index
29
+
30
+ begin
31
+ results[index] = mapper.call(list[index], index)
32
+ rescue => error
33
+ mutex.synchronize { first_error ||= error }
34
+ status << [:error, error]
35
+ break
36
+ end
37
+ end
38
+ ensure
39
+ status << [:done, nil]
40
+ end
41
+ end
42
+
43
+ done = 0
44
+ while done < workers.length
45
+ type, error = status.pop
46
+ if type == :error
47
+ workers.each { |worker| worker.kill if worker.alive? }
48
+ workers.each(&:join)
49
+ raise error
50
+ end
51
+
52
+ done += 1
53
+ end
54
+
55
+ raise first_error if first_error
56
+
57
+ results
58
+ end
59
+
60
+ def normalized_concurrency(concurrency, item_count)
61
+ raw = begin
62
+ Float(concurrency).floor
63
+ rescue ArgumentError, TypeError
64
+ 1
65
+ end
66
+ raw = 1 if raw < 1
67
+ [raw, item_count].min
68
+ end
69
+ end
70
+ end
@@ -128,8 +128,9 @@ module BetterAuth
128
128
  end
129
129
 
130
130
  scheme = request.get_header("rack.url_scheme") || request.scheme
131
+ scheme = "https" unless valid_proxy_proto?(scheme.to_s)
131
132
  host_header = request.get_header("HTTP_HOST")
132
- return "#{scheme}://#{host_header}" if host_header && !host_header.empty?
133
+ return "#{scheme}://#{host_header}" if host_header && valid_proxy_host?(host_header.to_s)
133
134
 
134
135
  host = request.get_header("SERVER_NAME") || request.host
135
136
  port = (request.get_header("SERVER_PORT") || request.port).to_i
@@ -35,9 +35,9 @@ module BetterAuth
35
35
 
36
36
  def delete(where, model, custom: nil, context: nil)
37
37
  entity = adapter.find_one(model: model, where: where)
38
- return custom ? custom.call(where) : adapter.delete(model: model, where: where) unless entity
38
+ return nil unless entity
39
39
 
40
- return nil if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
40
+ return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
41
41
 
42
42
  deleted = custom ? custom.call(where) : adapter.delete(model: model, where: where)
43
43
  after_hooks(model, :delete).each { |hook| hook.call(entity, context) }
@@ -47,7 +47,7 @@ module BetterAuth
47
47
  def delete_many(where, model, custom: nil, context: nil)
48
48
  entities = adapter.find_many(model: model, where: where)
49
49
  entities.each do |entity|
50
- return nil if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
50
+ return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
51
51
  end
52
52
  deleted = custom ? custom.call(where) : adapter.delete_many(model: model, where: where)
53
53
  entities.each { |entity| after_hooks(model, :delete).each { |hook| hook.call(entity, context) } }
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Deprecate
5
+ module_function
6
+
7
+ def wrap(message, logger: nil, &block)
8
+ warned = false
9
+ proc do |*args, **kwargs|
10
+ unless warned
11
+ warn_once("[Deprecation] #{message}", logger)
12
+ warned = true
13
+ end
14
+ kwargs.empty? ? block.call(*args) : block.call(*args, **kwargs)
15
+ end
16
+ end
17
+
18
+ def warn_once(message, logger)
19
+ if logger.respond_to?(:call)
20
+ logger.call(message)
21
+ elsif logger.respond_to?(:warn)
22
+ logger.warn(message)
23
+ else
24
+ warn(message)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -7,16 +7,18 @@ module BetterAuth
7
7
  attr_reader :path,
8
8
  :body_schema,
9
9
  :query_schema,
10
+ :params_schema,
10
11
  :headers_schema,
11
12
  :metadata,
12
13
  :use,
13
14
  :handler
14
15
 
15
- def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
16
+ def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, params_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
16
17
  @path = path
17
18
  @methods = Array(method || "*").map { |value| value.to_s.upcase }
18
19
  @body_schema = body_schema
19
20
  @query_schema = query_schema
21
+ @params_schema = params_schema
20
22
  @headers_schema = headers_schema
21
23
  @metadata = metadata || {}
22
24
  @use = Array(use)
@@ -47,6 +49,7 @@ module BetterAuth
47
49
  def apply_schemas!(context)
48
50
  context.body = validate_schema(:body, body_schema, context.body)
49
51
  context.query = validate_schema(:query, query_schema, context.query)
52
+ context.params = validate_schema(:params, params_schema, context.params)
50
53
  context.headers = context.send(:normalize_headers, validate_schema(:headers, headers_schema, context.headers))
51
54
  end
52
55
 
@@ -136,7 +139,7 @@ module BetterAuth
136
139
  return @raw_response if raw_response?
137
140
 
138
141
  body = if response.nil?
139
- [""]
142
+ [JSON.generate(nil)]
140
143
  elsif response.is_a?(String)
141
144
  [response]
142
145
  else