better_auth 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +4 -4
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +79 -28
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +142 -32
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +41 -14
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
# Shared OAuth state storage matching upstream's opaque-state adaptation.
|
|
7
|
+
# State data is encrypted client-side for the cookie strategy or consumed
|
|
8
|
+
# from verification storage for the database strategy. Signed JWT state is
|
|
9
|
+
# still accepted so in-flight legacy sign-in and link flows keep working.
|
|
10
|
+
module OAuthState
|
|
11
|
+
INTERNAL_KEYS = %w[
|
|
12
|
+
callbackURL codeVerifier errorURL newUserURL expiresAt oauthState link requestSignUp
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
class Error < BetterAuth::Error
|
|
16
|
+
attr_reader :code, :error_url
|
|
17
|
+
|
|
18
|
+
def initialize(code, error_url: nil)
|
|
19
|
+
@code = code
|
|
20
|
+
@error_url = error_url
|
|
21
|
+
super(code)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def generate(ctx, state_data)
|
|
28
|
+
state = Crypto.random_string(32)
|
|
29
|
+
payload = stringify_keys(state_data).merge("oauthState" => state)
|
|
30
|
+
|
|
31
|
+
if cookie_strategy?(ctx)
|
|
32
|
+
cookie = ctx.context.create_auth_cookie("oauth_state", max_age: 600)
|
|
33
|
+
encrypted = Crypto.symmetric_encrypt(
|
|
34
|
+
key: ctx.context.secret_config,
|
|
35
|
+
data: JSON.generate(payload)
|
|
36
|
+
)
|
|
37
|
+
ctx.set_cookie(cookie.name, encrypted, cookie.attributes)
|
|
38
|
+
else
|
|
39
|
+
cookie = ctx.context.create_auth_cookie("state", max_age: 300)
|
|
40
|
+
ctx.set_signed_cookie(cookie.name, state, ctx.context.secret, cookie.attributes)
|
|
41
|
+
verification = ctx.context.internal_adapter.create_verification_value(
|
|
42
|
+
identifier: state,
|
|
43
|
+
value: JSON.generate(payload),
|
|
44
|
+
expiresAt: Time.now + 600
|
|
45
|
+
)
|
|
46
|
+
raise Error, "state_generation_error" unless verification
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
state
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse(ctx, state)
|
|
53
|
+
raise Error, "state_not_found" if state.to_s.empty?
|
|
54
|
+
|
|
55
|
+
legacy = Crypto.verify_jwt(state.to_s, ctx.context.secret)
|
|
56
|
+
return parse_legacy(ctx, state, legacy) if legacy
|
|
57
|
+
|
|
58
|
+
cookie_strategy?(ctx) ? parse_cookie_state(ctx, state) : parse_database_state(ctx, state)
|
|
59
|
+
rescue JSON::ParserError
|
|
60
|
+
raise Error, "state_invalid"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_legacy(ctx, state, data)
|
|
64
|
+
cookie = ctx.context.create_auth_cookie("state", max_age: 600)
|
|
65
|
+
stored = ctx.get_signed_cookie(cookie.name, ctx.context.secret)
|
|
66
|
+
Cookies.expire_cookie(ctx, cookie) if ctx.request || stored
|
|
67
|
+
valid = ctx.request ? stored == state : (stored.nil? || stored == state)
|
|
68
|
+
return data if valid
|
|
69
|
+
|
|
70
|
+
raise Error.new("state_mismatch", error_url: recovered_error_url(data))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_cookie_state(ctx, state)
|
|
74
|
+
cookie = ctx.context.create_auth_cookie("oauth_state")
|
|
75
|
+
encrypted = ctx.get_cookie(cookie.name)
|
|
76
|
+
raise Error, "state_mismatch" if encrypted.to_s.empty?
|
|
77
|
+
|
|
78
|
+
data = begin
|
|
79
|
+
decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: encrypted)
|
|
80
|
+
JSON.parse(decrypted)
|
|
81
|
+
rescue JSON::ParserError, ArgumentError, TypeError
|
|
82
|
+
raise Error, "state_invalid"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
expected = data["oauthState"] || data["state"]
|
|
86
|
+
unless expected == state
|
|
87
|
+
raise Error.new("state_mismatch", error_url: recovered_error_url(data))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Cookies.expire_cookie(ctx, cookie)
|
|
91
|
+
validate_expiration!(data)
|
|
92
|
+
data
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_database_state(ctx, state)
|
|
96
|
+
preview = ctx.context.internal_adapter.find_verification_value(state)
|
|
97
|
+
raise Error, "state_mismatch" unless preview
|
|
98
|
+
|
|
99
|
+
data = JSON.parse(preview.fetch("value"))
|
|
100
|
+
error_url = recovered_error_url(data)
|
|
101
|
+
expected = data["oauthState"] || data["state"]
|
|
102
|
+
raise Error.new("state_mismatch", error_url: error_url) if expected && expected != state
|
|
103
|
+
|
|
104
|
+
cookie = ctx.context.create_auth_cookie("state")
|
|
105
|
+
stored = ctx.get_signed_cookie(cookie.name, ctx.context.secret)
|
|
106
|
+
valid = ctx.request ? stored == state : (stored.nil? || stored == state)
|
|
107
|
+
raise Error.new("state_mismatch", error_url: error_url) unless valid
|
|
108
|
+
|
|
109
|
+
consumed = ctx.context.internal_adapter.consume_verification_value(state)
|
|
110
|
+
Cookies.expire_cookie(ctx, cookie) if ctx.request || stored
|
|
111
|
+
raise Error.new("state_mismatch", error_url: error_url) unless consumed
|
|
112
|
+
|
|
113
|
+
consumed_data = JSON.parse(consumed.fetch("value"))
|
|
114
|
+
consumed_expected = consumed_data["oauthState"] || consumed_data["state"]
|
|
115
|
+
same_payload = consumed.fetch("value") == preview.fetch("value")
|
|
116
|
+
expected_matches = !consumed_expected || consumed_expected == state
|
|
117
|
+
unless same_payload && expected_matches
|
|
118
|
+
raise Error.new("state_mismatch", error_url: recovered_error_url(consumed_data) || error_url)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
validate_expiration!(consumed_data)
|
|
122
|
+
consumed_data
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate_expiration!(data)
|
|
126
|
+
expires_at = data["expiresAt"].to_i
|
|
127
|
+
return data unless expires_at.positive? && expires_at < Time.now.to_i
|
|
128
|
+
|
|
129
|
+
raise Error.new("state_mismatch", error_url: recovered_error_url(data))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def recovered_error_url(data)
|
|
133
|
+
data["errorURL"] || data["errorCallbackURL"]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def cookie_strategy?(ctx)
|
|
137
|
+
ctx.context.options.account[:store_state_strategy].to_s == "cookie"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def stringify_keys(value)
|
|
141
|
+
return value.each_with_object({}) { |(key, object), result| result[key.to_s] = stringify_keys(object) } if value.is_a?(Hash)
|
|
142
|
+
return value.map { |entry| stringify_keys(entry) } if value.is_a?(Array)
|
|
143
|
+
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Plugins
|
|
5
|
+
# Maps `BetterAuth::Plugins` factory methods and nested modules to source files.
|
|
6
|
+
PLUGIN_FILES = {
|
|
7
|
+
additional_fields: "plugins/additional_fields",
|
|
8
|
+
custom_session: "plugins/custom_session",
|
|
9
|
+
multi_session: "plugins/multi_session",
|
|
10
|
+
last_login_method: "plugins/last_login_method",
|
|
11
|
+
bearer: "plugins/bearer",
|
|
12
|
+
jwt: "plugins/jwt",
|
|
13
|
+
open_api: "plugins/open_api",
|
|
14
|
+
access: "plugins/access",
|
|
15
|
+
username: "plugins/username",
|
|
16
|
+
anonymous: "plugins/anonymous",
|
|
17
|
+
magic_link: "plugins/magic_link",
|
|
18
|
+
email_otp: "plugins/email_otp",
|
|
19
|
+
phone_number: "plugins/phone_number",
|
|
20
|
+
one_time_token: "plugins/one_time_token",
|
|
21
|
+
one_tap: "plugins/one_tap",
|
|
22
|
+
siwe: "plugins/siwe",
|
|
23
|
+
generic_oauth: "plugins/generic_oauth",
|
|
24
|
+
oauth_popup: "plugins/oauth_popup",
|
|
25
|
+
dub: "plugins/dub",
|
|
26
|
+
oauth_proxy: "plugins/oauth_proxy",
|
|
27
|
+
passkey: "plugins/passkey",
|
|
28
|
+
organization_schema: "plugins/organization/schema",
|
|
29
|
+
organization: "plugins/organization",
|
|
30
|
+
admin_schema: "plugins/admin/schema",
|
|
31
|
+
admin: "plugins/admin",
|
|
32
|
+
oauth_protocol: "plugins/oauth_protocol",
|
|
33
|
+
oauth_provider: "plugins/oauth_provider",
|
|
34
|
+
device_authorization: "plugins/device_authorization",
|
|
35
|
+
two_factor: "plugins/two_factor",
|
|
36
|
+
captcha: "plugins/captcha",
|
|
37
|
+
have_i_been_pwned: "plugins/have_i_been_pwned",
|
|
38
|
+
api_key: "plugins/api_key",
|
|
39
|
+
sso: "plugins/sso",
|
|
40
|
+
scim: "plugins/scim",
|
|
41
|
+
stripe: "plugins/stripe",
|
|
42
|
+
expo: "plugins/expo",
|
|
43
|
+
i18n: "plugins/i18n"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
PLUGIN_DEPENDENCIES = {
|
|
47
|
+
organization: %i[organization_schema access],
|
|
48
|
+
admin_schema: %i[organization_schema],
|
|
49
|
+
admin: %i[admin_schema access],
|
|
50
|
+
device_authorization: %i[oauth_protocol]
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
# Core route metadata helpers; loaded at boot because base endpoints reference OpenAPI.
|
|
54
|
+
BOOT_PLUGINS = %i[open_api].freeze
|
|
55
|
+
|
|
56
|
+
PLUGIN_ID_TO_LOADER = {
|
|
57
|
+
"additional-fields" => :additional_fields,
|
|
58
|
+
"custom-session" => :custom_session,
|
|
59
|
+
"multi-session" => :multi_session,
|
|
60
|
+
"last-login-method" => :last_login_method,
|
|
61
|
+
"bearer" => :bearer,
|
|
62
|
+
"jwt" => :jwt,
|
|
63
|
+
"open-api" => :open_api,
|
|
64
|
+
"username" => :username,
|
|
65
|
+
"anonymous" => :anonymous,
|
|
66
|
+
"magic-link" => :magic_link,
|
|
67
|
+
"email-otp" => :email_otp,
|
|
68
|
+
"phone-number" => :phone_number,
|
|
69
|
+
"one-time-token" => :one_time_token,
|
|
70
|
+
"one-tap" => :one_tap,
|
|
71
|
+
"siwe" => :siwe,
|
|
72
|
+
"generic-oauth" => :generic_oauth,
|
|
73
|
+
"oauth-popup" => :oauth_popup,
|
|
74
|
+
"dub" => :dub,
|
|
75
|
+
"oauth-proxy" => :oauth_proxy,
|
|
76
|
+
"passkey" => :passkey,
|
|
77
|
+
"organization" => :organization,
|
|
78
|
+
"admin" => :admin,
|
|
79
|
+
"oauth-provider" => :oauth_provider,
|
|
80
|
+
"device-authorization" => :device_authorization,
|
|
81
|
+
"two-factor" => :two_factor,
|
|
82
|
+
"captcha" => :captcha,
|
|
83
|
+
"have-i-been-pwned" => :have_i_been_pwned,
|
|
84
|
+
"api-key" => :api_key,
|
|
85
|
+
"sso" => :sso,
|
|
86
|
+
"scim" => :scim,
|
|
87
|
+
"stripe" => :stripe,
|
|
88
|
+
"expo" => :expo,
|
|
89
|
+
"i18n" => :i18n
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
NESTED_MODULE_LOADERS = {
|
|
93
|
+
OAuthProtocol: :oauth_protocol,
|
|
94
|
+
JWT: :jwt,
|
|
95
|
+
OrganizationSchema: :organization_schema,
|
|
96
|
+
AdminSchema: :admin_schema
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
LAZY_PLUGIN_METHODS = PLUGIN_FILES.keys.freeze
|
|
100
|
+
|
|
101
|
+
@loaded_plugins = {}
|
|
102
|
+
|
|
103
|
+
EXTERNAL_PLUGIN_IMPLEMENTATIONS = {
|
|
104
|
+
sso: :SSO_PLUGIN_IMPLEMENTATION,
|
|
105
|
+
scim: :SCIM_PLUGIN_IMPLEMENTATION,
|
|
106
|
+
api_key: :API_KEY_PLUGIN_IMPLEMENTATION,
|
|
107
|
+
passkey: :PASSKEY_PLUGIN_IMPLEMENTATION,
|
|
108
|
+
stripe: :STRIPE_PLUGIN_IMPLEMENTATION,
|
|
109
|
+
oauth_provider: :OAUTH_PROVIDER_PLUGIN_IMPLEMENTATION
|
|
110
|
+
}.freeze
|
|
111
|
+
|
|
112
|
+
module_function
|
|
113
|
+
|
|
114
|
+
def ensure_external_plugin_loaded!(gem_name:, entry:, implementation_constant:)
|
|
115
|
+
return if const_defined?(implementation_constant, false)
|
|
116
|
+
|
|
117
|
+
spec = Gem.loaded_specs[gem_name] || Gem::Specification.find_by_name(gem_name)
|
|
118
|
+
entry_path = File.join(spec.full_gem_path, entry)
|
|
119
|
+
load entry_path unless $LOADED_FEATURES.include?(entry_path)
|
|
120
|
+
|
|
121
|
+
return if const_defined?(implementation_constant, false)
|
|
122
|
+
|
|
123
|
+
raise LoadError,
|
|
124
|
+
"BetterAuth requires the #{gem_name} gem. Add it to your Gemfile and require its entrypoint."
|
|
125
|
+
rescue Gem::MissingSpecError
|
|
126
|
+
raise LoadError,
|
|
127
|
+
"BetterAuth requires the #{gem_name} gem. Add it to your Gemfile and require its entrypoint."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def call_external_plugin!(method_name, *args, implementation_constant:, gem_name:, entry:, &block)
|
|
131
|
+
loader = method(method_name)
|
|
132
|
+
ensure_external_plugin_loaded!(
|
|
133
|
+
gem_name: gem_name,
|
|
134
|
+
entry: entry,
|
|
135
|
+
implementation_constant: implementation_constant
|
|
136
|
+
)
|
|
137
|
+
resolved = method(method_name)
|
|
138
|
+
if resolved == loader
|
|
139
|
+
raise LoadError,
|
|
140
|
+
"BetterAuth::Plugins.#{method_name} requires the #{gem_name} gem. Add it to your Gemfile and require its entrypoint."
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
resolved.call(*args, &block)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def load_plugin!(name)
|
|
147
|
+
name = name.to_sym
|
|
148
|
+
return true if @loaded_plugins[name]
|
|
149
|
+
implementation_constant = EXTERNAL_PLUGIN_IMPLEMENTATIONS[name]
|
|
150
|
+
if implementation_constant && const_defined?(implementation_constant, false)
|
|
151
|
+
@loaded_plugins[name] = true
|
|
152
|
+
return true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Array(PLUGIN_DEPENDENCIES[name]).each { |dependency| load_plugin!(dependency) }
|
|
156
|
+
|
|
157
|
+
relative_path = PLUGIN_FILES.fetch(name) do
|
|
158
|
+
raise ArgumentError, "Unknown plugin loader: #{name}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
absolute_path = File.expand_path(relative_path, __dir__)
|
|
162
|
+
unless File.file?("#{absolute_path}.rb")
|
|
163
|
+
@loaded_plugins[name] = true
|
|
164
|
+
return false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
require_relative relative_path
|
|
168
|
+
@loaded_plugins[name] = true
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def plugin_loaded?(name)
|
|
173
|
+
@loaded_plugins.key?(name.to_sym)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def ensure_plugin_loaded_for!(plugin)
|
|
177
|
+
return unless plugin.is_a?(BetterAuth::Plugin)
|
|
178
|
+
|
|
179
|
+
loader = PLUGIN_ID_TO_LOADER[plugin.id]
|
|
180
|
+
load_plugin!(loader) if loader
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def lazy_plugin_method?(name)
|
|
184
|
+
!plugin_loader_for_method(name).nil?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def plugin_loader_for_method(name)
|
|
188
|
+
symbol = name.to_sym
|
|
189
|
+
return symbol if LAZY_PLUGIN_METHODS.include?(symbol)
|
|
190
|
+
|
|
191
|
+
PLUGIN_FILES.each_key do |plugin|
|
|
192
|
+
return plugin if name.to_s.start_with?("#{plugin}_")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def load_plugin_for_constant!(name)
|
|
199
|
+
if (loader = NESTED_MODULE_LOADERS[name])
|
|
200
|
+
return load_plugin!(loader)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if name == :OpenAPI
|
|
204
|
+
return load_plugin!(:open_api)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
constant = name.to_s
|
|
208
|
+
if constant.end_with?("_ERROR_CODES")
|
|
209
|
+
plugin_name = constant.delete_suffix("_ERROR_CODES").downcase.to_sym
|
|
210
|
+
return load_plugin!(plugin_name) if PLUGIN_FILES.key?(plugin_name)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def load_boot_plugins!
|
|
217
|
+
BOOT_PLUGINS.each { |plugin| load_plugin!(plugin) }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -10,36 +10,33 @@ module BetterAuth
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def authorize(request, connector = "AND")
|
|
13
|
+
connector = (connector == "OR") ? "OR" : "AND"
|
|
13
14
|
success = false
|
|
14
15
|
stringify_request(request).each do |resource, requested_actions|
|
|
15
16
|
allowed_actions = statements[resource]
|
|
16
17
|
unless allowed_actions
|
|
18
|
+
next if connector == "OR"
|
|
19
|
+
|
|
17
20
|
return {success: false, error: "You are not allowed to access resource: #{resource}"}
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
success = if requested_actions.is_a?(Array)
|
|
21
|
-
requested_actions.all? { |action| allowed_actions.include?(action
|
|
24
|
+
requested_actions.any? && requested_actions.all? { |action| action.is_a?(String) && allowed_actions.include?(action) }
|
|
22
25
|
elsif requested_actions.is_a?(Hash)
|
|
23
|
-
unless requested_actions.key?("actions") || requested_actions.key?(:actions)
|
|
24
|
-
raise Error, "Invalid access control request"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
26
|
raw_actions = requested_actions["actions"] || requested_actions[:actions]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
actions = Array(raw_actions).map(&:to_s)
|
|
31
|
-
action_connector = (requested_actions["connector"] || requested_actions[:connector] || "AND").to_s.upcase
|
|
27
|
+
actions = raw_actions.is_a?(Array) ? raw_actions : []
|
|
28
|
+
action_connector = ((requested_actions["connector"] || requested_actions[:connector]) == "OR") ? "OR" : "AND"
|
|
32
29
|
if action_connector == "OR"
|
|
33
|
-
actions.any? { |action| allowed_actions.include?(action) }
|
|
30
|
+
actions.any? { |action| action.is_a?(String) && allowed_actions.include?(action) }
|
|
34
31
|
else
|
|
35
|
-
actions.all? { |action| allowed_actions.include?(action) }
|
|
32
|
+
actions.any? && actions.all? { |action| action.is_a?(String) && allowed_actions.include?(action) }
|
|
36
33
|
end
|
|
37
34
|
else
|
|
38
35
|
raise Error, "Invalid access control request"
|
|
39
36
|
end
|
|
40
37
|
|
|
41
|
-
return {success: true} if success && connector
|
|
42
|
-
return {success: false, error: "unauthorized to access resource \"#{resource}\""} if !success && connector
|
|
38
|
+
return {success: true} if success && connector == "OR"
|
|
39
|
+
return {success: false, error: "unauthorized to access resource \"#{resource}\""} if !success && connector == "AND"
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
success ? {success: true} : {success: false, error: "Not authorized"}
|
|
@@ -25,15 +25,17 @@ module BetterAuth
|
|
|
25
25
|
"YOU_CANNOT_REMOVE_YOURSELF" => "You cannot remove yourself",
|
|
26
26
|
"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE" => "You are not allowed to set a non-existent role value",
|
|
27
27
|
"YOU_CANNOT_IMPERSONATE_ADMINS" => "You cannot impersonate admins",
|
|
28
|
-
"INVALID_ROLE_TYPE" => "Invalid role type"
|
|
28
|
+
"INVALID_ROLE_TYPE" => "Invalid role type",
|
|
29
|
+
"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL" => "You are not allowed to update users email",
|
|
30
|
+
"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER" => "Password cannot be updated through update-user. Use the set-user-password endpoint instead"
|
|
29
31
|
}.freeze
|
|
30
32
|
|
|
31
33
|
ADMIN_DEFAULT_STATEMENTS = {
|
|
32
|
-
user: ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"],
|
|
34
|
+
user: ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "set-email", "get", "update"],
|
|
33
35
|
session: ["list", "revoke", "delete"]
|
|
34
36
|
}.freeze
|
|
35
37
|
ADMIN_DEFAULT_ROLE_STATEMENTS = {
|
|
36
|
-
user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"],
|
|
38
|
+
user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "set-email", "get", "update"],
|
|
37
39
|
session: ["list", "revoke", "delete"]
|
|
38
40
|
}.freeze
|
|
39
41
|
|
|
@@ -158,6 +160,8 @@ module BetterAuth
|
|
|
158
160
|
user_id = body[:user_id].to_s
|
|
159
161
|
raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
|
|
160
162
|
update = {role: admin_validate_roles!(body[:role], config)}
|
|
163
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(user_id)
|
|
164
|
+
|
|
161
165
|
user = ctx.context.internal_adapter.update_user(user_id, update)
|
|
162
166
|
ctx.json({user: Schema.parse_output(ctx.context.options, "user", user || {})})
|
|
163
167
|
end
|
|
@@ -189,7 +193,7 @@ module BetterAuth
|
|
|
189
193
|
elsif query[:email]
|
|
190
194
|
ctx.context.internal_adapter.find_user_by_email(query[:email])&.fetch(:user)
|
|
191
195
|
end
|
|
192
|
-
raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless user
|
|
196
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless user
|
|
193
197
|
ctx.json(Schema.parse_output(ctx.context.options, "user", user))
|
|
194
198
|
end
|
|
195
199
|
end
|
|
@@ -212,10 +216,10 @@ module BetterAuth
|
|
|
212
216
|
required: ["email", "name"]
|
|
213
217
|
)
|
|
214
218
|
) do |ctx|
|
|
215
|
-
session = Routes.current_session(ctx, allow_nil: true)
|
|
219
|
+
session = Routes.current_session(ctx, allow_nil: true, sensitive: true)
|
|
216
220
|
if session
|
|
217
221
|
unless admin_permission?(session[:user], session[:user]["role"], {user: ["create"]}, config)
|
|
218
|
-
raise APIError.new("FORBIDDEN", message: ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS"))
|
|
222
|
+
raise APIError.new("FORBIDDEN", code: "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS", message: ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS"))
|
|
219
223
|
end
|
|
220
224
|
elsif !ctx.headers.empty?
|
|
221
225
|
raise APIError.new("UNAUTHORIZED")
|
|
@@ -223,18 +227,25 @@ module BetterAuth
|
|
|
223
227
|
|
|
224
228
|
body = normalize_hash(ctx.body)
|
|
225
229
|
email = body[:email].to_s.downcase
|
|
226
|
-
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("INVALID_EMAIL")) unless Routes::EMAIL_PATTERN.match?(email)
|
|
230
|
+
raise APIError.new("BAD_REQUEST", code: "INVALID_EMAIL", message: BASE_ERROR_CODES.fetch("INVALID_EMAIL")) unless Routes::EMAIL_PATTERN.match?(email)
|
|
227
231
|
|
|
228
232
|
if ctx.context.internal_adapter.find_user_by_email(email)
|
|
229
|
-
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"))
|
|
233
|
+
raise APIError.new("BAD_REQUEST", code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", message: ADMIN_ERROR_CODES.fetch("USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"))
|
|
230
234
|
end
|
|
231
235
|
data = normalize_hash(body[:data]).each_with_object({}) { |(key, value), result| result[Schema.storage_key(key)] = value }
|
|
236
|
+
requested_role = body.key?(:role) ? body[:role] : data.delete("role")
|
|
237
|
+
if !requested_role.nil? && session
|
|
238
|
+
admin_require_permission!(ctx, config, {user: ["set-role"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE"))
|
|
239
|
+
end
|
|
240
|
+
if session && ["banned", "banReason", "banExpires"].any? { |field| data.key?(field) }
|
|
241
|
+
admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
|
|
242
|
+
end
|
|
232
243
|
user = ctx.context.internal_adapter.create_user(data.merge(
|
|
233
244
|
name: body[:name].to_s,
|
|
234
245
|
email: email,
|
|
235
|
-
role:
|
|
246
|
+
role: requested_role.nil? ? config[:default_role] : admin_validate_roles!(requested_role, config)
|
|
236
247
|
).merge(body.key?(:image) ? {image: body[:image]} : {}), context: ctx)
|
|
237
|
-
raise APIError.new("INTERNAL_SERVER_ERROR", message: ADMIN_ERROR_CODES.fetch("FAILED_TO_CREATE_USER")) unless user
|
|
248
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", code: "FAILED_TO_CREATE_USER", message: ADMIN_ERROR_CODES.fetch("FAILED_TO_CREATE_USER")) unless user
|
|
238
249
|
|
|
239
250
|
if body[:password].to_s != ""
|
|
240
251
|
ctx.context.internal_adapter.link_account(userId: user["id"], providerId: "credential", accountId: user["id"], password: Routes.hash_password(ctx, body[:password]))
|
|
@@ -269,12 +280,38 @@ module BetterAuth
|
|
|
269
280
|
admin_require_permission!(ctx, config, {user: ["update"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS"))
|
|
270
281
|
body = normalize_hash(ctx.body)
|
|
271
282
|
data = normalize_hash(body[:data] || body).except(:user_id, :data)
|
|
272
|
-
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("NO_DATA_TO_UPDATE")) if data.empty?
|
|
283
|
+
raise APIError.new("BAD_REQUEST", code: "NO_DATA_TO_UPDATE", message: ADMIN_ERROR_CODES.fetch("NO_DATA_TO_UPDATE")) if data.empty?
|
|
284
|
+
if data.key?(:password)
|
|
285
|
+
raise APIError.new("BAD_REQUEST", code: "PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER", message: ADMIN_ERROR_CODES.fetch("PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER"))
|
|
286
|
+
end
|
|
273
287
|
if data.key?(:role)
|
|
274
288
|
admin_require_permission!(ctx, config, {user: ["set-role"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE"))
|
|
275
289
|
data[:role] = admin_validate_roles!(data[:role], config)
|
|
276
290
|
end
|
|
277
|
-
|
|
291
|
+
if [:banned, :ban_reason, :ban_expires].any? { |field| data.key?(field) }
|
|
292
|
+
session = admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
|
|
293
|
+
if data[:banned] == true && body[:user_id] == session[:user]["id"]
|
|
294
|
+
raise APIError.new("BAD_REQUEST", code: "YOU_CANNOT_BAN_YOURSELF", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_BAN_YOURSELF"))
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
if data.key?(:email) || data.key?(:email_verified)
|
|
298
|
+
admin_require_permission!(ctx, config, {user: ["set-email"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL"))
|
|
299
|
+
if data.key?(:email)
|
|
300
|
+
email = data[:email].to_s.downcase
|
|
301
|
+
raise APIError.new("BAD_REQUEST", code: "INVALID_EMAIL", message: BASE_ERROR_CODES.fetch("INVALID_EMAIL")) unless Routes::EMAIL_PATTERN.match?(email)
|
|
302
|
+
|
|
303
|
+
existing = ctx.context.internal_adapter.find_user_by_email(email)
|
|
304
|
+
if existing && existing[:user]["id"] != body[:user_id]
|
|
305
|
+
raise APIError.new("BAD_REQUEST", code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", message: ADMIN_ERROR_CODES.fetch("USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"))
|
|
306
|
+
end
|
|
307
|
+
data[:email] = email
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(body[:user_id])
|
|
311
|
+
|
|
312
|
+
stored_data = data.each_with_object({}) { |(key, value), result| result[Schema.storage_key(key)] = value }
|
|
313
|
+
user = ctx.context.internal_adapter.update_user(body[:user_id], stored_data)
|
|
314
|
+
ctx.context.internal_adapter.delete_user_sessions(body[:user_id]) if data[:banned] == true
|
|
278
315
|
ctx.json(Schema.parse_output(ctx.context.options, "user", user))
|
|
279
316
|
end
|
|
280
317
|
end
|
|
@@ -357,11 +394,11 @@ module BetterAuth
|
|
|
357
394
|
session = admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
|
|
358
395
|
body = normalize_hash(ctx.body)
|
|
359
396
|
found = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
|
|
360
|
-
raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless found
|
|
361
|
-
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_BAN_YOURSELF")) if body[:user_id] == session[:user]["id"]
|
|
397
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless found
|
|
398
|
+
raise APIError.new("BAD_REQUEST", code: "YOU_CANNOT_BAN_YOURSELF", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_BAN_YOURSELF")) if body[:user_id] == session[:user]["id"]
|
|
362
399
|
expires_in = body[:ban_expires_in] || config[:default_ban_expires_in]
|
|
363
400
|
user = ctx.context.internal_adapter.update_user(body[:user_id], banned: true, banReason: body[:ban_reason] || config[:default_ban_reason] || "No reason", banExpires: expires_in ? Time.now + expires_in.to_i : nil, updatedAt: Time.now)
|
|
364
|
-
ctx.context.internal_adapter.
|
|
401
|
+
ctx.context.internal_adapter.delete_user_sessions(body[:user_id])
|
|
365
402
|
ctx.json({user: Schema.parse_output(ctx.context.options, "user", user)})
|
|
366
403
|
end
|
|
367
404
|
end
|
|
@@ -381,7 +418,10 @@ module BetterAuth
|
|
|
381
418
|
)
|
|
382
419
|
) do |ctx|
|
|
383
420
|
admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
|
|
384
|
-
|
|
421
|
+
user_id = normalize_hash(ctx.body)[:user_id]
|
|
422
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(user_id)
|
|
423
|
+
|
|
424
|
+
user = ctx.context.internal_adapter.update_user(user_id, banned: false, banReason: nil, banExpires: nil, updatedAt: Time.now)
|
|
385
425
|
ctx.json({user: Schema.parse_output(ctx.context.options, "user", user)})
|
|
386
426
|
end
|
|
387
427
|
end
|
|
@@ -411,11 +451,11 @@ module BetterAuth
|
|
|
411
451
|
session = admin_require_permission!(ctx, config, {user: ["impersonate"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS"))
|
|
412
452
|
body = normalize_hash(ctx.body)
|
|
413
453
|
target = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
|
|
414
|
-
raise APIError.new("NOT_FOUND",
|
|
454
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless target
|
|
415
455
|
can_impersonate_admins = config[:allow_impersonating_admins] ||
|
|
416
456
|
admin_permission?(session[:user], session[:user]["role"], {user: ["impersonate-admins"]}, config)
|
|
417
457
|
if !can_impersonate_admins && admin_user?(target, config)
|
|
418
|
-
raise APIError.new("FORBIDDEN", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_IMPERSONATE_ADMINS"))
|
|
458
|
+
raise APIError.new("FORBIDDEN", code: "YOU_CANNOT_IMPERSONATE_ADMINS", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_IMPERSONATE_ADMINS"))
|
|
419
459
|
end
|
|
420
460
|
impersonated = ctx.context.internal_adapter.create_session(target["id"], true, {impersonatedBy: session[:user]["id"], expiresAt: Time.now + config[:impersonation_session_duration].to_i}, true, ctx)
|
|
421
461
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: ADMIN_ERROR_CODES.fetch("FAILED_TO_CREATE_USER")) unless impersonated
|
|
@@ -524,7 +564,7 @@ module BetterAuth
|
|
|
524
564
|
}
|
|
525
565
|
) do |ctx|
|
|
526
566
|
admin_require_permission!(ctx, config, {session: ["revoke"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS"))
|
|
527
|
-
ctx.context.internal_adapter.
|
|
567
|
+
ctx.context.internal_adapter.delete_user_sessions(normalize_hash(ctx.body)[:user_id])
|
|
528
568
|
ctx.json({success: true})
|
|
529
569
|
end
|
|
530
570
|
end
|
|
@@ -553,8 +593,8 @@ module BetterAuth
|
|
|
553
593
|
) do |ctx|
|
|
554
594
|
session = admin_require_permission!(ctx, config, {user: ["delete"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS"))
|
|
555
595
|
user_id = normalize_hash(ctx.body)[:user_id]
|
|
556
|
-
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_REMOVE_YOURSELF")) if user_id == session[:user]["id"]
|
|
557
|
-
raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(user_id)
|
|
596
|
+
raise APIError.new("BAD_REQUEST", code: "YOU_CANNOT_REMOVE_YOURSELF", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_REMOVE_YOURSELF")) if user_id == session[:user]["id"]
|
|
597
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(user_id)
|
|
558
598
|
ctx.context.internal_adapter.delete_user(user_id)
|
|
559
599
|
ctx.json({success: true})
|
|
560
600
|
end
|
|
@@ -590,9 +630,17 @@ module BetterAuth
|
|
|
590
630
|
raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
|
|
591
631
|
min = ctx.context.options.email_and_password[:min_password_length]
|
|
592
632
|
max = ctx.context.options.email_and_password[:max_password_length]
|
|
593
|
-
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("PASSWORD_TOO_SHORT")) if password.length < min
|
|
594
|
-
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("PASSWORD_TOO_LONG")) if password.length > max
|
|
595
|
-
ctx.context.internal_adapter.
|
|
633
|
+
raise APIError.new("BAD_REQUEST", code: "PASSWORD_TOO_SHORT", message: BASE_ERROR_CODES.fetch("PASSWORD_TOO_SHORT")) if password.length < min
|
|
634
|
+
raise APIError.new("BAD_REQUEST", code: "PASSWORD_TOO_LONG", message: BASE_ERROR_CODES.fetch("PASSWORD_TOO_LONG")) if password.length > max
|
|
635
|
+
raise APIError.new("NOT_FOUND", code: "USER_NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(user_id)
|
|
636
|
+
|
|
637
|
+
hashed_password = Routes.hash_password(ctx, password)
|
|
638
|
+
credential_account = ctx.context.internal_adapter.find_accounts(user_id).find { |account| account["providerId"] == "credential" }
|
|
639
|
+
if credential_account
|
|
640
|
+
ctx.context.internal_adapter.update_password(user_id, hashed_password)
|
|
641
|
+
else
|
|
642
|
+
ctx.context.internal_adapter.create_account(userId: user_id, providerId: "credential", accountId: user_id, password: hashed_password)
|
|
643
|
+
end
|
|
596
644
|
ctx.json({status: true})
|
|
597
645
|
end
|
|
598
646
|
end
|
|
@@ -631,7 +679,7 @@ module BetterAuth
|
|
|
631
679
|
}
|
|
632
680
|
}
|
|
633
681
|
) do |ctx|
|
|
634
|
-
session = Routes.current_session(ctx, allow_nil: true)
|
|
682
|
+
session = Routes.current_session(ctx, allow_nil: true, sensitive: true)
|
|
635
683
|
body = normalize_hash(ctx.body)
|
|
636
684
|
permissions = body[:permissions] || body[:permission]
|
|
637
685
|
unless permissions
|
|
@@ -723,7 +771,10 @@ module BetterAuth
|
|
|
723
771
|
session = Routes.current_session(ctx, sensitive: true)
|
|
724
772
|
return session if admin_permission?(session[:user], session[:user]["role"], permissions, config)
|
|
725
773
|
|
|
726
|
-
|
|
774
|
+
code = ADMIN_ERROR_CODES.key(message)
|
|
775
|
+
raise Error, "Unknown admin permission error: #{message}" unless code
|
|
776
|
+
|
|
777
|
+
raise APIError.new("FORBIDDEN", code: code, message: message)
|
|
727
778
|
end
|
|
728
779
|
|
|
729
780
|
def admin_permission?(user, role_string, permissions, config)
|
|
@@ -750,7 +801,7 @@ module BetterAuth
|
|
|
750
801
|
|
|
751
802
|
def admin_validate_roles!(roles, config)
|
|
752
803
|
unless Array(roles).all? { |role| role.is_a?(String) || role.is_a?(Symbol) }
|
|
753
|
-
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("INVALID_ROLE_TYPE"))
|
|
804
|
+
raise APIError.new("BAD_REQUEST", code: "INVALID_ROLE_TYPE", message: ADMIN_ERROR_CODES.fetch("INVALID_ROLE_TYPE"))
|
|
754
805
|
end
|
|
755
806
|
|
|
756
807
|
parsed = admin_parse_roles(roles)
|
|
@@ -758,7 +809,7 @@ module BetterAuth
|
|
|
758
809
|
defined_roles = (config[:roles] || {}).transform_keys(&:to_s)
|
|
759
810
|
invalid = parsed.split(",", -1).reject { |role| admin_role_for(defined_roles, role) }
|
|
760
811
|
if invalid.any?
|
|
761
|
-
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE"))
|
|
812
|
+
raise APIError.new("BAD_REQUEST", code: "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE", message: ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE"))
|
|
762
813
|
end
|
|
763
814
|
end
|
|
764
815
|
|