better_auth-rails 0.1.2 → 0.2.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: 752aa9b17208fc8152307184bb45d4e36e910ac73d2fee25bfdab567a2eae552
4
- data.tar.gz: e5cd67d1e60e41e950beef732238129a840adf596a4d9e70a5e1c44153119bf5
3
+ metadata.gz: c39998081e1fe16b8a01b980681d982c6acb3d2b6aaf6c748056a952372bc38c
4
+ data.tar.gz: 128d2b3d19a7ae498182a2e163a3b843e90c018c2a4033aff2c18d5591cae6ab
5
5
  SHA512:
6
- metadata.gz: ffe3a3fe1c2f7fdeb761532155dd984c0126008928fa79c3e90177be8289805967cdf0a6c370b8b0aff55af997477f7d81769fec0e752a245e6e4baeb7f07e91
7
- data.tar.gz: 513fbba0f325cdb0c54c12d790f87018a6bbe7979ade4d91b222fa69ab42c83e2668dd2d0c2a86ed06ea0fe8c67f6042e4b7d30ea20b42a6e07e46367d0f923d
6
+ metadata.gz: e764e03d032c66031c7ae09e28b671e5600ae9cf1ca0a7d5304064b4aec3b8124c61fdf637dfeafbcd565c63406cdfe82e98b61c823698a96d5ba65645fdac23
7
+ data.tar.gz: 2f8c7440c4a2a94145fecadda191055c7898d3063d08bb008a3ced7043587ed6ba4a6fe5bf3790552e7ca080cdfceffe7b12b53bd9f058a8a01ba7e604e60d09
data/CHANGELOG.md CHANGED
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
 
14
14
  - Fixed gemspec files list to use `Dir.glob` instead of `git ls-files` for better CI compatibility
15
15
  - Fixed dependency constraints for railties and activesupport (now `>= 6.0, < 9`)
16
- - Fixed better_auth_rails compatibility gem dependency version
16
+ - Fixed `better_auth_rails` compatibility gem dependency version
17
17
 
18
18
  ## [0.1.1] - 2026-03-17
19
19
 
data/README.md CHANGED
@@ -7,7 +7,7 @@ Rails adapter for Better Auth Ruby. Provides seamless integration with Ruby on R
7
7
  Add this line to your application's Gemfile:
8
8
 
9
9
  ```ruby
10
- gem 'better_auth-rails'
10
+ gem "better_auth-rails"
11
11
  ```
12
12
 
13
13
  ### Defensive alias package
@@ -29,87 +29,181 @@ bundle install
29
29
  Add to your `config/application.rb`:
30
30
 
31
31
  ```ruby
32
- require 'better_auth/rails'
32
+ require "better_auth/rails"
33
33
  ```
34
34
 
35
35
  Compatibility require is also supported:
36
36
 
37
37
  ```ruby
38
- require 'better_auth_rails'
38
+ require "better_auth_rails"
39
39
  ```
40
40
 
41
41
  Or in your Gemfile:
42
42
 
43
43
  ```ruby
44
- gem 'better_auth-rails', require: 'better_auth/rails'
44
+ gem "better_auth-rails", require: "better_auth/rails"
45
45
  ```
46
46
 
47
- ### Controller Helpers
47
+ ### Initializer And Migration
48
48
 
49
- Include the controller helpers in your ApplicationController:
49
+ Create the default initializer and base migration:
50
50
 
51
- ```ruby
52
- class ApplicationController < ActionController::Base
53
- include BetterAuth::Rails::ControllerHelpers
54
- end
51
+ ```bash
52
+ bin/rails generate better_auth:install
55
53
  ```
56
54
 
57
- Now you have access to authentication methods:
55
+ The same install path is available as a Rails task:
56
+
57
+ ```bash
58
+ bin/rails better_auth:init
59
+ ```
60
+
61
+ To generate only the base migration:
62
+
63
+ ```bash
64
+ bin/rails generate better_auth:migration
65
+ bin/rails better_auth:generate:migration
66
+ ```
67
+
68
+ The generators skip an existing `config/initializers/better_auth.rb` or existing `*_create_better_auth_tables.rb` migration instead of overwriting them.
69
+
70
+ ### Configuration
71
+
72
+ The install generator creates `config/initializers/better_auth.rb`:
58
73
 
59
74
  ```ruby
60
- class PostsController < ApplicationController
61
- before_action :authenticate_user!
75
+ BetterAuth::Rails.configure do |config|
76
+ config.secret =
77
+ Rails.application.credentials.dig(:better_auth, :secret) ||
78
+ Rails.application.credentials.secret_key_base ||
79
+ Rails.application.secret_key_base
80
+
81
+ config.base_url = ENV["BETTER_AUTH_URL"]
82
+ config.base_path = "/api/auth"
83
+ config.database = ->(options) { BetterAuth::Rails::ActiveRecordAdapter.new(options) }
84
+ config.trusted_origins = [
85
+ ENV["BETTER_AUTH_URL"]
86
+ ].compact
87
+
88
+ config.session = {
89
+ cookie_cache: {
90
+ enabled: true,
91
+ max_age: 5 * 60,
92
+ strategy: "jwe"
93
+ }
94
+ }
62
95
 
