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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +10 -6
- data/lib/better_auth/rails/active_record_adapter.rb +37 -5
- data/lib/better_auth/rails/controller_helpers.rb +31 -1
- data/lib/better_auth/rails/migration.rb +173 -25
- data/lib/better_auth/rails/mounted_app.rb +48 -5
- data/lib/better_auth/rails/routing.rb +1 -0
- data/lib/better_auth/rails/version.rb +1 -1
- data/lib/better_auth/rails.rb +24 -0
- data/lib/generators/better_auth/migration/migration_generator.rb +22 -2
- data/lib/tasks/better_auth.rake +10 -2
- metadata +20 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fc15052f4d320ccab9d999008d9207285c443f478c3db4391c83aa07b99e5ac
|
|
4
|
+
data.tar.gz: d7d386939905a2af32351bcfcf07252060a69250856db09a746f119b7ac13d74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
85
|
+
config.base_url = BetterAuth::Env.get("BETTER_AUTH_URL")
|
|
82
86
|
config.base_path = "/api/auth"
|
|
83
87
|
config.trusted_origins = [
|
|
84
|
-
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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},
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
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("#{
|
|
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
|
|
data/lib/better_auth/rails.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/tasks/better_auth.rake
CHANGED
|
@@ -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 :
|
|
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 :
|
|
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.
|
|
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
|