better_auth-rails 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d53af856a214f6ba70cca2dc5f8ff61c9429adf62fa8850cd8a41ba66154ee8b
4
- data.tar.gz: 1cd3587e1ded94d8600908036a80b900495de4f49647404b17d28cd3141e3581
3
+ metadata.gz: 9fc15052f4d320ccab9d999008d9207285c443f478c3db4391c83aa07b99e5ac
4
+ data.tar.gz: d7d386939905a2af32351bcfcf07252060a69250856db09a746f119b7ac13d74
5
5
  SHA512:
6
- metadata.gz: 1e55f73a678d9e328543ca7082a981df44340a41d61dc9978b37eb74121ef8d5d7e6874fa4f0021cf2422cd65d1c7ab2c08906c0f676f9d9ef33ef5a71540706
7
- data.tar.gz: 1b004d2801b43274b8bb3803e943424e7c434813560856f369e294978d41747927e37e84815df71732040a8a58be889f8172013b8b3ad59898bb1bba4a52a91d
6
+ metadata.gz: 33243ae75ed8e845d78c2931fb894000a3d62fe00b6db495019cd20f74fe32025e003b590191625ec4228f94c1e6dd3593d74d8aa43be9f56b175c9c1d26b8ea
7
+ data.tar.gz: '089f99e56796caeb863d1f5ffe1d62ecbac7b28f0dee8b2c09f867e20115690368e2b0b9a1236aa1fcedde79e930fefc1a6eef92dc38e576a3e1d2713f0fd7a5'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ 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
+ - Preserved mounted auth responses in Rails apps.
15
+ - Improved migration generation, Active Record adapter behavior, routing, and database integration coverage.
16
+
10
17
  ## [0.7.0] - 2026-05-05
11
18
 
12
19
  ### Fixed
data/README.md CHANGED
@@ -63,9 +63,13 @@ To generate only the base migration:
63
63
  ```bash
64
64
  bin/rails generate better_auth:migration
65
65
  bin/rails better_auth:generate:migration
66
+ bin/rails better_auth:doctor
66
67
  ```
67
68
 
68
69
  The generators skip an existing `config/initializers/better_auth.rb` or existing `*_create_better_auth_tables.rb` migration instead of overwriting them.
70
+ When the base migration already exists and Rails can connect to the current
71
+ database, `better_auth:generate:migration` creates a new incremental update
72
+ migration for missing plugin tables, additional fields, and indexes.
69
73
 
70
74
  ### Configuration
71
75
 
@@ -78,10 +82,10 @@ BetterAuth::Rails.configure do |config|
78
82
  Rails.application.credentials.secret_key_base ||
79
83
  Rails.application.secret_key_base
80
84
 
81
- config.base_url = ENV["BETTER_AUTH_URL"]
85
+ config.base_url = BetterAuth::Env.get("BETTER_AUTH_URL")
82
86
  config.base_path = "/api/auth"
83
87
  config.trusted_origins = [
84
- ENV["BETTER_AUTH_URL"]
88
+ BetterAuth::Env.get("BETTER_AUTH_URL")
85
89
  ].compact
86
90
 
87
91
  config.session do |session|
@@ -140,7 +144,7 @@ Core Better Auth also reads `BETTER_AUTH_SECRETS` when `secrets` is not configur
140
144
 
141
145
  #### Trusted origins
142
146
 
143
- The generated initializer derives `trusted_origins` from `ENV["BETTER_AUTH_URL"]`. If that environment variable is unset, Rails passes an empty list. Browser clients should set trusted origins explicitly in deployment so origin checks and callback URLs do not depend on an empty environment value. See [`host-app-responsibilities.md`](../../.docs/features/host-app-responsibilities.md) for the boundary between Better Auth origin checks, browser CORS headers, and host-app CSRF policy.
147
+ The generated initializer derives `trusted_origins` from `BetterAuth::Env.get("BETTER_AUTH_URL")`. If that environment variable is unset, Rails passes an empty list. Browser clients should set trusted origins explicitly in deployment so origin checks and callback URLs do not depend on an empty environment value. See [`host-app-responsibilities.md`](../../.docs/features/host-app-responsibilities.md) for the boundary between Better Auth origin checks, browser CORS headers, and host-app CSRF policy.
144
148
 