63
- def index
64
- @posts = current_user.posts
65
- end
96
+ config.advanced = {
97
+ ip_address: {
98
+ ip_address_headers: ["x-forwarded-for"],
99
+ disable_ip_tracking: false
100
+ }
101
+ }
66
102
 
67
- def create
68
- @post = current_user.posts.build(post_params)
69
- # ...
70
- end
103
+ config.experimental = {
104
+ joins: false
105
+ }
106
+
107
+ config.social_providers = {
108
+ # github: BetterAuth::SocialProviders.github(
109
+ # client_id: ENV.fetch("GITHUB_CLIENT_ID"),
110
+ # client_secret: ENV.fetch("GITHUB_CLIENT_SECRET")
111
+ # )
112
+ }
113
+
114
+ config.plugins = []
115
+ config.hooks = {
116
+ before: [],
117
+ after: []
118
+ }
71
119
  end
72
120
  ```
73
121
 
74
- ### Available Methods
122
+ Rails configuration is a thin option builder for the core Rack auth object. The same option concepts are available in core Ruby through `BetterAuth.auth(...)`; Rails places them in `config/initializers/better_auth.rb` so applications can rely on credentials, ActiveRecord, and Rails environment configuration.
75
123
 
76
- - `current_user` - Returns the currently authenticated user
77
- - `authenticate_user!` - Redirects to login if not authenticated
78
- - `user_signed_in?` - Returns true if user is authenticated
79
- - `sign_in(user)` - Signs in a user
80
- - `sign_out` - Signs out the current user
124
+ The ActiveRecord adapter uses whichever database adapter the Rails app is already configured with, including PostgreSQL and MySQL.
81
125
 
82
- ### Configuration
126
+ ### JavaScript Client
127
+
128
+ Ruby Better Auth exposes the same HTTP route surface. Frontend apps should use the upstream Better Auth JavaScript client and point it at the Ruby server:
129
+
130
+ ```ts
131
+ import { createAuthClient } from "better-auth/client";
132
+
133
+ export const authClient = createAuthClient({
134
+ baseURL: "http://localhost:3000",
135
+ basePath: "/api/auth",
136
+ });
137
+ ```
83
138
 
84
- Create an initializer `config/initializers/better_auth.rb`:
139
+ Plugin schemas are included in generated migrations through the same configuration:
85
140
 
86
141
  ```ruby
87
- BetterAuth.configure do |config|
88
- config.secret_key = Rails.application.credentials.secret_key_base
89
- config.database_url = Rails.application.credentials.database_url
90
-
91
- # Optional: Configure session store
92
- config.session_store = :redis
93
- config.session_options = {
94
- url: ENV['REDIS_URL']
95
- }
142
+ require "better_auth/api_key"
143
+
144
+ BetterAuth::Rails.configure do |config|
145
+ config.plugins = [
146
+ BetterAuth::Plugins.api_key
147
+ ]
96
148
  end
149
+
150
+ # Then regenerate before migrating if this is a new app:
151
+ # bin/rails generate better_auth:migration
97
152
  ```
98
153
 
99
154
  ### Routes
100
155
 
101
- Mount the Better Auth engine in your routes:
156
+ Mount the Better Auth Rack app in your routes:
157
+
158
+ ```ruby
159
+ Rails.application.routes.draw do
160
+ better_auth
161
+ end
162
+ ```
163
+
164
+ By default this mounts at `/api/auth`. Rails mounts the core Rack auth app through a small wrapper so Better Auth still sees the full auth path after Rails moves the mount prefix into `SCRIPT_NAME`. To customize the path:
102
165
 
103
166
  ```ruby
104
167
  Rails.application.routes.draw do
105
- mount BetterAuth::Rails::Engine => '/auth'
106
-
107
- # Your routes...
168
+ better_auth at: "/auth"
108
169
  end
109
170
  ```
110
171
 
172
+ The Better Auth core router handles internal routes such as `/callback/:providerId`.
173
+
174
+ ### Controller Helpers
175
+
176
+ Include the controller helpers in your ApplicationController:
177
+
178
+ ```ruby
179
+ class ApplicationController < ActionController::Base
180
+ include BetterAuth::Rails::ControllerHelpers
181
+ end
182
+ ```
183
+
184
+ Now you have access to authentication methods:
185
+
186
+ ```ruby
187
+ class PostsController < ApplicationController
188
+ before_action :require_authentication
189
+
190
+ def index
191
+ @user = current_user
192
+ end
193
+ end
194
+ ```
195
+
196
+ ### Available Methods
197
+
198
+ - `current_session` - Returns the current Better Auth session hash
199
+ - `current_user` - Returns the current Better Auth user hash
200
+ - `authenticated?` - Returns true when a user is present
201
+ - `require_authentication` - Halts with `head :unauthorized` and returns `false` when no user is present
202
+
111
203
  ## Development
112
204
 
205
+ 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.
206
+
113
207
  ### Setup
114
208
 
115
209
  ```bash
