better_auth 0.2.0 → 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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +5 -3
  4. data/lib/better_auth/adapters/internal_adapter.rb +168 -18
  5. data/lib/better_auth/adapters/memory.rb +4 -1
  6. data/lib/better_auth/adapters/mongodb.rb +5 -365
  7. data/lib/better_auth/adapters/sql.rb +17 -1
  8. data/lib/better_auth/api.rb +1 -1
  9. data/lib/better_auth/context.rb +2 -1
  10. data/lib/better_auth/plugin.rb +14 -1
  11. data/lib/better_auth/plugins/oauth_protocol.rb +403 -57
  12. data/lib/better_auth/plugins/organization.rb +5 -0
  13. data/lib/better_auth/rate_limiter.rb +19 -2
  14. data/lib/better_auth/router.rb +14 -1
  15. data/lib/better_auth/routes/email_verification.rb +5 -2
  16. data/lib/better_auth/routes/password.rb +19 -0
  17. data/lib/better_auth/routes/session.rb +27 -4
  18. data/lib/better_auth/routes/sign_in.rb +1 -1
  19. data/lib/better_auth/routes/sign_up.rb +52 -1
  20. data/lib/better_auth/routes/social.rb +201 -22
  21. data/lib/better_auth/routes/user.rb +14 -2
  22. data/lib/better_auth/schema/sql.rb +11 -0
  23. data/lib/better_auth/schema.rb +16 -0
  24. data/lib/better_auth/social_providers/apple.rb +44 -8
  25. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  26. data/lib/better_auth/social_providers/base.rb +262 -4
  27. data/lib/better_auth/social_providers/cognito.rb +32 -0
  28. data/lib/better_auth/social_providers/discord.rb +27 -5
  29. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  30. data/lib/better_auth/social_providers/facebook.rb +35 -0
  31. data/lib/better_auth/social_providers/figma.rb +31 -0
  32. data/lib/better_auth/social_providers/github.rb +21 -6
  33. data/lib/better_auth/social_providers/gitlab.rb +16 -3
  34. data/lib/better_auth/social_providers/google.rb +38 -13
  35. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  36. data/lib/better_auth/social_providers/kakao.rb +32 -0
  37. data/lib/better_auth/social_providers/kick.rb +32 -0
  38. data/lib/better_auth/social_providers/line.rb +33 -0
  39. data/lib/better_auth/social_providers/linear.rb +44 -0
  40. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  41. data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
  42. data/lib/better_auth/social_providers/naver.rb +31 -0
  43. data/lib/better_auth/social_providers/notion.rb +33 -0
  44. data/lib/better_auth/social_providers/paybin.rb +31 -0
  45. data/lib/better_auth/social_providers/paypal.rb +36 -0
  46. data/lib/better_auth/social_providers/polar.rb +31 -0
  47. data/lib/better_auth/social_providers/railway.rb +49 -0
  48. data/lib/better_auth/social_providers/reddit.rb +32 -0
  49. data/lib/better_auth/social_providers/roblox.rb +31 -0
  50. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  51. data/lib/better_auth/social_providers/slack.rb +30 -0
  52. data/lib/better_auth/social_providers/spotify.rb +31 -0
  53. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  54. data/lib/better_auth/social_providers/twitch.rb +39 -0
  55. data/lib/better_auth/social_providers/twitter.rb +32 -0
  56. data/lib/better_auth/social_providers/vercel.rb +47 -0
  57. data/lib/better_auth/social_providers/vk.rb +34 -0
  58. data/lib/better_auth/social_providers/wechat.rb +104 -0
  59. data/lib/better_auth/social_providers/zoom.rb +31 -0
  60. data/lib/better_auth/social_providers.rb +29 -0
  61. data/lib/better_auth/version.rb +1 -1
  62. data/lib/better_auth.rb +0 -1
  63. metadata +30 -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
@@ -379,6 +380,7 @@ module BetterAuth
379
380
  return value ? 1 : 0 if dialect == :sqlite && attributes[:type] == "boolean"
380
381
  return value.iso8601(6) if dialect == :sqlite && attributes[:type] == "date" && value.respond_to?(:iso8601)
381
382
  return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
383
+ return JSON.generate(value) if json_like?(attributes) && !value.is_a?(String)
382
384
 
383
385
  value
384
386
  end
@@ -387,10 +389,21 @@ module BetterAuth
387
389
  return value if value.nil?
388
390
  return coerce_boolean(value) if attributes[:type] == "boolean"
389
391
  return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
392
+ return parse_json_value(value) if json_like?(attributes) && value.is_a?(String)
390
393
 
391
394
  value
392
395
  end
393
396
 
397
+ def json_like?(attributes)
398
+ %w[json string[] number[]].include?(attributes[:type])
399
+ end
400
+
401
+ def parse_json_value(value)
402
+ JSON.parse(value)
403
+ rescue JSON::ParserError
404
+ value
405
+ end
406
+
394
407
  def coerce_boolean(value)
395
408
  return value if value == true || value == false
396
409
  return false if value == 0 || value.to_s == "0" || value.to_s.downcase == "f" || value.to_s.downcase == "false"
@@ -406,7 +419,10 @@ module BetterAuth
406
419
  end
407
420
 
408
421
  def fetch_key(hash, key)
409
- hash[key] || hash[key.to_s] || hash[storage_key(key)] || hash[storage_key(key).to_sym]
422
+ [key, key.to_s, storage_key(key), storage_key(key).to_sym].each do |candidate|
423
+ return hash[candidate] if hash.key?(candidate)
424
+ end
425
+ nil
410
426
  end
411
427
 
412
428
  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] || {},
@@ -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
@@ -11,6 +11,8 @@ module BetterAuth
11
11
  :schema,
12
12
  :migrations,
13
13
  :options,
14
+ :version,
15
+ :client,
14
16
  :rate_limit,
15
17
  :error_codes,
16
18
  :on_request,
@@ -28,7 +30,8 @@ module BetterAuth
28
30
 
29
31
  def initialize(data = {}, **keywords)
30
32
  data = data.to_h if data.respond_to?(:to_h) && !data.is_a?(Hash)
31
- raw = normalize_hash((data || {}).merge(keywords))
33
+ input = (data || {}).merge(keywords)
34
+ raw = normalize_hash(input)
32
35
 
33
36
  @id = raw[:id].to_s
34
37
  @init = raw[:init]
@@ -38,6 +41,8 @@ module BetterAuth
38
41
  @schema = raw[:schema] || {}
39
42
  @migrations = raw[:migrations] || {}
40
43
  @options = raw[:options] || {}
44
+ @version = raw[:version]
45
+ @client = stringify_hash(input[:client] || input["client"])
41
46
  @rate_limit = Array(raw[:rate_limit])
42
47
  @error_codes = normalize_error_codes(raw)
43
48
  @on_request = raw[:on_request]
@@ -107,6 +112,14 @@ module BetterAuth
107
112
  end
108
113
  end
109
114
 
115
+ def stringify_hash(value)
116
+ return nil unless value.is_a?(Hash)
117
+
118
+ value.each_with_object({}) do |(key, object), result|
119
+ result[key.to_s] = object.is_a?(Hash) ? stringify_hash(object) : object
120
+ end
121
+ end
122
+
110
123
  def normalize_key(key)
111
124
  key.to_s
112
125
  .delete_prefix("$")