better_auth-roda 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 +7 -0
- data/CHANGELOG.md +8 -0
- data/README.md +62 -0
- data/lib/better_auth/roda/configuration.rb +75 -0
- data/lib/better_auth/roda/migration.rb +45 -0
- data/lib/better_auth/roda/mounted_app.rb +108 -0
- data/lib/better_auth/roda/plugin.rb +166 -0
- data/lib/better_auth/roda/tasks.rb +82 -0
- data/lib/better_auth/roda/version.rb +7 -0
- data/lib/better_auth/roda.rb +97 -0
- metadata +159 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 77eb658098b1736ed8bfe2217147b77081c8d0503b546047d09d31c968ca2122
|
|
4
|
+
data.tar.gz: b852714e6e2a39c99b4ab79c61b85de9a98506edc759c16a029d517f3f338ed9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7a87f3f1b578f3f1a1feb6504408612f9b29ba6b5a5197152589e90fd1a8eb1bb13e813b5f387c0ba726a56eae888ba4988415a4c3871dd9f24986a5660d4bde
|
|
7
|
+
data.tar.gz: fc456faca0a278968ce208d3a2feb78e01a923be18c9eb2cbf281f5c14ac58ec93235bfadb26e9964e1970a4233f85be7c5e12afb16fa30484a3fd90e0731bf8
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Better Auth Roda
|
|
2
|
+
|
|
3
|
+
Roda adapter for Better Auth Ruby. This package is a thin integration around
|
|
4
|
+
the core Rack auth object, with a Roda plugin, request helpers, and SQL
|
|
5
|
+
migration tasks.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "better_auth-roda"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "roda"
|
|
17
|
+
require "better_auth/roda"
|
|
18
|
+
|
|
19
|
+
class App < Roda
|
|
20
|
+
plugin :better_auth
|
|
21
|
+
|
|
22
|
+
better_auth at: "/api/auth" do |config|
|
|
23
|
+
config.secret = ENV.fetch("BETTER_AUTH_SECRET")
|
|
24
|
+
config.base_url = ENV["BETTER_AUTH_URL"]
|
|
25
|
+
config.database = :memory
|
|
26
|
+
config.email_and_password = {enabled: true}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
route do |r|
|
|
30
|
+
r.better_auth
|
|
31
|
+
|
|
32
|
+
r.get "dashboard" do
|
|
33
|
+
current_user.to_json
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Rake Tasks
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require "better_auth/roda/tasks"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rake better_auth:install
|
|
47
|
+
rake better_auth:generate:migration
|
|
48
|
+
rake better_auth:migrate
|
|
49
|
+
rake better_auth:migrate:status
|
|
50
|
+
rake better_auth:doctor
|
|
51
|
+
rake better_auth:routes
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When a SQL adapter is configured, migration generation introspects the current
|
|
55
|
+
database and emits only missing Better Auth tables, columns, and indexes.
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd packages/better_auth-roda
|
|
61
|
+
bundle exec rake ci
|
|
62
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Roda
|
|
5
|
+
class Configuration
|
|
6
|
+
AUTH_OPTION_NAMES = %i[
|
|
7
|
+
app_name
|
|
8
|
+
base_url
|
|
9
|
+
base_path
|
|
10
|
+
secret
|
|
11
|
+
secrets
|
|
12
|
+
database
|
|
13
|
+
plugins
|
|
14
|
+
trusted_origins
|
|
15
|
+
rate_limit
|
|
16
|
+
session
|
|
17
|
+
account
|
|
18
|
+
user
|
|
19
|
+
verification
|
|
20
|
+
advanced
|
|
21
|
+
email_and_password
|
|
22
|
+
password_hasher
|
|
23
|
+
email_verification
|
|
24
|
+
social_providers
|
|
25
|
+
experimental
|
|
26
|
+
secondary_storage
|
|
27
|
+
database_hooks
|
|
28
|
+
hooks
|
|
29
|
+
on_api_error
|
|
30
|
+
disabled_paths
|
|
31
|
+
logger
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
attr_accessor(*AUTH_OPTION_NAMES)
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
@base_path = BetterAuth::Configuration::DEFAULT_BASE_PATH
|
|
38
|
+
@plugins = []
|
|
39
|
+
@trusted_origins = []
|
|
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
|
+
|
|
52
|
+
def copy
|
|
53
|
+
self.class.new.tap do |copy|
|
|
54
|
+
AUTH_OPTION_NAMES.each do |name|
|
|
55
|
+
value = public_send(name)
|
|
56
|
+
copy.public_send("#{name}=", deep_dup(value))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def deep_dup(value)
|
|
64
|
+
case value
|
|
65
|
+
when Hash
|
|
66
|
+
value.transform_values { |entry| deep_dup(entry) }
|
|
67
|
+
when Array
|
|
68
|
+
value.map { |entry| deep_dup(entry) }
|
|
69
|
+
else
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "better_auth/sql_migration"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Roda
|
|
7
|
+
module Migration
|
|
8
|
+
DEFAULT_MIGRATIONS_PATH = BetterAuth::SQLMigration::DEFAULT_MIGRATIONS_PATH
|
|
9
|
+
MISSING_MIGRATIONS_TABLE_MESSAGES = BetterAuth::SQLMigration::MISSING_MIGRATIONS_TABLE_MESSAGES
|
|
10
|
+
UnsupportedAdapterError = BetterAuth::SQLMigration::UnsupportedAdapterError
|
|
11
|
+
GENERATOR = "better_auth-roda"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def render(options, dialect:)
|
|
16
|
+
BetterAuth::SQLMigration.render(options, dialect: dialect, generator: GENERATOR)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate(options, dialect:, migrations_path: DEFAULT_MIGRATIONS_PATH, timestamp: Time.now.utc.strftime("%Y%m%d%H%M%S"), connection: nil)
|
|
20
|
+
BetterAuth::SQLMigration.generate(
|
|
21
|
+
options,
|
|
22
|
+
dialect: dialect,
|
|
23
|
+
generator: GENERATOR,
|
|
24
|
+
migrations_path: migrations_path,
|
|
25
|
+
timestamp: timestamp,
|
|
26
|
+
connection: connection
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def migrate(auth_or_options, migrations_path: DEFAULT_MIGRATIONS_PATH)
|
|
31
|
+
BetterAuth::SQLMigration.migrate(auth_or_options, migrations_path: migrations_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
35
|
+
return BetterAuth::SQLMigration.public_send(name, *args, **kwargs, &block) if BetterAuth::SQLMigration.respond_to?(name)
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def respond_to_missing?(name, include_private = false)
|
|
41
|
+
BetterAuth::SQLMigration.respond_to?(name, include_private) || super
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Roda
|
|
7
|
+
class MountedApp
|
|
8
|
+
def initialize(auth, mount_path:)
|
|
9
|
+
@auth = auth
|
|
10
|
+
@mount_path = normalize_path(mount_path)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def mount_matches?(env)
|
|
14
|
+
return false if @mount_path == "/"
|
|
15
|
+
|
|
16
|
+
path_info = normalize_path(env["PATH_INFO"], trim: true)
|
|
17
|
+
return true if path_info == @mount_path || path_info.start_with?("#{@mount_path}/")
|
|
18
|
+
return true if script_mount_matches?(normalize_path(env["SCRIPT_NAME"], trim: true))
|
|
19
|
+
|
|
20
|
+
full = full_request_path(env)
|
|
21
|
+
full == @mount_path || full.start_with?("#{@mount_path}/")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
@auth.call(next_env(env))
|
|
26
|
+
rescue BetterAuth::APIError, JSON::ParserError
|
|
27
|
+
raise
|
|
28
|
+
rescue => error
|
|
29
|
+
handle_unexpected_error(error, env)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def next_env(env)
|
|
35
|
+
rewritten_path = mounted_path_info(env)
|
|
36
|
+
env.merge("PATH_INFO" => rewritten_path).tap do |next_env|
|
|
37
|
+
next_env["SCRIPT_NAME"] = "" if shared_mount_rewrite?(env, rewritten_path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def full_request_path(env)
|
|
42
|
+
script = env.fetch("SCRIPT_NAME", "").to_s
|
|
43
|
+
path = env.fetch("PATH_INFO", "").to_s
|
|
44
|
+
normalize_path("#{script}#{path}", trim: true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mounted_path_info(env)
|
|
48
|
+
path_info = normalize_path(env["PATH_INFO"], trim: false)
|
|
49
|
+
comparable_path = normalize_path(env["PATH_INFO"], trim: true)
|
|
50
|
+
return path_info if comparable_path == @mount_path || comparable_path.start_with?("#{@mount_path}/")
|
|
51
|
+
|
|
52
|
+
script_name = normalize_path(env["SCRIPT_NAME"], trim: true)
|
|
53
|
+
return normalize_path("#{@mount_path}/#{path_info.delete_prefix("/")}", trim: false) if script_mount_matches?(script_name)
|
|
54
|
+
|
|
55
|
+
prefix = (script_name == "/") ? @mount_path : script_name
|
|
56
|
+
return path_info if comparable_path == prefix || comparable_path.start_with?("#{prefix}/")
|
|
57
|
+
|
|
58
|
+
normalize_path("#{prefix}/#{path_info.delete_prefix("/")}", trim: false)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def script_mount_matches?(script_name)
|
|
62
|
+
script_name == @mount_path || script_name.end_with?(@mount_path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def shared_mount_rewrite?(env, rewritten_path)
|
|
66
|
+
script_name = normalize_path(env["SCRIPT_NAME"], trim: true)
|
|
67
|
+
original_path = normalize_path(env["PATH_INFO"], trim: true)
|
|
68
|
+
script_name != "/" &&
|
|
69
|
+
!original_path.start_with?("#{@mount_path}/") &&
|
|
70
|
+
rewritten_path.start_with?("#{@mount_path}/")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def normalize_path(path, trim: true)
|
|
74
|
+
normalized = path.to_s
|
|
75
|
+
normalized = "/#{normalized}" unless normalized.start_with?("/")
|
|
76
|
+
normalized = normalized.squeeze("/")
|
|
77
|
+
normalized = normalized.delete_suffix("/") if trim && normalized != "/"
|
|
78
|
+
normalized.empty? ? "/" : normalized
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def handle_unexpected_error(error, env)
|
|
82
|
+
options = @auth.options
|
|
83
|
+
on_api_error = options.on_api_error || {}
|
|
84
|
+
raise error if on_api_error[:throw] || on_api_error["throw"]
|
|
85
|
+
|
|
86
|
+
callback = on_api_error[:on_error] || on_api_error[:onError] || on_api_error["on_error"] || on_api_error["onError"]
|
|
87
|
+
callback.call(error, error_context(env)) if callback.respond_to?(:call)
|
|
88
|
+
|
|
89
|
+
api_error = BetterAuth::APIError.new("INTERNAL_SERVER_ERROR")
|
|
90
|
+
[
|
|
91
|
+
api_error.status_code,
|
|
92
|
+
{"content-type" => "application/json"},
|
|
93
|
+
[JSON.generate(api_error.to_h)]
|
|
94
|
+
]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def error_context(env)
|
|
98
|
+
path = mounted_path_info(env)
|
|
99
|
+
route_path = if path == @mount_path
|
|
100
|
+
"/"
|
|
101
|
+
else
|
|
102
|
+
path.delete_prefix(@mount_path)
|
|
103
|
+
end
|
|
104
|
+
Struct.new(:path, :env).new(normalize_path(route_path), env)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
class ::Roda
|
|
6
|
+
module RodaPlugins
|
|
7
|
+
module BetterAuthPlugin
|
|
8
|
+
def self.configure(app)
|
|
9
|
+
app.opts[:better_auth_configured] = false unless app.opts.key?(:better_auth_configured)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
def better_auth(at: BetterAuth::Configuration::DEFAULT_BASE_PATH, auth: nil, **overrides)
|
|
14
|
+
mount_path = normalize_better_auth_mount_path(at)
|
|
15
|
+
if mount_path == "/"
|
|
16
|
+
raise ArgumentError,
|
|
17
|
+
"better_auth mount path cannot be '/' (it would capture every request). " \
|
|
18
|
+
"Use a prefix such as #{BetterAuth::Configuration::DEFAULT_BASE_PATH.inspect}."
|
|
19
|
+
end
|
|
20
|
+
if opts[:better_auth_configured] && opts[:better_auth_owner].equal?(self)
|
|
21
|
+
raise ArgumentError, "better_auth is already configured for this app"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
config = BetterAuth::Roda.configuration.copy
|
|
25
|
+
yield config if block_given?
|
|
26
|
+
config.base_path = mount_path
|
|
27
|
+
options = config.to_auth_options.merge(overrides).merge(base_path: mount_path)
|
|
28
|
+
auth_instance = auth || BetterAuth.auth(options)
|
|
29
|
+
|
|
30
|
+
opts[:better_auth_auth] = auth_instance
|
|
31
|
+
opts[:better_auth_mount_path] = mount_path
|
|
32
|
+
opts[:better_auth_mounted_app] = BetterAuth::Roda::MountedApp.new(auth_instance, mount_path: mount_path)
|
|
33
|
+
opts[:better_auth_owner] = self
|
|
34
|
+
opts[:better_auth_configured] = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def normalize_better_auth_mount_path(path)
|
|
40
|
+
normalized = path.to_s
|
|
41
|
+
normalized = "/#{normalized}" unless normalized.start_with?("/")
|
|
42
|
+
normalized = normalized.squeeze("/")
|
|
43
|
+
(normalized == "/") ? normalized : normalized.delete_suffix("/")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module RequestMethods
|
|
48
|
+
def better_auth
|
|
49
|
+
app = scope.opts[:better_auth_mounted_app]
|
|
50
|
+
return unless app&.mount_matches?(env)
|
|
51
|
+
|
|
52
|
+
halt app.call(env)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module InstanceMethods
|
|
57
|
+
def current_session
|
|
58
|
+
data = better_auth_session_data
|
|
59
|
+
data&.fetch(:session, nil) || data&.fetch("session", nil)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def current_user
|
|
63
|
+
data = better_auth_session_data
|
|
64
|
+
data&.fetch(:user, nil) || data&.fetch("user", nil)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def authenticated?
|
|
68
|
+
!current_user.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def require_authentication
|
|
72
|
+
return true if authenticated?
|
|
73
|
+
|
|
74
|
+
if prefers_json_response?
|
|
75
|
+
error = BetterAuth::APIError.new("UNAUTHORIZED")
|
|
76
|
+
request.halt [
|
|
77
|
+
401,
|
|
78
|
+
{"content-type" => "application/json"},
|
|
79
|
+
[JSON.generate(error.to_h)]
|
|
80
|
+
]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
request.halt [401, {}, [""]]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def prefers_json_response?
|
|
89
|
+
accept = request.env["HTTP_ACCEPT"].to_s
|
|
90
|
+
return false if accept.empty? || accept == "*/*"
|
|
91
|
+
|
|
92
|
+
preferred = accept.split(",").filter_map do |entry|
|
|
93
|
+
media_type, *params = entry.split(";").map(&:strip)
|
|
94
|
+
next unless media_type == "application/json" || media_type.end_with?("+json") || media_type == "text/html"
|
|
95
|
+
|
|
96
|
+
q = params.find { |param| param.start_with?("q=") }&.split("=", 2)&.last&.to_f || 1.0
|
|
97
|
+
[media_type, q]
|
|
98
|
+
end.max_by { |_media_type, q| q }
|
|
99
|
+
|
|
100
|
+
if preferred
|
|
101
|
+
media_type, q = preferred
|
|
102
|
+
return q.positive? && (media_type == "application/json" || media_type.end_with?("+json"))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
accept.split(",").any? do |entry|
|
|
106
|
+
media_type = entry.split(";", 2).first.to_s.strip
|
|
107
|
+
media_type == "application/json" || media_type.end_with?("+json")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def better_auth_session_data
|
|
112
|
+
return request.env["better_auth.session"] if request.env.key?("better_auth.session")
|
|
113
|
+
|
|
114
|
+
request.env["better_auth.session"] = resolve_better_auth_session
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def resolve_better_auth_session
|
|
118
|
+
auth = better_auth_auth
|
|
119
|
+
result = auth.api.get_session(
|
|
120
|
+
request: Rack::Request.new(request.env),
|
|
121
|
+
method: "GET",
|
|
122
|
+
as_response: true
|
|
123
|
+
)
|
|
124
|
+
return resolve_better_auth_response(result) if result.respond_to?(:headers) && result.respond_to?(:body)
|
|
125
|
+
|
|
126
|
+
apply_better_auth_response_headers(result[:headers] || result["headers"] || {})
|
|
127
|
+
result[:response] || result["response"]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def resolve_better_auth_response(auth_response)
|
|
131
|
+
apply_better_auth_response_headers(auth_response.headers || {})
|
|
132
|
+
body = auth_response.body.respond_to?(:join) ? auth_response.body.join : auth_response.body.to_s
|
|
133
|
+
payload = body.empty? ? nil : JSON.parse(body)
|
|
134
|
+
raise_better_auth_response_error(auth_response, payload) if auth_response.status.to_i >= 400
|
|
135
|
+
|
|
136
|
+
payload
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def raise_better_auth_response_error(auth_response, payload)
|
|
140
|
+
payload = payload.is_a?(Hash) ? payload : {}
|
|
141
|
+
status = BetterAuth::APIError::STATUS_CODES.key(auth_response.status.to_i) || "INTERNAL_SERVER_ERROR"
|
|
142
|
+
raise BetterAuth::APIError.new(
|
|
143
|
+
status,
|
|
144
|
+
message: payload["message"],
|
|
145
|
+
code: payload["code"],
|
|
146
|
+
headers: auth_response.headers || {}
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def apply_better_auth_response_headers(headers)
|
|
151
|
+
set_cookie = headers["set-cookie"] || headers["Set-Cookie"] || headers[:set_cookie]
|
|
152
|
+
return if set_cookie.to_s.empty?
|
|
153
|
+
|
|
154
|
+
existing = response.headers["set-cookie"].to_s
|
|
155
|
+
response.headers["set-cookie"] = [existing, set_cookie.to_s].reject(&:empty?).join("\n")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def better_auth_auth
|
|
159
|
+
opts[:better_auth_auth] || BetterAuth::Roda.auth
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
register_plugin(:better_auth, BetterAuthPlugin)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rake"
|
|
5
|
+
require "better_auth/roda"
|
|
6
|
+
|
|
7
|
+
namespace :better_auth do
|
|
8
|
+
desc "Create the Better Auth Roda config and migration directory"
|
|
9
|
+
task :install do
|
|
10
|
+
config_path = "config/better_auth.rb"
|
|
11
|
+
FileUtils.mkdir_p(File.dirname(config_path))
|
|
12
|
+
if File.exist?(config_path)
|
|
13
|
+
puts "skip #{config_path} already exists"
|
|
14
|
+
else
|
|
15
|
+
File.write(config_path, BetterAuth::Roda.default_config_template)
|
|
16
|
+
puts "create #{config_path}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
FileUtils.mkdir_p(BetterAuth::Roda::Migration::DEFAULT_MIGRATIONS_PATH)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
namespace :generate do
|
|
23
|
+
desc "Create the Better Auth SQL migration"
|
|
24
|
+
task :migration do
|
|
25
|
+
BetterAuth::Roda.load_app_config!
|
|
26
|
+
dialect = BetterAuth::Roda::Migration.normalize_dialect(BetterAuth::Env.get("BETTER_AUTH_DIALECT") || BetterAuth::Env.get("BETTER_AUTH_DATABASE_DIALECT") || "postgres")
|
|
27
|
+
config = BetterAuth::Roda.migration_configuration
|
|
28
|
+
adapter = begin
|
|
29
|
+
BetterAuth::Roda.auth.context.adapter
|
|
30
|
+
rescue
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
connection = if adapter&.respond_to?(:connection) && adapter.respond_to?(:dialect) && BetterAuth::Roda::Migration.normalize_dialect(adapter.dialect) == dialect
|
|
34
|
+
adapter.connection
|
|
35
|
+
end
|
|
36
|
+
path = BetterAuth::Roda::Migration.generate(config, dialect: dialect, connection: connection)
|
|
37
|
+
puts(path ? "create #{path}" : "no migrations needed")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Run pending Better Auth SQL migrations"
|
|
42
|
+
task :migrate do
|
|
43
|
+
BetterAuth::Roda.load_app_config!
|
|
44
|
+
BetterAuth::Roda::Migration.migrate(BetterAuth::Roda.auth)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
namespace :migrate do
|
|
48
|
+
desc "Print pending Better Auth SQL migration status"
|
|
49
|
+
task :status do
|
|
50
|
+
BetterAuth::Roda.load_app_config!
|
|
51
|
+
auth = BetterAuth::Roda.auth
|
|
52
|
+
adapter = auth.context.adapter
|
|
53
|
+
unless adapter.respond_to?(:connection) && adapter.respond_to?(:dialect)
|
|
54
|
+
raise BetterAuth::Roda::Migration::UnsupportedAdapterError, "Better Auth SQL migrations require core SQL adapters with connection and dialect support"
|
|
55
|
+
end
|
|
56
|
+
plan = BetterAuth::Roda::Migration.plan(auth.options, connection: adapter.connection, dialect: adapter.dialect)
|
|
57
|
+
if plan.empty?
|
|
58
|
+
puts "No migrations needed."
|
|
59
|
+
else
|
|
60
|
+
plan.to_create.each { |change| puts "create table #{change.table_name}" }
|
|
61
|
+
plan.to_add.each { |change| puts "add #{change.fields.keys.join(", ")} to #{change.table_name}" }
|
|
62
|
+
plan.to_index.each { |change| puts "create index #{change.name}" }
|
|
63
|
+
plan.warnings.each { |warning| puts "warning: #{warning}" }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "Check Better Auth configuration and schema health"
|
|
69
|
+
task :doctor do
|
|
70
|
+
BetterAuth::Roda.load_app_config!
|
|
71
|
+
exit_code = BetterAuth::Doctor.print(BetterAuth::Doctor.check(BetterAuth::Roda.migration_configuration), stdout: $stdout, stderr: $stderr)
|
|
72
|
+
abort if exit_code != 0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc "Print Better Auth Roda mount information"
|
|
76
|
+
task :routes do
|
|
77
|
+
BetterAuth::Roda.load_app_config!
|
|
78
|
+
mount_path = BetterAuth::Roda.configuration.base_path
|
|
79
|
+
puts "#{mount_path}/* -> BetterAuth.auth"
|
|
80
|
+
puts "Core routes are handled by Better Auth; use the OpenAPI plugin or HTTP API docs for endpoint details."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "better_auth"
|
|
4
|
+
require "roda"
|
|
5
|
+
require_relative "roda/version"
|
|
6
|
+
require_relative "roda/configuration"
|
|
7
|
+
require_relative "roda/mounted_app"
|
|
8
|
+
require_relative "roda/migration"
|
|
9
|
+
require_relative "roda/plugin"
|
|
10
|
+
|
|
11
|
+
module BetterAuth
|
|
12
|
+
module Roda
|
|
13
|
+
class << self
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield configuration
|
|
20
|
+
@auth = nil
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset!
|
|
25
|
+
@configuration = nil
|
|
26
|
+
@auth = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def auth(overrides = nil)
|
|
30
|
+
options = configuration.to_auth_options
|
|
31
|
+
return @auth ||= BetterAuth.auth(options) if overrides.nil? || overrides.empty?
|
|
32
|
+
|
|
33
|
+
BetterAuth.auth(options.merge(overrides))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def migration_configuration
|
|
37
|
+
options = configuration.to_auth_options
|
|
38
|
+
options[:secret] ||= BetterAuth::Configuration::DEFAULT_SECRET
|
|
39
|
+
BetterAuth::Configuration.new(options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def app_config_path(path = nil)
|
|
43
|
+
path || BetterAuth::Env.get("BETTER_AUTH_CONFIG") || "config/better_auth.rb"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def load_app_config(path = nil)
|
|
47
|
+
config_path = app_config_path(path)
|
|
48
|
+
return false unless File.exist?(config_path)
|
|
49
|
+
|
|
50
|
+
load config_path
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def load_app_config!(path = nil)
|
|
55
|
+
config_path = app_config_path(path)
|
|
56
|
+
return true if load_app_config(config_path)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"Better Auth Roda config not found at #{config_path.inspect}. " \
|
|
60
|
+
"Run `rake better_auth:install` or set BETTER_AUTH_CONFIG to a shared config file."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def default_config_template
|
|
64
|
+
<<~RUBY
|
|
65
|
+
# frozen_string_literal: true
|
|
66
|
+
|
|
67
|
+
require "better_auth/roda"
|
|
68
|
+
|
|
69
|
+
BetterAuth::Roda.configure do |config|
|
|
70
|
+
config.secret = BetterAuth::Env.fetch("BETTER_AUTH_SECRET", "change-me-roda-secret-12345678901234567890")
|
|
71
|
+
config.base_url = BetterAuth::Env.get("BETTER_AUTH_URL")
|
|
72
|
+
config.base_path = "/api/auth"
|
|
73
|
+
|
|
74
|
+
config.database = ->(options) do
|
|
75
|
+
case BetterAuth::Env.fetch("BETTER_AUTH_DATABASE_DIALECT", "postgres")
|
|
76
|
+
when "postgres", "postgresql"
|
|
77
|
+
BetterAuth::Adapters::Postgres.new(options, url: ENV.fetch("DATABASE_URL"))
|
|
78
|
+
when "mysql"
|
|
79
|
+
BetterAuth::Adapters::MySQL.new(options, url: ENV.fetch("DATABASE_URL"))
|
|
80
|
+
when "sqlite", "sqlite3"
|
|
81
|
+
BetterAuth::Adapters::SQLite.new(options, path: ENV.fetch("DATABASE_URL", "db/better_auth.sqlite3"))
|
|
82
|
+
else
|
|
83
|
+
raise "Unsupported BETTER_AUTH_DATABASE_DIALECT for better_auth-roda"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
config.email_and_password = {
|
|
88
|
+
enabled: true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
config.plugins = []
|
|
92
|
+
end
|
|
93
|
+
RUBY
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: better_auth-roda
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.10.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sebastian Sala
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: better_auth
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: roda
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '4'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.0'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '4'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: bundler
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.5'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.5'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rack-test
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '2.2'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '2.2'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rake
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '13.2'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '13.2'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rspec
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '3.13'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '3.13'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: standardrb
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '1.0'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '1.0'
|
|
116
|
+
description: Roda integration for Better Auth Ruby. Better Auth Ruby is an independent
|
|
117
|
+
modern authentication framework for Ruby inspired by Better Auth. Provides a Roda
|
|
118
|
+
plugin, request helpers, and SQL migration tasks.
|
|
119
|
+
email:
|
|
120
|
+
- sebastian.sala.tech@gmail.com
|
|
121
|
+
executables: []
|
|
122
|
+
extensions: []
|
|
123
|
+
extra_rdoc_files: []
|
|
124
|
+
files:
|
|
125
|
+
- CHANGELOG.md
|
|
126
|
+
- README.md
|
|
127
|
+
- lib/better_auth/roda.rb
|
|
128
|
+
- lib/better_auth/roda/configuration.rb
|
|
129
|
+
- lib/better_auth/roda/migration.rb
|
|
130
|
+
- lib/better_auth/roda/mounted_app.rb
|
|
131
|
+
- lib/better_auth/roda/plugin.rb
|
|
132
|
+
- lib/better_auth/roda/tasks.rb
|
|
133
|
+
- lib/better_auth/roda/version.rb
|
|
134
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
135
|
+
licenses:
|
|
136
|
+
- MIT
|
|
137
|
+
metadata:
|
|
138
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
139
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
140
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-roda/CHANGELOG.md
|
|
141
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
142
|
+
rdoc_options: []
|
|
143
|
+
require_paths:
|
|
144
|
+
- lib
|
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - ">="
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: 3.2.0
|
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: '0'
|
|
155
|
+
requirements: []
|
|
156
|
+
rubygems_version: 3.6.9
|
|
157
|
+
specification_version: 4
|
|
158
|
+
summary: Roda adapter for Better Auth
|
|
159
|
+
test_files: []
|