@@ -125,13 +219,13 @@ bundle install
125
219
 
126
220
  ```bash
127
221
  # Run all tests
128
- bundle exec rspec
222
+ rbenv exec bundle exec rspec
129
223
 
130
224
  # Run with coverage
131
- COVERAGE=true bundle exec rspec
225
+ COVERAGE=true rbenv exec bundle exec rspec
132
226
 
133
227
  # Run specific test
134
- bundle exec rspec spec/better_auth/rails/controller_helpers_spec.rb
228
+ rbenv exec bundle exec rspec spec/better_auth/rails/controller_helpers_spec.rb
135
229
  ```
136
230
 
137
231
  ### Code Style
@@ -140,10 +234,10 @@ We use StandardRB for linting:
140
234
 
141
235
  ```bash
142
236
  # Check style
143
- bundle exec standardrb
237
+ RUBOCOP_CACHE_ROOT=/private/var/folders/7x/jrsz946d2w73n42fb1_ff5000000gn/T/rubocop_cache_rails rbenv exec bundle exec standardrb
144
238
 
145
239
  # Auto-fix issues
146
- bundle exec standardrb --fix
240
+ RUBOCOP_CACHE_ROOT=/private/var/folders/7x/jrsz946d2w73n42fb1_ff5000000gn/T/rubocop_cache_rails rbenv exec bundle exec standardrb --fix
147
241
  ```
148
242
 
