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
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "better_auth/migration_plan"
|
|
5
|
+
|
|
6
|
+
module BetterAuth
|
|
7
|
+
module SQLMigration
|
|
8
|
+
DEFAULT_MIGRATIONS_PATH = "db/better_auth/migrate"
|
|
9
|
+
MISSING_MIGRATIONS_TABLE_MESSAGES = [
|
|
10
|
+
/no such table/i,
|
|
11
|
+
/relation .* does not exist/i,
|
|
12
|
+
/table .* doesn't exist/i,
|
|
13
|
+
/undefined table/i,
|
|
14
|
+
/invalid object name/i
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
class UnsupportedAdapterError < StandardError; end
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def render(options, dialect:, generator:)
|
|
22
|
+
dialect = normalize_dialect(dialect)
|
|
23
|
+
config = configuration_for(options)
|
|
24
|
+
statements = BetterAuth::Schema::SQL.create_statements(config, dialect: dialect)
|
|
25
|
+
[
|
|
26
|
+
"-- Generated by #{generator}",
|
|
27
|
+
"-- Dialect: #{dialect}",
|
|
28
|
+
"",
|
|
29
|
+
statements.join("\n\n"),
|
|
30
|
+
""
|
|
31
|
+
].join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def plan(options, connection:, dialect:)
|
|
35
|
+
dialect = normalize_dialect(dialect)
|
|
36
|
+
plan_from_existing(options, existing: current_schema(connection, dialect), dialect: dialect)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def plan_from_existing(options, existing:, dialect:)
|
|
40
|
+
dialect = normalize_dialect(dialect)
|
|
41
|
+
config = configuration_for(options)
|
|
42
|
+
desired = BetterAuth::Schema.auth_tables(config)
|
|
43
|
+
to_create = []
|
|
44
|
+
to_add = []
|
|
45
|
+
to_index = []
|
|
46
|
+
warnings = []
|
|
47
|
+
|
|
48
|
+
desired.each do |logical_name, table|
|
|
49
|
+
table_name = table.fetch(:model_name)
|
|
50
|
+
existing_table = existing[table_name]
|
|
51
|
+
|
|
52
|
+
unless existing_table
|
|
53
|
+
to_create << BetterAuth::MigrationPlan::TableChange.new(
|
|
54
|
+
logical_name: logical_name,
|
|
55
|
+
table_name: table_name,
|
|
56
|
+
table: table,
|
|
57
|
+
order: table[:order] || Float::INFINITY
|
|
58
|
+
)
|
|
59
|
+
table.fetch(:fields).each do |field, attributes|
|
|
60
|
+
next unless indexable_field?(attributes, dialect)
|
|
61
|
+
|
|
62
|
+
column = attributes[:field_name] || physical_name(field)
|
|
63
|
+
to_index << index_change(table_name, column, attributes, dialect: dialect, unique: !!attributes[:unique])
|
|
64
|
+
end
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
missing_fields = {}
|
|
69
|
+
table.fetch(:fields).each do |field, attributes|
|
|
70
|
+
column = attributes[:field_name] || physical_name(field)
|
|
71
|
+
existing_type = existing_table.fetch(:columns)[column]
|
|
72
|
+
if existing_type.nil?
|
|
73
|
+
missing_fields[field] = attributes
|
|
74
|
+
elsif !matching_type?(existing_type, attributes[:type], dialect)
|
|
75
|
+
warnings << "Type mismatch for #{table_name}.#{column}: expected #{attributes[:type]} but found #{existing_type.to_s.downcase}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
next unless attributes[:index] || attributes[:unique]
|
|
79
|
+
next if index_present?(existing_table, column, unique: !!attributes[:unique])
|
|
80
|
+
|
|
81
|
+
to_index << index_change(table_name, column, attributes, dialect: dialect, unique: !!attributes[:unique])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
next if missing_fields.empty?
|
|
85
|
+
|
|
86
|
+
to_add << BetterAuth::MigrationPlan::FieldChange.new(
|
|
87
|
+
logical_name: logical_name,
|
|
88
|
+
table_name: table_name,
|
|
89
|
+
fields: missing_fields,
|
|
90
|
+
table: table,
|
|
91
|
+
order: table[:order] || Float::INFINITY
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
BetterAuth::MigrationPlan::Plan.new(
|
|
96
|
+
to_create: to_create.sort_by(&:order),
|
|
97
|
+
to_add: to_add.sort_by(&:order),
|
|
98
|
+
to_index: to_index,
|
|
99
|
+
warnings: warnings,
|
|
100
|
+
dialect: dialect,
|
|
101
|
+
tables: desired
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_pending(options, connection:, dialect:, generator:)
|
|
106
|
+
dialect = normalize_dialect(dialect)
|
|
107
|
+
migration_plan = plan(options, connection: connection, dialect: dialect)
|
|
108
|
+
statements = BetterAuth::Schema::SQL.pending_statements(migration_plan)
|
|
109
|
+
return "" if statements.empty?
|
|
110
|
+
|
|
111
|
+
[
|
|
112
|
+
"-- Generated by #{generator}",
|
|
113
|
+
"-- Dialect: #{dialect}",
|
|
114
|
+
"",
|
|
115
|
+
statements.join("\n\n"),
|
|
116
|
+
""
|
|
117
|
+
].join("\n")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def generate(options, dialect:, generator:, migrations_path: DEFAULT_MIGRATIONS_PATH, timestamp: Time.now.utc.strftime("%Y%m%d%H%M%S"), connection: nil)
|
|
121
|
+
dialect = normalize_dialect(dialect)
|
|
122
|
+
FileUtils.mkdir_p(migrations_path)
|
|
123
|
+
path = File.join(migrations_path, "#{timestamp}_create_better_auth_tables.sql")
|
|
124
|
+
return path if File.exist?(path)
|
|
125
|
+
|
|
126
|
+
sql = if connection
|
|
127
|
+
render_pending(options, connection: connection, dialect: dialect, generator: generator)
|
|
128
|
+
else
|
|
129
|
+
render(options, dialect: dialect, generator: generator)
|
|
130
|
+
end
|
|
131
|
+
return nil if sql.empty?
|
|
132
|
+
|
|
133
|
+
File.write(path, sql)
|
|
134
|
+
path
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def migrate(auth_or_options, migrations_path: DEFAULT_MIGRATIONS_PATH)
|
|
138
|
+
auth = auth_for(auth_or_options)
|
|
139
|
+
adapter = auth.context.adapter
|
|
140
|
+
unless adapter.respond_to?(:dialect) && adapter.respond_to?(:connection)
|
|
141
|
+
raise UnsupportedAdapterError, "Better Auth SQL migrations require core SQL adapters with connection and dialect support"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
connection = adapter.connection
|
|
145
|
+
dialect = normalize_dialect(adapter.dialect)
|
|
146
|
+
files = Dir[File.join(migrations_path, "*.sql")].sort
|
|
147
|
+
ensure_schema_migrations!(connection, dialect)
|
|
148
|
+
applied = applied_migrations(connection, dialect)
|
|
149
|
+
|
|
150
|
+
files.reject { |file| applied.include?(File.basename(file)) }.each do |file|
|
|
151
|
+
execute_sql(connection, File.read(file))
|
|
152
|
+
record_migration(connection, dialect, File.basename(file))
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def migrate_pending(auth_or_options)
|
|
157
|
+
auth = auth_for(auth_or_options)
|
|
158
|
+
adapter = auth.context.adapter
|
|
159
|
+
unless adapter.respond_to?(:dialect) && adapter.respond_to?(:connection)
|
|
160
|
+
raise UnsupportedAdapterError, "Better Auth SQL migrations require core SQL adapters with connection and dialect support"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
connection = adapter.connection
|
|
164
|
+
dialect = normalize_dialect(adapter.dialect)
|
|
165
|
+
sql = render_pending(auth.options, connection: connection, dialect: dialect, generator: "better_auth")
|
|
166
|
+
return false if sql.empty?
|
|
167
|
+
|
|
168
|
+
if adapter.respond_to?(:transaction)
|
|
169
|
+
adapter.transaction { execute_sql(connection, sql) }
|
|
170
|
+
else
|
|
171
|
+
execute_sql(connection, sql)
|
|
172
|
+
end
|
|
173
|
+
true
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def configuration_for(options)
|
|
177
|
+
return options.options if options.is_a?(BetterAuth::Auth)
|
|
178
|
+
return options if options.is_a?(BetterAuth::Configuration)
|
|
179
|
+
|
|
180
|
+
BetterAuth::Configuration.new(options)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def auth_for(value)
|
|
184
|
+
return value if value.is_a?(BetterAuth::Auth)
|
|
185
|
+
|
|
186
|
+
BetterAuth.auth(value)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def ensure_schema_migrations!(connection, dialect)
|
|
190
|
+
sql = case dialect
|
|
191
|
+
when :postgres, :sqlite
|
|
192
|
+
%(CREATE TABLE IF NOT EXISTS #{quote("better_auth_schema_migrations", dialect)} (#{quote("version", dialect)} text PRIMARY KEY);)
|
|
193
|
+
when :mysql
|
|
194
|
+
%(CREATE TABLE IF NOT EXISTS #{quote("better_auth_schema_migrations", dialect)} (#{quote("version", dialect)} varchar(191) PRIMARY KEY) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;)
|
|
195
|
+
when :mssql
|
|
196
|
+
%(IF OBJECT_ID(N'#{quote("better_auth_schema_migrations", dialect)}', N'U') IS NULL CREATE TABLE #{quote("better_auth_schema_migrations", dialect)} (#{quote("version", dialect)} varchar(255) PRIMARY KEY);)
|
|
197
|
+
else
|
|
198
|
+
raise UnsupportedAdapterError, "Unsupported SQL dialect for Better Auth migrations: #{dialect}"
|
|
199
|
+
end
|
|
200
|
+
execute_sql(connection, sql)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def applied_migrations(connection, dialect)
|
|
204
|
+
rows = execute_sql(connection, "SELECT #{quote("version", dialect)} FROM #{quote("better_auth_schema_migrations", dialect)};")
|
|
205
|
+
Array(rows).map { |row| row["version"] || row[:version] }
|
|
206
|
+
rescue UnsupportedAdapterError
|
|
207
|
+
raise
|
|
208
|
+
rescue => error
|
|
209
|
+
raise error unless missing_schema_migrations_table?(error)
|
|
210
|
+
|
|
211
|
+
[]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def record_migration(connection, dialect, version)
|
|
215
|
+
sql = "INSERT INTO #{quote("better_auth_schema_migrations", dialect)} (#{quote("version", dialect)}) VALUES (#{literal(version)});"
|
|
216
|
+
execute_sql(connection, sql)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def execute_sql(connection, sql)
|
|
220
|
+
statements(sql).each_with_object([]) do |statement, results|
|
|
221
|
+
result =
|
|
222
|
+
if connection.respond_to?(:exec)
|
|
223
|
+
connection.exec(statement)
|
|
224
|
+
elsif connection.respond_to?(:execute)
|
|
225
|
+
connection.execute(statement)
|
|
226
|
+
elsif connection.respond_to?(:query)
|
|
227
|
+
connection.query(statement)
|
|
228
|
+
else
|
|
229
|
+
raise UnsupportedAdapterError, "SQL connection does not support exec, execute, or query"
|
|
230
|
+
end
|
|
231
|
+
results.concat(result.to_a) if result.respond_to?(:to_a)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def statements(sql)
|
|
236
|
+
normalized = sql.to_s.gsub("\r\n", "\n").strip
|
|
237
|
+
return [] if normalized.empty?
|
|
238
|
+
|
|
239
|
+
split_sql_statements(normalized)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def split_sql_statements(sql)
|
|
243
|
+
output = []
|
|
244
|
+
buffer = +""
|
|
245
|
+
index = 0
|
|
246
|
+
quote = nil
|
|
247
|
+
line_comment = false
|
|
248
|
+
block_comment = false
|
|
249
|
+
dollar_tag = nil
|
|
250
|
+
|
|
251
|
+
while index < sql.length
|
|
252
|
+
state = {
|
|
253
|
+
quote: quote,
|
|
254
|
+
line_comment: line_comment,
|
|
255
|
+
block_comment: block_comment,
|
|
256
|
+
dollar_tag: dollar_tag
|
|
257
|
+
}
|
|
258
|
+
index, quote, line_comment, block_comment, dollar_tag = scan_sql_character(sql, buffer, index, state, output)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
tail = buffer.strip
|
|
262
|
+
output << tail unless tail.empty?
|
|
263
|
+
output
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def scan_sql_character(sql, buffer, index, state, output)
|
|
267
|
+
char = sql[index]
|
|
268
|
+
next_char = sql[index + 1]
|
|
269
|
+
quote = state[:quote]
|
|
270
|
+
line_comment = state[:line_comment]
|
|
271
|
+
block_comment = state[:block_comment]
|
|
272
|
+
dollar_tag = state[:dollar_tag]
|
|
273
|
+
|
|
274
|
+
return scan_line_comment(buffer, index, char) + [quote, false, block_comment, dollar_tag] if line_comment && char == "\n"
|
|
275
|
+
return scan_line_comment(buffer, index, char) + [quote, true, block_comment, dollar_tag] if line_comment
|
|
276
|
+
return scan_block_comment(buffer, index, char, next_char, quote, line_comment, dollar_tag) if block_comment
|
|
277
|
+
return scan_dollar_quote(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag) if dollar_tag
|
|
278
|
+
return scan_quoted_string(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag) if quote
|
|
279
|
+
return [index + 2, quote, true, block_comment, dollar_tag].tap { buffer << char << next_char } if char == "-" && next_char == "-"
|
|
280
|
+
return [index + 2, quote, line_comment, true, dollar_tag].tap { buffer << char << next_char } if char == "/" && next_char == "*"
|
|
281
|
+
|
|
282
|
+
tag = dollar_quote_tag_at(sql, index)
|
|
283
|
+
return [index + tag.length, quote, line_comment, block_comment, tag].tap { buffer << tag } if tag
|
|
284
|
+
return [index + 1, char, line_comment, block_comment, dollar_tag].tap { buffer << char } if char == "'" || char == "\""
|
|
285
|
+
|
|
286
|
+
if char == ";"
|
|
287
|
+
statement = buffer.strip
|
|
288
|
+
output << statement unless statement.empty?
|
|
289
|
+
buffer.clear
|
|
290
|
+
return [index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
buffer << char
|
|
294
|
+
[index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def scan_line_comment(buffer, index, char)
|
|
298
|
+
buffer << char
|
|
299
|
+
[index + 1]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def scan_block_comment(buffer, index, char, next_char, quote, line_comment, dollar_tag)
|
|
303
|
+
buffer << char
|
|
304
|
+
if char == "*" && next_char == "/"
|
|
305
|
+
buffer << next_char
|
|
306
|
+
[index + 2, quote, line_comment, false, dollar_tag]
|
|
307
|
+
else
|
|
308
|
+
[index + 1, quote, line_comment, true, dollar_tag]
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def scan_dollar_quote(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag)
|
|
313
|
+
if sql[index, dollar_tag.length] == dollar_tag
|
|
314
|
+
buffer << dollar_tag
|
|
315
|
+
[index + dollar_tag.length, quote, line_comment, block_comment, nil]
|
|
316
|
+
else
|
|
317
|
+
buffer << char
|
|
318
|
+
[index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def scan_quoted_string(sql, buffer, index, char, quote, line_comment, block_comment, dollar_tag)
|
|
323
|
+
buffer << char
|
|
324
|
+
if char == quote && sql[index + 1] == quote
|
|
325
|
+
buffer << sql[index + 1]
|
|
326
|
+
[index + 2, quote, line_comment, block_comment, dollar_tag]
|
|
327
|
+
elsif char == quote
|
|
328
|
+
[index + 1, nil, line_comment, block_comment, dollar_tag]
|
|
329
|
+
else
|
|
330
|
+
[index + 1, quote, line_comment, block_comment, dollar_tag]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def dollar_quote_tag_at(sql, index)
|
|
335
|
+
match = sql[index..]&.match(/\A\$[A-Za-z_][A-Za-z0-9_]*\$|\A\$\$/)
|
|
336
|
+
match&.[](0)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def missing_schema_migrations_table?(error)
|
|
340
|
+
message = error.message.to_s
|
|
341
|
+
MISSING_MIGRATIONS_TABLE_MESSAGES.any? { |pattern| message.match?(pattern) }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def quote(identifier, dialect)
|
|
345
|
+
BetterAuth::Schema::SQL.quote(identifier, dialect)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def literal(value)
|
|
349
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def normalize_dialect(value)
|
|
353
|
+
dialect = case value.to_s.downcase
|
|
354
|
+
when "postgresql"
|
|
355
|
+
:postgres
|
|
356
|
+
when "sqlite3"
|
|
357
|
+
:sqlite
|
|
358
|
+
else
|
|
359
|
+
value.to_sym
|
|
360
|
+
end
|
|
361
|
+
return dialect if [:postgres, :sqlite, :mysql, :mssql].include?(dialect)
|
|
362
|
+
|
|
363
|
+
raise UnsupportedAdapterError, "Unsupported SQL dialect for Better Auth migrations: #{dialect}"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def current_schema(connection, dialect)
|
|
367
|
+
case dialect
|
|
368
|
+
when :sqlite
|
|
369
|
+
sqlite_schema(connection)
|
|
370
|
+
when :postgres
|
|
371
|
+
information_schema(connection, dialect, postgres_columns_sql, postgres_indexes_sql)
|
|
372
|
+
when :mysql
|
|
373
|
+
information_schema(connection, dialect, mysql_columns_sql, mysql_indexes_sql)
|
|
374
|
+
when :mssql
|
|
375
|
+
information_schema(connection, dialect, mssql_columns_sql, mssql_indexes_sql)
|
|
376
|
+
else
|
|
377
|
+
raise UnsupportedAdapterError, "Unsupported SQL dialect for Better Auth migrations: #{dialect}"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def sqlite_schema(connection)
|
|
382
|
+
table_rows = execute_sql(connection, "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';")
|
|
383
|
+
table_rows.each_with_object({}) do |row, schema|
|
|
384
|
+
table_name = row["name"] || row[:name]
|
|
385
|
+
columns = execute_sql(connection, "PRAGMA table_info(#{quote(table_name, :sqlite)});").each_with_object({}) do |column, result|
|
|
386
|
+
result[column["name"] || column[:name]] = column["type"] || column[:type]
|
|
387
|
+
end
|
|
388
|
+
indexes = empty_index_metadata
|
|
389
|
+
execute_sql(connection, "PRAGMA index_list(#{quote(table_name, :sqlite)});").each do |index|
|
|
390
|
+
index_name = index["name"] || index[:name]
|
|
391
|
+
unique = (index["unique"] || index[:unique]).to_i == 1
|
|
392
|
+
indexes[:names] << index_name
|
|
393
|
+
execute_sql(connection, "PRAGMA index_info(#{quote(index_name, :sqlite)});").each do |field|
|
|
394
|
+
column = field["name"] || field[:name]
|
|
395
|
+
indexes[:columns] << column
|
|
396
|
+
indexes[:unique_columns] << column if unique
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
schema[table_name] = {name: table_name, columns: columns, indexes: indexes}
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def information_schema(connection, dialect, columns_sql, indexes_sql)
|
|
404
|
+
schema = {}
|
|
405
|
+
execute_sql(connection, columns_sql).each do |row|
|
|
406
|
+
table_name = row_value(row, "table_name")
|
|
407
|
+
column_name = row_value(row, "column_name")
|
|
408
|
+
data_type = row_value(row, "data_type")
|
|
409
|
+
schema[table_name] ||= {name: table_name, columns: {}, indexes: empty_index_metadata}
|
|
410
|
+
schema[table_name][:columns][column_name] = data_type
|
|
411
|
+
end
|
|
412
|
+
execute_sql(connection, indexes_sql).each do |row|
|
|
413
|
+
table_name = row_value(row, "table_name")
|
|
414
|
+
column_name = row_value(row, "column_name")
|
|
415
|
+
index_name = row_value(row, "index_name")
|
|
416
|
+
unique_value = row_value(row, "unique") || row_value(row, "is_unique")
|
|
417
|
+
non_unique_value = row_value(row, "non_unique")
|
|
418
|
+
unique = (!unique_value.nil?) ? truthy_database_value?(unique_value) : non_unique_value.to_i == 0
|
|
419
|
+
schema[table_name] ||= {name: table_name, columns: {}, indexes: empty_index_metadata}
|
|
420
|
+
schema[table_name][:indexes][:names] << index_name if index_name
|
|
421
|
+
schema[table_name][:indexes][:columns] << column_name if column_name
|
|
422
|
+
schema[table_name][:indexes][:unique_columns] << column_name if column_name && !!unique
|
|
423
|
+
end
|
|
424
|
+
schema
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def row_value(row, key)
|
|
428
|
+
candidates = [
|
|
429
|
+
key,
|
|
430
|
+
key.to_sym,
|
|
431
|
+
key.upcase,
|
|
432
|
+
key.upcase.to_sym,
|
|
433
|
+
camelize_lower(key),
|
|
434
|
+
camelize_lower(key).to_sym
|
|
435
|
+
]
|
|
436
|
+
candidates.each { |candidate| return row[candidate] if row.key?(candidate) }
|
|
437
|
+
nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def truthy_database_value?(value)
|
|
441
|
+
return value if value == true || value == false
|
|
442
|
+
|
|
443
|
+
value.to_i != 0
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def camelize_lower(value)
|
|
447
|
+
parts = value.to_s.split("_")
|
|
448
|
+
([parts.first] + parts.drop(1).map(&:capitalize)).join
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def empty_index_metadata
|
|
452
|
+
{names: Set.new, columns: Set.new, unique_columns: Set.new}
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def index_change(table_name, column, attributes, dialect:, unique:)
|
|
456
|
+
BetterAuth::MigrationPlan::IndexChange.new(
|
|
457
|
+
table_name: table_name,
|
|
458
|
+
field_name: column,
|
|
459
|
+
name: index_name(table_name, column, attributes, dialect),
|
|
460
|
+
unique: unique,
|
|
461
|
+
field: attributes
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def indexable_field?(attributes, dialect)
|
|
466
|
+
attributes[:index] || filtered_unique_index?(attributes, dialect)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def filtered_unique_index?(attributes, dialect)
|
|
470
|
+
dialect == :mssql && attributes[:unique] && !attributes[:required]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def index_name(table_name, column, attributes, dialect)
|
|
474
|
+
filtered_unique_index?(attributes, dialect) ? "uniq_#{table_name}_#{column}" : "index_#{table_name}_on_#{column}"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def index_present?(table, column, unique:)
|
|
478
|
+
indexes = table.fetch(:indexes)
|
|
479
|
+
if unique
|
|
480
|
+
indexes[:unique_columns].include?(column) || indexes[:names].include?("index_#{table.fetch(:name, "")}_on_#{column}")
|
|
481
|
+
else
|
|
482
|
+
indexes[:columns].include?(column)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def matching_type?(actual_type, expected_type, dialect)
|
|
487
|
+
normalized = actual_type.to_s.downcase.split("(").first.strip
|
|
488
|
+
type = expected_type.to_s
|
|
489
|
+
expected = case type
|
|
490
|
+
when "string"
|
|
491
|
+
%w[text varchar character varying char uuid nvarchar uniqueidentifier]
|
|
492
|
+
when "number"
|
|
493
|
+
%w[integer int int4 bigint smallint numeric real decimal float double]
|
|
494
|
+
when "boolean"
|
|
495
|
+
%w[bool boolean integer tinyint bit smallint]
|
|
496
|
+
when "date"
|
|
497
|
+
%w[timestamptz timestamp date datetime datetime2]
|
|
498
|
+
when "json", "string[]", "number[]"
|
|
499
|
+
(dialect == :postgres) ? %w[json jsonb text] : %w[json text varchar nvarchar]
|
|
500
|
+
else
|
|
501
|
+
[normalized]
|
|
502
|
+
end
|
|
503
|
+
expected.include?(normalized)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def postgres_columns_sql
|
|
507
|
+
<<~SQL
|
|
508
|
+
SELECT table_name, column_name, data_type
|
|
509
|
+
FROM information_schema.columns
|
|
510
|
+
WHERE table_schema = current_schema();
|
|
511
|
+
SQL
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def postgres_indexes_sql
|
|
515
|
+
<<~SQL
|
|
516
|
+
SELECT t.relname AS table_name, a.attname AS column_name, i.relname AS index_name, ix.indisunique AS unique
|
|
517
|
+
FROM pg_class t
|
|
518
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
519
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
520
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
521
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
522
|
+
WHERE t.relkind = 'r' AND n.nspname = current_schema();
|
|
523
|
+
SQL
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def mysql_columns_sql
|
|
527
|
+
<<~SQL
|
|
528
|
+
SELECT table_name, column_name, data_type
|
|
529
|
+
FROM information_schema.columns
|
|
530
|
+
WHERE table_schema = DATABASE();
|
|
531
|
+
SQL
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def mysql_indexes_sql
|
|
535
|
+
<<~SQL
|
|
536
|
+
SELECT table_name, column_name, index_name, CASE WHEN non_unique = 0 THEN 1 ELSE 0 END AS is_unique
|
|
537
|
+
FROM information_schema.statistics
|
|
538
|
+
WHERE table_schema = DATABASE();
|
|
539
|
+
SQL
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def mssql_columns_sql
|
|
543
|
+
<<~SQL
|
|
544
|
+
SELECT t.name AS table_name, c.name AS column_name, ty.name AS data_type
|
|
545
|
+
FROM sys.tables t
|
|
546
|
+
JOIN sys.columns c ON c.object_id = t.object_id
|
|
547
|
+
JOIN sys.types ty ON ty.user_type_id = c.user_type_id;
|
|
548
|
+
SQL
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def mssql_indexes_sql
|
|
552
|
+
<<~SQL
|
|
553
|
+
SELECT t.name AS table_name, c.name AS column_name, i.name AS index_name, i.is_unique
|
|
554
|
+
FROM sys.tables t
|
|
555
|
+
JOIN sys.indexes i ON i.object_id = t.object_id
|
|
556
|
+
JOIN sys.index_columns ic ON ic.object_id = t.object_id AND ic.index_id = i.index_id
|
|
557
|
+
JOIN sys.columns c ON c.object_id = t.object_id AND c.column_id = ic.column_id
|
|
558
|
+
WHERE i.name IS NOT NULL;
|
|
559
|
+
SQL
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def physical_name(value)
|
|
563
|
+
BetterAuth::Schema.send(:physical_name, value)
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
end
|
|
@@ -178,7 +178,7 @@ module BetterAuth
|
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
def env_base_url(base_path)
|
|
181
|
-
url =
|
|
181
|
+
url = Env.get("BETTER_AUTH_URL")
|
|
182
182
|
url ||= ENV["BASE_URL"] if ENV["BASE_URL"] && ENV["BASE_URL"] != "/"
|
|
183
183
|
url ? with_path(url, base_path) : nil
|
|
184
184
|
end
|
data/lib/better_auth/version.rb
CHANGED
data/lib/better_auth.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "better_auth/version"
|
|
|
4
4
|
require_relative "better_auth/core"
|
|
5
5
|
require_relative "better_auth/error"
|
|
6
6
|
require_relative "better_auth/api_error"
|
|
7
|
+
require_relative "better_auth/env"
|
|
7
8
|
require_relative "better_auth/secret_config"
|
|
8
9
|
require_relative "better_auth/crypto"
|
|
9
10
|
require_relative "better_auth/host"
|
|
@@ -14,12 +15,15 @@ require_relative "better_auth/async"
|
|
|
14
15
|
require_relative "better_auth/deprecate"
|
|
15
16
|
require_relative "better_auth/logger"
|
|
16
17
|
require_relative "better_auth/instrumentation"
|
|
18
|
+
require_relative "better_auth/http_client"
|
|
17
19
|
require_relative "better_auth/oauth2"
|
|
18
20
|
require_relative "better_auth/password"
|
|
19
21
|
require_relative "better_auth/plugin"
|
|
20
22
|
require_relative "better_auth/configuration"
|
|
21
23
|
require_relative "better_auth/schema"
|
|
22
24
|
require_relative "better_auth/schema/sql"
|
|
25
|
+
require_relative "better_auth/migration_plan"
|
|
26
|
+
require_relative "better_auth/doctor"
|
|
23
27
|
require_relative "better_auth/adapters/base"
|
|
24
28
|
require_relative "better_auth/adapters/join_support"
|
|
25
29
|
require_relative "better_auth/adapters/memory"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -245,8 +245,10 @@ dependencies:
|
|
|
245
245
|
- - "~>"
|
|
246
246
|
- !ruby/object:Gem::Version
|
|
247
247
|
version: '2.1'
|
|
248
|
-
description: Better Auth is
|
|
249
|
-
|
|
248
|
+
description: Better Auth Ruby is an independent, modern authentication framework for
|
|
249
|
+
Ruby, inspired by Better Auth. It is not affiliated with, maintained by, or endorsed
|
|
250
|
+
by the Better Auth project. It provides a framework-agnostic Rack core, auth routes,
|
|
251
|
+
sessions, adapters, and a plugin system.
|
|
250
252
|
email:
|
|
251
253
|
- sebastian.sala.tech@gmail.com
|
|
252
254
|
executables: []
|
|
@@ -279,12 +281,16 @@ files:
|
|
|
279
281
|
- lib/better_auth/crypto/jwe.rb
|
|
280
282
|
- lib/better_auth/database_hooks.rb
|
|
281
283
|
- lib/better_auth/deprecate.rb
|
|
284
|
+
- lib/better_auth/doctor.rb
|
|
282
285
|
- lib/better_auth/endpoint.rb
|
|
286
|
+
- lib/better_auth/env.rb
|
|
283
287
|
- lib/better_auth/error.rb
|
|
284
288
|
- lib/better_auth/host.rb
|
|
289
|
+
- lib/better_auth/http_client.rb
|
|
285
290
|
- lib/better_auth/instrumentation.rb
|
|
286
291
|
- lib/better_auth/logger.rb
|
|
287
292
|
- lib/better_auth/middleware/origin_check.rb
|
|
293
|
+
- lib/better_auth/migration_plan.rb
|
|
288
294
|
- lib/better_auth/oauth2.rb
|
|
289
295
|
- lib/better_auth/password.rb
|
|
290
296
|
- lib/better_auth/plugin.rb
|
|
@@ -397,16 +403,17 @@ files:
|
|
|
397
403
|
- lib/better_auth/social_providers/vk.rb
|
|
398
404
|
- lib/better_auth/social_providers/wechat.rb
|
|
399
405
|
- lib/better_auth/social_providers/zoom.rb
|
|
406
|
+
- lib/better_auth/sql_migration.rb
|
|
400
407
|
- lib/better_auth/url_helpers.rb
|
|
401
408
|
- lib/better_auth/version.rb
|
|
402
|
-
homepage: https://github.com/sebasxsala/better-auth
|
|
409
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
403
410
|
licenses:
|
|
404
411
|
- MIT
|
|
405
412
|
metadata:
|
|
406
|
-
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
407
|
-
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
408
|
-
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth/CHANGELOG.md
|
|
409
|
-
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
413
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
414
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
415
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth/CHANGELOG.md
|
|
416
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
410
417
|
rdoc_options: []
|
|
411
418
|
require_paths:
|
|
412
419
|
- lib
|