better_auth 0.8.0 → 0.11.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 +92 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -95
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +303 -54
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +390 -63
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +48 -47
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +119 -15
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +108 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/http_client.rb +46 -0
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +85 -29
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +77 -5
- data/lib/better_auth/plugins/dub.rb +10 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +162 -140
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +44 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +233 -46
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +78 -65
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +77 -9
- data/lib/better_auth/plugins/organization.rb +546 -102
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +25 -27
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +205 -19
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +300 -133
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +44 -8
- data/lib/better_auth/routes/email_verification.rb +93 -6
- data/lib/better_auth/routes/password.rb +6 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +197 -54
- data/lib/better_auth/routes/user.rb +17 -10
- data/lib/better_auth/schema/sql.rb +189 -34
- data/lib/better_auth/schema.rb +159 -5
- data/lib/better_auth/session.rb +29 -10
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +45 -23
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/github.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +14 -41
- metadata +14 -18
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -215
- data/lib/better_auth/plugins/oidc_provider.rb +0 -710
|
@@ -7,18 +7,47 @@ module BetterAuth
|
|
|
7
7
|
|
|
8
8
|
def create_statements(options, dialect:)
|
|
9
9
|
dialect = dialect.to_sym
|
|
10
|
-
tables = Schema.
|
|
11
|
-
|
|
10
|
+
tables = Schema.migration_tables(options)
|
|
11
|
+
delete_actions = mssql_delete_actions(tables, dialect)
|
|
12
|
+
statements = tables.map do |logical_name, table|
|
|
13
|
+
create_table_statement(logical_name, table, dialect, tables, delete_actions: delete_actions)
|
|
14
|
+
end
|
|
12
15
|
statements.concat(tables.flat_map { |_logical_name, table| index_statements(table, dialect) })
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
def
|
|
18
|
+
def pending_statements(plan)
|
|
19
|
+
delete_actions = mssql_delete_actions(plan.tables, plan.dialect)
|
|
20
|
+
statements = plan.to_create.map do |change|
|
|
21
|
+
create_table_statement(change.logical_name, change.table, plan.dialect, plan.tables, delete_actions: delete_actions)
|
|
22
|
+
end
|
|
23
|
+
statements.concat(plan.to_add.flat_map do |change|
|
|
24
|
+
change.fields.map do |logical_field, attributes|
|
|
25
|
+
if logical_name(logical_field, attributes) == "id" && plan.dialect == :postgres
|
|
26
|
+
add_postgres_id_column_statements(change.table_name)
|
|
27
|
+
else
|
|
28
|
+
add_column_statement(change.table_name, logical_field, attributes, plan.dialect)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end.flatten)
|
|
32
|
+
statements.concat(plan.to_index.map do |change|
|
|
33
|
+
index_statement(
|
|
34
|
+
change.table_name,
|
|
35
|
+
change.field_name,
|
|
36
|
+
change.name,
|
|
37
|
+
plan.dialect,
|
|
38
|
+
unique: change.unique,
|
|
39
|
+
where_not_null: filtered_unique_index?(change.field, plan.dialect)
|
|
40
|
+
)
|
|
41
|
+
end)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_table_statement(logical_name, table, dialect, tables = nil, delete_actions: nil)
|
|
16
45
|
table_name = table.fetch(:model_name)
|
|
17
46
|
columns = table.fetch(:fields).map do |logical_field, attributes|
|
|
18
47
|
column_definition(table_name, logical_field, attributes, dialect)
|
|
19
48
|
end
|
|
20
49
|
constraints = table.fetch(:fields).flat_map do |logical_field, attributes|
|
|
21
|
-
field_constraints(table_name, logical_field, attributes, dialect, tables)
|
|
50
|
+
field_constraints(table_name, logical_field, attributes, dialect, tables, delete_actions: delete_actions)
|
|
22
51
|
end
|
|
23
52
|
body = (columns + constraints).join(",\n ")
|
|
24
53
|
|
|
@@ -28,7 +57,7 @@ module BetterAuth
|
|
|
28
57
|
when :mysql
|
|
29
58
|
%(CREATE TABLE IF NOT EXISTS #{quote(table_name, dialect)} (\n #{body}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;)
|
|
30
59
|
when :mssql
|
|
31
|
-
%(
|
|
60
|
+
%(#{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
61
|
else
|
|
33
62
|
raise ArgumentError, "Unsupported SQL dialect: #{dialect}"
|
|
34
63
|
end
|
|
@@ -36,8 +65,8 @@ module BetterAuth
|
|
|
36
65
|
|
|
37
66
|
def column_definition(table_name, logical_field, attributes, dialect)
|
|
38
67
|
column = quote(attributes[:field_name] || physical_name(logical_field), dialect)
|
|
39
|
-
parts = [column, sql_type(logical_field, attributes, dialect)]
|
|
40
|
-
parts << "PRIMARY KEY" if logical_field == "id"
|
|
68
|
+
parts = [column, sql_type(logical_name(logical_field, attributes), attributes, dialect)]
|
|
69
|
+
parts << "PRIMARY KEY" if logical_name(logical_field, attributes) == "id"
|
|
41
70
|
if attributes[:required]
|
|
42
71
|
parts << "NOT NULL"
|
|
43
72
|
elsif dialect == :mssql
|
|
@@ -48,17 +77,17 @@ module BetterAuth
|
|
|
48
77
|
parts.join(" ")
|
|
49
78
|
end
|
|
50
79
|
|
|
51
|
-
def field_constraints(table_name, logical_field, attributes, dialect, tables = nil)
|
|
80
|
+
def field_constraints(table_name, logical_field, attributes, dialect, tables = nil, delete_actions: nil)
|
|
52
81
|
constraints = []
|
|
53
82
|
column = attributes[:field_name] || physical_name(logical_field)
|
|
54
83
|
|
|
55
|
-
if attributes[:unique] && logical_field != "id"
|
|
84
|
+
if attributes[:unique] && logical_name(logical_field, attributes) != "id" && !(dialect == :mssql && !attributes[:required])
|
|
56
85
|
constraints << unique_constraint(table_name, column, dialect)
|
|
57
86
|
end
|
|
58
87
|
|
|
59
88
|
reference = attributes[:references]
|
|
60
89
|
if reference
|
|
61
|
-
constraints << foreign_key_constraint(table_name, column, reference, dialect, tables)
|
|
90
|
+
constraints << foreign_key_constraint(table_name, column, reference, dialect, tables, delete_actions: delete_actions)
|
|
62
91
|
end
|
|
63
92
|
|
|
64
93
|
constraints
|
|
@@ -67,23 +96,65 @@ module BetterAuth
|
|
|
67
96
|
def index_statements(table, dialect)
|
|
68
97
|
table_name = table.fetch(:model_name)
|
|
69
98
|
table.fetch(:fields).filter_map do |logical_field, attributes|
|
|
70
|
-
|
|
99
|
+
nullable_unique_mssql = dialect == :mssql && attributes[:unique] && logical_name(logical_field, attributes) != "id" && !attributes[:required]
|
|
100
|
+
next if attributes[:unique] && !nullable_unique_mssql
|
|
101
|
+
next unless attributes[:index] || nullable_unique_mssql
|
|
71
102
|
|
|
72
103
|
column = attributes[:field_name] || Schema.physical_name(logical_field)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
%(CREATE INDEX IF NOT EXISTS #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
77
|
-
when :mysql
|
|
78
|
-
%(CREATE INDEX #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
79
|
-
when :mssql
|
|
80
|
-
%(IF NOT EXISTS (SELECT name FROM sys.indexes WHERE name = '#{name.gsub("'", "''")}' AND object_id = OBJECT_ID(N'#{quote(table_name, dialect)}')) CREATE INDEX #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
81
|
-
end
|
|
104
|
+
unique = attributes[:unique] && dialect == :mssql
|
|
105
|
+
name = unique ? "uniq_#{table_name}_#{column}" : "index_#{table_name}_on_#{column}"
|
|
106
|
+
index_statement(table_name, column, name, dialect, unique: unique, where_not_null: filtered_unique_index?(attributes, dialect))
|
|
82
107
|
end
|
|
83
108
|
end
|
|
84
109
|
|
|
110
|
+
def add_column_statement(table_name, logical_field, attributes, dialect)
|
|
111
|
+
keyword = (dialect == :mssql) ? "ADD" : "ADD COLUMN"
|
|
112
|
+
%(ALTER TABLE #{quote(table_name, dialect)} #{keyword} #{column_definition(table_name, logical_field, attributes, dialect)};)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def add_postgres_id_column_statements(table_name)
|
|
116
|
+
quoted_table = quote(table_name, :postgres)
|
|
117
|
+
quoted_id = quote("id", :postgres)
|
|
118
|
+
[
|
|
119
|
+
%(ALTER TABLE #{quoted_table} ADD COLUMN #{quoted_id} text;),
|
|
120
|
+
%(UPDATE #{quoted_table} SET #{quoted_id} = md5(random()::text || clock_timestamp()::text || ctid::text) WHERE #{quoted_id} IS NULL;),
|
|
121
|
+
%(ALTER TABLE #{quoted_table} ALTER COLUMN #{quoted_id} SET NOT NULL;),
|
|
122
|
+
%(ALTER TABLE #{quoted_table} ADD PRIMARY KEY (#{quoted_id});)
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def index_statement(table_name, column, name, dialect, unique: false, where_not_null: false)
|
|
127
|
+
unique_prefix = unique ? "UNIQUE " : ""
|
|
128
|
+
case dialect
|
|
129
|
+
when :postgres, :sqlite
|
|
130
|
+
%(CREATE #{unique_prefix}INDEX IF NOT EXISTS #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
131
|
+
when :mysql
|
|
132
|
+
%(CREATE #{unique_prefix}INDEX #{quote(name, dialect)} ON #{quote(table_name, dialect)} (#{quote(column, dialect)});)
|
|
133
|
+
when :mssql
|
|
134
|
+
filter = where_not_null ? " WHERE #{quote(column, dialect)} IS NOT NULL" : ""
|
|
135
|
+
%(#{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};)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def filtered_unique_index?(attributes, dialect)
|
|
140
|
+
dialect == :mssql && attributes[:unique] && !attributes[:required]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def mssql_required_set_options
|
|
144
|
+
<<~SQL.strip
|
|
145
|
+
SET ANSI_NULLS ON;
|
|
146
|
+
SET QUOTED_IDENTIFIER ON;
|
|
147
|
+
SET ANSI_WARNINGS ON;
|
|
148
|
+
SET ANSI_PADDING ON;
|
|
149
|
+
SET CONCAT_NULL_YIELDS_NULL ON;
|
|
150
|
+
SET ARITHABORT ON;
|
|
151
|
+
SET NUMERIC_ROUNDABORT OFF;
|
|
152
|
+
SQL
|
|
153
|
+
end
|
|
154
|
+
|
|
85
155
|
def sql_type(logical_field, attributes, dialect)
|
|
86
|
-
|
|
156
|
+
type = attributes[:type] || "string"
|
|
157
|
+
case type
|
|
87
158
|
when "boolean"
|
|
88
159
|
case dialect
|
|
89
160
|
when :mysql
|
|
@@ -119,16 +190,22 @@ module BetterAuth
|
|
|
119
190
|
else
|
|
120
191
|
"text"
|
|
121
192
|
end
|
|
193
|
+
when "string"
|
|
194
|
+
indexed_string_sql_type(logical_field, attributes, dialect)
|
|
122
195
|
else
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
raise BetterAuth::Error, "Unsupported field type: #{type}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def indexed_string_sql_type(logical_field, attributes, dialect)
|
|
201
|
+
if dialect == :mysql
|
|
202
|
+
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references] || attributes[:sortable] || attributes.key?(:default_value)
|
|
203
|
+
indexed ? "varchar(191)" : "text"
|
|
204
|
+
elsif dialect == :mssql
|
|
205
|
+
indexed = logical_field == "id" || attributes[:unique] || attributes[:index] || attributes[:references] || attributes[:sortable]
|
|
206
|
+
indexed ? "varchar(255)" : "varchar(8000)"
|
|
207
|
+
else
|
|
208
|
+
"text"
|
|
132
209
|
end
|
|
133
210
|
end
|
|
134
211
|
|
|
@@ -163,10 +240,12 @@ module BetterAuth
|
|
|
163
240
|
end
|
|
164
241
|
end
|
|
165
242
|
|
|
166
|
-
def foreign_key_constraint(table_name, column, reference, dialect, tables = nil)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
243
|
+
def foreign_key_constraint(table_name, column, reference, dialect, tables = nil, delete_actions: nil)
|
|
244
|
+
target_table = foreign_key_target_table(reference, tables)
|
|
245
|
+
target_model = target_table&.fetch(:model_name) || reference.fetch(:model)
|
|
246
|
+
target_field = foreign_key_target_field(reference, target_table)
|
|
247
|
+
delete_action = delete_actions&.fetch([table_name.to_s, column.to_s], reference[:on_delete]) || reference[:on_delete]
|
|
248
|
+
on_delete = delete_action ? " ON DELETE #{delete_action.to_s.upcase}" : ""
|
|
170
249
|
|
|
171
250
|
case dialect
|
|
172
251
|
when :postgres, :sqlite
|
|
@@ -178,6 +257,78 @@ module BetterAuth
|
|
|
178
257
|
end
|
|
179
258
|
end
|
|
180
259
|
|
|
260
|
+
def mssql_delete_actions(tables, dialect)
|
|
261
|
+
return unless dialect.to_sym == :mssql
|
|
262
|
+
|
|
263
|
+
graph = {}
|
|
264
|
+
tables.each_with_object({}) do |(_logical_name, table), actions|
|
|
265
|
+
child = table.fetch(:model_name).to_s
|
|
266
|
+
table.fetch(:fields).each do |logical_field, attributes|
|
|
267
|
+
reference = attributes[:references]
|
|
268
|
+
next unless reference
|
|
269
|
+
|
|
270
|
+
column = (attributes[:field_name] || physical_name(logical_field)).to_s
|
|
271
|
+
action = reference[:on_delete]&.to_s
|
|
272
|
+
key = [child, column]
|
|
273
|
+
unless mssql_cascading_action?(action)
|
|
274
|
+
actions[key] = action
|
|
275
|
+
next
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
target_table = foreign_key_target_table(reference, tables)
|
|
279
|
+
parent = (target_table&.fetch(:model_name) || reference.fetch(:model)).to_s
|
|
280
|
+
if mssql_cascade_conflict?(graph, parent, child)
|
|
281
|
+
actions[key] = "no action"
|
|
282
|
+
else
|
|
283
|
+
graph[parent] ||= []
|
|
284
|
+
graph[parent] << child unless graph[parent].include?(child)
|
|
285
|
+
actions[key] = action
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def mssql_cascading_action?(action)
|
|
292
|
+
%w[cascade set\ null set\ default].include?(action.to_s.downcase.tr("_", " "))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def mssql_cascade_conflict?(graph, parent, child)
|
|
296
|
+
nodes = (graph.keys + graph.values.flatten + [parent, child]).uniq
|
|
297
|
+
nodes.any? do |source|
|
|
298
|
+
mssql_reachable?(graph, source, parent) && mssql_reachable?(graph, source, child)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def mssql_reachable?(graph, source, target, visited = {})
|
|
303
|
+
return true if source == target
|
|
304
|
+
return false if visited[source]
|
|
305
|
+
|
|
306
|
+
visited[source] = true
|
|
307
|
+
Array(graph[source]).any? { |child| mssql_reachable?(graph, child, target, visited) }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def foreign_key_target_table(reference, tables)
|
|
311
|
+
return unless tables
|
|
312
|
+
|
|
313
|
+
model = reference.fetch(:model).to_s
|
|
314
|
+
tables.fetch(model, nil) || tables.each_value.find { |table| table.fetch(:model_name).to_s == model }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def foreign_key_target_field(reference, target_table)
|
|
318
|
+
field = reference.fetch(:field).to_s
|
|
319
|
+
return field unless target_table
|
|
320
|
+
|
|
321
|
+
fields = target_table.fetch(:fields)
|
|
322
|
+
attributes = fields.fetch(field, nil)
|
|
323
|
+
return attributes[:field_name] || physical_name(field) if attributes
|
|
324
|
+
|
|
325
|
+
if fields.each_value.any? { |data| data[:field_name].to_s == field }
|
|
326
|
+
field
|
|
327
|
+
else
|
|
328
|
+
physical_name(field)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
181
332
|
def quote(identifier, dialect)
|
|
182
333
|
case dialect
|
|
183
334
|
when :postgres, :sqlite
|
|
@@ -197,6 +348,10 @@ module BetterAuth
|
|
|
197
348
|
.tr("-", "_")
|
|
198
349
|
.downcase
|
|
199
350
|
end
|
|
351
|
+
|
|
352
|
+
def logical_name(logical_field, attributes)
|
|
353
|
+
(attributes[:logical_name] || logical_field).to_s
|
|
354
|
+
end
|
|
200
355
|
end
|
|
201
356
|
end
|
|
202
357
|
end
|
data/lib/better_auth/schema.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Schema
|
|
5
7
|
module_function
|
|
@@ -18,9 +20,44 @@ module BetterAuth
|
|
|
18
20
|
tables.delete("verification") if secondary_storage?(options) && !verification_option(options, :store_in_database)
|
|
19
21
|
tables.merge!(plugin_schema)
|
|
20
22
|
tables["rateLimit"] = rate_limit_table(options) if rate_limit_option(options, :storage) == "database"
|
|
23
|
+
ensure_id_fields!(tables)
|
|
21
24
|
tables.sort_by { |_name, table| table[:order] || Float::INFINITY }.to_h
|
|
22
25
|
end
|
|
23
26
|
|
|
27
|
+
def migration_tables(options)
|
|
28
|
+
tables = auth_tables(options)
|
|
29
|
+
grouped = {}
|
|
30
|
+
|
|
31
|
+
tables.each_with_index do |(logical_name, table), index|
|
|
32
|
+
table_name = table.fetch(:model_name)
|
|
33
|
+
target = grouped[table_name] ||= {
|
|
34
|
+
model_name: table_name,
|
|
35
|
+
fields: {},
|
|
36
|
+
order: table[:order] || Float::INFINITY,
|
|
37
|
+
source_order: index,
|
|
38
|
+
logical_names: [],
|
|
39
|
+
disable_migration: false
|
|
40
|
+
}
|
|
41
|
+
target[:order] = [target[:order], table[:order] || Float::INFINITY].min
|
|
42
|
+
target[:source_order] = [target[:source_order], index].min
|
|
43
|
+
target[:logical_names] << logical_name
|
|
44
|
+
target[:disable_migration] ||= table[:disable_migration] == true
|
|
45
|
+
|
|
46
|
+
table.fetch(:fields).each do |logical_field, attributes|
|
|
47
|
+
column = attributes[:field_name] || physical_name(logical_field)
|
|
48
|
+
physical_attributes = physical_field_attributes(logical_field, attributes, tables)
|
|
49
|
+
merge_physical_field!(target, column, physical_attributes)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
grouped
|
|
54
|
+
.reject { |_name, table| table[:disable_migration] }
|
|
55
|
+
.sort_by { |_name, table| [table[:order], table[:source_order]] }
|
|
56
|
+
.to_h do |name, table|
|
|
57
|
+
[name, table.except(:source_order, :disable_migration)]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
24
61
|
def storage_model_name(options, model)
|
|
25
62
|
table = auth_tables(options).fetch(model.to_s)
|
|
26
63
|
table[:model_name]
|
|
@@ -48,10 +85,48 @@ module BetterAuth
|
|
|
48
85
|
data
|
|
49
86
|
end
|
|
50
87
|
|
|
88
|
+
def parse_provider_profile_user_input(options, profile, action:)
|
|
89
|
+
action = action.to_sym
|
|
90
|
+
raise ArgumentError, "action must be :create or :update" unless [:create, :update].include?(action)
|
|
91
|
+
|
|
92
|
+
input = (profile || {}).each_with_object({}) do |(key, value), result|
|
|
93
|
+
result[storage_key(key)] = value
|
|
94
|
+
end
|
|
95
|
+
core_fields = %w[id email emailVerified name image createdAt updatedAt]
|
|
96
|
+
fields = auth_tables(options).fetch("user").fetch(:fields).except(*core_fields)
|
|
97
|
+
|
|
98
|
+
fields.each_with_object({}) do |(field, attributes), result|
|
|
99
|
+
provider_value_allowed = attributes[:input] != false && input.key?(field)
|
|
100
|
+
if provider_value_allowed
|
|
101
|
+
value = coerce_provider_profile_value(input[field], attributes)
|
|
102
|
+
transform = attributes[:transform]
|
|
103
|
+
value = transform.call(value) if transform.respond_to?(:call)
|
|
104
|
+
validator = attributes[:validator]
|
|
105
|
+
if validator.respond_to?(:call) && !validator.call(value)
|
|
106
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is invalid")
|
|
107
|
+
end
|
|
108
|
+
result[field] = value
|
|
109
|
+
elsif action == :create && attributes.key?(:default_value)
|
|
110
|
+
default = attributes[:default_value]
|
|
111
|
+
result[field] = default.respond_to?(:call) ? default.call : default
|
|
112
|
+
elsif action == :create && attributes[:required]
|
|
113
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is required")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private_class_method def self.coerce_provider_profile_value(value, attributes)
|
|
119
|
+
return value if value.nil?
|
|
120
|
+
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
121
|
+
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
51
125
|
private_class_method def self.user_table(options, plugin_table)
|
|
52
126
|
table(
|
|
53
127
|
model_name: model_option(options, :user, :model_name) || "users",
|
|
54
128
|
order: 1,
|
|
129
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
55
130
|
fields: id_field.merge(
|
|
56
131
|
"name" => field("string", required: true, sortable: true, field_name: mapped_field(options, :user, "name")),
|
|
57
132
|
"email" => field("string", required: true, unique: true, sortable: true, field_name: mapped_field(options, :user, "email")),
|
|
@@ -66,6 +141,7 @@ module BetterAuth
|
|
|
66
141
|
table(
|
|
67
142
|
model_name: model_option(options, :session, :model_name) || "sessions",
|
|
68
143
|
order: 2,
|
|
144
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
69
145
|
fields: base_fields.merge(
|
|
70
146
|
"expiresAt" => field("date", required: true, field_name: mapped_field(options, :session, "expiresAt")),
|
|
71
147
|
"token" => field("string", required: true, unique: true, field_name: mapped_field(options, :session, "token")),
|
|
@@ -81,6 +157,7 @@ module BetterAuth
|
|
|
81
157
|
table(
|
|
82
158
|
model_name: model_option(options, :account, :model_name) || "accounts",
|
|
83
159
|
order: 3,
|
|
160
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
84
161
|
fields: base_fields.merge(
|
|
85
162
|
"accountId" => field("string", required: true, field_name: mapped_field(options, :account, "accountId")),
|
|
86
163
|
"providerId" => field("string", required: true, field_name: mapped_field(options, :account, "providerId")),
|
|
@@ -101,6 +178,7 @@ module BetterAuth
|
|
|
101
178
|
table(
|
|
102
179
|
model_name: model_option(options, :verification, :model_name) || "verifications",
|
|
103
180
|
order: 4,
|
|
181
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
104
182
|
fields: base_fields.merge(
|
|
105
183
|
"identifier" => field("string", required: true, index: true, field_name: mapped_field(options, :verification, "identifier")),
|
|
106
184
|
"value" => field("string", required: true, field_name: mapped_field(options, :verification, "value")),
|
|
@@ -121,6 +199,58 @@ module BetterAuth
|
|
|
121
199
|
}
|
|
122
200
|
end
|
|
123
201
|
|
|
202
|
+
private_class_method def self.ensure_id_fields!(tables)
|
|
203
|
+
tables.each_value do |table|
|
|
204
|
+
fields = table.fetch(:fields)
|
|
205
|
+
next if fields.key?("id")
|
|
206
|
+
|
|
207
|
+
table[:fields] = id_field.merge(fields)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private_class_method def self.physical_field_attributes(logical_field, attributes, tables)
|
|
212
|
+
column = attributes[:field_name] || physical_name(logical_field)
|
|
213
|
+
physical_attributes = attributes.merge(field_name: column, logical_name: logical_field.to_s)
|
|
214
|
+
reference = attributes[:references]
|
|
215
|
+
return physical_attributes unless reference
|
|
216
|
+
|
|
217
|
+
physical_attributes.merge(references: physical_reference(reference, tables))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private_class_method def self.physical_reference(reference, tables)
|
|
221
|
+
model = reference.fetch(:model).to_s
|
|
222
|
+
target_table = tables.fetch(model, nil) || tables.each_value.find { |table| table.fetch(:model_name).to_s == model }
|
|
223
|
+
return reference unless target_table
|
|
224
|
+
|
|
225
|
+
field = reference.fetch(:field).to_s
|
|
226
|
+
target_field = target_table.fetch(:fields).fetch(field, nil)
|
|
227
|
+
physical_field = if target_field
|
|
228
|
+
target_field[:field_name] || physical_name(field)
|
|
229
|
+
elsif target_table.fetch(:fields).each_value.any? { |attributes| attributes[:field_name].to_s == field }
|
|
230
|
+
field
|
|
231
|
+
else
|
|
232
|
+
physical_name(field)
|
|
233
|
+
end
|
|
234
|
+
reference.merge(model: target_table.fetch(:model_name), field: physical_field)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private_class_method def self.merge_physical_field!(table, column, attributes)
|
|
238
|
+
existing = table.fetch(:fields)[column]
|
|
239
|
+
unless existing
|
|
240
|
+
table.fetch(:fields)[column] = attributes
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
return if compatible_physical_field?(existing, attributes)
|
|
245
|
+
|
|
246
|
+
raise BetterAuth::Error, "Conflicting schema metadata for #{table.fetch(:model_name)}.#{column}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private_class_method def self.compatible_physical_field?(left, right)
|
|
250
|
+
keys = [:type, :required, :references, :index, :unique, :sortable, :bigint]
|
|
251
|
+
keys.all? { |key| left[key] == right[key] }
|
|
252
|
+
end
|
|
253
|
+
|
|
124
254
|
private_class_method def self.base_fields
|
|
125
255
|
id_field.merge(timestamp_fields)
|
|
126
256
|
end
|
|
@@ -138,12 +268,12 @@ module BetterAuth
|
|
|
138
268
|
}
|
|
139
269
|
end
|
|
140
270
|
|
|
141
|
-
private_class_method def self.table(model_name:, fields:, extra_fields:, order:)
|
|
271
|
+
private_class_method def self.table(model_name:, fields:, extra_fields:, order:, disable_migration: false)
|
|
142
272
|
{
|
|
143
273
|
model_name: model_name,
|
|
144
274
|
fields: merge_fields(fields, *extra_fields),
|
|
145
275
|
order: order
|
|
146
|
-
}
|
|
276
|
+
}.tap { |data| data[:disable_migration] = true if disable_migration == true }
|
|
147
277
|
end
|
|
148
278
|
|
|
149
279
|
private_class_method def self.merge_fields(base, *extras)
|
|
@@ -162,14 +292,20 @@ module BetterAuth
|
|
|
162
292
|
schema.each do |raw_key, raw_table|
|
|
163
293
|
key = storage_key(raw_key)
|
|
164
294
|
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] ||
|
|
295
|
+
existing = tables[key] || {model_name: table_data[:model_name] || physical_table_name(key), fields: {}}
|
|
296
|
+
existing[:model_name] = table_data[:model_name] || existing[:model_name] || physical_table_name(key)
|
|
167
297
|
existing[:fields] = existing[:fields].merge(normalize_fields(table_data[:fields] || {}))
|
|
298
|
+
existing[:disable_migration] = true if table_data[:disable_migration] == true
|
|
299
|
+
existing[:fields] = id_field.merge(existing[:fields]) unless core_table?(key) || existing[:fields].key?("id")
|
|
168
300
|
tables[key] = existing
|
|
169
301
|
end
|
|
170
302
|
end
|
|
171
303
|
end
|
|
172
304
|
|
|
305
|
+
private_class_method def self.core_table?(key)
|
|
306
|
+
%w[user session account verification].include?(key.to_s)
|
|
307
|
+
end
|
|
308
|
+
|
|
173
309
|
private_class_method def self.normalize_fields(fields)
|
|
174
310
|
fields.each_with_object({}) do |(raw_key, raw_value), result|
|
|
175
311
|
key = storage_key(raw_key)
|
|
@@ -270,6 +406,24 @@ module BetterAuth
|
|
|
270
406
|
underscore(value.to_s)
|
|
271
407
|
end
|
|
272
408
|
|
|
409
|
+
private_class_method def self.physical_table_name(value)
|
|
410
|
+
pluralize_table_name(physical_name(value))
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
private_class_method def self.pluralize_table_name(value)
|
|
414
|
+
special = {
|
|
415
|
+
"apikey" => "api_keys",
|
|
416
|
+
"api_key" => "api_keys",
|
|
417
|
+
"wallet_address" => "wallet_addresses"
|
|
418
|
+
}
|
|
419
|
+
return special.fetch(value) if special.key?(value)
|
|
420
|
+
return value if value.end_with?("s")
|
|
421
|
+
return "#{value[0...-1]}ies" if value.end_with?("y") && value.match?(/[^aeiou]y\z/)
|
|
422
|
+
return "#{value}es" if value.match?(/(s|x|z|ch|sh)\z/)
|
|
423
|
+
|
|
424
|
+
"#{value}s"
|
|
425
|
+
end
|
|
426
|
+
|
|
273
427
|
private_class_method def self.camelize_lower(value)
|
|
274
428
|
parts = underscore(value).split("_")
|
|
275
429
|
([parts.first] + parts.drop(1).map(&:capitalize)).join
|
|
@@ -286,6 +440,6 @@ module BetterAuth
|
|
|
286
440
|
(Time.now.to_f * 1000).to_i
|
|
287
441
|
end
|
|
288
442
|
|
|
289
|
-
public_class_method :storage_key
|
|
443
|
+
public_class_method :physical_name, :storage_key
|
|
290
444
|
end
|
|
291
445
|
end
|
data/lib/better_auth/session.rb
CHANGED
|
@@ -7,15 +7,21 @@ module BetterAuth
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def find_current(ctx, disable_cookie_cache: false, disable_refresh: false, sensitive: false)
|
|
10
|
-
|
|
10
|
+
authoritative = sensitive && stateful?(ctx)
|
|
11
|
+
if ctx.context.current_session && !authoritative
|
|
11
12
|
return ctx.context.current_session
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
token_cookie = ctx.context.auth_cookies[:session_token]
|
|
15
16
|
token = ctx.get_signed_cookie(token_cookie.name, ctx.context.secret)
|
|
16
|
-
|
|
17
|
+
unless token
|
|
18
|
+
present = ctx.get_cookie(token_cookie.name)
|
|
19
|
+
return missing_session(ctx) if present && !present.empty?
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
return nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
cached = cached_session(ctx, token, disable_cookie_cache: disable_cookie_cache, sensitive: authoritative)
|
|
19
25
|
if cached
|
|
20
26
|
ctx.context.set_current_session(cached) if ctx.context.respond_to?(:set_current_session)
|
|
21
27
|
return cached
|
|
@@ -45,7 +51,8 @@ module BetterAuth
|
|
|
45
51
|
strategy: config[:strategy] || "compact",
|
|
46
52
|
version: config[:version],
|
|
47
53
|
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)
|
|
54
|
+
is_secure: ctx.context.auth_cookies[:session_data].name.start_with?(Cookies::SECURE_COOKIE_PREFIX),
|
|
55
|
+
cookie_full_name: ctx.context.auth_cookies[:session_data].name
|
|
49
56
|
)
|
|
50
57
|
return nil unless payload
|
|
51
58
|
return nil if payload["session"]["token"] && payload["session"]["token"] != token
|
|
@@ -56,10 +63,16 @@ module BetterAuth
|
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def missing_session(ctx)
|
|
66
|
+
ctx.context.set_current_session(nil) if ctx.context.respond_to?(:set_current_session)
|
|
59
67
|
Cookies.delete_session_cookie(ctx)
|
|
60
68
|
nil
|
|
61
69
|
end
|
|
62
70
|
|
|
71
|
+
def stateful?(ctx)
|
|
72
|
+
options = ctx.context.options
|
|
73
|
+
!options.database.nil? || !options.secondary_storage.nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
63
76
|
def expired?(session)
|
|
64
77
|
expires_at = normalize_time(session["expiresAt"])
|
|
65
78
|
expires_at && expires_at <= Time.now
|
|
@@ -83,7 +96,12 @@ module BetterAuth
|
|
|
83
96
|
"expiresAt" => expires_at,
|
|
84
97
|
"updatedAt" => now
|
|
85
98
|
)
|
|
86
|
-
|
|
99
|
+
unless updated
|
|
100
|
+
missing_session(ctx)
|
|
101
|
+
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_SESSION"])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
session = stringify_keys(updated)
|
|
87
105
|
refreshed = {session: session, user: result[:user]}
|
|
88
106
|
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx))
|
|
89
107
|
refreshed
|
|
@@ -91,12 +109,13 @@ module BetterAuth
|
|
|
91
109
|
|
|
92
110
|
def refresh_cached_session(ctx, result)
|
|
93
111
|
now = Time.now
|
|
94
|
-
session = stringify_keys(result[:session])
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
session = stringify_keys(result[:session])
|
|
113
|
+
expires_at = normalize_time(session["expiresAt"])
|
|
114
|
+
return missing_session(ctx) if expires_at && expires_at <= now
|
|
115
|
+
|
|
116
|
+
remaining = expires_at && [(expires_at - now).floor, 0].max
|
|
98
117
|
refreshed = {session: session, user: result[:user]}
|
|
99
|
-
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx))
|
|
118
|
+
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx), max_age: remaining)
|
|
100
119
|
refreshed
|
|
101
120
|
end
|
|
102
121
|
|
|
@@ -6,7 +6,7 @@ module BetterAuth
|
|
|
6
6
|
ESTIMATED_EMPTY_COOKIE_SIZE = 200
|
|
7
7
|
CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE
|
|
8
8
|
|
|
9
|
-
CookieValue = Struct.new(:name, :value, :attributes
|
|
9
|
+
CookieValue = Struct.new(:name, :value, :attributes)
|
|
10
10
|
|
|
11
11
|
attr_reader :cookie_name, :cookie_options, :context, :chunks
|
|
12
12
|
|
|
@@ -45,8 +45,13 @@ module BetterAuth
|
|
|
45
45
|
algorithms: ["RS256"],
|
|
46
46
|
issuers: "https://appleid.apple.com",
|
|
47
47
|
audience: audiences,
|
|
48
|
-
nonce:
|
|
48
|
+
nonce: nil
|
|
49
49
|
)
|
|
50
|
+
if nonce
|
|
51
|
+
token_nonce = profile&.fetch("nonce", nil)
|
|
52
|
+
expected_nonces = [nonce, OpenSSL::Digest::SHA256.hexdigest(nonce.to_s)]
|
|
53
|
+
return false unless token_nonce.is_a?(String) && expected_nonces.include?(token_nonce)
|
|
54
|
+
end
|
|
50
55
|
!!profile&.fetch("sub", nil)
|
|
51
56
|
end,
|
|
52
57
|
get_user_info: lambda do |tokens|
|