149
243
  ## Contributing
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ class ActiveRecordAdapter < BetterAuth::Adapters::Base
6
+ begin
7
+ require "active_record" unless defined?(::ActiveRecord)
8
+ rescue LoadError
9
+ # ActiveRecord is required only when the adapter is instantiated in a Rails app.
10
+ end
11
+
12
+ if defined?(::ActiveRecord::Base)
13
+ class ApplicationRecord < ::ActiveRecord::Base
14
+ self.abstract_class = true
15
+ end
16
+ else
17
+ class ApplicationRecord
18
+ end
19
+ end
20
+
21
+ attr_reader :connection
22
+
23
+ def initialize(options, connection: nil)
24
+ super(options)
25
+ @connection = connection || ::ActiveRecord::Base
26
+ @models = {}
27
+ end
28
+
29
+ def create(model:, data:, force_allow_id: false)
30
+ model = model.to_s
31
+ input = transform_input(model, data, "create", force_allow_id)
32
+ record = model_class(model).create!(physical_attributes(model, input))
33
+ normalize_record(model, record)
34
+ end
35
+
36
+ def find_one(model:, where: [], select: nil, join: nil)
37
+ find_many(model: model, where: where, select: select, join: join, limit: 1).first
38
+ end
39
+
40
+ def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
41
+ model = model.to_s
42
+ relation = relation_for(model, where: where, sort_by: sort_by, limit: limit, offset: offset, select: select, join: join)
43
+ records = relation.map { |record| normalize_record(model, record, join: join) }
44
+ collection_join?(model, join) ? aggregate_collection_joins(records) : records
45
+ end
46
+
47
+ def update(model:, where:, update:)
48
+ model = model.to_s
49
+ record = relation_for(model, where: where).first
50
+ return nil unless record
51
+
52
+ record.update!(physical_attributes(model, transform_input(model, update, "update", true)))
53
+ normalize_record(model, record)
54
+ end
55
+
56
+ def update_many(model:, where:, update:, returning: false)
57
+ model = model.to_s
58
+ attributes = physical_attributes(model, transform_input(model, update, "update", true))
59
+ relation = relation_for(model, where: where)
60
+ if returning
61
+ relation.map do |record|
62
+ record.update!(attributes)
63
+ normalize_record(model, record)
64
+ end
65
+ else
66
+ relation.update_all(attributes)
67
+ end
68
+ end
69
+
70
+ def delete(model:, where:)
71
+ model = model.to_s
72
+ record = relation_for(model, where: where).first
73
+ record&.destroy!
74
+ nil
75
+ end
76
+
77
+ def delete_many(model:, where:)
78
+ relation_for(model.to_s, where: where).delete_all
79
+ end
80
+
81
+ def count(model:, where: nil)
82
+ relation_for(model.to_s, where: where || []).count
83
+ end
84
+
85
+ def transaction
86
+ connection.connection.transaction { yield self }
87
+ end
88
+
89
+ private
90
+
91
+ def model_class(model)
92
+ model = model.to_s
93
+ return @models[model] if @models.key?(model)
94
+
95
+ klass = Class.new(ApplicationRecord)
96
+ model_namespace.const_set(class_name_for(model), klass)
97
+ klass.table_name = table_for(model) if klass.respond_to?(:table_name=)
98
+ klass.primary_key = storage_field(model, "id") if klass.respond_to?(:primary_key=)
99
+ @models[model] = klass
100
+ define_join_associations(model, klass)
101
+ klass
102
+ end
103
+
104
+ def relation_for(model, where:, sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
105
+ relation = model_class(model).all
106
+ relation = apply_where(model, relation, where || [])
107
+ relation = apply_select(model, relation, select) if select
108
+ relation = apply_join_includes(model, relation, join) if join
109
+ relation = apply_order(model, relation, sort_by) if sort_by
110
+ relation = relation.limit(Integer(limit)) if limit
111
+ relation = relation.offset(Integer(offset)) if offset
112
+ relation
113
+ end
114
+
115
+ def apply_where(model, relation, where)
116
+ Array(where).reduce(relation) do |scope, clause|
117
+ field = storage_key(fetch_key(clause, :field))
118
+ column = storage_field(model, field)
119
+ operator = (fetch_key(clause, :operator) || "eq").to_s
120
+ value = fetch_key(clause, :value)
121
+ apply_operator(scope, column, operator, value)
122
+ end
123
+ end
124
+
125
+ def apply_operator(scope, column, operator, value)
126
+ case operator
127
+ when "in" then scope.where(column => Array(value))
128
+ when "not_in" then scope.where.not(column => Array(value))
129
+ when "ne" then scope.where.not(column => value)
130
+ when "gt" then scope.where("#{column} > ?", value)
131
+ when "gte" then scope.where("#{column} >= ?", value)
132
+ when "lt" then scope.where("#{column} < ?", value)
133
+ when "lte" then scope.where("#{column} <= ?", value)
134
+ when "contains" then scope.where("#{column} LIKE ?", "%#{value}%")
135
+ when "starts_with" then scope.where("#{column} LIKE ?", "#{value}%")
136
+ when "ends_with" then scope.where("#{column} LIKE ?", "%#{value}")
137
+ else scope.where(column => value)
138
+ end
139
+ end
140
+
141
+ def apply_select(model, relation, select)
142
+ columns = Array(select).map { |field| storage_field(model, storage_key(field)) }
143
+ relation.select(*columns)
144
+ end
145
+
146
+ def apply_order(model, relation, sort_by)
147
+ field = storage_key(fetch_key(sort_by, :field))
148
+ direction = (fetch_key(sort_by, :direction).to_s.downcase == "desc") ? :desc : :asc
149
+ relation.order(storage_field(model, field) => direction)
150
+ end
151
+
152
+ def transform_input(model, data, action, force_allow_id)
153
+ fields = schema_for(model).fetch(:fields)
154
+ input = stringify_keys(data)
155
+ output = {}
156
+ fields.each do |field, attributes|
157
+ next if field == "id" && input.key?(field) && !force_allow_id
158
+
159
+ value_provided = input.key?(field)
160
+ value = input[field]
161
+ if !value_provided && action == "create" && attributes.key?(:default_value)
162
+ value = resolve_default(attributes[:default_value])
163
+ value_provided = true
164
+ elsif !value_provided && action == "update" && attributes[:on_update]
165
+ value = resolve_default(attributes[:on_update])
166
+ value_provided = true
167
+ end
168
+ output[field] = value if value_provided
169
+ end
170
+ output["id"] = SecureRandom.urlsafe_base64(16) if action == "create" && !output.key?("id")
171
+ output
172
+ end
173
+
174
+ def physical_attributes(model, logical)
175
+ logical.each_with_object({}) do |(field, value), attributes|
176
+ attributes[storage_field(model, field)] = value
177
+ end
178
+ end
179
+
180
+ def normalize_record(model, record, join: nil)
181
+ return nil unless record
182
+
183
+ attributes = record.respond_to?(:attributes) ? record.attributes : record
184
+ normalized = schema_for(model).fetch(:fields).each_with_object({}) do |(field, config), output|
185
+ column = config[:field_name] || physical_name(field)
186
+ output[field] = attributes[column] if attributes.key?(column)
187
+ end
188
+ attach_joins(model, normalized, record, join)
189
+ end
190
+
191
+ def attach_joins(model, normalized, record, join)
192
+ return normalized unless join
193
+
194
+ join.each_key do |join_model|
195
+ join_model = join_model.to_s
196
+ definition = join_definition(model, join_model)
197
+ next unless definition
198
+
199
+ association = definition.fetch(:association)
200
+ next unless record.respond_to?(association)
201
+
202
+ joined = record.public_send(association)
203
+ normalized[join_model] = if definition[:collection]
204
+ Array(joined).map { |joined_record| normalize_record(join_model, joined_record) }
205
+ else
206
+ normalize_record(join_model, joined)
207
+ end
208
+ end
209
+ normalized
210
+ end
211
+
212
+ def apply_join_includes(model, relation, join)
213
+ associations = join.filter_map do |join_model, _enabled|
214
+ join_definition(model, join_model.to_s)&.fetch(:association)
215
+ end
216
+ return relation if associations.empty? || !relation.respond_to?(:includes)
217
+
218
+ relation.includes(*associations)
219
+ end
220
+
221
+ def define_join_associations(model, klass)
222
+ case model
223
+ when "session", "account"
224
+ return unless klass.respond_to?(:belongs_to)
225
+
226
+ klass.belongs_to(
227
+ :user,
228
+ class_name: model_class("user").name,
229
+ foreign_key: storage_field(model, "userId"),
230
+ primary_key: storage_field("user", "id"),
231
+ optional: true
232
+ )
233
+ when "user"
234
+ return unless klass.respond_to?(:has_many)
235
+
236
+ klass.has_many(
237
+ :accounts,
238
+ class_name: model_class("account").name,
239
+ foreign_key: storage_field("account", "userId"),
240
+ primary_key: storage_field("user", "id")
241
+ )
242
+ end
243
+ end
244
+
245
+ def join_definition(model, join_model)
246
+ case [model.to_s, join_model.to_s]
247
+ when ["session", "user"], ["account", "user"]
248
+ {association: :user, collection: false}
249
+ when ["user", "account"]
250
+ {association: :accounts, collection: true}
251
+ end
252
+ end
253
+
254
+ def model_namespace
255
+ @model_namespace ||= BetterAuth::Rails.const_set("ActiveRecordAdapterModels#{object_id}", Module.new)
256
+ end
257
+
258
+ def class_name_for(model)
259
+ physical_name(model).split("_").map(&:capitalize).join
260
+ end
261
+
262
+ def collection_join?(model, join)
263
+ model == "user" && join&.keys&.any? { |join_model| join_model.to_s == "account" }
264
+ end
265
+
266
+ def aggregate_collection_joins(records)
267
+ records
268
+ end
269
+
270
+ def table_for(model)
271
+ schema_for(model).fetch(:model_name)
272
+ end
273
+
274
+ def schema_for(model)
275
+ BetterAuth::Schema.auth_tables(options).fetch(model.to_s)
276
+ end
277
+
278
+ def storage_field(model, field)
279
+ schema_for(model).fetch(:fields).fetch(field.to_s).fetch(:field_name, physical_name(field))
280
+ end
281
+
282
+ def stringify_keys(value)
283
+ return {} unless value.respond_to?(:each)
284
+
285
+ value.each_with_object({}) { |(key, object), result| result[storage_key(key)] = object }
286
+ end
287
+
288
+ def fetch_key(hash, key)
289
+ hash[key] || hash[key.to_s] || hash[storage_key(key)] || hash[storage_key(key).to_sym]
290
+ end
291
+
292
+ def storage_key(value)
293
+ BetterAuth::Schema.send(:storage_key, value)
294
+ end
295
+
296
+ def physical_name(value)
297
+ BetterAuth::Schema.send(:physical_name, value)
298
+ end
299
+
300
+ def resolve_default(value)
301
+ value.respond_to?(:call) ? value.call : value
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ class Configuration
6
+ AUTH_OPTION_NAMES = %i[
7
+ app_name
8
+ base_url
9
+ base_path
10
+ secret
11
+ database
12
+ plugins
13
+ trusted_origins
14
+ rate_limit
15
+ session
16
+ account
17
+ user
18
+ verification
19
+ advanced
20
+ email_and_password
21
+ password_hasher
22
+ email_verification
23
+ social_providers
24
+ experimental
25
+ secondary_storage
26
+ database_hooks
27
+ hooks
28
+ on_api_error
29
+ disabled_paths
30
+ logger
31
+ ].freeze
32
+
33
+ attr_accessor(*AUTH_OPTION_NAMES)
34
+
35
+ def initialize
36
+ @base_path = BetterAuth::Configuration::DEFAULT_BASE_PATH
37
+ @plugins = []
38
+ @trusted_origins = []
39
+ @database = ->(options) { ActiveRecordAdapter.new(options) }
40
+ end
41
+
42
+ def to_auth_options
43
+ AUTH_OPTION_NAMES.each_with_object({}) do |name, options|
44
+ value = public_send(name)
45
+ next if value.nil?
46
+ next if value.respond_to?(:empty?) && value.empty?
47
+
48
+ options[name] = value
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ module ControllerHelpers
6
+ def current_session
7
+ data = better_auth_session_data
8
+ data&.fetch(:session, nil) || data&.fetch("session", nil)
9
+ end
10
+
11
+ def current_user
12
+ data = better_auth_session_data
13
+ data&.fetch(:user, nil) || data&.fetch("user", nil)
14
+ end
15
+
16
+ def authenticated?
17
+ !current_user.nil?
18
+ end
19
+
20
+ def require_authentication
21
+ return true if authenticated?
22
+
23
+ head(:unauthorized) if respond_to?(:head)
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ def better_auth_session_data
30
+ return request.env["better_auth.session"] if request.env.key?("better_auth.session")
31
+
32
+ request.env["better_auth.session"] = resolve_better_auth_session
33
+ end
34
+
35
+ def resolve_better_auth_session
36
+ context = BetterAuth::Endpoint::Context.new(
37
+ path: request.path,
38
+ method: request.request_method,
39
+ query: request.query_parameters,
40
+ body: {},
41
+ params: {},
42
+ headers: {"cookie" => request.get_header("HTTP_COOKIE")},
43
+ context: BetterAuth::Rails.auth.context,
44
+ request: request
45
+ )
46
+ BetterAuth::Session.find_current(context, disable_refresh: true)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ module Migration
6
+ module_function
7
+
8
+ def render(options, migration_version: nil)
9
+ migration_version ||= self.migration_version
10
+ tables = BetterAuth::Schema.auth_tables(options)
11
+ lines = [
12
+ "# frozen_string_literal: true",
13
+ "",
14
+ "class CreateBetterAuthTables < ActiveRecord::Migration[#{migration_version}]",
15
+ " def change"
16
+ ]
17
+ tables.each_value { |table| lines.concat(create_table_lines(table)) }
18
+ tables.each_value { |table| lines.concat(primary_key_lines(table)) }
19
+ tables.each_value { |table| lines.concat(index_lines(table)) }
20
+ tables.each_value { |table| lines.concat(foreign_key_lines(table, options)) }
21
+ lines.concat([" end", "end", ""])
22
+ lines.join("\n")
23
+ end
24
+
25
+ def migration_version
26
+ return ::ActiveRecord::Migration.current_version if defined?(::ActiveRecord::Migration)
27
+
28
+ "7.0"
29
+ end
30
+
31
+ def create_table_lines(table)
32
+ table_name = table.fetch(:model_name)
33
+ lines = ["", " create_table :#{table_name}, id: false do |t|"]
34
+ table.fetch(:fields).each do |logical_field, attributes|
35
+ lines << column_line(logical_field, attributes)
36
+ end
37
+ lines << " end"
38
+ end
39
+
40
+ def column_line(logical_field, attributes)
41
+ column = attributes[:field_name] || physical_name(logical_field)
42
+ parts = ["t.#{rails_type(attributes)} :#{column}"]
43
+ parts << "null: false" if attributes[:required]
44
+ default = default_value(attributes)
45
+ parts << "default: #{default}" unless default.nil?
46
+ " #{parts.join(", ")}"
47
+ end
48
+
49
+ def index_lines(table)
50
+ table_name = table.fetch(:model_name)
51
+ table.fetch(:fields).filter_map do |logical_field, attributes|
52
+ next unless attributes[:unique] || attributes[:index]
53
+
54
+ column = attributes[:field_name] || physical_name(logical_field)
55
+ unique = attributes[:unique] ? ", unique: true" : ""
56
+ " add_index :#{table_name}, :#{column}#{unique}"
57
+ end
58
+ end
59
+
60
+ def primary_key_lines(table)
61
+ table_name = table.fetch(:model_name)
62
+ return [] unless table.fetch(:fields).key?("id")
63
+
64
+ [
65
+ %( execute "ALTER TABLE \#{quote_table_name(:#{table_name})} ADD PRIMARY KEY (\#{quote_column_name(:id)})")
66
+ ]
67
+ end
68
+
69
+ def foreign_key_lines(table, options)
70
+ table_name = table.fetch(:model_name)
71
+ table.fetch(:fields).filter_map do |logical_field, attributes|
72
+ reference = attributes[:references]
73
+ next unless reference
74
+
75
+ column = attributes[:field_name] || physical_name(logical_field)
76
+ target = foreign_key_target(reference.fetch(:model), options)
77
+ on_delete = reference[:on_delete] ? ", on_delete: :#{reference[:on_delete]}" : ""
78
+ " add_foreign_key :#{table_name}, :#{target}, column: :#{column}#{on_delete}"
79
+ end
80
+ end
81
+
82
+ def rails_type(attributes)
83
+ case attributes[:type]
84
+ when "boolean" then "boolean"
85
+ when "date" then "datetime"
86
+ when "number" then attributes[:bigint] ? "bigint" : "integer"
87
+ else "string"
88
+ end
89
+ end
90
+
91
+ def default_value(attributes)
92
+ default = attributes[:default_value]
93
+ return if default.respond_to?(:call)
94
+
95
+ case default
96
+ when true then "true"
97
+ when false then "false"
98
+ when Numeric then default.to_s
99
+ when String then default.inspect
100
+ end
101
+ end
102
+
103
+ def physical_name(value)
104
+ BetterAuth::Schema.send(:physical_name, value)
105
+ end
106
+
107
+ def foreign_key_target(model, options)
108
+ BetterAuth::Schema.auth_tables(options).fetch(model.to_s, nil)&.fetch(:model_name) || model
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ class MountedApp
6
+ def initialize(auth, mount_path:)
7
+ @auth = auth
8
+ @mount_path = normalize_path(mount_path)
9
+ end
10
+
11
+ def call(env)
12
+ @auth.call(env.merge("PATH_INFO" => mounted_path_info(env)))
13
+ end
14
+
15
+ private
16
+
17
+ def mounted_path_info(env)
18
+ 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}/")
23
+
24
+ normalize_path("#{prefix}/#{path_info.delete_prefix("/")}")
25
+ end
26
+
27
+ def normalize_path(path)
28
+ normalized = path.to_s
29
+ normalized = "/#{normalized}" unless normalized.start_with?("/")
30
+ normalized = normalized.squeeze("/")
31
+ normalized = normalized.delete_suffix("/") unless normalized == "/"
32
+ normalized.empty? ? "/" : normalized
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ initializer "better_auth_rails.routes" do
7
+ ActiveSupport.on_load(:action_dispatch_routing) do
8
+ include BetterAuth::Rails::Routing
9
+ end
10
+ end
11
+
12
+ rake_tasks do
13
+ load File.expand_path("../../tasks/better_auth.rake", __dir__)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Rails
5
+ module Routing
6
+ def better_auth(auth: nil, at: BetterAuth::Configuration::DEFAULT_BASE_PATH)
7
+ mount_path = normalize_better_auth_mount_path(at)
8
+ auth ||= BetterAuth::Rails.auth(base_path: mount_path)
9
+ mount BetterAuth::Rails::MountedApp.new(auth, mount_path: mount_path), at: mount_path
10
+ end
11
+
12
+ private
13
+
14
+ def normalize_better_auth_mount_path(path)
15
+ normalized = path.to_s
16
+ normalized = "/#{normalized}" unless normalized.start_with?("/")
17
+ normalized = normalized.squeeze("/")
18
+ (normalized == "/") ? normalized : normalized.delete_suffix("/")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Rails
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -1,10 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "better_auth"
3
4
  require_relative "rails/version"