145
149
  #### Option builder keys
146
150
 
@@ -227,13 +231,13 @@ end
227
231
 
228
232
  ## Development
229
233
 
230
- Full documentation is being adapted in the root [`docs/`](/Users/sebastiansala/projects/better-auth/docs/README.md) app. The Rails guide lives at `docs/content/docs/integrations/rails.mdx`; pages with a Ruby port warning still contain upstream TypeScript examples for reference.
234
+ Full documentation is being adapted in the root `docs/` app. The Rails guide lives at `docs/content/docs/integrations/rails.mdx`; pages with a Ruby port warning still contain upstream TypeScript examples for reference.
231
235
 
232
236
  ### Setup
233
237
 
234
238
  ```bash
235
239
  # Clone the monorepo
236
- git clone --recursive https://github.com/sebasxsala/better-auth.git
240
+ git clone --recursive https://github.com/sebasxsala/better-auth-rb.git
237
241
  cd better-auth/packages/better_auth-rails
238
242
 
239
243
  # Install dependencies
@@ -267,7 +271,7 @@ RUBOCOP_CACHE_ROOT=/private/var/folders/7x/jrsz946d2w73n42fb1_ff5000000gn/T/rubo
267
271
 
268
272
  ## Contributing
269
273
 
270
- Bug reports and pull requests are welcome on GitHub at https://github.com/sebasxsala/better-auth.
274
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sebasxsala/better-auth-rb.
271
275
 
272
276
  When contributing:
273
277
  1. Fork the repository
@@ -56,16 +56,21 @@ module BetterAuth
56
56
 
57
57
  def update(model:, where:, update:)
58
58
  model = model.to_s
59
- existing = find_one(model: model, where: where, select: ["id"])
59
+ ensure_update_input_has_fields!(model, update)
60
+ existing = find_one(model: model, where: where)
60
61
  return nil unless existing
61
62
 
62
63
  update_many(model: model, where: where, update: update)
63
- find_one(model: model, where: [{field: "id", value: existing.fetch("id")}])
64
+ lookup = record_lookup(model, existing)
65
+ lookup ? find_one(model: model, where: [lookup]) : find_one(model: model, where: where)
64
66
  end
65
67
 
66
68
  def update_many(model:, where:, update:, returning: false)
67
69
  model = model.to_s
68
- attributes = physical_attributes(model, transform_input(model, update, "update", true))
70
+ ensure_update_input_has_fields!(model, update)
71
+ data = transform_input(model, update, "update", true)
72
+ ensure_update_data!(data)
73
+ attributes = physical_attributes(model, data)
69
74
  relation = relation_for(model, where: where)
70
75
  if returning
71
76
  relation.map do |record|
@@ -103,7 +108,7 @@ module BetterAuth
103
108
  klass = Class.new(ApplicationRecord)
104
109
  model_namespace.const_set(class_name_for(model), klass)
105
110
  klass.table_name = table_for(model) if klass.respond_to?(:table_name=)
106
- klass.primary_key = storage_field(model, "id") if klass.respond_to?(:primary_key=)
111
+ klass.primary_key = storage_field(model, "id") if klass.respond_to?(:primary_key=) && schema_for(model).fetch(:fields).key?("id")
107
112
  @models[model] = klass
108
113
  define_join_associations(model, klass)
109
114
  klass
@@ -228,10 +233,37 @@ module BetterAuth
228
233
  end
229
234
  output[field] = coerce_value(value, attributes) if value_provided
230
235
  end
231
- output["id"] = generated_id if action == "create" && !output.key?("id")
236
+ output["id"] = generated_id if action == "create" && !output.key?("id") && fields.key?("id")
232
237
  output
233
238
  end
234
239
 
240
+ def ensure_update_data!(data)
241
+ raise APIError.new("BAD_REQUEST", message: "No fields to update") if data.empty?
242
+ end
243
+
244
+ def ensure_update_input_has_fields!(model, update)
245
+ raise APIError.new("BAD_REQUEST", message: "No fields to update") unless update.is_a?(Hash)
246
+
247
+ fields = schema_for(model).fetch(:fields)
248
+ input = stringify_keys(update)
249
+ has_updatable_field = input.any? do |field, _value|
250
+ next false if field == "id" || field == "_id"
251
+
252
+ fields.key?(field) || fields.any? { |logical_field, attributes| storage_key(attributes[:field_name] || logical_field) == field }
253
+ end
254
+ raise APIError.new("BAD_REQUEST", message: "No fields to update") unless has_updatable_field
255
+ end
256
+
257
+ def record_lookup(model, record)
258
+ fields = schema_for(model).fetch(:fields)
259
+ return {field: "id", value: record.fetch("id")} if fields.key?("id") && record.key?("id")
260
+
261
+ unique_field = fields.find { |field, attributes| attributes[:unique] && record.key?(field) }
262
+ return {field: unique_field.first, value: record.fetch(unique_field.first)} if unique_field
263
+
264
+ nil
265
+ end
266
+
235
267
  def physical_attributes(model, logical)
236
268
  logical.each_with_object({}) do |(field, value), attributes|
237
269
  attributes[storage_field(model, field)] = value
@@ -26,6 +26,10 @@ module BetterAuth
26
26
 
27
27
  private
28
28
 
29
+ def better_auth_auth
30
+ BetterAuth::Rails.auth_for_mount
31
+ end
32
+
29
33
  def better_auth_session_data
30
34
  return request.env["better_auth.session"] if request.env.key?("better_auth.session")
31
35
 
@@ -33,7 +37,7 @@ module BetterAuth
33
37
  end
34
38
 
35
39
  def resolve_better_auth_session
36
- auth_context = BetterAuth::Rails.auth.context
40
+ auth_context = better_auth_auth.context
37
41
  auth_context.prepare_for_request!(request) if auth_context.respond_to?(:prepare_for_request!)
38
42
  context = BetterAuth::Endpoint::Context.new(
39
43
  path: request.path,
@@ -46,6 +50,32 @@ module BetterAuth
46
50
  request: request
47
51
  )
48
52
  BetterAuth::Session.find_current(context, disable_refresh: true)
53
+ ensure
54
+ copy_better_auth_response_headers(context) if defined?(context) && context
55
+ auth_context.clear_runtime! if defined?(auth_context) && auth_context&.respond_to?(:clear_runtime!)
56
+ end
57
+
58
+ def copy_better_auth_response_headers(context)
59
+ return unless respond_to?(:response) && response
60
+
61
+ context.response_headers.each do |key, value|
62
+ write_better_auth_response_header(key, value)
63
+ end
64
+ end
65
+
66
+ def write_better_auth_response_header(key, value)
67
+ header_name = canonical_response_header(key)
68
+ if response.respond_to?(:set_header)
69
+ response.set_header(header_name, value)
70
+ elsif response.respond_to?(:headers)
71
+ response.headers[header_name] = value
72
+ end
73
+ end
74
+
75
+ def canonical_response_header(key)
76
+ return "Set-Cookie" if key.to_s.downcase == "set-cookie"
77
+
78
+ key.to_s.split("-").map(&:capitalize).join("-")
49
79
  end
50
80
  end
51
81
  end
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "better_auth/sql_migration"
4
+
3
5
  module BetterAuth
4
6
  module Rails
5
7
  module Migration
8
+ BOUNDED_STRING_LIMIT = 191
9
+
6
10
  module_function
7
11
 
8
- def render(options, migration_version: nil)
12
+ def render(options, migration_version: nil, dialect: nil)
9
13
  migration_version ||= self.migration_version
14
+ dialect ||= active_record_connection ? active_record_dialect(active_record_connection) : :rails
10
15
  tables = BetterAuth::Schema.auth_tables(options)
11
16
  lines = [
12
17
  "# frozen_string_literal: true",
@@ -14,36 +19,132 @@ module BetterAuth
14
19
  "class CreateBetterAuthTables < ActiveRecord::Migration[#{migration_version}]",
15
20
  " def change"
16
21
  ]
17
- tables.each_value { |table| lines.concat(create_table_lines(table)) }
18
- tables.each_value { |table| lines.concat(primary_key_lines(table)) }
22
+ tables.each_value { |table| lines.concat(create_table_lines(table, dialect: dialect)) }
19
23
  tables.each_value { |table| lines.concat(index_lines(table)) }
20
24
  tables.each_value { |table| lines.concat(foreign_key_lines(table, options)) }
21
25
  lines.concat([" end", "end", ""])
22
26
  lines.join("\n")
23
27
  end
24
28
 
29
+ def render_pending(plan, class_name: "UpdateBetterAuthTables", migration_version: nil)
30
+ migration_version ||= self.migration_version
31
+ created_tables = plan.to_create.map(&:table_name).to_set
32
+ lines = [
33
+ "# frozen_string_literal: true",
34
+ "",
35
+ "class #{class_name} < ActiveRecord::Migration[#{migration_version}]",
36
+ " def change"
37
+ ]
38
+ plan.to_create.each { |change| lines.concat(create_table_lines(change.table, dialect: plan.dialect)) }
39
+ plan.to_create.each { |change| lines.concat(index_lines(change.table)) }
40
+ plan.to_create.each { |change| lines.concat(foreign_key_lines(change.table, plan.tables)) }
41
+ plan.to_add.each { |change| lines.concat(add_column_lines(change, dialect: plan.dialect)) }
42
+ plan.to_index.reject { |change| created_tables.include?(change.table_name) }.each do |change|
43
+ lines << index_line(change.table_name, change.field_name, unique: change.unique)
44
+ end
45
+ plan.to_add.each do |change|
46
+ lines.concat(foreign_key_lines({model_name: change.table_name, fields: change.fields}, plan.tables))
47
+ end
48
+ lines.concat([" end", "end", ""])
49
+ lines.join("\n")
50
+ end
51
+
52
+ def plan_pending(options, connection: active_record_connection)
53
+ dialect = active_record_dialect(connection)
54
+ BetterAuth::SQLMigration.plan_from_existing(
55
+ options,
56
+ existing: current_schema(connection),
57
+ dialect: dialect
58
+ )
59
+ end
60
+
61
+ def active_record_connection
62
+ ::ActiveRecord::Base.connection if defined?(::ActiveRecord::Base)
63
+ rescue
64
+ nil
65
+ end
66
+
67
+ def active_record_dialect(connection)
68
+ adapter = connection.respond_to?(:adapter_name) ? connection.adapter_name.to_s.downcase : ""
69
+ case adapter
70
+ when /postgres/
71
+ :postgres
72
+ when /mysql/
73
+ :mysql
74
+ when /sqlite/
75
+ :sqlite
76
+ when /sqlserver|sql_server|mssql/
77
+ :mssql
78
+ else
79
+ :postgres
80
+ end
81
+ end
82
+
83
+ def current_schema(connection)
84
+ connection.tables.each_with_object({}) do |table_name, schema|
85
+ columns = connection.columns(table_name).each_with_object({}) do |column, result|
86
+ result[column.name.to_s] = column.respond_to?(:sql_type) ? column.sql_type.to_s : column.type.to_s
87
+ end
88
+ indexes = {names: Set.new, columns: Set.new, unique_columns: Set.new}
89
+ connection.indexes(table_name).each do |index|
90
+ indexes[:names] << index.name.to_s
91
+ Array(index.columns).each do |column|
92
+ column = column.to_s
93
+ indexes[:columns] << column
94
+ indexes[:unique_columns] << column if index.unique
95
+ end
96
+ end
97
+ schema[table_name.to_s] = {name: table_name.to_s, columns: columns, indexes: indexes}
98
+ end
99
+ end
100
+
25
101
  def migration_version
26
102
  return ::ActiveRecord::Migration.current_version if defined?(::ActiveRecord::Migration)
27
103
 
28
104
  "7.0"
29
105
  end
30
106
 
31
- def create_table_lines(table)
107
+ def create_table_lines(table, dialect: :rails)
32
108
  table_name = table.fetch(:model_name)
33
- lines = ["", " create_table :#{table_name}, id: false do |t|"]
109
+ lines = ["", " create_table :#{table_name}, #{primary_key_options(table, dialect: dialect)} do |t|"]
34
110
  table.fetch(:fields).each do |logical_field, attributes|
35
- lines << column_line(logical_field, attributes)
111
+ next if logical_field == "id"
112
+
113
+ lines << column_line(logical_field, attributes, dialect: dialect)
36
114
  end
37
115
  lines << " end"
38
116
  end
39
117
 
40
- def column_line(logical_field, attributes)
118
+ def primary_key_options(table, dialect: :rails)
119
+ attributes = table.fetch(:fields)["id"]
120
+ return "id: false" unless attributes
121
+
122
+ column = attributes[:field_name] || physical_name("id")
123
+ parts = ["id: :#{rails_type("id", attributes, dialect)}"]
124
+ parts << "limit: #{BOUNDED_STRING_LIMIT}" if limited_string?("id", attributes)
125
+ parts << "primary_key: :#{column}" unless column == "id"
126
+ parts.join(", ")
127
+ end
128
+
129
+ def column_line(logical_field, attributes, dialect: :rails)
41
130
  column = attributes[:field_name] || physical_name(logical_field)
42
- parts = ["t.#{rails_type(attributes)} :#{column}"]
131
+ type = rails_type(logical_field, attributes, dialect)
132
+ parts = if type == "timestamptz"
133
+ ["t.column :#{column}, :timestamptz"]
134
+ else
135
+ ["t.#{type} :#{column}"]
136
+ end
137
+ parts.concat(column_options(logical_field, attributes))
138
+ " #{parts.join(", ")}"
139
+ end
140
+
141
+ def column_options(logical_field, attributes)
142
+ parts = []
143
+ parts << "limit: #{BOUNDED_STRING_LIMIT}" if limited_string?(logical_field, attributes)
43
144
  parts << "null: false" if attributes[:required]
44
145
  default = default_value(attributes)
45
146
  parts << "default: #{default}" unless default.nil?
46
- " #{parts.join(", ")}"
147
+ parts
47
148
  end
48
149
 
49
150
  def index_lines(table)
@@ -52,43 +153,66 @@ module BetterAuth
52
153
  next unless attributes[:unique] || attributes[:index]
53
154
 
54
155
  column = attributes[:field_name] || physical_name(logical_field)
55
- unique = attributes[:unique] ? ", unique: true" : ""
56
- " add_index :#{table_name}, :#{column}#{unique}"
156
+ index_line(table_name, column, unique: attributes[:unique])
57
157
  end
58
158
  end
59
159
 
60
- def primary_key_lines(table)
61
- table_name = table.fetch(:model_name)
62
- return [] unless table.fetch(:fields).key?("id")
160
+ def add_column_lines(change, dialect: :rails)
161
+ change.fields.map do |logical_field, attributes|
162
+ column = attributes[:field_name] || physical_name(logical_field)
163
+ parts = [" add_column :#{change.table_name}, :#{column}, :#{rails_type(logical_field, attributes, dialect)}"]
164
+ parts.concat(column_options(logical_field, attributes))
165
+ parts.join(", ")
166
+ end
167
+ end
63
168
 
64
- [
65
- %( execute "ALTER TABLE \#{quote_table_name(:#{table_name})} ADD PRIMARY KEY (\#{quote_column_name(:id)})")
66
- ]
169
+ def index_line(table_name, column, unique: false)
170
+ unique_option = unique ? ", unique: true" : ""
171
+ " add_index :#{table_name}, :#{column}#{unique_option}"
67
172
  end
68
173
 
69
174
  def foreign_key_lines(table, options)
70
175
  table_name = table.fetch(:model_name)
176
+ tables = table_map(options)
71
177
  table.fetch(:fields).filter_map do |logical_field, attributes|
72
178
  reference = attributes[:references]
73
179
  next unless reference
74
180
 
75
181
  column = attributes[:field_name] || physical_name(logical_field)
76
- target = foreign_key_target(reference.fetch(:model), options)
182
+ target_table = foreign_key_target_table(reference, tables)
183
+ target = target_table&.fetch(:model_name) || reference.fetch(:model)
184
+ target_field = foreign_key_target_field(reference, target_table)
185
+ primary_key = (target_field.to_s == "id") ? "" : ", primary_key: :#{target_field}"
77
186
  on_delete = reference[:on_delete] ? ", on_delete: :#{reference[:on_delete]}" : ""
78
- " add_foreign_key :#{table_name}, :#{target}, column: :#{column}#{on_delete}"
187
+ " add_foreign_key :#{table_name}, :#{target}, column: :#{column}#{primary_key}#{on_delete}"
79
188
  end
80
189
  end
81
190
 
82
- def rails_type(attributes)
191
+ def rails_type(logical_field, attributes, dialect = :rails)
83
192
  case attributes[:type]
84
193
  when "boolean" then "boolean"
85
- when "date" then "datetime"
194
+ when "date" then (dialect == :postgres) ? "timestamptz" : "datetime"
86
195
  when "number" then attributes[:bigint] ? "bigint" : "integer"
87
- when "json", "string[]", "number[]" then "json"
88
- else "string"
196
+ when "json", "string[]", "number[]" then (dialect == :postgres) ? "jsonb" : "json"
197
+ when "string" then bounded_string?(logical_field, attributes) ? "string" : "text"
198
+ else "text"
89
199
  end
90
200
  end
91
201
 
202
+ def bounded_string?(logical_field, attributes)
203
+ logical_field.to_s == "id" ||
204
+ logical_field.to_s.end_with?("Id") ||
205
+ attributes[:unique] ||
206
+ attributes[:index] ||
207
+ attributes[:sortable] ||
208
+ attributes[:references] ||
209
+ attributes.key?(:default_value)
210
+ end
211
+
212
+ def limited_string?(logical_field, attributes)
213
+ attributes[:type] == "string" && bounded_string?(logical_field, attributes)
214
+ end
215
+
92
216
  def default_value(attributes)
93
217
  default = attributes[:default_value]
94
218
  return if default.respond_to?(:call)
@@ -105,8 +229,32 @@ module BetterAuth
105
229
  BetterAuth::Schema.send(:physical_name, value)
106
230
  end
107
231
 
108
- def foreign_key_target(model, options)
109
- BetterAuth::Schema.auth_tables(options).fetch(model.to_s, nil)&.fetch(:model_name) || model
232
+ def table_map(options)
233
+ if options.respond_to?(:values) && options.values.all? { |value| value.respond_to?(:fetch) && value.key?(:fields) }
234
+ options
235
+ else
236
+ BetterAuth::Schema.auth_tables(options)
237
+ end
238
+ end
239
+
240
+ def foreign_key_target_table(reference, tables)
241
+ model = reference.fetch(:model).to_s
242
+ tables.fetch(model, nil) || tables.each_value.find { |table| table.fetch(:model_name).to_s == model }
243
+ end
244
+
245
+ def foreign_key_target_field(reference, target_table)
246
+ field = reference.fetch(:field).to_s
247
+ return field unless target_table
248
+
249
+ fields = target_table.fetch(:fields)
250
+ attributes = fields.fetch(field, nil)
251
+ return attributes[:field_name] || physical_name(field) if attributes
252
+
253
+ if fields.each_value.any? { |data| data[:field_name].to_s == field }
254
+ field
255
+ else
256
+ physical_name(field)
257
+ end
110
258
  end
111
259
  end
112
260
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module BetterAuth
4
6
  module Rails
5
7
  class MountedApp
@@ -10,18 +12,19 @@ module BetterAuth
10
12
 
11
13
  def call(env)
12
14
  @auth.call(env.merge("PATH_INFO" => mounted_path_info(env)))
15
+ rescue BetterAuth::APIError, JSON::ParserError
16
+ raise
17
+ rescue => error
18
+ handle_unexpected_error(error, env)
13
19
  end
14
20
 
15
21
  private
16
22
 
17
23
  def mounted_path_info(env)
18
24
  path_info = normalize_path(env["PATH_INFO"])
19
- script_name = normalize_path(env["SCRIPT_NAME"])
20
- prefix = (script_name == "/") ? @mount_path : script_name
21
-
22
- return path_info if path_info == prefix || path_info.start_with?("#{prefix}/")
25
+ return path_info if path_info == @mount_path || path_info.start_with?("#{@mount_path}/")
23
26
 
24
- normalize_path("#{prefix}/#{path_info.delete_prefix("/")}")
27
+ normalize_path("#{@mount_path}/#{path_info.delete_prefix("/")}")
25
28
  end
26
29
 
27
30
  def normalize_path(path)
@@ -31,6 +34,46 @@ module BetterAuth
31
34
  normalized = normalized.delete_suffix("/") unless normalized == "/"
32
35
  normalized.empty? ? "/" : normalized
33
36
  end
37
+
38
+ def handle_unexpected_error(error, env)
39
+ log_unexpected_error(error, env)
40
+ options = @auth.respond_to?(:options) ? @auth.options : nil
41
+ on_api_error = options&.on_api_error || {}
42
+ raise error if on_api_error[:throw] || on_api_error["throw"]
43
+
44
+ callback = on_api_error[:on_error] || on_api_error[:onError] || on_api_error["on_error"] || on_api_error["onError"]
45
+ callback.call(error, error_context(env)) if callback.respond_to?(:call)
46
+
47
+ api_error = BetterAuth::APIError.new("INTERNAL_SERVER_ERROR")
48
+ [
49
+ api_error.status_code,
50
+ {"content-type" => "application/json"},
51
+ [JSON.generate(api_error.to_h)]
52
+ ]
53
+ end
54
+
55
+ def log_unexpected_error(error, env)
56
+ message = "BetterAuth::Rails mounted app error: #{error.class}: #{error.message}\n"
57
+ message << Array(error.backtrace).join("\n")
58
+
59
+ if env["rack.errors"]
60
+ env["rack.errors"].puts(message)
61
+ elsif defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
62
+ ::Rails.logger.error(message)
63
+ end
64
+ rescue
65
+ nil
66
+ end
67
+
68
+ def error_context(env)
69
+ path = mounted_path_info(env)
70
+ route_path = if path == @mount_path
71
+ "/"
72
+ else
73
+ path.delete_prefix(@mount_path)
74
+ end
75
+ Struct.new(:path, :env).new(normalize_path(route_path), env)
76
+ end
34
77
  end
35
78
  end
36
79
  end
@@ -6,6 +6,7 @@ module BetterAuth
6
6
  def better_auth(auth: nil, at: BetterAuth::Configuration::DEFAULT_BASE_PATH)
7
7
  mount_path = normalize_better_auth_mount_path(at)
8
8
  auth ||= BetterAuth::Rails.auth(base_path: mount_path)
9
+ BetterAuth::Rails.register_auth(auth, mount_path: mount_path)
9
10
  mount BetterAuth::Rails::MountedApp.new(auth, mount_path: mount_path), at: mount_path
10
11
  end
11
12
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Rails
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
@@ -21,6 +21,7 @@ module BetterAuth
21
21
  def configure
22
22
  yield configuration
23
23
  @auth = nil
24
+ @mounted_auth = nil
24
25
  end
25
26
 
26
27
  def auth(overrides = nil)
@@ -29,6 +30,29 @@ module BetterAuth
29
30
 
30
31
  BetterAuth.auth(options.merge(overrides))
31
32
  end
33
+
34
+ def register_auth(auth, mount_path:)
35
+ mounted_auth[normalize_mount_path(mount_path)] = auth
36
+ end
37
+
38
+ def auth_for_mount(mount_path = nil)
39
+ return mounted_auth[normalize_mount_path(mount_path)] if mount_path
40
+
41
+ mounted_auth[configuration.base_path] || mounted_auth.values.first || auth
42
+ end
43
+
44
+ private
45
+
46
+ def mounted_auth
47
+ @mounted_auth ||= {}
48
+ end
49
+
50
+ def normalize_mount_path(path)
51
+ normalized = path.to_s
52
+ normalized = "/#{normalized}" unless normalized.start_with?("/")
53
+ normalized = normalized.squeeze("/")
54
+ (normalized == "/") ? normalized : normalized.delete_suffix("/")
55
+ end
32
56
  end
33
57
  end
34
58
  end
@@ -8,7 +8,7 @@ module BetterAuth
8
8
  class MigrationGenerator < ::Rails::Generators::Base
9
9
  def create_migration
10
10
  if existing_migration?
11
- say_status :skip, "db/migrate/*_create_better_auth_tables.rb already exists"
11
+ create_incremental_migration
12
12
  return
13
13
  end
14
14
 
@@ -21,12 +21,32 @@ module BetterAuth
21
21
  Dir[File.join(destination_root, "db/migrate/*_create_better_auth_tables.rb")].any?
22
22
  end
23
23
 
24
+ def create_incremental_migration
25
+ plan = BetterAuth::Rails::Migration.plan_pending(generator_config)
26
+ if plan.empty?
27
+ say_status :skip, "Better Auth schema is up to date"
28
+ return
29
+ end
30
+
31
+ create_file incremental_migration_path, BetterAuth::Rails::Migration.render_pending(plan, class_name: incremental_class_name)
32
+ rescue => _error
33
+ say_status :skip, "db/migrate/*_create_better_auth_tables.rb already exists"
34
+ end
35
+
24
36
  def migration_path
25
37
  File.join("db/migrate", "#{timestamp}_create_better_auth_tables.rb")
26
38
  end
27
39
 
40
+ def incremental_migration_path
41
+ File.join("db/migrate", "#{timestamp}_update_better_auth_tables_#{timestamp}.rb")
42
+ end
43
+
44
+ def incremental_class_name
45
+ "UpdateBetterAuthTables#{timestamp}"
46
+ end
47
+
28
48
  def timestamp
29
- Time.now.utc.strftime("%Y%m%d%H%M%S")
49
+ @timestamp ||= Time.now.utc.strftime("%Y%m%d%H%M%S")
30
50
  end
31
51
 
32
52
  def generator_config
@@ -2,16 +2,24 @@
2
2
 
3
3
  namespace :better_auth do
4
4
  desc "Create the Better Auth initializer and base migration"
5
- task :init do
5
+ task init: :environment do
6
6
  require "generators/better_auth/install/install_generator"
7
7
  BetterAuth::Generators::InstallGenerator.start([])
8
8
  end
9
9
 
10
10
  namespace :generate do
11
11
  desc "Create the Better Auth base migration"
12
- task :migration do
12
+ task migration: :environment do
13
13
  require "generators/better_auth/migration/migration_generator"
14
14
  BetterAuth::Generators::MigrationGenerator.start([])
15
15
  end
16
16
  end
17
+
18
+ desc "Check Better Auth configuration and schema health"
19
+ task doctor: :environment do
20
+ options = BetterAuth::Rails.configuration.to_auth_options
21
+ config = BetterAuth::Configuration.new(options)
22
+ exit_code = BetterAuth::Doctor.print(BetterAuth::Doctor.check(config), stdout: $stdout, stderr: $stderr)
23
+ abort if exit_code != 0
24
+ end
17
25
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -181,6 +181,20 @@ dependencies:
181
181
  - - "~>"
182
182
  - !ruby/object:Gem::Version
183
183
  version: '0.5'
184
+ - !ruby/object:Gem::Dependency
185
+ name: better_auth-passkey
186
+ requirement: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - "~>"
189
+ - !ruby/object:Gem::Version
190
+ version: '0.8'
191
+ type: :development
192
+ prerelease: false
193
+ version_requirements: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - "~>"
196
+ - !ruby/object:Gem::Version
197
+ version: '0.8'
184
198
  description: Rails integration for Better Auth Ruby. Better Auth Ruby is an independent
185
199
  modern authentication framework for Ruby inspired by Better Auth. Provides middleware,
186
200
  controller helpers, and generators.
@@ -208,14 +222,14 @@ files:
208
222
  - lib/generators/better_auth/install/templates/initializer.rb.tt
209
223
  - lib/generators/better_auth/migration/migration_generator.rb
210
224
  - lib/tasks/better_auth.rake
211
- homepage: https://github.com/sebasxsala/better-auth
225
+ homepage: https://github.com/sebasxsala/better-auth-rb
212
226
  licenses:
213
227
  - MIT
214
228
  metadata:
215
- homepage_uri: https://github.com/sebasxsala/better-auth
216
- source_code_uri: https://github.com/sebasxsala/better-auth
217
- changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-rails/CHANGELOG.md
218
- bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
229
+ homepage_uri: https://github.com/sebasxsala/better-auth-rb
230
+ source_code_uri: https://github.com/sebasxsala/better-auth-rb
231
+ changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-rails/CHANGELOG.md
232
+ bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
219
233
  rdoc_options: []
220
234
  require_paths:
221
235
  - lib