better_auth-grape 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 +10 -0
- data/README.md +76 -0
- data/lib/better_auth/grape/configuration.rb +75 -0
- data/lib/better_auth/grape/extension.rb +106 -0
- data/lib/better_auth/grape/helpers.rb +98 -0
- data/lib/better_auth/grape/migration.rb +52 -0
- data/lib/better_auth/grape/mounted_app.rb +87 -0
- data/lib/better_auth/grape/tasks.rb +82 -0
- data/lib/better_auth/grape/version.rb +7 -0
- data/lib/better_auth/grape.rb +103 -0
- data/lib/better_auth_grape.rb +3 -0
- metadata +161 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cebdcf11ac8f2e83526cb61c135d129fd04c67118dd1c95d2d36b1ad47cf887a
|
|
4
|
+
data.tar.gz: 8a110152aa341e4a70e1621057e98cffd6902634a7d0b852b6471cbffe530f27
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1423fd9801e4bad997878b453687d154e54aa81da8fa6628b9dcb1988b8d262c1f636fa03cedadf46f95ce7976600d290da2615c9e194b07e4da4eb3e2e9fbaf
|
|
7
|
+
data.tar.gz: dde1aba6f7e74a99a5d28cee11f6298923ba66d8b9536fc4559d1579e4feb25b932dc452f342fb22a1bd20088b7a66211bce7022bfd18ef2a32b7535f2b1f21a
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# better_auth-grape
|
|
2
|
+
|
|
3
|
+
Grape integration for Better Auth Ruby.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "better_auth-grape"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "grape"
|
|
15
|
+
require "better_auth/grape"
|
|
16
|
+
|
|
17
|
+
class API < Grape::API
|
|
18
|
+
include BetterAuth::Grape
|
|
19
|
+
|
|
20
|
+
format :json
|
|
21
|
+
|
|
22
|
+
better_auth at: "/api/auth" do |config|
|
|
23
|
+
config.secret = ENV.fetch("BETTER_AUTH_SECRET")
|
|
24
|
+
config.base_url = ENV.fetch("BETTER_AUTH_URL", "http://localhost:9292")
|
|
25
|
+
config.database = ->(options) {
|
|
26
|
+
BetterAuth::Adapters::Postgres.new(options, url: ENV.fetch("DATABASE_URL"))
|
|
27
|
+
}
|
|
28
|
+
config.email_and_password = {
|
|
29
|
+
enabled: true
|
|
30
|
+
}
|
|
31
|
+
config.plugins = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
get "/dashboard" do
|
|
35
|
+
require_authentication
|
|
36
|
+
{email: current_user.fetch("email")}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When the API uses Grape path prefixes or path-based versioning, pass a relative
|
|
42
|
+
auth path and the adapter mounts below the effective Grape path:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class API < Grape::API
|
|
46
|
+
include BetterAuth::Grape
|
|
47
|
+
|
|
48
|
+
prefix :api
|
|
49
|
+
version "v1", using: :path
|
|
50
|
+
|
|
51
|
+
better_auth at: "/auth" do |config|
|
|
52
|
+
config.secret = ENV.fetch("BETTER_AUTH_SECRET")
|
|
53
|
+
config.base_url = ENV.fetch("BETTER_AUTH_URL")
|
|
54
|
+
config.database = :memory
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This exposes Better Auth at `/api/v1/auth`.
|
|
60
|
+
|
|
61
|
+
Load Rake tasks from your application Rakefile:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require "better_auth/grape/tasks"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
rake better_auth:install
|
|
69
|
+
BETTER_AUTH_DIALECT=postgres rake better_auth:generate:migration
|
|
70
|
+
rake better_auth:migrate
|
|
71
|
+
rake better_auth:migrate:status
|
|
72
|
+
rake better_auth:doctor
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
When a SQL adapter is configured, migration generation introspects the current
|
|
76
|
+
database and emits only missing Better Auth tables, columns, and indexes.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Grape
|
|
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,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Grape
|
|
5
|
+
module Extension
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend ClassMethods
|
|
8
|
+
base.helpers Helpers if base.respond_to?(:helpers)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ClassMethods
|
|
12
|
+
attr_reader :better_auth_auth, :better_auth_mount_path
|
|
13
|
+
|
|
14
|
+
def better_auth(at: BetterAuth::Configuration::DEFAULT_BASE_PATH, auth: nil, **overrides)
|
|
15
|
+
mount_path = effective_better_auth_mount_path(at)
|
|
16
|
+
if mount_path == "/"
|
|
17
|
+
raise ArgumentError,
|
|
18
|
+
"better_auth mount path cannot be '/' (it would capture every request). " \
|
|
19
|
+
"Use a prefix such as #{BetterAuth::Configuration::DEFAULT_BASE_PATH.inspect}."
|
|
20
|
+
end
|
|
21
|
+
raise ArgumentError, "better_auth is already configured for this API" if @better_auth_auth
|
|
22
|
+
|
|
23
|
+
config = BetterAuth::Grape.configuration.copy
|
|
24
|
+
yield config if block_given?
|
|
25
|
+
config.base_path = mount_path
|
|
26
|
+
options = config.to_auth_options.merge(overrides).merge(base_path: mount_path)
|
|
27
|
+
auth_instance = auth || BetterAuth.auth(options)
|
|
28
|
+
@better_auth_auth = auth_instance
|
|
29
|
+
@better_auth_mount_path = mount_path
|
|
30
|
+
|
|
31
|
+
mounted_app = BetterAuth::Grape::MountedApp.new(auth_instance, mount_path: mount_path)
|
|
32
|
+
helpers do
|
|
33
|
+
define_method(:better_auth_auth) { auth_instance }
|
|
34
|
+
end
|
|
35
|
+
mount({mounted_app => mount_path})
|
|
36
|
+
route_setting :better_auth_internal, true
|
|
37
|
+
route(:any, "/*better_auth_path") do
|
|
38
|
+
path_info = env.fetch("PATH_INFO", "").to_s
|
|
39
|
+
normalized_path_info = path_info.start_with?("/") ? path_info.squeeze("/") : "/#{path_info}".squeeze("/")
|
|
40
|
+
script_name = env.fetch("SCRIPT_NAME", "").to_s
|
|
41
|
+
normalized_script_name = script_name.start_with?("/") ? script_name.squeeze("/") : "/#{script_name}".squeeze("/")
|
|
42
|
+
unless normalized_path_info == mount_path ||
|
|
43
|
+
normalized_path_info.start_with?("#{mount_path}/") ||
|
|
44
|
+
normalized_script_name == mount_path ||
|
|
45
|
+
normalized_script_name.end_with?(mount_path)
|
|
46
|
+
error!({error: "Not Found"}, 404)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
rack_status, rack_headers, rack_body = mounted_app.call(env)
|
|
50
|
+
status rack_status
|
|
51
|
+
rack_headers.each { |key, value| header key, value }
|
|
52
|
+
env[::Grape::Env::API_FORMAT] = :txt
|
|
53
|
+
body rack_body.each.to_a.join
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def effective_better_auth_mount_path(path)
|
|
61
|
+
mount_path = normalize_better_auth_mount_path(path)
|
|
62
|
+
api_prefix = better_auth_api_prefix
|
|
63
|
+
return mount_path if api_prefix == "/"
|
|
64
|
+
return mount_path if mount_path == api_prefix || mount_path.start_with?("#{api_prefix}/")
|
|
65
|
+
|
|
66
|
+
normalize_better_auth_mount_path("#{api_prefix}/#{mount_path.delete_prefix("/")}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def better_auth_api_prefix
|
|
70
|
+
parts = [better_auth_root_prefix]
|
|
71
|
+
version_prefix = better_auth_path_version_prefix
|
|
72
|
+
parts << version_prefix unless version_prefix == "/"
|
|
73
|
+
normalize_better_auth_mount_path(parts.reject { |part| part == "/" }.join("/"))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def better_auth_root_prefix
|
|
77
|
+
return "/" unless respond_to?(:prefix)
|
|
78
|
+
|
|
79
|
+
configured_prefix = prefix
|
|
80
|
+
return "/" if configured_prefix.nil? || configured_prefix.to_s.empty?
|
|
81
|
+
|
|
82
|
+
normalize_better_auth_mount_path(configured_prefix)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def better_auth_path_version_prefix
|
|
86
|
+
settings = inheritable_setting.namespace_inheritable if respond_to?(:inheritable_setting)
|
|
87
|
+
version_options = settings&.[](:version_options) || {}
|
|
88
|
+
return "/" unless version_options[:using]&.to_sym == :path
|
|
89
|
+
|
|
90
|
+
versions = settings&.[](:version)
|
|
91
|
+
version = Array(versions).first
|
|
92
|
+
return "/" if version.nil? || version.to_s.empty?
|
|
93
|
+
|
|
94
|
+
normalize_better_auth_mount_path(version)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def normalize_better_auth_mount_path(path)
|
|
98
|
+
normalized = path.to_s
|
|
99
|
+
normalized = "/#{normalized}" unless normalized.start_with?("/")
|
|
100
|
+
normalized = normalized.squeeze("/")
|
|
101
|
+
(normalized == "/") ? normalized : normalized.delete_suffix("/")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Grape
|
|
7
|
+
module Helpers
|
|
8
|
+
def current_session
|
|
9
|
+
data = better_auth_session_data
|
|
10
|
+
data&.fetch(:session, nil) || data&.fetch("session", nil)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def current_user
|
|
14
|
+
data = better_auth_session_data
|
|
15
|
+
data&.fetch(:user, nil) || data&.fetch("user", nil)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticated?
|
|
19
|
+
!current_user.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def require_authentication
|
|
23
|
+
return true if authenticated?
|
|
24
|
+
|
|
25
|
+
error = BetterAuth::APIError.new("UNAUTHORIZED")
|
|
26
|
+
if prefers_json_response?
|
|
27
|
+
error!(error.to_h, 401, {"content-type" => "application/json"})
|
|
28
|
+
else
|
|
29
|
+
error!("", 401)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def prefers_json_response?
|
|
36
|
+
accept = env["HTTP_ACCEPT"].to_s
|
|
37
|
+
return false if accept.empty? || accept == "*/*"
|
|
38
|
+
|
|
39
|
+
accept.split(",").any? do |entry|
|
|
40
|
+
media_type = entry.split(";", 2).first.to_s.strip
|
|
41
|
+
media_type == "application/json" || media_type.end_with?("+json")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def better_auth_session_data
|
|
46
|
+
return env["better_auth.session"] if env.key?("better_auth.session")
|
|
47
|
+
|
|
48
|
+
env["better_auth.session"] = resolve_better_auth_session
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_better_auth_session
|
|
52
|
+
auth = better_auth_auth
|
|
53
|
+
result = auth.api.get_session(
|
|
54
|
+
request: Rack::Request.new(env),
|
|
55
|
+
method: "GET",
|
|
56
|
+
as_response: true
|
|
57
|
+
)
|
|
58
|
+
return resolve_better_auth_response(result) if result.respond_to?(:headers) && result.respond_to?(:body)
|
|
59
|
+
|
|
60
|
+
apply_better_auth_response_headers(result[:headers] || result["headers"] || {})
|
|
61
|
+
result[:response] || result["response"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resolve_better_auth_response(response)
|
|
65
|
+
apply_better_auth_response_headers(response.headers || {})
|
|
66
|
+
body = response.body.respond_to?(:join) ? response.body.join : response.body.to_s
|
|
67
|
+
payload = body.empty? ? nil : JSON.parse(body)
|
|
68
|
+
raise_better_auth_response_error(response, payload) if response.status.to_i >= 400
|
|
69
|
+
|
|
70
|
+
payload
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def raise_better_auth_response_error(response, payload)
|
|
74
|
+
payload = payload.is_a?(Hash) ? payload : {}
|
|
75
|
+
status = BetterAuth::APIError::STATUS_CODES.key(response.status.to_i) || "INTERNAL_SERVER_ERROR"
|
|
76
|
+
raise BetterAuth::APIError.new(
|
|
77
|
+
status,
|
|
78
|
+
message: payload["message"],
|
|
79
|
+
code: payload["code"],
|
|
80
|
+
headers: response.headers || {}
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def apply_better_auth_response_headers(headers)
|
|
85
|
+
set_cookie = headers["set-cookie"] || headers["Set-Cookie"] || headers[:set_cookie]
|
|
86
|
+
return if set_cookie.to_s.empty?
|
|
87
|
+
|
|
88
|
+
existing = header["Set-Cookie"].to_s
|
|
89
|
+
header "Set-Cookie", [existing, set_cookie.to_s].reject(&:empty?).join("\n")
|
|
90
|
+
env["better_auth.set_cookie"] = [env["better_auth.set_cookie"], set_cookie.to_s].compact.join("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def better_auth_auth
|
|
94
|
+
self.class.respond_to?(:better_auth_auth) ? self.class.better_auth_auth : BetterAuth::Grape.auth
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "better_auth/sql_migration"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Grape
|
|
7
|
+
module Migration
|
|
8
|
+
DEFAULT_MIGRATIONS_PATH = BetterAuth::SQLMigration::DEFAULT_MIGRATIONS_PATH
|
|
9
|
+
UnsupportedAdapterError = BetterAuth::SQLMigration::UnsupportedAdapterError
|
|
10
|
+
GENERATOR = "better_auth-grape"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def render(options, dialect:)
|
|
15
|
+
BetterAuth::SQLMigration.render(options, dialect: dialect, generator: GENERATOR)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate(options, dialect:, migrations_path: DEFAULT_MIGRATIONS_PATH, timestamp: Time.now.utc.strftime("%Y%m%d%H%M%S"), connection: nil)
|
|
19
|
+
BetterAuth::SQLMigration.generate(
|
|
20
|
+
options,
|
|
21
|
+
dialect: dialect,
|
|
22
|
+
generator: GENERATOR,
|
|
23
|
+
migrations_path: migrations_path,
|
|
24
|
+
timestamp: timestamp,
|
|
25
|
+
connection: connection
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def migrate(auth_or_options, migrations_path: DEFAULT_MIGRATIONS_PATH)
|
|
30
|
+
BetterAuth::SQLMigration.migrate(auth_or_options, migrations_path: migrations_path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def statements(sql)
|
|
34
|
+
BetterAuth::SQLMigration.statements(sql)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def normalize_dialect(value)
|
|
38
|
+
BetterAuth::SQLMigration.normalize_dialect(value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
42
|
+
return BetterAuth::SQLMigration.public_send(name, *args, **kwargs, &block) if BetterAuth::SQLMigration.respond_to?(name)
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def respond_to_missing?(name, include_private = false)
|
|
48
|
+
BetterAuth::SQLMigration.respond_to?(name, include_private) || super
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Grape
|
|
7
|
+
class MountedApp
|
|
8
|
+
def initialize(auth, mount_path:)
|
|
9
|
+
@auth = auth
|
|
10
|
+
@mount_path = normalize_path(mount_path)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
rewritten_path = mounted_path_info(env)
|
|
15
|
+
next_env = env.merge("PATH_INFO" => rewritten_path)
|
|
16
|
+
next_env["SCRIPT_NAME"] = "" if shared_mount_rewrite?(env, rewritten_path)
|
|
17
|
+
@auth.call(next_env)
|
|
18
|
+
rescue BetterAuth::APIError, JSON::ParserError
|
|
19
|
+
raise
|
|
20
|
+
rescue => error
|
|
21
|
+
handle_unexpected_error(error, env)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def mounted_path_info(env)
|
|
27
|
+
path_info = normalize_path(env["PATH_INFO"], trim: false)
|
|
28
|
+
comparable_path = normalize_path(env["PATH_INFO"], trim: true)
|
|
29
|
+
return path_info if comparable_path == @mount_path || comparable_path.start_with?("#{@mount_path}/")
|
|
30
|
+
|
|
31
|
+
script_name = normalize_path(env["SCRIPT_NAME"], trim: true)
|
|
32
|
+
return normalize_path("#{@mount_path}/#{path_info.delete_prefix("/")}", trim: false) if script_mount_matches?(script_name)
|
|
33
|
+
|
|
34
|
+
prefix = (script_name == "/") ? @mount_path : script_name
|
|
35
|
+
return path_info if comparable_path == prefix || comparable_path.start_with?("#{prefix}/")
|
|
36
|
+
|
|
37
|
+
normalize_path("#{prefix}/#{path_info.delete_prefix("/")}", trim: false)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def script_mount_matches?(script_name)
|
|
41
|
+
script_name == @mount_path || script_name.end_with?(@mount_path.to_s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def shared_mount_rewrite?(env, rewritten_path)
|
|
45
|
+
script_name = normalize_path(env["SCRIPT_NAME"], trim: true)
|
|
46
|
+
original_path = normalize_path(env["PATH_INFO"], trim: true)
|
|
47
|
+
script_name != "/" &&
|
|
48
|
+
!original_path.start_with?("#{@mount_path}/") &&
|
|
49
|
+
rewritten_path.start_with?("#{@mount_path}/")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_path(path, trim: true)
|
|
53
|
+
normalized = path.to_s
|
|
54
|
+
normalized = "/#{normalized}" unless normalized.start_with?("/")
|
|
55
|
+
normalized = normalized.squeeze("/")
|
|
56
|
+
normalized = normalized.delete_suffix("/") if trim && normalized != "/"
|
|
57
|
+
normalized.empty? ? "/" : normalized
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_unexpected_error(error, env)
|
|
61
|
+
options = @auth.options
|
|
62
|
+
on_api_error = options.on_api_error || {}
|
|
63
|
+
raise error if on_api_error[:throw] || on_api_error["throw"]
|
|
64
|
+
|
|
65
|
+
callback = on_api_error[:on_error] || on_api_error[:onError] || on_api_error["on_error"] || on_api_error["onError"]
|
|
66
|
+
callback.call(error, error_context(env)) if callback.respond_to?(:call)
|
|
67
|
+
|
|
68
|
+
api_error = BetterAuth::APIError.new("INTERNAL_SERVER_ERROR")
|
|
69
|
+
[
|
|
70
|
+
api_error.status_code,
|
|
71
|
+
{"content-type" => "application/json"},
|
|
72
|
+
[JSON.generate(api_error.to_h)]
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def error_context(env)
|
|
77
|
+
path = mounted_path_info(env)
|
|
78
|
+
route_path = if path == @mount_path
|
|
79
|
+
"/"
|
|
80
|
+
else
|
|
81
|
+
path.delete_prefix(@mount_path)
|
|
82
|
+
end
|
|
83
|
+
Struct.new(:path, :env).new(normalize_path(route_path), env)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rake"
|
|
5
|
+
require "better_auth/grape"
|
|
6
|
+
|
|
7
|
+
namespace :better_auth do
|
|
8
|
+
desc "Create the Better Auth Grape 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::Grape.default_config_template)
|
|
16
|
+
puts "create #{config_path}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
FileUtils.mkdir_p(BetterAuth::Grape::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::Grape.load_app_config!
|
|
26
|
+
dialect = BetterAuth::Grape::Migration.normalize_dialect(BetterAuth::Env.get("BETTER_AUTH_DIALECT") || BetterAuth::Env.get("BETTER_AUTH_DATABASE_DIALECT") || "postgres")
|
|
27
|
+
config = BetterAuth::Grape.migration_configuration
|
|
28
|
+
adapter = begin
|
|
29
|
+
BetterAuth::Grape.auth.context.adapter
|
|
30
|
+
rescue
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
connection = if adapter&.respond_to?(:connection) && adapter.respond_to?(:dialect) && BetterAuth::Grape::Migration.normalize_dialect(adapter.dialect) == dialect
|
|
34
|
+
adapter.connection
|
|
35
|
+
end
|
|
36
|
+
path = BetterAuth::Grape::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::Grape.load_app_config!
|
|
44
|
+
BetterAuth::Grape::Migration.migrate(BetterAuth::Grape.auth)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
namespace :migrate do
|
|
48
|
+
desc "Print pending Better Auth SQL migration status"
|
|
49
|
+
task :status do
|
|
50
|
+
BetterAuth::Grape.load_app_config!
|
|
51
|
+
auth = BetterAuth::Grape.auth
|
|
52
|
+
adapter = auth.context.adapter
|
|
53
|
+
unless adapter.respond_to?(:connection) && adapter.respond_to?(:dialect)
|
|
54
|
+
raise BetterAuth::Grape::Migration::UnsupportedAdapterError, "Better Auth SQL migrations require core SQL adapters with connection and dialect support"
|
|
55
|
+
end
|
|
56
|
+
plan = BetterAuth::Grape::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::Grape.load_app_config!
|
|
71
|
+
exit_code = BetterAuth::Doctor.print(BetterAuth::Doctor.check(BetterAuth::Grape.migration_configuration), stdout: $stdout, stderr: $stderr)
|
|
72
|
+
abort if exit_code != 0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc "Print Better Auth Grape mount information"
|
|
76
|
+
task :routes do
|
|
77
|
+
BetterAuth::Grape.load_app_config!
|
|
78
|
+
mount_path = BetterAuth::Grape.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,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "better_auth"
|
|
4
|
+
require "grape"
|
|
5
|
+
|
|
6
|
+
require_relative "grape/version"
|
|
7
|
+
require_relative "grape/configuration"
|
|
8
|
+
require_relative "grape/mounted_app"
|
|
9
|
+
require_relative "grape/helpers"
|
|
10
|
+
require_relative "grape/extension"
|
|
11
|
+
require_relative "grape/migration"
|
|
12
|
+
|
|
13
|
+
module BetterAuth
|
|
14
|
+
module Grape
|
|
15
|
+
def self.included(base)
|
|
16
|
+
Extension.included(base)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def configuration
|
|
21
|
+
@configuration ||= Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure
|
|
25
|
+
yield configuration
|
|
26
|
+
@auth = nil
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset!
|
|
31
|
+
@configuration = nil
|
|
32
|
+
@auth = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def auth(overrides = nil)
|
|
36
|
+
options = configuration.to_auth_options
|
|
37
|
+
return @auth ||= BetterAuth.auth(options) if overrides.nil? || overrides.empty?
|
|
38
|
+
|
|
39
|
+
BetterAuth.auth(options.merge(overrides))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def migration_configuration
|
|
43
|
+
options = configuration.to_auth_options
|
|
44
|
+
options[:secret] ||= BetterAuth::Configuration::DEFAULT_SECRET
|
|
45
|
+
BetterAuth::Configuration.new(options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def app_config_path(path = nil)
|
|
49
|
+
path || BetterAuth::Env.get("BETTER_AUTH_CONFIG") || "config/better_auth.rb"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load_app_config(path = nil)
|
|
53
|
+
config_path = app_config_path(path)
|
|
54
|
+
return false unless File.exist?(config_path)
|
|
55
|
+
|
|
56
|
+
load config_path
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def load_app_config!(path = nil)
|
|
61
|
+
config_path = app_config_path(path)
|
|
62
|
+
return true if load_app_config(config_path)
|
|
63
|
+
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"Better Auth Grape config not found at #{config_path.inspect}. " \
|
|
66
|
+
"Run `rake better_auth:install` or set BETTER_AUTH_CONFIG to a shared config file."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def default_config_template
|
|
70
|
+
<<~RUBY
|
|
71
|
+
# frozen_string_literal: true
|
|
72
|
+
|
|
73
|
+
require "better_auth/grape"
|
|
74
|
+
|
|
75
|
+
BetterAuth::Grape.configure do |config|
|
|
76
|
+
config.secret = BetterAuth::Env.fetch("BETTER_AUTH_SECRET", "change-me-grape-secret-12345678901234567890")
|
|
77
|
+
config.base_url = BetterAuth::Env.get("BETTER_AUTH_URL")
|
|
78
|
+
config.base_path = "/api/auth"
|
|
79
|
+
|
|
80
|
+
config.database = ->(options) do
|
|
81
|
+
case BetterAuth::Env.fetch("BETTER_AUTH_DATABASE_DIALECT", "postgres")
|
|
82
|
+
when "postgres", "postgresql"
|
|
83
|
+
BetterAuth::Adapters::Postgres.new(options, url: ENV.fetch("DATABASE_URL"))
|
|
84
|
+
when "mysql"
|
|
85
|
+
BetterAuth::Adapters::MySQL.new(options, url: ENV.fetch("DATABASE_URL"))
|
|
86
|
+
when "sqlite", "sqlite3"
|
|
87
|
+
BetterAuth::Adapters::SQLite.new(options, path: ENV.fetch("DATABASE_URL", "db/better_auth.sqlite3"))
|
|
88
|
+
else
|
|
89
|
+
raise "Unsupported BETTER_AUTH_DATABASE_DIALECT for better_auth-grape"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
config.email_and_password = {
|
|
94
|
+
enabled: true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
config.plugins = []
|
|
98
|
+
end
|
|
99
|
+
RUBY
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: better_auth-grape
|
|
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: grape
|
|
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: Grape integration for Better Auth Ruby. Better Auth Ruby is an independent
|
|
117
|
+
modern authentication framework for Ruby inspired by Better Auth. Provides mounting
|
|
118
|
+
helpers, 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/grape.rb
|
|
128
|
+
- lib/better_auth/grape/configuration.rb
|
|
129
|
+
- lib/better_auth/grape/extension.rb
|
|
130
|
+
- lib/better_auth/grape/helpers.rb
|
|
131
|
+
- lib/better_auth/grape/migration.rb
|
|
132
|
+
- lib/better_auth/grape/mounted_app.rb
|
|
133
|
+
- lib/better_auth/grape/tasks.rb
|
|
134
|
+
- lib/better_auth/grape/version.rb
|
|
135
|
+
- lib/better_auth_grape.rb
|
|
136
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
137
|
+
licenses:
|
|
138
|
+
- MIT
|
|
139
|
+
metadata:
|
|
140
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
141
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
142
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-grape/CHANGELOG.md
|
|
143
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
144
|
+
rdoc_options: []
|
|
145
|
+
require_paths:
|
|
146
|
+
- lib
|
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: 3.2.0
|
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: '0'
|
|
157
|
+
requirements: []
|
|
158
|
+
rubygems_version: 3.6.9
|
|
159
|
+
specification_version: 4
|
|
160
|
+
summary: Grape adapter for Better Auth
|
|
161
|
+
test_files: []
|