better_auth 0.7.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 +10 -1
- data/README.md +6 -6
- data/lib/better_auth/adapters/memory.rb +131 -17
- data/lib/better_auth/adapters/mongodb.rb +3 -3
- data/lib/better_auth/adapters/sql.rb +139 -57
- data/lib/better_auth/auth.rb +68 -1
- data/lib/better_auth/configuration.rb +15 -10
- data/lib/better_auth/context.rb +1 -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/env.rb +38 -0
- 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/url_helpers.rb +1 -1
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +4 -0
- metadata +15 -8
|
@@ -12,6 +12,31 @@ module BetterAuth
|
|
|
12
12
|
statements.concat(tables.flat_map { |_logical_name, table| index_statements(table, dialect) })
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def pending_statements(plan)
|
|
16
|
+
statements = plan.to_create.map do |change|
|
|
17
|
+
create_table_statement(change.logical_name, change.table, plan.dialect, plan.tables)
|
|
18
|
+
end
|
|
19
|
+
statements.concat(plan.to_add.flat_map do |change|
|
|
20
|
+
change.fields.map do |logical_field, attributes|
|
|
21
|
+
if logical_field.to_s == "id" && plan.dialect == :postgres
|
|
22
|
+
add_postgres_id_column_statements(change.table_name)
|
|
23
|
+
else
|
|
24
|
+
add_column_statement(change.table_name, logical_field, attributes, plan.dialect)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end.flatten)
|
|
28
|
+
statements.concat(plan.to_index.map do |change|
|
|
29
|
+
index_statement(
|
|
30
|
+
change.table_name,
|
|
31
|
+
change.field_name,
|
|
32
|
+
change.name,
|
|
33
|
+
plan.dialect,
|
|
34
|
+
unique: change.unique,
|
|
35
|
+
where_not_null: filtered_unique_index?(change.field, plan.dialect)
|
|
36
|
+
)
|
|
37
|
+
end)
|
|
38
|
+
end
|
|
39
|
+
|
|
15
40
|
def create_table_statement(logical_name, table, dialect, tables = nil)
|
|
16
41
|
table_name = table.fetch(:model_name)
|
|
17
42
|
columns = table.fetch(:fields).map do |logical_field, attributes|
|
|
@@ -28,7 +53,7 @@ module BetterAuth
|
|
|
28
53
|
when :mysql
|
|
29
54
|
%(CREATE TABLE IF NOT EXISTS #{quote(table_name, dialect)} (\n #{body}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;)
|
|
30
55
|
when :mssql
|
|
31
|
-
%(
|
|
56
|
+
%(#{mssql_required_set_options}\nIF OBJECT_ID(N'#{quote(table_name, dialect)}', N'U') IS NULL\nCREATE TABLE #{quote(table_name, dialect)} (\n #{body}\n);)
|
|
32
57
|
else
|
|
33
58
|
raise ArgumentError, "Unsupported SQL dialect: #{dialect}"
|
|
34
59
|
end
|
|
@@ -52,7 +77,7 @@ module BetterAuth
|
|
|
52
77
|
constraints = []
|
|
53
78
|
column = attributes[:field_name] || physical_name(logical_field)
|
|
54
79
|
|
|
55
|
-
if attributes[:unique] && logical_field != "id"
|
|
80
|
+
if attributes[:unique] && logical_field != "id" && !(dialect == :mssql && !attributes[:required])
|
|
56
81
|
constraints << unique_constraint(table_name, column, dialect)
|
|
57
82
|
end
|
|
58
83
|
|
|
@@ -67,21 +92,62 @@ module BetterAuth
|
|
|
67
92
|
def index_statements(table, dialect)
|
|
68
93
|
table_name = table.fetch(:model_name)
|
|
69
94
|
table.fetch(:fields).filter_map do |logical_field, attributes|
|
|
70
|
-
|
|
95
|
+
nullable_unique_mssql = dialect == :mssql && attributes[:unique] && logical_field != "id" && !attributes[:required]
|
|
96
|
+
next if attributes[:unique] && !nullable_unique_mssql
|
|
97
|
+
next unless attributes[:index] || nullable_unique_mssql
|
|
71
98
|
|
|
72
99
|
column = attributes[:field_name] || Schema.physical_name(logical_field)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
unique = attributes[:unique] && dialect == :mssql
|
|
101
|
+
name = unique ? "uniq_#{table_name}_#{column}" : "index_#{table_name}_on_#{column}"
|
|
102
|
+
index_statement(table_name, column, name, dialect, unique: unique, where_not_null: filtered_unique_index?(attributes, dialect))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_column_statement(table_name, logical_field, attributes, dialect)
|
|
107
|
+
keyword = (dialect == :mssql) ? "ADD" : "ADD COLUMN"
|
|
108
|
+
%(ALTER TABLE #{quote(table_name, dialect)} #{keyword} #{column_definition(table_name, logical_field, attributes, dialect)};)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def add_postgres_id_column_statements(table_name)
|
|
112
|
+
quoted_table = quote(table_name, :postgres)
|
|
113
|
+
quoted_id = quote("id", :postgres)
|
|
114
|
+
[
|
|
115
|
+
%(ALTER TABLE #{quoted_table} ADD COLUMN #{quoted_id} text;),
|
|
116
|
+
%(UPDATE #{quoted_table} SET #{quoted_id} = md5(random()::text || clock_timestamp()::text || ctid::text) WHERE #{quoted_id} IS NULL;),
|
|
117
|
+
%(ALTER TABLE #{quoted_table} ALTER COLUMN #{quoted_id} SET NOT NULL;),
|
|
118
|
+
%(ALTER TABLE #{quoted_table} ADD PRIMARY KEY (#{quoted_id});)
|
|
119
|
+
]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def index_statement(table_name, column, name, dialect, unique: false, where_not_null: false)
|
|
123
|
+
unique_prefix = unique ? "UNIQUE " : ""
|
|
124
|
+
case dialect
|
|
125
|
+
when :postgres, :sqlite
|
|
126
|
+
%(CREATE #{unique_prefix}INDEX IF NOT EXISTS #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
127
|
+
when :mysql
|
|
128
|
+
%(CREATE #{unique_prefix}INDEX #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
129
|
+
when :mssql
|
|
130
|
+
filter = where_not_null ? " WHERE #{quote(column, dialect)} IS NOT NULL" : ""
|
|
131
|
+
%(#{mssql_required_set_options}\nIF NOT EXISTS (SELECT name FROM sys.indexes WHERE name = '#{name.gsub("'", "''")}' AND object_id = OBJECT_ID(N'#{quote(table_name, dialect)}')) CREATE #{unique_prefix}INDEX #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)})#{filter};)
|
|
82
132
|
end
|
|
83
133
|
end
|
|
84
134
|
|
|
135
|
+
def filtered_unique_index?(attributes, dialect)
|
|
136
|
+
dialect == :mssql && attributes[:unique] && !attributes[:required]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def mssql_required_set_options
|
|
140
|
+
<<~SQL.strip
|
|
141
|
+
SET ANSI_NULLS ON;
|
|
142
|
+
SET QUOTED_IDENTIFIER ON;
|
|
143
|
+
SET ANSI_WARNINGS ON;
|
|
144
|
+
SET ANSI_PADDING ON;
|
|
145
|
+
SET CONCAT_NULL_YIELDS_NULL ON;
|
|
146
|
+
SET ARITHABORT ON;
|
|
147
|
+
SET NUMERIC_ROUNDABORT OFF;
|
|
148
|
+
SQL
|
|
149
|
+
end
|
|
150
|
+
|
|
85
151
|
def sql_type(logical_field, attributes, dialect)
|
|
86
152
|
case attributes[:type]
|
|
87
153
|
when "boolean"
|
|
@@ -121,7 +187,7 @@ module BetterAuth
|
|
|
121
187
|
end
|
|
122
188
|
else
|
|
123
189
|
if dialect == :mysql
|
|
124
|
-
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references]
|
|
190
|
+
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references] || attributes[:sortable] || attributes.key?(:default_value)
|
|
125
191
|
indexed ? "varchar(191)" : "text"
|
|
126
192
|
elsif dialect == :mssql
|
|
127
193
|
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references] || attributes[:sortable]
|
|
@@ -164,8 +230,9 @@ module BetterAuth
|
|
|
164
230
|
end
|
|
165
231
|
|
|
166
232
|
def foreign_key_constraint(table_name, column, reference, dialect, tables = nil)
|
|
167
|
-
|
|
168
|
-
|
|
233
|
+
target_table = foreign_key_target_table(reference, tables)
|
|
234
|
+
target_model = target_table&.fetch(:model_name) || reference.fetch(:model)
|
|
235
|
+
target_field = foreign_key_target_field(reference, target_table)
|
|
169
236
|
on_delete = reference[:on_delete] ? " ON DELETE #{reference[:on_delete].to_s.upcase}" : ""
|
|
170
237
|
|
|
171
238
|
case dialect
|
|
@@ -178,6 +245,28 @@ module BetterAuth
|
|
|
178
245
|
end
|
|
179
246
|
end
|
|
180
247
|
|
|
248
|
+
def foreign_key_target_table(reference, tables)
|
|
249
|
+
return unless tables
|
|
250
|
+
|
|
251
|
+
model = reference.fetch(:model).to_s
|
|
252
|
+
tables.fetch(model, nil) || tables.each_value.find { |table| table.fetch(:model_name).to_s == model }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def foreign_key_target_field(reference, target_table)
|
|
256
|
+
field = reference.fetch(:field).to_s
|
|
257
|
+
return field unless target_table
|
|
258
|
+
|
|
259
|
+
fields = target_table.fetch(:fields)
|
|
260
|
+
attributes = fields.fetch(field, nil)
|
|
261
|
+
return attributes[:field_name] || physical_name(field) if attributes
|
|
262
|
+
|
|
263
|
+
if fields.each_value.any? { |data| data[:field_name].to_s == field }
|
|
264
|
+
field
|
|
265
|
+
else
|
|
266
|
+
physical_name(field)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
181
270
|
def quote(identifier, dialect)
|
|
182
271
|
case dialect
|
|
183
272
|
when :postgres, :sqlite
|
data/lib/better_auth/schema.rb
CHANGED
|
@@ -18,6 +18,7 @@ module BetterAuth
|
|
|
18
18
|
tables.delete("verification") if secondary_storage?(options) && !verification_option(options, :store_in_database)
|
|
19
19
|
tables.merge!(plugin_schema)
|
|
20
20
|
tables["rateLimit"] = rate_limit_table(options) if rate_limit_option(options, :storage) == "database"
|
|
21
|
+
ensure_id_fields!(tables)
|
|
21
22
|
tables.sort_by { |_name, table| table[:order] || Float::INFINITY }.to_h
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -121,6 +122,15 @@ module BetterAuth
|
|
|
121
122
|
}
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
private_class_method def self.ensure_id_fields!(tables)
|
|
126
|
+
tables.each_value do |table|
|
|
127
|
+
fields = table.fetch(:fields)
|
|
128
|
+
next if fields.key?("id")
|
|
129
|
+
|
|
130
|
+
table[:fields] = id_field.merge(fields)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
124
134
|
private_class_method def self.base_fields
|
|
125
135
|
id_field.merge(timestamp_fields)
|
|
126
136
|
end
|
|
@@ -162,14 +172,19 @@ module BetterAuth
|
|
|
162
172
|
schema.each do |raw_key, raw_table|
|
|
163
173
|
key = storage_key(raw_key)
|
|
164
174
|
table_data = symbolize_hash(raw_table || {})
|
|
165
|
-
existing = tables[key] || {model_name: table_data[:model_name] ||
|
|
166
|
-
existing[:model_name] = table_data[:model_name] || existing[:model_name] ||
|
|
175
|
+
existing = tables[key] || {model_name: table_data[:model_name] || physical_table_name(key), fields: {}}
|
|
176
|
+
existing[:model_name] = table_data[:model_name] || existing[:model_name] || physical_table_name(key)
|
|
167
177
|
existing[:fields] = existing[:fields].merge(normalize_fields(table_data[:fields] || {}))
|
|
178
|
+
existing[:fields] = id_field.merge(existing[:fields]) unless core_table?(key) || existing[:fields].key?("id")
|
|
168
179
|
tables[key] = existing
|
|
169
180
|
end
|
|
170
181
|
end
|
|
171
182
|
end
|
|
172
183
|
|
|
184
|
+
private_class_method def self.core_table?(key)
|
|
185
|
+
%w[user session account verification].include?(key.to_s)
|
|
186
|
+
end
|
|
187
|
+
|
|
173
188
|
private_class_method def self.normalize_fields(fields)
|
|
174
189
|
fields.each_with_object({}) do |(raw_key, raw_value), result|
|
|
175
190
|
key = storage_key(raw_key)
|
|
@@ -270,6 +285,24 @@ module BetterAuth
|
|
|
270
285
|
underscore(value.to_s)
|
|
271
286
|
end
|
|
272
287
|
|
|
288
|
+
private_class_method def self.physical_table_name(value)
|
|
289
|
+
pluralize_table_name(physical_name(value))
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private_class_method def self.pluralize_table_name(value)
|
|
293
|
+
special = {
|
|
294
|
+
"apikey" => "api_keys",
|
|
295
|
+
"api_key" => "api_keys",
|
|
296
|
+
"wallet_address" => "wallet_addresses"
|
|
297
|
+
}
|
|
298
|
+
return special.fetch(value) if special.key?(value)
|
|
299
|
+
return value if value.end_with?("s")
|
|
300
|
+
return "#{value[0...-1]}ies" if value.end_with?("y") && value.match?(/[^aeiou]y\z/)
|
|
301
|
+
return "#{value}es" if value.match?(/(s|x|z|ch|sh)\z/)
|
|
302
|
+
|
|
303
|
+
"#{value}s"
|
|
304
|
+
end
|
|
305
|
+
|
|
273
306
|
private_class_method def self.camelize_lower(value)
|
|
274
307
|
parts = underscore(value).split("_")
|
|
275
308
|
([parts.first] + parts.drop(1).map(&:capitalize)).join
|
data/lib/better_auth/session.rb
CHANGED
|
@@ -45,7 +45,8 @@ module BetterAuth
|
|
|
45
45
|
strategy: config[:strategy] || "compact",
|
|
46
46
|
version: config[:version],
|
|
47
47
|
cookie_prefix: ctx.context.options.advanced[:cookie_prefix] || "better-auth",
|
|
48
|
-
is_secure: ctx.context.auth_cookies[:session_data].name.start_with?(Cookies::SECURE_COOKIE_PREFIX)
|
|
48
|
+
is_secure: ctx.context.auth_cookies[:session_data].name.start_with?(Cookies::SECURE_COOKIE_PREFIX),
|
|
49
|
+
cookie_full_name: ctx.context.auth_cookies[:session_data].name
|
|
49
50
|
)
|
|
50
51
|
return nil unless payload
|
|
51
52
|
return nil if payload["session"]["token"] && payload["session"]["token"] != token
|
|
@@ -7,6 +7,7 @@ require "net/http"
|
|
|
7
7
|
require "openssl"
|
|
8
8
|
require "time"
|
|
9
9
|
require "uri"
|
|
10
|
+
require_relative "../http_client"
|
|
10
11
|
|
|
11
12
|
module BetterAuth
|
|
12
13
|
module SocialProviders
|
|
@@ -67,11 +68,7 @@ module BetterAuth
|
|
|
67
68
|
client_secret: client_secret
|
|
68
69
|
)
|
|
69
70
|
end,
|
|
70
|
-
verify_id_token: opts[:verify_id_token]
|
|
71
|
-
return false if opts[:disable_id_token_sign_in]
|
|
72
|
-
|
|
73
|
-
!decode_jwt_payload(token).empty?
|
|
74
|
-
end,
|
|
71
|
+
verify_id_token: opts[:verify_id_token],
|
|
75
72
|
get_user_info: lambda do |tokens|
|
|
76
73
|
custom = opts[:get_user_info]
|
|
77
74
|
profile = if custom
|
|
@@ -277,7 +274,7 @@ module BetterAuth
|
|
|
277
274
|
return nil if max_age && payload["iat"] && payload["iat"].to_i < Time.now.to_i - max_age.to_i
|
|
278
275
|
|
|
279
276
|
payload
|
|
280
|
-
rescue JWT::DecodeError, JSON::ParserError, ArgumentError, OpenSSL::PKey::PKeyError
|
|
277
|
+
rescue JWT::DecodeError, JSON::ParserError, ArgumentError, OpenSSL::PKey::PKeyError, Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError
|
|
281
278
|
nil
|
|
282
279
|
end
|
|
283
280
|
|
|
@@ -312,9 +309,7 @@ module BetterAuth
|
|
|
312
309
|
end
|
|
313
310
|
|
|
314
311
|
def request_json(uri, request)
|
|
315
|
-
|
|
316
|
-
http.request(request)
|
|
317
|
-
end
|
|
312
|
+
HTTPClient.request(uri, request)
|
|
318
313
|
end
|
|
319
314
|
|
|
320
315
|
def padded_base64(value)
|
|
@@ -28,7 +28,7 @@ module BetterAuth
|
|
|
28
28
|
},
|
|
29
29
|
**options
|
|
30
30
|
)
|
|
31
|
-
provider
|
|
31
|
+
provider.delete(:verify_id_token) unless provider[:verify_id_token]
|
|
32
32
|
provider
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -43,6 +43,8 @@ module BetterAuth
|
|
|
43
43
|
"User-Agent" => "better-auth"
|
|
44
44
|
}
|
|
45
45
|
profile = Base.get_json(user_info_endpoint, headers)
|
|
46
|
+
next nil unless profile
|
|
47
|
+
|
|
46
48
|
emails = Base.get_json(emails_endpoint, headers)
|
|
47
49
|
primary = Array(emails).find { |email| email["email"] == profile["email"] } ||
|
|
48
50
|
Array(emails).find { |email| email["primary"] } ||
|
|
@@ -26,7 +26,7 @@ module BetterAuth
|
|
|
26
26
|
},
|
|
27
27
|
**options
|
|
28
28
|
)
|
|
29
|
-
provider
|
|
29
|
+
provider.delete(:verify_id_token) unless provider[:verify_id_token]
|
|
30
30
|
provider
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -29,7 +29,7 @@ module BetterAuth
|
|
|
29
29
|
},
|
|
30
30
|
**options
|
|
31
31
|
)
|
|
32
|
-
provider
|
|
32
|
+
provider.delete(:verify_id_token) unless provider[:verify_id_token]
|
|
33
33
|
provider
|
|
34
34
|
end
|
|
35
35
|
end
|