better_auth-api-key 0.2.0 → 0.5.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 +5 -0
- data/lib/better_auth/api_key/adapter.rb +245 -0
- data/lib/better_auth/api_key/configuration.rb +78 -0
- data/lib/better_auth/api_key/error_codes.rb +39 -0
- data/lib/better_auth/api_key/keys.rb +55 -0
- data/lib/better_auth/api_key/org_authorization.rb +80 -0
- data/lib/better_auth/api_key/plugin_factory.rb +37 -0
- data/lib/better_auth/api_key/rate_limit.rb +41 -0
- data/lib/better_auth/api_key/routes/create_api_key.rb +53 -0
- data/lib/better_auth/api_key/routes/delete_all_expired_api_keys.rb +23 -0
- data/lib/better_auth/api_key/routes/delete_api_key.rb +35 -0
- data/lib/better_auth/api_key/routes/get_api_key.rb +33 -0
- data/lib/better_auth/api_key/routes/index.rb +72 -0
- data/lib/better_auth/api_key/routes/list_api_keys.rb +44 -0
- data/lib/better_auth/api_key/routes/update_api_key.rb +45 -0
- data/lib/better_auth/api_key/routes/verify_api_key.rb +43 -0
- data/lib/better_auth/api_key/schema.rb +40 -0
- data/lib/better_auth/api_key/session.rb +63 -0
- data/lib/better_auth/api_key/types.rb +30 -0
- data/lib/better_auth/api_key/utils.rb +93 -0
- data/lib/better_auth/api_key/validation.rb +137 -0
- data/lib/better_auth/api_key/version.rb +1 -1
- data/lib/better_auth/api_key.rb +20 -0
- data/lib/better_auth/plugins/api_key.rb +94 -802
- metadata +21 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
module DeleteAllExpiredAPIKeys
|
|
7
|
+
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/delete-all-expired-api-keys.ts"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def endpoint(config)
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/delete-all-expired-api-keys", method: "POST") do |ctx|
|
|
13
|
+
BetterAuth::Plugins.api_key_delete_expired(ctx.context, config, bypass_last_check: true)
|
|
14
|
+
ctx.json({success: true, error: nil})
|
|
15
|
+
rescue => error
|
|
16
|
+
ctx.context.logger.error("[API KEY PLUGIN] Failed to delete expired API keys: #{error.message}") if ctx.context.logger.respond_to?(:error)
|
|
17
|
+
ctx.json({success: false, error: error})
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
module DeleteAPIKey
|
|
7
|
+
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/delete-api-key.ts"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def endpoint(config)
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/delete", method: "POST") do |ctx|
|
|
13
|
+
session = BetterAuth::Routes.current_session(ctx)
|
|
14
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USER_BANNED"]) if session[:user]["banned"] == true
|
|
15
|
+
|
|
16
|
+
body = BetterAuth::Plugins.normalize_hash(ctx.body)
|
|
17
|
+
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, body[:config_id])
|
|
18
|
+
key_id = body[:key_id]
|
|
19
|
+
record = BetterAuth::Plugins.api_key_find_by_id(ctx, key_id, resolved_config)
|
|
20
|
+
unless record && BetterAuth::Plugins.api_key_config_id_matches?(BetterAuth::Plugins.api_key_record_config_id(record), resolved_config[:config_id])
|
|
21
|
+
raise BetterAuth::APIError.new("NOT_FOUND", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_NOT_FOUND"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
25
|
+
BetterAuth::Plugins.api_key_authorize_reference!(ctx, record_config, session[:user]["id"], BetterAuth::Plugins.api_key_record_reference_id(record), "delete")
|
|
26
|
+
|
|
27
|
+
BetterAuth::Plugins.api_key_delete_record(ctx, record, record_config)
|
|
28
|
+
BetterAuth::Plugins.api_key_delete_expired(ctx.context, record_config)
|
|
29
|
+
ctx.json({success: true})
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
module GetAPIKey
|
|
7
|
+
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/get-api-key.ts"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def endpoint(config)
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/get", method: "GET") do |ctx|
|
|
13
|
+
session = BetterAuth::Routes.current_session(ctx)
|
|
14
|
+
query = BetterAuth::Plugins.normalize_hash(ctx.query)
|
|
15
|
+
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, query[:config_id])
|
|
16
|
+
id = query[:id]
|
|
17
|
+
record = BetterAuth::Plugins.api_key_find_by_id(ctx, id, resolved_config)
|
|
18
|
+
unless record && BetterAuth::Plugins.api_key_config_id_matches?(BetterAuth::Plugins.api_key_record_config_id(record), resolved_config[:config_id])
|
|
19
|
+
raise BetterAuth::APIError.new("NOT_FOUND", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_NOT_FOUND"])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
23
|
+
BetterAuth::Plugins.api_key_authorize_reference!(ctx, record_config, session[:user]["id"], BetterAuth::Plugins.api_key_record_reference_id(record), "read")
|
|
24
|
+
|
|
25
|
+
record = BetterAuth::Plugins.api_key_migrate_legacy_metadata(ctx, record, record_config)
|
|
26
|
+
BetterAuth::Plugins.api_key_delete_expired(ctx.context, record_config)
|
|
27
|
+
ctx.json(BetterAuth::Plugins.api_key_public(record, include_key_field: false))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
ROUTE_NAMES = %i[
|
|
7
|
+
create_api_key
|
|
8
|
+
verify_api_key
|
|
9
|
+
get_api_key
|
|
10
|
+
update_api_key
|
|
11
|
+
delete_api_key
|
|
12
|
+
list_api_keys
|
|
13
|
+
delete_all_expired_api_keys
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def resolve_config(context, config, config_id = nil)
|
|
19
|
+
configurations = config.fetch(:configurations, [config])
|
|
20
|
+
return configurations.find { |entry| default_config_id?(entry[:config_id]) } || configurations.first if config_id.to_s.empty?
|
|
21
|
+
|
|
22
|
+
configurations.find { |entry| entry[:config_id].to_s == config_id.to_s } ||
|
|
23
|
+
begin
|
|
24
|
+
default = configurations.find { |entry| default_config_id?(entry[:config_id]) }
|
|
25
|
+
unless default
|
|
26
|
+
context.logger.error(BetterAuth::Plugins::API_KEY_ERROR_CODES["NO_DEFAULT_API_KEY_CONFIGURATION_FOUND"]) if context.respond_to?(:logger) && context.logger.respond_to?(:error)
|
|
27
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["NO_DEFAULT_API_KEY_CONFIGURATION_FOUND"])
|
|
28
|
+
end
|
|
29
|
+
default
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default_config_id?(value)
|
|
34
|
+
value.nil? || value.to_s.empty? || value.to_s == "default"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def config_id_matches?(record_config_id, expected_config_id)
|
|
38
|
+
return true if default_config_id?(record_config_id) && default_config_id?(expected_config_id)
|
|
39
|
+
|
|
40
|
+
record_config_id.to_s == expected_config_id.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@last_expired_check = nil
|
|
44
|
+
|
|
45
|
+
def delete_expired(context, config, bypass_last_check: false)
|
|
46
|
+
return unless config[:storage] == "database" || config[:fallback_to_database]
|
|
47
|
+
unless bypass_last_check
|
|
48
|
+
now = Time.now
|
|
49
|
+
return if @last_expired_check && ((now - @last_expired_check) * 1000) < 10_000
|
|
50
|
+
|
|
51
|
+
@last_expired_check = now
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
expired = context.adapter.find_many(model: BetterAuth::Plugins::API_KEY_TABLE_NAME).select do |record|
|
|
55
|
+
record["expiresAt"] && record["expiresAt"] < Time.now
|
|
56
|
+
end
|
|
57
|
+
expired.each do |record|
|
|
58
|
+
context.adapter.delete(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}])
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def schedule_cleanup(ctx, config)
|
|
63
|
+
task = -> { delete_expired(ctx.context, config) }
|
|
64
|
+
if config[:defer_updates] && BetterAuth::APIKey::Utils.background_tasks?(ctx)
|
|
65
|
+
ctx.context.run_in_background(task)
|
|
66
|
+
else
|
|
67
|
+
task.call
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
module ListAPIKeys
|
|
7
|
+
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/list-api-keys.ts"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def endpoint(config)
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/list", method: "GET") do |ctx|
|
|
13
|
+
session = BetterAuth::Routes.current_session(ctx)
|
|
14
|
+
query = BetterAuth::Plugins.normalize_hash(ctx.query)
|
|
15
|
+
BetterAuth::Plugins.api_key_validate_list_query!(query)
|
|
16
|
+
configs = query[:config_id] ? [BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, query[:config_id])] : config.fetch(:configurations, [config])
|
|
17
|
+
reference_id = query[:organization_id] || session[:user]["id"]
|
|
18
|
+
expected_reference = query[:organization_id] ? "organization" : "user"
|
|
19
|
+
BetterAuth::Plugins.api_key_check_org_permission!(ctx, session[:user]["id"], reference_id, "read") if query[:organization_id]
|
|
20
|
+
records = configs.flat_map { |entry| BetterAuth::Plugins.api_key_list_for_reference(ctx, reference_id, entry) }.uniq { |record| record["id"] }
|
|
21
|
+
records = records.select do |record|
|
|
22
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
23
|
+
record_config[:references].to_s == expected_reference &&
|
|
24
|
+
BetterAuth::Plugins.api_key_record_reference_id(record) == reference_id &&
|
|
25
|
+
(!query[:config_id] || BetterAuth::Plugins.api_key_config_id_matches?(BetterAuth::Plugins.api_key_record_config_id(record), query[:config_id]))
|
|
26
|
+
end
|
|
27
|
+
total = records.length
|
|
28
|
+
records = BetterAuth::Plugins.api_key_sort_records(records, query[:sort_by], query[:sort_direction])
|
|
29
|
+
offset = query.key?(:offset) ? query[:offset].to_i : nil
|
|
30
|
+
limit = query.key?(:limit) ? query[:limit].to_i : nil
|
|
31
|
+
records = records.drop(offset) if offset
|
|
32
|
+
records = records.first(limit) if limit
|
|
33
|
+
records.each { |record| BetterAuth::Plugins.api_key_delete_expired(ctx.context, BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))) }
|
|
34
|
+
api_keys = records.map do |record|
|
|
35
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
36
|
+
BetterAuth::Plugins.api_key_public(BetterAuth::Plugins.api_key_migrate_legacy_metadata(ctx, record, record_config), include_key_field: false)
|
|
37
|
+
end
|
|
38
|
+
ctx.json({apiKeys: api_keys, total: total, limit: limit, offset: offset})
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
module UpdateAPIKey
|
|
7
|
+
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/update-api-key.ts"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def endpoint(config)
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/update", method: "POST") do |ctx|
|
|
13
|
+
body = BetterAuth::Plugins.api_key_normalize_body(ctx.body)
|
|
14
|
+
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, body[:config_id])
|
|
15
|
+
session = BetterAuth::Routes.current_session(ctx, allow_nil: true)
|
|
16
|
+
user_id = session&.dig(:user, "id") || body[:user_id]
|
|
17
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) unless user_id
|
|
18
|
+
if session && body[:user_id] && body[:user_id] != session[:user]["id"]
|
|
19
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
key_id = body[:key_id]
|
|
23
|
+
record = BetterAuth::Plugins.api_key_find_by_id(ctx, key_id, resolved_config)
|
|
24
|
+
raise BetterAuth::APIError.new("NOT_FOUND", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_NOT_FOUND"]) unless record
|
|
25
|
+
unless BetterAuth::Plugins.api_key_config_id_matches?(BetterAuth::Plugins.api_key_record_config_id(record), resolved_config[:config_id])
|
|
26
|
+
raise BetterAuth::APIError.new("NOT_FOUND", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_NOT_FOUND"])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
30
|
+
BetterAuth::Plugins.api_key_authorize_reference!(ctx, record_config, user_id, BetterAuth::Plugins.api_key_record_reference_id(record), "update")
|
|
31
|
+
|
|
32
|
+
BetterAuth::Plugins.api_key_validate_create_update!(body, record_config, create: false, client: BetterAuth::Plugins.api_key_auth_required?(ctx))
|
|
33
|
+
update = BetterAuth::Plugins.api_key_update_payload(body, record_config)
|
|
34
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["NO_VALUES_TO_UPDATE"]) if update.empty?
|
|
35
|
+
|
|
36
|
+
updated = BetterAuth::Plugins.api_key_update_record(ctx, record, update.merge(updatedAt: Time.now), record_config)
|
|
37
|
+
updated = BetterAuth::Plugins.api_key_migrate_legacy_metadata(ctx, updated, record_config)
|
|
38
|
+
BetterAuth::Plugins.api_key_delete_expired(ctx.context, record_config)
|
|
39
|
+
ctx.json(BetterAuth::Plugins.api_key_public(updated, include_key_field: false))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Routes
|
|
6
|
+
module VerifyAPIKey
|
|
7
|
+
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/verify-api-key.ts"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def endpoint(config)
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/verify", method: "POST") do |ctx|
|
|
13
|
+
body = BetterAuth::Plugins.normalize_hash(ctx.body)
|
|
14
|
+
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, body[:config_id])
|
|
15
|
+
key = body[:key]
|
|
16
|
+
if key.to_s.empty?
|
|
17
|
+
raise BetterAuth::APIError.new(
|
|
18
|
+
"FORBIDDEN",
|
|
19
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"],
|
|
20
|
+
code: "INVALID_API_KEY"
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if resolved_config[:custom_api_key_validator].respond_to?(:call) && !resolved_config[:custom_api_key_validator].call({ctx: ctx, key: key})
|
|
25
|
+
ctx.json({valid: false, error: {message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"], code: "KEY_NOT_FOUND"}, key: nil})
|
|
26
|
+
else
|
|
27
|
+
record = BetterAuth::Plugins.api_key_validate!(ctx, key, resolved_config, permissions: body[:permissions])
|
|
28
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
29
|
+
BetterAuth::Plugins.api_key_schedule_cleanup(ctx, record_config)
|
|
30
|
+
ctx.json({valid: true, error: nil, key: BetterAuth::Plugins.api_key_public(record, include_key_field: false)})
|
|
31
|
+
end
|
|
32
|
+
rescue BetterAuth::APIError => error
|
|
33
|
+
ctx.context.logger.error("Failed to validate API key: #{error.message}") if ctx.context.logger.respond_to?(:error)
|
|
34
|
+
ctx.json({valid: false, error: BetterAuth::Plugins.api_key_error_payload(error), key: nil})
|
|
35
|
+
rescue => error
|
|
36
|
+
ctx.context.logger.error("Failed to validate API key: #{error.message}") if ctx.context.logger.respond_to?(:error)
|
|
37
|
+
ctx.json({valid: false, error: {message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"], code: "INVALID_API_KEY"}, key: nil})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module SchemaDefinition
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def schema(config, custom_schema = nil)
|
|
9
|
+
base = {
|
|
10
|
+
apikey: {
|
|
11
|
+
fields: {
|
|
12
|
+
configId: {type: "string", required: true, default_value: "default", index: true},
|
|
13
|
+
name: {type: "string", required: false},
|
|
14
|
+
start: {type: "string", required: false},
|
|
15
|
+
prefix: {type: "string", required: false},
|
|
16
|
+
key: {type: "string", required: true, index: true},
|
|
17
|
+
referenceId: {type: "string", required: true, index: true},
|
|
18
|
+
refillInterval: {type: "number", required: false},
|
|
19
|
+
refillAmount: {type: "number", required: false},
|
|
20
|
+
lastRefillAt: {type: "date", required: false},
|
|
21
|
+
enabled: {type: "boolean", required: false, default_value: true},
|
|
22
|
+
rateLimitEnabled: {type: "boolean", required: false, default_value: true},
|
|
23
|
+
rateLimitTimeWindow: {type: "number", required: false, default_value: config[:rate_limit][:time_window]},
|
|
24
|
+
rateLimitMax: {type: "number", required: false, default_value: config[:rate_limit][:max_requests]},
|
|
25
|
+
requestCount: {type: "number", required: false, default_value: 0},
|
|
26
|
+
remaining: {type: "number", required: false},
|
|
27
|
+
lastRequest: {type: "date", required: false},
|
|
28
|
+
expiresAt: {type: "date", required: false},
|
|
29
|
+
createdAt: {type: "date", required: true},
|
|
30
|
+
updatedAt: {type: "date", required: true},
|
|
31
|
+
permissions: {type: "string", required: false},
|
|
32
|
+
metadata: {type: "string", required: false}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
BetterAuth::Plugins.deep_merge_hashes(base, BetterAuth::Plugins.normalize_hash(custom_schema || {}))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Session
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def header_config(ctx, config)
|
|
9
|
+
config.fetch(:configurations, [config]).find do |entry|
|
|
10
|
+
entry[:enable_session_for_api_keys] && BetterAuth::APIKey::Keys.from_headers(ctx, entry)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def hook(ctx, config)
|
|
15
|
+
config = header_config(ctx, config) || config
|
|
16
|
+
key = BetterAuth::APIKey::Keys.from_headers(ctx, config)
|
|
17
|
+
unless key.is_a?(String)
|
|
18
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY_GETTER_RETURN_TYPE"])
|
|
19
|
+
end
|
|
20
|
+
raise BetterAuth::APIError.new("FORBIDDEN", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"]) if key.length < config[:default_key_length].to_i
|
|
21
|
+
|
|
22
|
+
if config[:custom_api_key_validator].respond_to?(:call) && !config[:custom_api_key_validator].call({ctx: ctx, key: key})
|
|
23
|
+
raise BetterAuth::APIError.new("FORBIDDEN", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
record = BetterAuth::Plugins.api_key_validate!(ctx, key, config)
|
|
27
|
+
BetterAuth::APIKey::Routes.schedule_cleanup(ctx, config)
|
|
28
|
+
if config[:references].to_s != "user"
|
|
29
|
+
raise BetterAuth::APIError.new(
|
|
30
|
+
"UNAUTHORIZED",
|
|
31
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_REFERENCE_ID_FROM_API_KEY"],
|
|
32
|
+
code: "INVALID_REFERENCE_ID_FROM_API_KEY"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
reference_id = BetterAuth::APIKey::Types.record_reference_id(record)
|
|
36
|
+
user = ctx.context.internal_adapter.find_user_by_id(reference_id)
|
|
37
|
+
unless user
|
|
38
|
+
raise BetterAuth::APIError.new(
|
|
39
|
+
"UNAUTHORIZED",
|
|
40
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_REFERENCE_ID_FROM_API_KEY"],
|
|
41
|
+
code: "INVALID_REFERENCE_ID_FROM_API_KEY"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
session = {
|
|
46
|
+
user: user,
|
|
47
|
+
session: {
|
|
48
|
+
"id" => record["id"],
|
|
49
|
+
"token" => key,
|
|
50
|
+
"userId" => reference_id,
|
|
51
|
+
"userAgent" => ctx.headers["user-agent"],
|
|
52
|
+
"ipAddress" => BetterAuth::RequestIP.client_ip(ctx.request || ctx.headers, ctx.context.options),
|
|
53
|
+
"createdAt" => Time.now,
|
|
54
|
+
"updatedAt" => Time.now,
|
|
55
|
+
"expiresAt" => record["expiresAt"] || (Time.now + ctx.context.options.session[:expires_in].to_i)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
ctx.context.set_current_session(session)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Types
|
|
6
|
+
API_KEY_TABLE_NAME = "apikey"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def record_reference_id(record)
|
|
11
|
+
record["referenceId"] || record[:referenceId] || record["userId"] || record[:userId]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def record_user_id(record)
|
|
15
|
+
record["userId"] || record[:userId] || (BetterAuth::APIKey::Routes.default_config_id?(record["configId"] || record[:configId]) && (record["referenceId"] || record[:referenceId]))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def record_config_id(record)
|
|
19
|
+
record["configId"] || record[:configId] || "default"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default_permissions(config, reference_id, ctx)
|
|
23
|
+
permissions = config.dig(:permissions, :default_permissions) || config[:default_permissions]
|
|
24
|
+
return permissions.call(reference_id, ctx) if permissions.respond_to?(:call)
|
|
25
|
+
|
|
26
|
+
permissions
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module BetterAuth
|
|
7
|
+
module APIKey
|
|
8
|
+
module Utils
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def encode_json(value)
|
|
12
|
+
return nil if value.nil?
|
|
13
|
+
|
|
14
|
+
JSON.generate(value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def decode_json(value)
|
|
18
|
+
return nil if value.nil?
|
|
19
|
+
return value if value.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
parsed = JSON.parse(value.to_s)
|
|
22
|
+
parsed.is_a?(String) ? decode_json(parsed) : parsed
|
|
23
|
+
rescue JSON::ParserError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def normalize_time(value)
|
|
28
|
+
return value if value.is_a?(Time)
|
|
29
|
+
return nil if value.nil?
|
|
30
|
+
|
|
31
|
+
Time.parse(value.to_s)
|
|
32
|
+
rescue ArgumentError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def public_record(record, reveal_key: nil, include_key_field: false)
|
|
37
|
+
data = record.transform_keys(&:to_sym)
|
|
38
|
+
output = data.except(:key)
|
|
39
|
+
output[:configId] ||= BetterAuth::APIKey::Types.record_config_id(record)
|
|
40
|
+
output[:referenceId] ||= BetterAuth::APIKey::Types.record_reference_id(record)
|
|
41
|
+
output[:key] = reveal_key if include_key_field && reveal_key
|
|
42
|
+
output[:metadata] = decode_json(data[:metadata])
|
|
43
|
+
output[:permissions] = decode_json(data[:permissions])
|
|
44
|
+
output
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def sort_records(records, sort_by, direction)
|
|
48
|
+
return records unless sort_by
|
|
49
|
+
|
|
50
|
+
key = BetterAuth::Schema.storage_key(sort_by)
|
|
51
|
+
sorted = records.sort_by { |record| record[key] || record[key.to_sym] || "" }
|
|
52
|
+
if direction.to_s.downcase == "desc"
|
|
53
|
+
sorted.reverse
|
|
54
|
+
else
|
|
55
|
+
sorted
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_list_query!(query)
|
|
60
|
+
%i[limit offset].each do |key|
|
|
61
|
+
next unless query.key?(key)
|
|
62
|
+
|
|
63
|
+
value = query[key]
|
|
64
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: "Invalid #{key}") unless value.to_s.match?(/\A\d+\z/)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
direction = query[:sort_direction]
|
|
68
|
+
return if direction.nil? || %w[asc desc].include?(direction.to_s.downcase)
|
|
69
|
+
|
|
70
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: "Invalid sortDirection")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def error_code(error)
|
|
74
|
+
BetterAuth::Plugins::API_KEY_ERROR_CODES.key(error.message) || error.code.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def error_payload(error)
|
|
78
|
+
payload = error.to_h
|
|
79
|
+
return payload if payload.is_a?(Hash) && payload.key?(:details)
|
|
80
|
+
|
|
81
|
+
{message: error.message, code: error_code(error)}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def background_tasks?(ctx)
|
|
85
|
+
ctx.context.options.advanced.dig(:background_tasks, :handler).respond_to?(:call)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def auth_required?(ctx)
|
|
89
|
+
!!(ctx.request || (ctx.headers && !ctx.headers.empty?))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|