better_auth 0.8.0 → 0.10.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +4 -4
- data/lib/better_auth/adapters/memory.rb +131 -17
- data/lib/better_auth/adapters/sql.rb +139 -57
- data/lib/better_auth/configuration.rb +7 -1
- data/lib/better_auth/cookies.rb +11 -3
- data/lib/better_auth/doctor.rb +97 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/http_client.rb +46 -0
- data/lib/better_auth/migration_plan.rb +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- data/lib/better_auth/plugins/admin.rb +6 -1
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- data/lib/better_auth/plugins/captcha.rb +1 -1
- data/lib/better_auth/plugins/device_authorization.rb +34 -0
- data/lib/better_auth/plugins/dub.rb +8 -0
- data/lib/better_auth/plugins/generic_oauth.rb +34 -7
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/jwt.rb +10 -3
- data/lib/better_auth/plugins/mcp/schema.rb +13 -13
- data/lib/better_auth/plugins/mcp.rb +41 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +98 -21
- data/lib/better_auth/plugins/oidc_provider.rb +62 -3
- data/lib/better_auth/plugins/one_tap.rb +17 -5
- data/lib/better_auth/plugins/open_api.rb +42 -2
- data/lib/better_auth/plugins/organization.rb +122 -11
- data/lib/better_auth/plugins/phone_number.rb +1 -1
- data/lib/better_auth/plugins/two_factor.rb +21 -0
- data/lib/better_auth/rate_limiter.rb +7 -2
- data/lib/better_auth/routes/account.rb +4 -0
- data/lib/better_auth/routes/email_verification.rb +5 -1
- data/lib/better_auth/routes/password.rb +1 -0
- data/lib/better_auth/routes/social.rb +29 -1
- data/lib/better_auth/routes/user.rb +6 -2
- data/lib/better_auth/schema/sql.rb +104 -15
- data/lib/better_auth/schema.rb +35 -2
- data/lib/better_auth/session.rb +2 -1
- data/lib/better_auth/social_providers/base.rb +4 -9
- data/lib/better_auth/social_providers/facebook.rb +1 -1
- data/lib/better_auth/social_providers/github.rb +2 -0
- data/lib/better_auth/social_providers/line.rb +1 -1
- data/lib/better_auth/social_providers/paypal.rb +1 -1
- data/lib/better_auth/sql_migration.rb +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +3 -0
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5b3eeb719cd263152d0e166d19735bccbe118bd72a26093b57686105c456815
|
|
4
|
+
data.tar.gz: 6c7f3fc3653cba526e5f889bb2b1095fefcbcf3bb9b90c1ac50f042635e56071
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90a88bf3352d716dc78074b0d61de38d7188c7f6f98b5d930cd426e020ab51f5b5f21fe8d5765ad96adfbb955345113f23396cfdd65317e6b063613ed7a4da70
|
|
7
|
+
data.tar.gz: 28acb5fd682eff9fff9ff99cf7ac3e5c26b0dce26bb6c37c7a2f4ed6a75b2e9529626a280a244b6755e1edb40631c0057759c0b9e738f0d24c3d4a0d5f9baec4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-05-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed organization owner counting to page through adapter results instead of
|
|
15
|
+
relying on a single uncapped `find_many` call.
|
|
16
|
+
- Improved SQL, memory, cookie, rate-limit, plugin schema, social login, and
|
|
17
|
+
auth response edge cases for more consistent behavior across adapters.
|
|
18
|
+
|
|
10
19
|
## [0.7.0] - 2026-05-05
|
|
11
20
|
|
|
12
21
|
### Added
|
data/README.md
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
·
|
|
14
14
|
<a href="https://better-auth.com">Website</a>
|
|
15
15
|
·
|
|
16
|
-
<a href="https://github.com/sebasxsala/better-auth/issues">Issues</a>
|
|
16
|
+
<a href="https://github.com/sebasxsala/better-auth-rb/issues">Issues</a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
[](https://rubygems.org/gems/better_auth)
|
|
20
|
-
[](https://github.com/sebasxsala/better-auth/stargazers)
|
|
20
|
+
[](https://github.com/sebasxsala/better-auth-rb/stargazers)
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
## About the Project
|
|
@@ -212,7 +212,7 @@ Full documentation is being adapted in the root [`docs/`](/Users/sebastiansala/p
|
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
214
|
# 1. Clone the repository
|
|
215
|
-
git clone https://github.com/sebasxsala/better-auth.git
|
|
215
|
+
git clone https://github.com/sebasxsala/better-auth-rb.git
|
|
216
216
|
cd better-auth/packages/better_auth
|
|
217
217
|
|
|
218
218
|
# 2. Install dependencies
|
|
@@ -370,7 +370,7 @@ test/ # Core tests (Minitest)
|
|
|
370
370
|
|
|
371
371
|
## Contributing
|
|
372
372
|
|
|
373
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/sebasxsala/better-auth. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/sebasxsala/better-auth/blob/main/CODE_OF_CONDUCT.md).
|
|
373
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sebasxsala/better-auth-rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/sebasxsala/better-auth-rb/blob/main/CODE_OF_CONDUCT.md).
|
|
374
374
|
|
|
375
375
|
## License
|
|
376
376
|
|
|
@@ -6,6 +6,8 @@ require "time"
|
|
|
6
6
|
module BetterAuth
|
|
7
7
|
module Adapters
|
|
8
8
|
class Memory < Base
|
|
9
|
+
include JoinSupport
|
|
10
|
+
|
|
9
11
|
attr_reader :db
|
|
10
12
|
|
|
11
13
|
def initialize(options, db = nil)
|
|
@@ -35,17 +37,23 @@ module BetterAuth
|
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def update(model:, where:, update:)
|
|
40
|
+
model = model.to_s
|
|
41
|
+
ensure_update_input_has_fields!(model, update)
|
|
38
42
|
records = table_for(model).select { |record| matches_where?(record, where || []) }
|
|
39
|
-
data = transform_input(model
|
|
43
|
+
data = transform_input(model, update, "update", true)
|
|
44
|
+
ensure_update_data!(data)
|
|
40
45
|
records.each { |record| record.merge!(data) }
|
|
41
46
|
records.first
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
def update_many(model:, where:, update:)
|
|
50
|
+
model = model.to_s
|
|
51
|
+
ensure_update_input_has_fields!(model, update)
|
|
45
52
|
records = table_for(model).select { |record| matches_where?(record, where || []) }
|
|
46
|
-
data = transform_input(model
|
|
53
|
+
data = transform_input(model, update, "update", true)
|
|
54
|
+
ensure_update_data!(data)
|
|
47
55
|
records.each { |record| record.merge!(data) }
|
|
48
|
-
records.
|
|
56
|
+
records.length
|
|
49
57
|
end
|
|
50
58
|
|
|
51
59
|
def delete(model:, where:)
|
|
@@ -109,16 +117,34 @@ module BetterAuth
|
|
|
109
117
|
raise APIError.new("BAD_REQUEST", message: "#{field} is required") unless field == "id"
|
|
110
118
|
end
|
|
111
119
|
|
|
112
|
-
output[field] = coerce_value(value, attributes) if value_provided
|
|
120
|
+
output[field] = coerce_value(value, field, attributes) if value_provided
|
|
113
121
|
end
|
|
114
122
|
|
|
115
|
-
output["id"] = generated_id if action == "create" && !output.key?("id")
|
|
123
|
+
output["id"] = generated_id(model) if action == "create" && fields.key?("id") && (!output.key?("id") || output["id"].nil?)
|
|
116
124
|
output
|
|
117
125
|
end
|
|
118
126
|
|
|
119
|
-
def
|
|
127
|
+
def ensure_update_data!(data)
|
|
128
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") if data.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def ensure_update_input_has_fields!(model, update)
|
|
132
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless update.is_a?(Hash)
|
|
133
|
+
|
|
134
|
+
fields = Schema.auth_tables(options).fetch(model).fetch(:fields)
|
|
135
|
+
input = stringify_keys(update)
|
|
136
|
+
has_updatable_field = input.any? do |field, _value|
|
|
137
|
+
next false if field == "id" || field == "_id"
|
|
138
|
+
|
|
139
|
+
fields.key?(field) || fields.any? { |logical_field, attributes| Schema.storage_key(attributes[:field_name] || logical_field) == field }
|
|
140
|
+
end
|
|
141
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless has_updatable_field
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def generated_id(model)
|
|
120
145
|
generator = options.advanced.dig(:database, :generate_id)
|
|
121
146
|
return generator.call.to_s if generator.respond_to?(:call)
|
|
147
|
+
return table_for(model).length + 1 if generator == "serial"
|
|
122
148
|
return SecureRandom.uuid if generator == "uuid"
|
|
123
149
|
|
|
124
150
|
SecureRandom.hex(16)
|
|
@@ -128,8 +154,9 @@ module BetterAuth
|
|
|
128
154
|
default.respond_to?(:call) ? default.call : default
|
|
129
155
|
end
|
|
130
156
|
|
|
131
|
-
def coerce_value(value, attributes)
|
|
157
|
+
def coerce_value(value, field, attributes)
|
|
132
158
|
return value if value.nil?
|
|
159
|
+
return coerce_number(value) if serial_id_field?(field, attributes)
|
|
133
160
|
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
134
161
|
|
|
135
162
|
value
|
|
@@ -155,8 +182,10 @@ module BetterAuth
|
|
|
155
182
|
field = Schema.storage_key(fetch_key(clause, :field))
|
|
156
183
|
value = fetch_key(clause, :value)
|
|
157
184
|
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
185
|
+
mode = (fetch_key(clause, :mode) || "sensitive").to_s
|
|
158
186
|
current = record[field]
|
|
159
187
|
comparable = coerce_where_value(record, field, value, operator)
|
|
188
|
+
current, comparable = insensitive_values(current, comparable) if insensitive_comparison?(mode, current, comparable)
|
|
160
189
|
|
|
161
190
|
case operator
|
|
162
191
|
when "in"
|
|
@@ -187,9 +216,9 @@ module BetterAuth
|
|
|
187
216
|
def coerce_where_value(record, field, value, operator)
|
|
188
217
|
attributes = schema_for_record_field(record, field)
|
|
189
218
|
return value unless attributes
|
|
190
|
-
return Array(value).map { |entry| coerce_scalar_where_value(entry, attributes) } if %w[in not_in].include?(operator)
|
|
219
|
+
return Array(value).map { |entry| coerce_scalar_where_value(entry, field, attributes) } if %w[in not_in].include?(operator)
|
|
191
220
|
|
|
192
|
-
coerce_scalar_where_value(value, attributes)
|
|
221
|
+
coerce_scalar_where_value(value, field, attributes)
|
|
193
222
|
end
|
|
194
223
|
|
|
195
224
|
def schema_for_record_field(record, field)
|
|
@@ -201,8 +230,9 @@ module BetterAuth
|
|
|
201
230
|
nil
|
|
202
231
|
end
|
|
203
232
|
|
|
204
|
-
def coerce_scalar_where_value(value, attributes)
|
|
233
|
+
def coerce_scalar_where_value(value, field, attributes)
|
|
205
234
|
return value if value.nil?
|
|
235
|
+
return coerce_number(value) if serial_id_field?(field, attributes)
|
|
206
236
|
|
|
207
237
|
case attributes[:type]
|
|
208
238
|
when "boolean"
|
|
@@ -221,6 +251,32 @@ module BetterAuth
|
|
|
221
251
|
value
|
|
222
252
|
end
|
|
223
253
|
|
|
254
|
+
def serial_id_field?(field, attributes)
|
|
255
|
+
return false unless options.advanced.dig(:database, :generate_id) == "serial"
|
|
256
|
+
return true if field.to_s == "id"
|
|
257
|
+
|
|
258
|
+
reference = attributes[:references]
|
|
259
|
+
return false unless reference
|
|
260
|
+
|
|
261
|
+
(reference[:field] || reference["field"]).to_s == "id"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def insensitive_comparison?(mode, current, comparable)
|
|
265
|
+
return false unless mode == "insensitive"
|
|
266
|
+
|
|
267
|
+
current.is_a?(String) && (comparable.is_a?(String) || comparable.is_a?(Array))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def insensitive_values(current, comparable)
|
|
271
|
+
normalized_current = current.downcase
|
|
272
|
+
normalized_comparable = if comparable.is_a?(Array)
|
|
273
|
+
comparable.map { |entry| entry.is_a?(String) ? entry.downcase : entry }
|
|
274
|
+
else
|
|
275
|
+
comparable.downcase
|
|
276
|
+
end
|
|
277
|
+
[normalized_current, normalized_comparable]
|
|
278
|
+
end
|
|
279
|
+
|
|
224
280
|
def coerce_number(value)
|
|
225
281
|
return value unless value.is_a?(String)
|
|
226
282
|
return value.to_i if /\A-?\d+\z/.match?(value)
|
|
@@ -252,18 +308,76 @@ module BetterAuth
|
|
|
252
308
|
|
|
253
309
|
def apply_join(model, record, join)
|
|
254
310
|
joined = record.dup
|
|
255
|
-
join.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
311
|
+
normalized_join(model, join).each do |join_model, config|
|
|
312
|
+
matches = table_for(join_model).select do |join_record|
|
|
313
|
+
join_record[config.fetch(:to)] == record[config.fetch(:from)]
|
|
314
|
+
end.map(&:dup)
|
|
315
|
+
|
|
316
|
+
joined[join_model] = if one_to_one_join?(config)
|
|
317
|
+
matches.first
|
|
318
|
+
else
|
|
319
|
+
matches.first(join_limit(config))
|
|
262
320
|
end
|
|
263
321
|
end
|
|
264
322
|
joined
|
|
265
323
|
end
|
|
266
324
|
|
|
325
|
+
def one_to_one_join?(config)
|
|
326
|
+
config[:relation] == "one-to-one" || config[:unique] == true
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def join_limit(config)
|
|
330
|
+
value = config[:limit]
|
|
331
|
+
return 100 if value.nil?
|
|
332
|
+
|
|
333
|
+
parsed = Integer(value)
|
|
334
|
+
parsed.positive? ? parsed : 100
|
|
335
|
+
rescue ArgumentError, TypeError
|
|
336
|
+
100
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def inferred_join_config(model, join_model)
|
|
340
|
+
foreign_keys = schema_for(join_model).fetch(:fields).select do |_field, attributes|
|
|
341
|
+
reference_model_matches?(attributes, model)
|
|
342
|
+
end
|
|
343
|
+
forward_join = true
|
|
344
|
+
|
|
345
|
+
if foreign_keys.empty?
|
|
346
|
+
foreign_keys = schema_for(model).fetch(:fields).select do |_field, attributes|
|
|
347
|
+
reference_model_matches?(attributes, join_model)
|
|
348
|
+
end
|
|
349
|
+
forward_join = false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
raise Error, "No foreign key found for model #{join_model} and base model #{model} while performing join operation." if foreign_keys.empty?
|
|
353
|
+
raise 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
|
|
354
|
+
|
|
355
|
+
foreign_key, attributes = foreign_keys.first
|
|
356
|
+
reference = attributes.fetch(:references)
|
|
357
|
+
if forward_join
|
|
358
|
+
unique = attributes[:unique] == true
|
|
359
|
+
{from: reference.fetch(:field).to_s, to: foreign_key, relation: unique ? "one-to-one" : "one-to-many", unique: unique}
|
|
360
|
+
else
|
|
361
|
+
{from: foreign_key, to: reference.fetch(:field).to_s, relation: "one-to-one", unique: true}
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def schema_for(model)
|
|
366
|
+
Schema.auth_tables(options).fetch(model.to_s)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def reference_model_matches?(attributes, model)
|
|
370
|
+
reference = attributes[:references]
|
|
371
|
+
return false unless reference
|
|
372
|
+
|
|
373
|
+
reference_model = reference[:model] || reference["model"]
|
|
374
|
+
reference_model.to_s == model.to_s || reference_model.to_s == schema_for(model).fetch(:model_name)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def storage_key(field)
|
|
378
|
+
Schema.storage_key(field)
|
|
379
|
+
end
|
|
380
|
+
|
|
267
381
|
def stringify_keys(data)
|
|
268
382
|
data.each_with_object({}) do |(key, value), result|
|
|
269
383
|
result[Schema.storage_key(key)] = value
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "json"
|
|
5
|
+
require "monitor"
|
|
5
6
|
require "time"
|
|
6
7
|
|
|
7
8
|
module BetterAuth
|
|
@@ -15,6 +16,7 @@ module BetterAuth
|
|
|
15
16
|
super(options)
|
|
16
17
|
@connection = connection
|
|
17
18
|
@dialect = dialect.to_sym
|
|
19
|
+
@connection_lock = Monitor.new
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def create(model:, data:, force_allow_id: false)
|
|
@@ -30,7 +32,8 @@ module BetterAuth
|
|
|
30
32
|
row = rows.first
|
|
31
33
|
return normalize_record(model, row) if row
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
lookup = create_lookup(model, input)
|
|
36
|
+
lookup ? find_one(model: model, where: [lookup]) : input
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def find_one(model:, where: [], select: nil, join: nil)
|
|
@@ -61,21 +64,25 @@ module BetterAuth
|
|
|
61
64
|
|
|
62
65
|
def update(model:, where:, update:)
|
|
63
66
|
model = model.to_s
|
|
67
|
+
ensure_update_input_has_fields!(model, update)
|
|
64
68
|
if dialect == :postgres
|
|
65
69
|
records = update_many(model: model, where: where, update: update, returning: true)
|
|
66
70
|
return records.is_a?(Array) ? records.first : records
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
existing = find_one(model: model, where: where
|
|
73
|
+
existing = find_one(model: model, where: where)
|
|
70
74
|
return nil unless existing
|
|
71
75
|
|
|
72
76
|
update_many(model: model, where: where, update: update)
|
|
73
|
-
|
|
77
|
+
lookup = record_lookup(model, existing)
|
|
78
|
+
lookup ? find_one(model: model, where: [lookup]) : find_one(model: model, where: where)
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
def update_many(model:, where:, update:, returning: false)
|
|
77
82
|
model = model.to_s
|
|
83
|
+
ensure_update_input_has_fields!(model, update)
|
|
78
84
|
data = transform_input(model, update, "update", true)
|
|
85
|
+
ensure_update_data!(data)
|
|
79
86
|
params = []
|
|
80
87
|
assignments = data.each_key.map do |field|
|
|
81
88
|
params << data[field]
|
|
@@ -87,11 +94,11 @@ module BetterAuth
|
|
|
87
94
|
sql << " SET "
|
|
88
95
|
sql << assignments.join(", ")
|
|
89
96
|
sql << " WHERE #{where_sql}" unless where_sql.empty?
|
|
90
|
-
sql << " RETURNING *" if dialect == :postgres
|
|
91
|
-
|
|
92
|
-
return
|
|
97
|
+
sql << " RETURNING *" if dialect == :postgres && returning
|
|
98
|
+
result = execute(sql, params, affected_rows_result: !returning)
|
|
99
|
+
return result.map { |row| normalize_record(model, row) } if returning
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
affected_rows(result)
|
|
95
102
|
end
|
|
96
103
|
|
|
97
104
|
def delete(model:, where:)
|
|
@@ -106,7 +113,7 @@ module BetterAuth
|
|
|
106
113
|
sql = +"DELETE FROM "
|
|
107
114
|
sql << quote(table_for(model))
|
|
108
115
|
sql << " WHERE #{where_sql}" unless where_sql.empty?
|
|
109
|
-
result = execute(sql, params)
|
|
116
|
+
result = execute(sql, params, affected_rows_result: true)
|
|
110
117
|
affected_rows(result)
|
|
111
118
|
end
|
|
112
119
|
|
|
@@ -122,13 +129,15 @@ module BetterAuth
|
|
|
122
129
|
end
|
|
123
130
|
|
|
124
131
|
def transaction
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
@connection_lock.synchronize do
|
|
133
|
+
execute("BEGIN", [])
|
|
134
|
+
result = yield self
|
|
135
|
+
execute("COMMIT", [])
|
|
136
|
+
result
|
|
137
|
+
rescue
|
|
138
|
+
execute("ROLLBACK", [])
|
|
139
|
+
raise
|
|
140
|
+
end
|
|
132
141
|
end
|
|
133
142
|
|
|
134
143
|
private
|
|
@@ -160,10 +169,47 @@ module BetterAuth
|
|
|
160
169
|
output[field] = coerce_value(value, attributes) if value_provided
|
|
161
170
|
end
|
|
162
171
|
|
|
163
|
-
output["id"] = generated_id if action == "create" && !output.key?("id")
|
|
172
|
+
output["id"] = generated_id if action == "create" && !output.key?("id") && fields.key?("id")
|
|
164
173
|
output
|
|
165
174
|
end
|
|
166
175
|
|
|
176
|
+
def create_lookup(model, input)
|
|
177
|
+
fields = schema_for(model).fetch(:fields)
|
|
178
|
+
return {field: "id", value: input.fetch("id")} if fields.key?("id") && input.key?("id")
|
|
179
|
+
|
|
180
|
+
unique_field = fields.find { |field, attributes| attributes[:unique] && input.key?(field) }
|
|
181
|
+
return {field: unique_field.first, value: input.fetch(unique_field.first)} if unique_field
|
|
182
|
+
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def record_lookup(model, record)
|
|
187
|
+
fields = schema_for(model).fetch(:fields)
|
|
188
|
+
return {field: "id", value: record.fetch("id")} if fields.key?("id") && record.key?("id")
|
|
189
|
+
|
|
190
|
+
unique_field = fields.find { |field, attributes| attributes[:unique] && record.key?(field) }
|
|
191
|
+
return {field: unique_field.first, value: record.fetch(unique_field.first)} if unique_field
|
|
192
|
+
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def ensure_update_data!(data)
|
|
197
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") if data.empty?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def ensure_update_input_has_fields!(model, update)
|
|
201
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless update.is_a?(Hash)
|
|
202
|
+
|
|
203
|
+
fields = schema_for(model).fetch(:fields)
|
|
204
|
+
input = stringify_keys(update)
|
|
205
|
+
has_updatable_field = input.any? do |field, _value|
|
|
206
|
+
next false if field == "id" || field == "_id"
|
|
207
|
+
|
|
208
|
+
fields.key?(field) || fields.any? { |logical_field, attributes| storage_key(attributes[:field_name] || logical_field) == field }
|
|
209
|
+
end
|
|
210
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless has_updatable_field
|
|
211
|
+
end
|
|
212
|
+
|
|
167
213
|
def select_sql(model, select, join)
|
|
168
214
|
fields = Array(select).empty? ? schema_for(model).fetch(:fields).keys : Array(select).map { |field| storage_key(field) }
|
|
169
215
|
columns = fields.map do |field|
|
|
@@ -226,28 +272,35 @@ module BetterAuth
|
|
|
226
272
|
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
227
273
|
value = fetch_key(clause, :value)
|
|
228
274
|
attributes = schema_for(model).fetch(:fields).fetch(field)
|
|
275
|
+
insensitive = insensitive_string_predicate?(clause, attributes)
|
|
276
|
+
predicate_column = insensitive ? "LOWER(#{column})" : column
|
|
229
277
|
|
|
230
|
-
expression =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
placeholders = values.map do |entry|
|
|
234
|
-
params << entry
|
|
235
|
-
placeholder(params.length)
|
|
236
|
-
end.join(", ")
|
|
237
|
-
sql_operator = (operator == "not_in") ? "NOT IN" : "IN"
|
|
238
|
-
"#{column} #{sql_operator} (#{placeholders})"
|
|
239
|
-
when "contains", "starts_with", "ends_with"
|
|
240
|
-
escaped = escape_like(value)
|
|
241
|
-
pattern = case operator
|
|
242
|
-
when "starts_with" then "#{escaped}%"
|
|
243
|
-
when "ends_with" then "%#{escaped}"
|
|
244
|
-
else "%#{escaped}%"
|
|
245
|
-
end
|
|
246
|
-
params << pattern
|
|
247
|
-
"#{column} LIKE #{placeholder(params.length)} ESCAPE #{escape_literal}"
|
|
278
|
+
expression = if value.nil? && %w[eq ne].include?(operator)
|
|
279
|
+
null_operator = (operator == "ne") ? "IS NOT NULL" : "IS NULL"
|
|
280
|
+
"#{column} #{null_operator}"
|
|
248
281
|
else
|
|
249
|
-
|
|
250
|
-
"
|
|
282
|
+
case operator
|
|
283
|
+
when "in", "not_in"
|
|
284
|
+
values = Array(value).map { |entry| insensitive ? entry.to_s.downcase : coerce_where_value(entry, attributes) }
|
|
285
|
+
placeholders = values.map do |entry|
|
|
286
|
+
params << entry
|
|
287
|
+
placeholder(params.length)
|
|
288
|
+
end.join(", ")
|
|
289
|
+
sql_operator = (operator == "not_in") ? "NOT IN" : "IN"
|
|
290
|
+
"#{predicate_column} #{sql_operator} (#{placeholders})"
|
|
291
|
+
when "contains", "starts_with", "ends_with"
|
|
292
|
+
escaped = escape_like(insensitive ? value.to_s.downcase : value)
|
|
293
|
+
pattern = case operator
|
|
294
|
+
when "starts_with" then "#{escaped}%"
|
|
295
|
+
when "ends_with" then "%#{escaped}"
|
|
296
|
+
else "%#{escaped}%"
|
|
297
|
+
end
|
|
298
|
+
params << pattern
|
|
299
|
+
"#{predicate_column} LIKE #{placeholder(params.length)} ESCAPE #{escape_literal}"
|
|
300
|
+
else
|
|
301
|
+
params << (insensitive ? value.to_s.downcase : coerce_where_value(value, attributes))
|
|
302
|
+
"#{predicate_column} #{sql_operator(operator)} #{placeholder(params.length)}"
|
|
303
|
+
end
|
|
251
304
|
end
|
|
252
305
|
|
|
253
306
|
connector = (index.positive? && fetch_key(clause, :connector).to_s.upcase == "OR") ? "OR" : "AND"
|
|
@@ -286,32 +339,59 @@ module BetterAuth
|
|
|
286
339
|
}.fetch(operator, "=")
|
|
287
340
|
end
|
|
288
341
|
|
|
289
|
-
def
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
342
|
+
def insensitive_string_predicate?(clause, attributes)
|
|
343
|
+
fetch_key(clause, :mode).to_s == "insensitive" && attributes[:type] == "string"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def execute(sql, params, affected_rows_result: false)
|
|
347
|
+
@connection_lock.synchronize do
|
|
348
|
+
if connection.respond_to?(:exec_params)
|
|
349
|
+
result = connection.exec_params(sql, params)
|
|
350
|
+
return affected_rows_result ? result : [] if result.respond_to?(:fields) && result.fields.empty?
|
|
351
|
+
return result.to_a if result.respond_to?(:to_a)
|
|
352
|
+
|
|
353
|
+
result
|
|
354
|
+
elsif connection.respond_to?(:query) && params.empty?
|
|
355
|
+
result = connection.query(sql)
|
|
356
|
+
result.respond_to?(:to_a) ? result.to_a : result
|
|
357
|
+
elsif dialect == :sqlite && connection.respond_to?(:execute)
|
|
358
|
+
result = connection.execute(sql, params)
|
|
359
|
+
result.respond_to?(:to_a) ? result.to_a : result
|
|
360
|
+
elsif connection.respond_to?(:prepare)
|
|
361
|
+
statement = connection.prepare(sql)
|
|
362
|
+
result = nil
|
|
363
|
+
begin
|
|
364
|
+
result = statement.execute(*params)
|
|
365
|
+
if result.nil?
|
|
366
|
+
return [] unless affected_rows_result
|
|
367
|
+
return statement.affected_rows if statement.respond_to?(:affected_rows)
|
|
368
|
+
return connection.affected_rows if connection.respond_to?(:affected_rows)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
rows = result.respond_to?(:to_a) ? result.to_a : result
|
|
372
|
+
rows
|
|
373
|
+
ensure
|
|
374
|
+
if result.respond_to?(:close)
|
|
375
|
+
result.close
|
|
376
|
+
elsif statement.respond_to?(:close)
|
|
377
|
+
statement.close
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
elsif connection.respond_to?(:execute)
|
|
381
|
+
result = connection.execute(sql, params)
|
|
382
|
+
result.respond_to?(:to_a) ? result.to_a : result
|
|
383
|
+
else
|
|
384
|
+
raise Error, "SQL connection must respond to exec_params or prepare"
|
|
385
|
+
end
|
|
307
386
|
end
|
|
308
387
|
end
|
|
309
388
|
|
|
310
389
|
def affected_rows(result)
|
|
311
390
|
return result.cmd_tuples if result.respond_to?(:cmd_tuples)
|
|
312
391
|
return result.affected_rows if result.respond_to?(:affected_rows)
|
|
313
|
-
return connection.changes if connection.respond_to?(:changes)
|
|
314
392
|
return result.to_i if result.respond_to?(:to_i)
|
|
393
|
+
return connection.affected_rows if connection.respond_to?(:affected_rows)
|
|
394
|
+
return connection.changes if connection.respond_to?(:changes)
|
|
315
395
|
|
|
316
396
|
0
|
|
317
397
|
end
|
|
@@ -406,11 +486,11 @@ module BetterAuth
|
|
|
406
486
|
end
|
|
407
487
|
|
|
408
488
|
def escape_like(value)
|
|
409
|
-
value.to_s.gsub(/[
|
|
489
|
+
value.to_s.gsub(/[!%_]/) { |match| "!#{match}" }
|
|
410
490
|
end
|
|
411
491
|
|
|
412
492
|
def escape_literal
|
|
413
|
-
|
|
493
|
+
"'!'"
|
|
414
494
|
end
|
|
415
495
|
|
|
416
496
|
def resolve_default(default)
|
|
@@ -423,6 +503,7 @@ module BetterAuth
|
|
|
423
503
|
return value.iso8601(6) if dialect == :sqlite && attributes[:type] == "date" && value.respond_to?(:iso8601)
|
|
424
504
|
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
425
505
|
return JSON.generate(value) if json_like?(attributes) && !value.is_a?(String)
|
|
506
|
+
return value.encode(Encoding::UTF_8) if attributes[:type] == "string" && value.is_a?(String) && value.encoding == Encoding::ASCII_8BIT
|
|
426
507
|
|
|
427
508
|
value
|
|
428
509
|
end
|
|
@@ -446,6 +527,7 @@ module BetterAuth
|
|
|
446
527
|
def coerce_output_value(value, attributes)
|
|
447
528
|
return value if value.nil?
|
|
448
529
|
return coerce_boolean(value) if attributes[:type] == "boolean"
|
|
530
|
+
return coerce_number(value) if attributes[:type] == "number"
|
|
449
531
|
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
450
532
|
return parse_json_value(value) if json_like?(attributes) && value.is_a?(String)
|
|
451
533
|
|
|
@@ -75,7 +75,7 @@ module BetterAuth
|
|
|
75
75
|
@hooks = options[:hooks]
|
|
76
76
|
@on_api_error = symbolize_keys(options[:on_api_error] || options[:on_apierror] || {})
|
|
77
77
|
@telemetry = symbolize_keys(options[:telemetry] || {})
|
|
78
|
-
@social_providers =
|
|
78
|
+
@social_providers = normalize_social_providers(options[:social_providers])
|
|
79
79
|
@trusted_origins_callbacks = []
|
|
80
80
|
@trusted_origins_callbacks << options[:trusted_origins] if options[:trusted_origins].respond_to?(:call)
|
|
81
81
|
@trusted_origins_callback = combined_trusted_origins_callback
|
|
@@ -371,6 +371,12 @@ module BetterAuth
|
|
|
371
371
|
Array(value).compact.reject { |plugin| plugin == false }.map { |plugin| Plugin.coerce(plugin) }
|
|
372
372
|
end
|
|
373
373
|
|
|
374
|
+
def normalize_social_providers(value)
|
|
375
|
+
symbolize_keys(value || {}).reject do |_id, provider|
|
|
376
|
+
provider.nil? || provider == false || (provider.is_a?(Hash) && provider[:enabled] == false)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
374
380
|
def normalize_trusted_origins(value)
|
|
375
381
|
origins = []
|
|
376
382
|
origins << base_url unless base_url.nil? || base_url.empty?
|
data/lib/better_auth/cookies.rb
CHANGED
|
@@ -67,7 +67,7 @@ module BetterAuth
|
|
|
67
67
|
name, value = pair.split("=", 2)
|
|
68
68
|
next if name.to_s.empty? || value.nil?
|
|
69
69
|
|
|
70
|
-
result[name.strip] = value.strip
|
|
70
|
+
result[name.strip] = decode_cookie_value(value.strip)
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -150,12 +150,14 @@ module BetterAuth
|
|
|
150
150
|
Crypto.symmetric_decode_jwt(value, ctx.context.secret_config, "better-auth-account")
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil)
|
|
153
|
+
def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil, cookie_full_name: nil)
|
|
154
154
|
cookie_header = header_value(request_or_cookie_header)
|
|
155
155
|
return nil if cookie_header.to_s.empty?
|
|
156
156
|
|
|
157
157
|
parsed = parse_cookies(cookie_header)
|
|
158
|
-
name = if
|
|
158
|
+
name = if cookie_full_name
|
|
159
|
+
cookie_full_name
|
|
160
|
+
elsif is_secure.nil?
|
|
159
161
|
production_environment? ? "#{SECURE_COOKIE_PREFIX}#{cookie_prefix}.#{cookie_name}" : "#{cookie_prefix}.#{cookie_name}"
|
|
160
162
|
else
|
|
161
163
|
secure_prefix = is_secure ? SECURE_COOKIE_PREFIX : ""
|
|
@@ -248,6 +250,12 @@ module BetterAuth
|
|
|
248
250
|
chunks.sort_by(&:first).map(&:last).join
|
|
249
251
|
end
|
|
250
252
|
|
|
253
|
+
def decode_cookie_value(value)
|
|
254
|
+
URI.decode_uri_component(value)
|
|
255
|
+
rescue ArgumentError
|
|
256
|
+
value
|
|
257
|
+
end
|
|
258
|
+
|
|
251
259
|
def header_value(request_or_cookie_header)
|
|
252
260
|
return request_or_cookie_header.headers["cookie"] if request_or_cookie_header.respond_to?(:headers)
|
|
253
261
|
return request_or_cookie_header.get_header("HTTP_COOKIE") if request_or_cookie_header.respond_to?(:get_header)
|