5
+ require_relative "rails/configuration"
6
+ require_relative "rails/migration"
7
+ require_relative "rails/active_record_adapter"
8
+ require_relative "rails/mounted_app"
9
+ require_relative "rails/routing"
10
+ require_relative "rails/controller_helpers"
11
+ require_relative "rails/railtie" if defined?(::Rails::Railtie)
4
12
 
5
13
  module BetterAuth
6
14
  module Rails
7
- # Rails-specific authentication adapters
8
- # Provides middleware, controller helpers, and generators
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield configuration
22
+ @auth = nil
23
+ end
24
+
25
+ def auth(overrides = nil)
26
+ options = configuration.to_auth_options
27
+ return @auth ||= BetterAuth.auth(options) if overrides.nil? || overrides.empty?
28
+
29
+ BetterAuth.auth(options.merge(overrides))
30
+ end
31
+ end
9
32
  end
10
33
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "better_auth/rails"
5
+ require "generators/better_auth/migration/migration_generator"
6
+
7
+ module BetterAuth
8
+ module Generators
9
+ class InstallGenerator < ::Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+ class_option :database, type: :string, default: "active_record"
12
+
13
+ def create_initializer
14
+ initializer = "config/initializers/better_auth.rb"
15
+ if File.exist?(destination_path(initializer))
16
+ say_status :skip, "#{initializer} already exists"
17
+ return
18
+ end
19
+
20
+ template "initializer.rb.tt", initializer
21
+ end
22
+
23
+ def create_migration
24
+ MigrationGenerator.start([], destination_root: destination_root)
25
+ end
26
+
27
+ private
28
+
29
+ def destination_path(path)
30
+ File.join(destination_root, path)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ BetterAuth::Rails.configure do |config|
4
+ config.secret =
5
+ Rails.application.credentials.dig(:better_auth, :secret) ||
6
+ Rails.application.credentials.secret_key_base ||
7
+ Rails.application.secret_key_base
8
+
9
+ config.base_url = ENV["BETTER_AUTH_URL"]
10
+ config.base_path = "/api/auth"
11
+ config.database = ->(options) { BetterAuth::Rails::ActiveRecordAdapter.new(options) }
12
+ config.trusted_origins = [
13
+ ENV["BETTER_AUTH_URL"]
14
+ ].compact
15
+
16
+ config.session = {
17
+ cookie_cache: {
18
+ enabled: true,
19
+ max_age: 5 * 60,
20
+ strategy: "jwe"
21
+ }
22
+ }
23
+
24
+ config.advanced = {
25
+ ip_address: {
26
+ ip_address_headers: ["x-forwarded-for"],
27
+ disable_ip_tracking: false
28
+ }
29
+ }
30
+
31
+ config.experimental = {
32
+ joins: false
33
+ }
34
+
35
+ config.social_providers = {
36
+ # github: BetterAuth::SocialProviders.github(
37
+ # client_id: ENV.fetch("GITHUB_CLIENT_ID"),
38
+ # client_secret: ENV.fetch("GITHUB_CLIENT_SECRET")
39
+ # )
40
+ }
41
+
42
+ # Add Better Auth plugins here.
43
+ config.plugins = []
44
+
45
+ # Add Better Auth hooks here. Auth decisions still run through the core gem.
46
+ config.hooks = {
47
+ before: [],
48
+ after: []
49
+ }
50
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "better_auth/rails"
5
+
6
+ module BetterAuth
7
+ module Generators
8
+ class MigrationGenerator < ::Rails::Generators::Base
9
+ def create_migration
10
+ if existing_migration?
11
+ say_status :skip, "db/migrate/*_create_better_auth_tables.rb already exists"
12
+ return
13
+ end
14
+
15
+ create_file migration_path, BetterAuth::Rails::Migration.render(generator_config)
16
+ end
17
+
18
+ private
19
+
20
+ def existing_migration?
21
+ Dir[File.join(destination_root, "db/migrate/*_create_better_auth_tables.rb")].any?
22
+ end
23
+
24
+ def migration_path
25
+ File.join("db/migrate", "#{timestamp}_create_better_auth_tables.rb")
26
+ end
27
+
28
+ def timestamp
29
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
30
+ end
31
+
32
+ def generator_config
33
+ options = BetterAuth::Rails.configuration.to_auth_options
34
+ options[:secret] ||= BetterAuth::Configuration::DEFAULT_SECRET
35
+ options[:database] ||= :memory
36
+ BetterAuth::Configuration.new(options)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :better_auth do
4
+ desc "Create the Better Auth initializer and base migration"
5
+ task :init do
6
+ require "generators/better_auth/install/install_generator"
7
+ BetterAuth::Generators::InstallGenerator.start([])
8
+ end
9
+
10
+ namespace :generate do
11
+ desc "Create the Better Auth base migration"
12
+ task :migration do
13
+ require "generators/better_auth/migration/migration_generator"
14
+ BetterAuth::Generators::MigrationGenerator.start([])
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-03-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: better_auth
@@ -64,6 +63,26 @@ dependencies:
64
63
  - - "<"
