api_keys 0.2.0 → 0.3.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/.simplecov +36 -0
- data/AGENTS.md +5 -0
- data/Appraisals +17 -0
- data/CHANGELOG.md +14 -0
- data/CLAUDE.md +5 -0
- data/README.md +767 -3
- data/Rakefile +9 -4
- data/app/controllers/api_keys/keys_controller.rb +43 -11
- data/app/controllers/api_keys/security_controller.rb +9 -1
- data/app/views/api_keys/keys/_empty_state.html.erb +9 -0
- data/app/views/api_keys/keys/_form.html.erb +31 -2
- data/app/views/api_keys/keys/_key_actions.html.erb +20 -0
- data/app/views/api_keys/keys/_key_badges.html.erb +17 -0
- data/app/views/api_keys/keys/_key_row.html.erb +21 -35
- data/app/views/api_keys/keys/_key_status.html.erb +10 -0
- data/app/views/api_keys/keys/_keys_table.html.erb +2 -7
- data/app/views/api_keys/keys/_publishable_keys.html.erb +40 -0
- data/app/views/api_keys/keys/_secret_keys.html.erb +39 -0
- data/app/views/api_keys/keys/_show_token.html.erb +5 -1
- data/app/views/api_keys/keys/_token_display.html.erb +11 -0
- data/app/views/api_keys/keys/index.html.erb +40 -8
- data/app/views/api_keys/security/best_practices.html.erb +73 -47
- data/app/views/layouts/api_keys/application.html.erb +113 -7
- data/context7.json +4 -0
- data/gemfiles/rails_7.2.gemfile +21 -0
- data/gemfiles/rails_8.0.gemfile +21 -0
- data/gemfiles/rails_8.1.gemfile +21 -0
- data/lib/api_keys/configuration.rb +77 -0
- data/lib/api_keys/errors.rb +73 -0
- data/lib/api_keys/form_builder_extensions.rb +158 -0
- data/lib/api_keys/helpers/expiration_options.rb +131 -0
- data/lib/api_keys/helpers/token_session.rb +68 -0
- data/lib/api_keys/helpers/view_helpers.rb +216 -0
- data/lib/api_keys/models/api_key.rb +229 -17
- data/lib/api_keys/models/concerns/has_api_keys.rb +183 -3
- data/lib/api_keys/services/authenticator.rb +45 -2
- data/lib/api_keys/services/digestor.rb +6 -2
- data/lib/api_keys/tenant_resolution.rb +3 -1
- data/lib/api_keys/version.rb +1 -1
- data/lib/api_keys.rb +12 -0
- data/lib/generators/api_keys/add_key_types_generator.rb +68 -0
- data/lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb +18 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +9 -0
- data/lib/generators/api_keys/templates/initializer.rb +242 -120
- metadata +24 -58
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApiKeys
|
|
4
|
+
# Opt-in form builder extensions for API key forms.
|
|
5
|
+
#
|
|
6
|
+
# These helpers reduce boilerplate while letting you control the styling.
|
|
7
|
+
# Include them in your form builder to use:
|
|
8
|
+
#
|
|
9
|
+
# # In config/initializers/api_keys.rb:
|
|
10
|
+
# Rails.application.config.to_prepare do
|
|
11
|
+
# ActionView::Helpers::FormBuilder.include(ApiKeys::FormBuilderExtensions)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Expiration select
|
|
15
|
+
# <%= form.api_key_expiration_select(class: "my-select-class") %>
|
|
16
|
+
#
|
|
17
|
+
# @example Scopes checkboxes with block for custom rendering
|
|
18
|
+
# <%= form.api_key_scopes_checkboxes(@scopes) do |scope, checked| %>
|
|
19
|
+
# <label>
|
|
20
|
+
# <%= check_box_tag "api_key[scopes][]", scope, checked, class: "my-checkbox" %>
|
|
21
|
+
# <%= scope %>
|
|
22
|
+
# </label>
|
|
23
|
+
# <% end %>
|
|
24
|
+
#
|
|
25
|
+
module FormBuilderExtensions
|
|
26
|
+
# Renders a select field for API key expiration presets.
|
|
27
|
+
#
|
|
28
|
+
# @param options [Hash] Options passed to the select helper
|
|
29
|
+
# @param html_options [Hash] HTML attributes for the select element
|
|
30
|
+
# @return [String] HTML select element
|
|
31
|
+
#
|
|
32
|
+
# @example Basic usage
|
|
33
|
+
# <%= form.api_key_expiration_select %>
|
|
34
|
+
#
|
|
35
|
+
# @example With Tailwind classes
|
|
36
|
+
# <%= form.api_key_expiration_select(class: "w-full px-4 py-3 border rounded-lg") %>
|
|
37
|
+
#
|
|
38
|
+
# @example With custom default
|
|
39
|
+
# <%= form.api_key_expiration_select(selected: "30_days") %>
|
|
40
|
+
#
|
|
41
|
+
def api_key_expiration_select(options = {}, html_options = {})
|
|
42
|
+
# expires_at_preset is a form-only param, not a model attribute
|
|
43
|
+
# So we only use the provided :selected option or default
|
|
44
|
+
selected = options.delete(:selected) || ApiKeys::ExpirationOptions.default_value
|
|
45
|
+
|
|
46
|
+
select(
|
|
47
|
+
:expires_at_preset,
|
|
48
|
+
@template.options_for_select(ApiKeys::ExpirationOptions.for_select, selected),
|
|
49
|
+
options,
|
|
50
|
+
html_options
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Renders checkboxes for API key scopes.
|
|
55
|
+
#
|
|
56
|
+
# If a block is given, yields each scope and its checked state for custom rendering.
|
|
57
|
+
# If no block is given, returns an array of checkbox data for manual iteration.
|
|
58
|
+
#
|
|
59
|
+
# @param scopes [Array<String>] Available scopes to render
|
|
60
|
+
# @param checked [Symbol, Array] Which scopes should be checked:
|
|
61
|
+
# - :all (default for new records) - all scopes checked
|
|
62
|
+
# - :none - no scopes checked
|
|
63
|
+
# - Array - specific scopes to check
|
|
64
|
+
# - nil - uses the object's current scopes
|
|
65
|
+
# @return [String, Array] HTML if block given, otherwise array of scope data
|
|
66
|
+
#
|
|
67
|
+
# @example With block (recommended for custom styling)
|
|
68
|
+
# <%= form.api_key_scopes_checkboxes(@scopes) do |scope, checked| %>
|
|
69
|
+
# <label class="flex items-center gap-2">
|
|
70
|
+
# <%= check_box_tag "api_key[scopes][]", scope, checked, class: "rounded" %>
|
|
71
|
+
# <code><%= scope %></code>
|
|
72
|
+
# </label>
|
|
73
|
+
# <% end %>
|
|
74
|
+
#
|
|
75
|
+
# @example Simple rendering without block
|
|
76
|
+
# <% form.api_key_scopes_checkboxes(@scopes).each do |scope_data| %>
|
|
77
|
+
# <%= check_box_tag "api_key[scopes][]", scope_data[:value], scope_data[:checked] %>
|
|
78
|
+
# <%= scope_data[:value] %>
|
|
79
|
+
# <% end %>
|
|
80
|
+
#
|
|
81
|
+
def api_key_scopes_checkboxes(scopes, checked: nil, &block)
|
|
82
|
+
# Determine which scopes should be checked
|
|
83
|
+
checked_scopes = resolve_checked_scopes(scopes, checked)
|
|
84
|
+
|
|
85
|
+
scope_data = scopes.map do |scope|
|
|
86
|
+
{
|
|
87
|
+
value: scope,
|
|
88
|
+
checked: checked_scopes.include?(scope),
|
|
89
|
+
field_name: "#{object_name}[scopes][]"
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if block_given?
|
|
94
|
+
# Yield each scope for custom rendering
|
|
95
|
+
safe_buffer = ActiveSupport::SafeBuffer.new
|
|
96
|
+
scope_data.each do |data|
|
|
97
|
+
safe_buffer << @template.capture { yield(data[:value], data[:checked]) }
|
|
98
|
+
end
|
|
99
|
+
safe_buffer
|
|
100
|
+
else
|
|
101
|
+
# Return raw data for manual iteration
|
|
102
|
+
scope_data
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns structured data for building a token display UI.
|
|
107
|
+
# Useful when you need to build a custom token display with copy functionality.
|
|
108
|
+
#
|
|
109
|
+
# @return [Hash] Token display data with keys:
|
|
110
|
+
# - :masked [String] The masked token (e.g., "sk_live_••••abc")
|
|
111
|
+
# - :full [String, nil] The full token (only for viewable public keys)
|
|
112
|
+
# - :viewable [Boolean] Whether the full token can be displayed
|
|
113
|
+
# - :type [Symbol] The key type (:publishable, :secret, or nil)
|
|
114
|
+
# - :environment [String, nil] The environment (e.g., "live", "test")
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# <% data = form.api_key_token_data %>
|
|
118
|
+
# <code><%= data[:masked] %></code>
|
|
119
|
+
# <% if data[:viewable] %>
|
|
120
|
+
# <button data-token="<%= data[:full] %>">Copy</button>
|
|
121
|
+
# <% end %>
|
|
122
|
+
#
|
|
123
|
+
def api_key_token_data
|
|
124
|
+
return {} unless object.respond_to?(:masked_token)
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
masked: object.masked_token,
|
|
128
|
+
full: object.viewable_token,
|
|
129
|
+
viewable: object.respond_to?(:public_key_type?) && object.public_key_type?,
|
|
130
|
+
type: object.key_type&.to_sym,
|
|
131
|
+
environment: object.environment
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def resolve_checked_scopes(scopes, checked)
|
|
138
|
+
case checked
|
|
139
|
+
when :all
|
|
140
|
+
scopes.map(&:to_s)
|
|
141
|
+
when :none
|
|
142
|
+
[]
|
|
143
|
+
when Array
|
|
144
|
+
checked.map(&:to_s)
|
|
145
|
+
when nil
|
|
146
|
+
# For new records, default to all checked
|
|
147
|
+
# For existing records, use the object's scopes
|
|
148
|
+
if object&.persisted?
|
|
149
|
+
(object.scopes || []).map(&:to_s)
|
|
150
|
+
else
|
|
151
|
+
scopes.map(&:to_s)
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
scopes.map(&:to_s)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApiKeys
|
|
4
|
+
module Helpers
|
|
5
|
+
# Helper for handling API key expiration presets.
|
|
6
|
+
#
|
|
7
|
+
# Provides a consistent set of expiration options for use in forms,
|
|
8
|
+
# and parsing logic to convert preset strings to actual dates.
|
|
9
|
+
#
|
|
10
|
+
# @example In your view (form select)
|
|
11
|
+
# <%= form.select :expires_at_preset,
|
|
12
|
+
# ApiKeys::Helpers::ExpirationOptions.for_select %>
|
|
13
|
+
#
|
|
14
|
+
# @example In your controller
|
|
15
|
+
# expires_at = ApiKeys::Helpers::ExpirationOptions.parse(params[:expires_at_preset])
|
|
16
|
+
# @api_key = current_org.create_api_key!(expires_at: expires_at, ...)
|
|
17
|
+
#
|
|
18
|
+
class ExpirationOptions
|
|
19
|
+
# Default preset options with human-readable labels
|
|
20
|
+
DEFAULT_PRESETS = [
|
|
21
|
+
{ label: "No Expiration", value: "no_expiration", days: nil },
|
|
22
|
+
{ label: "7 days", value: "7_days", days: 7 },
|
|
23
|
+
{ label: "30 days", value: "30_days", days: 30 },
|
|
24
|
+
{ label: "60 days", value: "60_days", days: 60 },
|
|
25
|
+
{ label: "90 days", value: "90_days", days: 90 },
|
|
26
|
+
{ label: "1 year", value: "365_days", days: 365 }
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Returns options suitable for a Rails select helper.
|
|
31
|
+
#
|
|
32
|
+
# @param include_no_expiration [Boolean] Whether to include "No Expiration" option (default: true)
|
|
33
|
+
# @param presets [Array<Integer>, nil] Custom list of days to include (e.g., [7, 30, 90])
|
|
34
|
+
# If nil, uses DEFAULT_PRESETS
|
|
35
|
+
# @return [Array<Array>] Array of [label, value] pairs for select helper
|
|
36
|
+
#
|
|
37
|
+
# @example Default options
|
|
38
|
+
# ExpirationOptions.for_select
|
|
39
|
+
# # => [["No Expiration", "no_expiration"], ["7 days", "7_days"], ...]
|
|
40
|
+
#
|
|
41
|
+
# @example Custom presets
|
|
42
|
+
# ExpirationOptions.for_select(presets: [7, 30, 365])
|
|
43
|
+
# # => [["No Expiration", "no_expiration"], ["7 days", "7_days"], ["30 days", "30_days"], ["1 year", "365_days"]]
|
|
44
|
+
#
|
|
45
|
+
# @example Without "No Expiration"
|
|
46
|
+
# ExpirationOptions.for_select(include_no_expiration: false)
|
|
47
|
+
# # => [["7 days", "7_days"], ["30 days", "30_days"], ...]
|
|
48
|
+
#
|
|
49
|
+
def for_select(include_no_expiration: true, presets: nil)
|
|
50
|
+
options = if presets
|
|
51
|
+
build_custom_presets(presets, include_no_expiration)
|
|
52
|
+
else
|
|
53
|
+
filter_default_presets(include_no_expiration)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
options.map { |opt| [opt[:label], opt[:value]] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Parse an expiration preset string into an actual datetime.
|
|
60
|
+
#
|
|
61
|
+
# @param preset [String, nil] The preset value (e.g., "30_days", "no_expiration")
|
|
62
|
+
# @return [ActiveSupport::TimeWithZone, nil] The expiration date, or nil for no expiration
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# ExpirationOptions.parse("30_days") # => 30.days.from_now
|
|
66
|
+
# ExpirationOptions.parse("no_expiration") # => nil
|
|
67
|
+
# ExpirationOptions.parse(nil) # => nil
|
|
68
|
+
# ExpirationOptions.parse("invalid") # => nil
|
|
69
|
+
#
|
|
70
|
+
def parse(preset)
|
|
71
|
+
return nil if preset.blank? || preset == "no_expiration"
|
|
72
|
+
|
|
73
|
+
# Try to extract days from the preset string (e.g., "30_days" => 30)
|
|
74
|
+
if preset =~ /\A(\d+)_days?\z/
|
|
75
|
+
days = ::Regexp.last_match(1).to_i
|
|
76
|
+
# Reasonable max: ~10 years to prevent overflow issues
|
|
77
|
+
return days.days.from_now if days.positive? && days <= 3650
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check against known presets
|
|
81
|
+
known = DEFAULT_PRESETS.find { |p| p[:value] == preset }
|
|
82
|
+
return known[:days].days.from_now if known && known[:days]
|
|
83
|
+
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns the default preset value (useful for form defaults)
|
|
88
|
+
#
|
|
89
|
+
# @return [String] The default preset value
|
|
90
|
+
def default_value
|
|
91
|
+
"no_expiration"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def filter_default_presets(include_no_expiration)
|
|
97
|
+
if include_no_expiration
|
|
98
|
+
DEFAULT_PRESETS
|
|
99
|
+
else
|
|
100
|
+
DEFAULT_PRESETS.reject { |p| p[:value] == "no_expiration" }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_custom_presets(days_list, include_no_expiration)
|
|
105
|
+
options = []
|
|
106
|
+
|
|
107
|
+
if include_no_expiration
|
|
108
|
+
options << { label: "No Expiration", value: "no_expiration", days: nil }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
days_list.each do |days|
|
|
112
|
+
options << preset_for_days(days)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
options
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def preset_for_days(days)
|
|
119
|
+
label = case days
|
|
120
|
+
when 1 then "1 day"
|
|
121
|
+
when 365 then "1 year"
|
|
122
|
+
when ->(d) { d % 365 == 0 } then "#{days / 365} years"
|
|
123
|
+
else "#{days} days"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
{ label: label, value: "#{days}_days", days: days }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApiKeys
|
|
4
|
+
module Helpers
|
|
5
|
+
# Helper for managing API key tokens in the session.
|
|
6
|
+
#
|
|
7
|
+
# Secret keys can only be shown once (immediately after creation) because
|
|
8
|
+
# the plaintext token is not stored in the database. This helper provides
|
|
9
|
+
# a clean interface for the "show token once" pattern:
|
|
10
|
+
#
|
|
11
|
+
# 1. After creating a key, store the token in the session
|
|
12
|
+
# 2. On the success page, retrieve (and clear) the token
|
|
13
|
+
# 3. If the user refreshes, the token is gone
|
|
14
|
+
#
|
|
15
|
+
# @example In your controller
|
|
16
|
+
# # After creating a key:
|
|
17
|
+
# def create
|
|
18
|
+
# @api_key = current_org.create_api_key!(...)
|
|
19
|
+
# ApiKeys::Helpers::TokenSession.store(session, @api_key)
|
|
20
|
+
# redirect_to success_path
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # On the success page:
|
|
24
|
+
# def success
|
|
25
|
+
# @token = ApiKeys::Helpers::TokenSession.retrieve_once(session)
|
|
26
|
+
# redirect_to index_path, alert: "Token already shown" unless @token
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
class TokenSession
|
|
30
|
+
# Default session key for storing the token
|
|
31
|
+
DEFAULT_SESSION_KEY = :api_keys_new_token
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Store an API key's token in the session for later retrieval.
|
|
35
|
+
#
|
|
36
|
+
# @param session [ActionDispatch::Request::Session] The Rails session
|
|
37
|
+
# @param api_key [ApiKeys::ApiKey] The newly created API key
|
|
38
|
+
# @param key [Symbol] Optional custom session key (default: :api_keys_new_token)
|
|
39
|
+
# @return [String] The token that was stored
|
|
40
|
+
def store(session, api_key, key: DEFAULT_SESSION_KEY)
|
|
41
|
+
token = api_key.respond_to?(:token) ? api_key.token : api_key.to_s
|
|
42
|
+
session[key] = token
|
|
43
|
+
token
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Retrieve and clear the token from the session.
|
|
47
|
+
# Returns nil if no token is stored (e.g., page was refreshed).
|
|
48
|
+
#
|
|
49
|
+
# @param session [ActionDispatch::Request::Session] The Rails session
|
|
50
|
+
# @param key [Symbol] Optional custom session key (default: :api_keys_new_token)
|
|
51
|
+
# @return [String, nil] The token, or nil if not present
|
|
52
|
+
def retrieve_once(session, key: DEFAULT_SESSION_KEY)
|
|
53
|
+
session.delete(key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if a token is available in the session without removing it.
|
|
57
|
+
# Useful for conditional rendering.
|
|
58
|
+
#
|
|
59
|
+
# @param session [ActionDispatch::Request::Session] The Rails session
|
|
60
|
+
# @param key [Symbol] Optional custom session key (default: :api_keys_new_token)
|
|
61
|
+
# @return [Boolean] true if a token is stored
|
|
62
|
+
def available?(session, key: DEFAULT_SESSION_KEY)
|
|
63
|
+
session[key].present?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApiKeys
|
|
4
|
+
module Helpers
|
|
5
|
+
# View helpers for displaying API key information.
|
|
6
|
+
#
|
|
7
|
+
# These helpers provide formatted data without HTML opinions,
|
|
8
|
+
# allowing integrators to build their own UI while using
|
|
9
|
+
# consistent data formatting.
|
|
10
|
+
#
|
|
11
|
+
# @example Include in your ApplicationHelper
|
|
12
|
+
# module ApplicationHelper
|
|
13
|
+
# include ApiKeys::Helpers::ViewHelpers
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @example Or include in a specific controller
|
|
17
|
+
# class Settings::ApiKeysController < ApplicationController
|
|
18
|
+
# helper ApiKeys::Helpers::ViewHelpers
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
module ViewHelpers
|
|
22
|
+
# Returns the status of an API key as a symbol.
|
|
23
|
+
#
|
|
24
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
25
|
+
# @return [Symbol] :active, :expired, or :revoked
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# api_key_status(@key) # => :active
|
|
29
|
+
#
|
|
30
|
+
def api_key_status(api_key)
|
|
31
|
+
return :revoked if api_key.revoked?
|
|
32
|
+
return :expired if api_key.expired?
|
|
33
|
+
|
|
34
|
+
:active
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a human-readable status label for an API key.
|
|
38
|
+
#
|
|
39
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
40
|
+
# @return [String] "Active", "Expired", or "Revoked"
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# api_key_status_label(@key) # => "Active"
|
|
44
|
+
#
|
|
45
|
+
def api_key_status_label(api_key)
|
|
46
|
+
case api_key_status(api_key)
|
|
47
|
+
when :active then "Active"
|
|
48
|
+
when :expired then "Expired"
|
|
49
|
+
when :revoked then "Revoked"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the environment label for an API key.
|
|
54
|
+
#
|
|
55
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
56
|
+
# @return [String] "Test", "Live", or "Default" (if no environment set)
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# api_key_environment_label(@key) # => "Live"
|
|
60
|
+
#
|
|
61
|
+
def api_key_environment_label(api_key)
|
|
62
|
+
return "Default" if api_key.environment.blank?
|
|
63
|
+
|
|
64
|
+
api_key.environment.to_s.capitalize
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns the key type label for an API key.
|
|
68
|
+
#
|
|
69
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
70
|
+
# @return [String] "Publishable", "Secret", or the key_type capitalized
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# api_key_type_label(@key) # => "Secret"
|
|
74
|
+
#
|
|
75
|
+
def api_key_type_label(api_key)
|
|
76
|
+
return "Secret" if api_key.key_type.blank?
|
|
77
|
+
|
|
78
|
+
case api_key.key_type.to_s
|
|
79
|
+
when "publishable" then "Publishable"
|
|
80
|
+
when "secret" then "Secret"
|
|
81
|
+
else api_key.key_type.to_s.capitalize
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns whether the key is a publishable (public) key type.
|
|
86
|
+
# Useful for conditional rendering (e.g., showing/hiding copy button).
|
|
87
|
+
#
|
|
88
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
89
|
+
# @return [Boolean] true if the key is publishable
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# <% if api_key_publishable?(@key) %>
|
|
93
|
+
# <%= button_tag "Copy", data: { token: @key.viewable_token } %>
|
|
94
|
+
# <% end %>
|
|
95
|
+
#
|
|
96
|
+
def api_key_publishable?(api_key)
|
|
97
|
+
api_key.key_type.to_s == "publishable"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns whether the key is a secret key type.
|
|
101
|
+
#
|
|
102
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
103
|
+
# @return [Boolean] true if the key is secret (or legacy with no type)
|
|
104
|
+
#
|
|
105
|
+
def api_key_secret?(api_key)
|
|
106
|
+
api_key.key_type.blank? || api_key.key_type.to_s == "secret"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Detects the environment from a token string by parsing its prefix.
|
|
110
|
+
# Useful for displaying environment info when you only have the token.
|
|
111
|
+
#
|
|
112
|
+
# @param token [String] The full token string (e.g., "sk_test_abc123...")
|
|
113
|
+
# @return [Symbol, nil] The detected environment (:test, :live, etc.) or nil
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# api_key_environment_from_token("sk_test_abc123") # => :test
|
|
117
|
+
# api_key_environment_from_token("pk_live_xyz789") # => :live
|
|
118
|
+
# api_key_environment_from_token("ak_abc123") # => nil
|
|
119
|
+
#
|
|
120
|
+
def api_key_environment_from_token(token)
|
|
121
|
+
return nil if token.blank?
|
|
122
|
+
return nil if token.length > 500 # Reasonable max length for tokens
|
|
123
|
+
|
|
124
|
+
config = ApiKeys.configuration
|
|
125
|
+
return nil unless config.environments.present?
|
|
126
|
+
|
|
127
|
+
# Check each configured environment's prefix segment
|
|
128
|
+
config.environments.each do |env_name, env_config|
|
|
129
|
+
segment = env_config[:prefix_segment]
|
|
130
|
+
next if segment.blank?
|
|
131
|
+
|
|
132
|
+
# Match pattern like _test_ or _live_ in the token
|
|
133
|
+
if token.include?("_#{segment}_")
|
|
134
|
+
return env_name
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns a human-readable environment label from a token string.
|
|
142
|
+
# Convenience wrapper around api_key_environment_from_token.
|
|
143
|
+
#
|
|
144
|
+
# @param token [String] The full token string
|
|
145
|
+
# @return [String] "Test mode", "Live mode", or "Default" if unknown
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# api_key_environment_label_from_token("sk_test_abc") # => "Test mode"
|
|
149
|
+
# api_key_environment_label_from_token("sk_live_xyz") # => "Live mode"
|
|
150
|
+
#
|
|
151
|
+
def api_key_environment_label_from_token(token)
|
|
152
|
+
env = api_key_environment_from_token(token)
|
|
153
|
+
return "Default" if env.nil?
|
|
154
|
+
|
|
155
|
+
"#{env.to_s.capitalize} mode"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Returns a hash of status information for an API key.
|
|
159
|
+
# Useful for building custom status badges.
|
|
160
|
+
#
|
|
161
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
162
|
+
# @return [Hash] Hash with :status, :label, and :color keys
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# info = api_key_status_info(@key)
|
|
166
|
+
# # => { status: :active, label: "Active", color: :green }
|
|
167
|
+
#
|
|
168
|
+
def api_key_status_info(api_key)
|
|
169
|
+
status = api_key_status(api_key)
|
|
170
|
+
{
|
|
171
|
+
status: status,
|
|
172
|
+
label: api_key_status_label(api_key),
|
|
173
|
+
color: status_color(status)
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns a hash of type information for an API key.
|
|
178
|
+
# Useful for building custom type badges.
|
|
179
|
+
#
|
|
180
|
+
# @param api_key [ApiKeys::ApiKey] The API key
|
|
181
|
+
# @return [Hash] Hash with :type, :label, and :color keys
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# info = api_key_type_info(@key)
|
|
185
|
+
# # => { type: :publishable, label: "Publishable", color: :green }
|
|
186
|
+
#
|
|
187
|
+
def api_key_type_info(api_key)
|
|
188
|
+
type = api_key.key_type.presence&.to_sym || :secret
|
|
189
|
+
{
|
|
190
|
+
type: type,
|
|
191
|
+
label: api_key_type_label(api_key),
|
|
192
|
+
color: type_color(type)
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def status_color(status)
|
|
199
|
+
case status
|
|
200
|
+
when :active then :green
|
|
201
|
+
when :expired then :red
|
|
202
|
+
when :revoked then :gray
|
|
203
|
+
else :gray
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def type_color(type)
|
|
208
|
+
case type
|
|
209
|
+
when :publishable then :green
|
|
210
|
+
when :secret then :amber
|
|
211
|
+
else :gray
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|