65
64
  - !ruby/object:Gem::Version
66
65
  version: '9'
66
+ - !ruby/object:Gem::Dependency
67
+ name: activerecord
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '6.0'
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '9'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '6.0'
83
+ - - "<"
84
+ - !ruby/object:Gem::Version
85
+ version: '9'
67
86
  - !ruby/object:Gem::Dependency
68
87
  name: bundler
69
88
  requirement: !ruby/object:Gem::Requirement
@@ -134,6 +153,20 @@ dependencies:
134
153
  - - "~>"
135
154
  - !ruby/object:Gem::Version
136
155
  version: '0.22'
156
+ - !ruby/object:Gem::Dependency
157
+ name: pg
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - "~>"
161
+ - !ruby/object:Gem::Version
162
+ version: '1.5'
163
+ type: :development
164
+ prerelease: false
165
+ version_requirements: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - "~>"
168
+ - !ruby/object:Gem::Version
169
+ version: '1.5'
137
170
  description: Rails integration for Better Auth Ruby. Provides middleware, controller
138
171
  helpers, and generators.
139
172
  email:
@@ -146,8 +179,19 @@ files:
146
179
  - LICENSE.md
147
180
  - README.md
148
181
  - lib/better_auth/rails.rb
182
+ - lib/better_auth/rails/active_record_adapter.rb
183
+ - lib/better_auth/rails/configuration.rb
184
+ - lib/better_auth/rails/controller_helpers.rb
185
+ - lib/better_auth/rails/migration.rb
186
+ - lib/better_auth/rails/mounted_app.rb
187
+ - lib/better_auth/rails/railtie.rb
188
+ - lib/better_auth/rails/routing.rb
149
189
  - lib/better_auth/rails/version.rb
150
190
  - lib/better_auth_rails.rb
191
+ - lib/generators/better_auth/install/install_generator.rb
192
+ - lib/generators/better_auth/install/templates/initializer.rb.tt
193
+ - lib/generators/better_auth/migration/migration_generator.rb
194
+ - lib/tasks/better_auth.rake
151
195
  homepage: https://github.com/sebasxsala/better-auth
152
196
  licenses:
153
197
  - MIT
@@ -156,7 +200,6 @@ metadata:
156
200
  source_code_uri: https://github.com/sebasxsala/better-auth
157
201
  changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-rails/CHANGELOG.md
158
202
  bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
159
- post_install_message:
160
203
  rdoc_options: []
161
204
  require_paths:
162
205
  - lib
@@ -171,8 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
214
  - !ruby/object:Gem::Version
172
215
  version: '0'
173
216
  requirements: []
174
- rubygems_version: 3.5.22
175
- signing_key:
217
+ rubygems_version: 3.6.9
176
218
  specification_version: 4
177
219
  summary: Rails adapter for Better Auth
178
220
  test_files: []