api_keys 0.2.1 → 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 +9 -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 +8 -0
- 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
data/Rakefile
CHANGED
|
@@ -16,17 +16,22 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
|
16
16
|
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# Removed loading of engine tasks to avoid pulling in dummy app/vendor tests
|
|
20
|
+
# APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
21
|
+
# load "rails/tasks/engine.rake"
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
# Removed Rails statistics task; not needed for gem tests
|
|
24
|
+
# load "rails/tasks/statistics.rake"
|
|
23
25
|
|
|
24
26
|
require "rake/testtask"
|
|
25
27
|
|
|
28
|
+
# Ensure any test task created by engine.rake is cleared so only this suite runs
|
|
29
|
+
Rake::Task[:test].clear if Rake::Task.task_defined?(:test)
|
|
30
|
+
|
|
26
31
|
Rake::TestTask.new(:test) do |t|
|
|
27
32
|
t.libs << "test"
|
|
28
|
-
t.pattern = "test/**/*_test.rb"
|
|
29
33
|
t.verbose = false
|
|
34
|
+
t.test_files = FileList['test/**/*_test.rb'].exclude('test/dummy/**/*')
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
task default: :test
|
|
@@ -4,13 +4,31 @@ module ApiKeys
|
|
|
4
4
|
# Controller for managing API keys belonging to the current owner.
|
|
5
5
|
class KeysController < ApplicationController
|
|
6
6
|
before_action :set_api_key, only: [:show, :edit, :update, :revoke]
|
|
7
|
+
helper_method :key_types_feature_enabled?
|
|
7
8
|
|
|
8
9
|
# GET /keys
|
|
9
10
|
def index
|
|
10
|
-
#
|
|
11
|
-
|
|
11
|
+
# Start with all keys for the owner
|
|
12
|
+
base_scope = current_api_keys_owner.api_keys
|
|
13
|
+
|
|
14
|
+
# Filter by environment if key_types feature is enabled and cross-environment is disabled
|
|
15
|
+
if key_types_feature_enabled? && !ApiKeys.configuration.dashboard_allow_cross_environment
|
|
16
|
+
current_env = resolve_current_environment
|
|
17
|
+
base_scope = base_scope.where(environment: [current_env.to_s, nil, ""])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Fetch only active keys for the main list, sorted by creation date
|
|
21
|
+
@api_keys = base_scope.active.order(created_at: :desc)
|
|
12
22
|
# Optionally, fetch inactive ones for a separate section or filter
|
|
13
|
-
@inactive_api_keys =
|
|
23
|
+
@inactive_api_keys = base_scope.inactive.order(created_at: :desc)
|
|
24
|
+
|
|
25
|
+
# When key_types feature is enabled, separate publishable and secret keys
|
|
26
|
+
if key_types_feature_enabled?
|
|
27
|
+
@publishable_keys = @api_keys.select(&:public_key_type?)
|
|
28
|
+
@secret_keys = @api_keys.reject(&:public_key_type?)
|
|
29
|
+
@inactive_publishable_keys = @inactive_api_keys.select(&:public_key_type?)
|
|
30
|
+
@inactive_secret_keys = @inactive_api_keys.reject(&:public_key_type?)
|
|
31
|
+
end
|
|
14
32
|
end
|
|
15
33
|
|
|
16
34
|
# GET /keys/:id
|
|
@@ -41,7 +59,8 @@ module ApiKeys
|
|
|
41
59
|
@api_key = current_api_keys_owner.create_api_key!(
|
|
42
60
|
name: api_key_params[:name],
|
|
43
61
|
scopes: api_key_params[:scopes],
|
|
44
|
-
expires_at: parse_expiration(api_key_params[:expires_at_preset])
|
|
62
|
+
expires_at: parse_expiration(api_key_params[:expires_at_preset]),
|
|
63
|
+
key_type: api_key_params[:key_type].presence&.to_sym
|
|
45
64
|
# Metadata could be added here if needed
|
|
46
65
|
)
|
|
47
66
|
|
|
@@ -81,12 +100,13 @@ module ApiKeys
|
|
|
81
100
|
|
|
82
101
|
# POST /keys/:id/revoke
|
|
83
102
|
def revoke
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
@api_key.revoke!
|
|
104
|
+
redirect_to keys_path, notice: "API key revoked successfully."
|
|
105
|
+
rescue ApiKeys::Errors::KeyNotRevocableError
|
|
106
|
+
redirect_to keys_path, alert: "This API key cannot be revoked."
|
|
107
|
+
rescue => e
|
|
108
|
+
# This shouldn't typically fail unless there's a callback issue
|
|
109
|
+
redirect_to keys_path, alert: "Failed to revoke API key: #{e.message}"
|
|
90
110
|
end
|
|
91
111
|
|
|
92
112
|
private
|
|
@@ -100,8 +120,9 @@ module ApiKeys
|
|
|
100
120
|
|
|
101
121
|
# Only allow a list of trusted parameters through for creation.
|
|
102
122
|
# Added :expires_at_preset for the dropdown selector.
|
|
123
|
+
# Added :key_type for the key types feature.
|
|
103
124
|
def api_key_params
|
|
104
|
-
permitted_params = params.require(:api_key).permit(:name, :expires_at_preset, scopes: [])
|
|
125
|
+
permitted_params = params.require(:api_key).permit(:name, :expires_at_preset, :key_type, scopes: [])
|
|
105
126
|
permitted_params[:scopes]&.reject!(&:blank?) # Filter out blank strings
|
|
106
127
|
permitted_params
|
|
107
128
|
end
|
|
@@ -125,5 +146,16 @@ module ApiKeys
|
|
|
125
146
|
else nil # Default to no expiration if invalid preset
|
|
126
147
|
end
|
|
127
148
|
end
|
|
149
|
+
|
|
150
|
+
# Check if key types feature is enabled
|
|
151
|
+
def key_types_feature_enabled?
|
|
152
|
+
ApiKeys.configuration.key_types.present? && ApiKeys.configuration.key_types.any?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get the current environment from configuration
|
|
156
|
+
def resolve_current_environment
|
|
157
|
+
env_config = ApiKeys.configuration.current_environment
|
|
158
|
+
env_config.respond_to?(:call) ? env_config.call : env_config
|
|
159
|
+
end
|
|
128
160
|
end
|
|
129
161
|
end
|
|
@@ -6,11 +6,19 @@ module ApiKeys
|
|
|
6
6
|
# Skip the user authentication requirement for these static pages
|
|
7
7
|
# as they contain general information.
|
|
8
8
|
skip_before_action :authenticate_api_keys_owner!, only: [:best_practices]
|
|
9
|
+
helper_method :key_types_feature_enabled?
|
|
9
10
|
|
|
10
11
|
# GET /security/best-practices
|
|
11
12
|
def best_practices
|
|
12
13
|
# Renders app/views/api_keys/security/best_practices.html.erb
|
|
13
14
|
# The view will contain the static content.
|
|
14
15
|
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# Check if key types feature is enabled
|
|
20
|
+
def key_types_feature_enabled?
|
|
21
|
+
ApiKeys.configuration.key_types.present? && ApiKeys.configuration.key_types.any?
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%# Partial for displaying empty state when no keys exist %>
|
|
2
|
+
<%# Locals: message (optional) - Custom message to display %>
|
|
3
|
+
|
|
4
|
+
<% message ||= "You don't have any API keys yet!" %>
|
|
5
|
+
|
|
6
|
+
<div class="api-keys-empty-state" style="text-align: center; padding: 2em;">
|
|
7
|
+
<h4><%= message %></h4>
|
|
8
|
+
<p>Create your first API key to get started.</p>
|
|
9
|
+
</div>
|
|
@@ -13,6 +13,35 @@
|
|
|
13
13
|
|
|
14
14
|
<%# Fields only available on NEW %>
|
|
15
15
|
<% unless api_key.persisted? %>
|
|
16
|
+
<%# Key Type selector (only shown when key_types feature is enabled) %>
|
|
17
|
+
<% if ApiKeys.configuration.key_types.present? && ApiKeys.configuration.key_types.any? %>
|
|
18
|
+
<div>
|
|
19
|
+
<%= form.label :key_type, "Key Type" %>
|
|
20
|
+
<%= form.select :key_type,
|
|
21
|
+
options_for_select(
|
|
22
|
+
ApiKeys.configuration.key_types.map { |type, config|
|
|
23
|
+
label = type.to_s.humanize
|
|
24
|
+
label += " (#{config[:prefix]}_)" if config[:prefix]
|
|
25
|
+
[label, type.to_s]
|
|
26
|
+
},
|
|
27
|
+
api_key.key_type
|
|
28
|
+
),
|
|
29
|
+
{ prompt: "Select key type..." }
|
|
30
|
+
%>
|
|
31
|
+
<% ApiKeys.configuration.key_types.each do |type, config| %>
|
|
32
|
+
<% if config[:permissions] != :all %>
|
|
33
|
+
<small style="display: block; color: #666;">
|
|
34
|
+
<strong><%= type.to_s.humanize %>:</strong>
|
|
35
|
+
Limited to <%= config[:permissions]&.join(", ") || "no" %> permissions
|
|
36
|
+
<% unless config.fetch(:revocable, true) %>
|
|
37
|
+
<em>(cannot be revoked)</em>
|
|
38
|
+
<% end %>
|
|
39
|
+
</small>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
16
45
|
<div>
|
|
17
46
|
<%= form.label :name, "Name (optional)" %>
|
|
18
47
|
<%= form.text_field :name, placeholder: "e.g., myproject-production-key" %>
|
|
@@ -52,7 +81,7 @@
|
|
|
52
81
|
scope, # Value submitted when checked
|
|
53
82
|
nil # Value submitted when unchecked (not needed here)
|
|
54
83
|
%>
|
|
55
|
-
<%= form.label "scopes_#{scope.parameterize}",
|
|
84
|
+
<%= form.label "scopes_#{scope.parameterize}", class: "form-check-label" do %><code><%= scope %></code><% end %>
|
|
56
85
|
</div>
|
|
57
86
|
<% end %>
|
|
58
87
|
</div>
|
|
@@ -85,7 +114,7 @@
|
|
|
85
114
|
nil # Value submitted when unchecked (not needed here)
|
|
86
115
|
%>
|
|
87
116
|
<%# Using the scope name directly for the label's `for` attribute requires matching the checkbox ID %>
|
|
88
|
-
<%= form.label "scopes_#{scope.parameterize}_edit",
|
|
117
|
+
<%= form.label "scopes_#{scope.parameterize}_edit", class: "form-check-label" do %><code><%= scope %></code><% end %>
|
|
89
118
|
</div>
|
|
90
119
|
<% end %>
|
|
91
120
|
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%# Partial for displaying key action buttons (edit, revoke) %>
|
|
2
|
+
<%# Locals: key (required) - The ApiKey record %>
|
|
3
|
+
|
|
4
|
+
<% if key.active? %>
|
|
5
|
+
<%= link_to api_keys.edit_key_path(key), title: "Edit Key", class: "api-keys-action-edit" do %>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16.793 2.793a3.121 3.121 0 1 1 4.414 4.414l-8.5 8.5A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l8.5-8.5Zm3 1.414a1.121 1.121 0 0 0-1.586 0L10 12.414V14h1.586l8.207-8.207a1.121 1.121 0 0 0 0-1.586ZM6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H6Z" clip-rule="evenodd"></path></svg>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<% if key.revocable? %>
|
|
10
|
+
<%= button_to api_keys.revoke_key_path(key), title: "Revoke Key", class: "api-keys-action-revoke", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
|
|
11
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.556 4a1 1 0 0 0-.97.751l-.292 1.14h5.421l-.293-1.14A1 1 0 0 0 13.453 4h-2.897Zm6.224 1.892-.421-1.639A3 3 0 0 0 13.453 2h-2.897A3 3 0 0 0 7.65 4.253l-.421 1.639H4a1 1 0 1 0 0 2h.1l1.215 11.425A3 3 0 0 0 8.3 22h7.4a3 3 0 0 0 2.984-2.683l1.214-11.425H20a1 1 0 1 0 0-2h-3.22Zm1.108 2H6.112l1.192 11.214A1 1 0 0 0 8.3 20h7.4a1 1 0 0 0 .995-.894l1.192-11.214ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% else %>
|
|
14
|
+
<span title="This key cannot be revoked" class="api-keys-action-disabled" style="color: var(--api-keys-muted-color); cursor: help;">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" clip-rule="evenodd"></path></svg>
|
|
16
|
+
</span>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% else %>
|
|
19
|
+
—
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# Partial for displaying key type and environment badges %>
|
|
2
|
+
<%# Locals: key (required) - The ApiKey record %>
|
|
3
|
+
|
|
4
|
+
<% if key.key_type.present? %>
|
|
5
|
+
<% type_config = key.key_type_config %>
|
|
6
|
+
<% is_publishable = type_config&.dig(:revocable) == false %>
|
|
7
|
+
<span class="api-keys-badge api-keys-badge-type <%= is_publishable ? 'api-keys-badge-publishable' : 'api-keys-badge-secret' %>" style="display: inline-block; padding: 2px 6px; font-size: 0.75em; border-radius: 3px; background-color: var(<%= is_publishable ? '--api-keys-badge-publishable-bg' : '--api-keys-badge-secret-bg' %>); color: var(<%= is_publishable ? '--api-keys-badge-publishable-color' : '--api-keys-badge-secret-color' %>); margin-left: 4px;">
|
|
8
|
+
<%= key.key_type.humanize %>
|
|
9
|
+
</span>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<% if key.environment.present? %>
|
|
13
|
+
<% is_live = key.environment == 'live' %>
|
|
14
|
+
<span class="api-keys-badge api-keys-badge-env <%= is_live ? 'api-keys-badge-live' : 'api-keys-badge-test' %>" style="display: inline-block; padding: 2px 6px; font-size: 0.75em; border-radius: 3px; background-color: var(<%= is_live ? '--api-keys-badge-live-bg' : '--api-keys-badge-test-bg' %>); color: var(<%= is_live ? '--api-keys-badge-live-color' : '--api-keys-badge-test-color' %>); margin-left: 4px;">
|
|
15
|
+
<%= key.environment.upcase %>
|
|
16
|
+
</span>
|
|
17
|
+
<% end %>
|
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<span style="color: red;">[Expired]</span>
|
|
10
|
-
<% end %>
|
|
11
|
-
<%= key.name.presence || "Secret key" %>
|
|
1
|
+
<%# Partial for displaying a single API key row in the table %>
|
|
2
|
+
<%# Locals: key (required) - The ApiKey record %>
|
|
3
|
+
|
|
4
|
+
<tr class="api-keys-row <%= 'api-keys-row-inactive' if defined?(inactive) && inactive %>">
|
|
5
|
+
<td class="api-keys-cell-name">
|
|
6
|
+
<%= render partial: 'api_keys/keys/key_status', locals: { key: key } %>
|
|
7
|
+
<%= key.name.presence || (key.key_type.present? ? "#{key.key_type.humanize} key" : "API key") %>
|
|
8
|
+
<%= render partial: 'api_keys/keys/key_badges', locals: { key: key } %>
|
|
12
9
|
</td>
|
|
13
|
-
<td><code><%= key.masked_token %></code></td>
|
|
14
10
|
|
|
11
|
+
<td class="api-keys-cell-token api-keys-token-cell">
|
|
12
|
+
<%= render partial: 'api_keys/keys/token_display', locals: { key: key } %>
|
|
13
|
+
</td>
|
|
15
14
|
|
|
16
|
-
<td title="<%= key.created_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
|
|
15
|
+
<td class="api-keys-cell-created" title="<%= key.created_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
|
|
17
16
|
<%= time_ago_in_words(key.created_at) %> ago
|
|
18
17
|
</td>
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
<td>
|
|
19
|
+
<td class="api-keys-cell-expires">
|
|
22
20
|
<% if key.expires_at? %>
|
|
23
21
|
<% if key.expired? %>
|
|
24
|
-
<strong style="color:
|
|
22
|
+
<strong style="color: var(--api-keys-status-expired-color);" title="<%= key.expires_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
|
|
25
23
|
Expired <%= time_ago_in_words(key.expires_at) %> ago
|
|
26
24
|
</strong>
|
|
27
25
|
<% else %>
|
|
@@ -34,39 +32,27 @@
|
|
|
34
32
|
<% end %>
|
|
35
33
|
</td>
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
<td>
|
|
35
|
+
<td class="api-keys-cell-last-used">
|
|
39
36
|
<% if key.last_used_at? %>
|
|
40
37
|
<span title="<%= key.last_used_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
|
|
41
38
|
<%= time_ago_in_words(key.last_used_at) %> ago
|
|
42
39
|
</span>
|
|
43
|
-
<%# TODO: Add relative time check (e.g., "within last 3 months") %>
|
|
44
40
|
<% else %>
|
|
45
41
|
<em>Never used</em>
|
|
46
42
|
<% end %>
|
|
47
43
|
</td>
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
<td>
|
|
45
|
+
<td class="api-keys-cell-scopes">
|
|
51
46
|
<% if key.scopes.present? %>
|
|
52
47
|
<% key.scopes.each do |scope| %>
|
|
53
|
-
<kbd class="tag is-small"><%= scope %></kbd>
|
|
48
|
+
<kbd class="api-keys-scope-tag tag is-small"><%= scope %></kbd>
|
|
54
49
|
<% end %>
|
|
55
50
|
<% else %>
|
|
56
51
|
—
|
|
57
52
|
<% end %>
|
|
58
53
|
</td>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16.793 2.793a3.121 3.121 0 1 1 4.414 4.414l-8.5 8.5A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l8.5-8.5Zm3 1.414a1.121 1.121 0 0 0-1.586 0L10 12.414V14h1.586l8.207-8.207a1.121 1.121 0 0 0 0-1.586ZM6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H6Z" clip-rule="evenodd"></path></svg>
|
|
63
|
-
<% end %>
|
|
64
|
-
<%= button_to api_keys.revoke_key_path(key), title: "Revoke Key", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
|
|
65
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.556 4a1 1 0 0 0-.97.751l-.292 1.14h5.421l-.293-1.14A1 1 0 0 0 13.453 4h-2.897Zm6.224 1.892-.421-1.639A3 3 0 0 0 13.453 2h-2.897A3 3 0 0 0 7.65 4.253l-.421 1.639H4a1 1 0 1 0 0 2h.1l1.215 11.425A3 3 0 0 0 8.3 22h7.4a3 3 0 0 0 2.984-2.683l1.214-11.425H20a1 1 0 1 0 0-2h-3.22Zm1.108 2H6.112l1.192 11.214A1 1 0 0 0 8.3 20h7.4a1 1 0 0 0 .995-.894l1.192-11.214ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
|
|
66
|
-
<% end %>
|
|
67
|
-
<% else %>
|
|
68
|
-
<%# No actions available for inactive/revoked/expired keys %>
|
|
69
|
-
—
|
|
70
|
-
<% end %>
|
|
54
|
+
|
|
55
|
+
<td class="api-keys-cell-actions api-keys-action-buttons">
|
|
56
|
+
<%= render partial: 'api_keys/keys/key_actions', locals: { key: key } %>
|
|
71
57
|
</td>
|
|
72
|
-
</tr>
|
|
58
|
+
</tr>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# Partial for displaying key status indicator %>
|
|
2
|
+
<%# Locals: key (required) - The ApiKey record %>
|
|
3
|
+
|
|
4
|
+
<% if key.active? %>
|
|
5
|
+
<span class="api-keys-status api-keys-status-active" style="color: var(--api-keys-status-active-color);"></span>
|
|
6
|
+
<% elsif key.revoked? %>
|
|
7
|
+
<span class="api-keys-status api-keys-status-revoked" style="color: var(--api-keys-status-revoked-color);">[Revoked]</span>
|
|
8
|
+
<% elsif key.expired? %>
|
|
9
|
+
<span class="api-keys-status api-keys-status-expired" style="color: var(--api-keys-status-expired-color);">[Expired]</span>
|
|
10
|
+
<% end %>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<thead>
|
|
13
13
|
<tr>
|
|
14
14
|
<th>Name</th>
|
|
15
|
-
<th>
|
|
15
|
+
<th>API Key</th>
|
|
16
16
|
<th>Created</th>
|
|
17
17
|
<th>Expires</th>
|
|
18
18
|
<th>Last Used</th>
|
|
@@ -38,12 +38,7 @@
|
|
|
38
38
|
</tbody>
|
|
39
39
|
</table>
|
|
40
40
|
<% else %>
|
|
41
|
-
|
|
42
|
-
<h4>You don't have any API keys yet!</h4>
|
|
43
|
-
<p>Create your first API key to get started.</p>
|
|
44
|
-
<%# Consider adding a primary "Create Key" button here %>
|
|
45
|
-
<%#= link_to "Create New API Key", new_key_path, class: "button primary" %>
|
|
46
|
-
</div>
|
|
41
|
+
<%= render partial: 'api_keys/keys/empty_state' %>
|
|
47
42
|
<% end %>
|
|
48
43
|
</div>
|
|
49
44
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<%# Partial for displaying publishable keys section %>
|
|
2
|
+
<%# Locals: active_keys (Active publishable keys), inactive_keys (Inactive publishable keys) %>
|
|
3
|
+
|
|
4
|
+
<section class="api-keys-section api-keys-publishable-section" aria-labelledby="publishable-keys-heading">
|
|
5
|
+
<h2 id="publishable-keys-heading">Publishable Keys</h2>
|
|
6
|
+
<p class="api-keys-section-description">
|
|
7
|
+
These keys are safe to embed in client-side applications and browser code.
|
|
8
|
+
You can view them anytime.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<div class="api-keys-table-wrapper">
|
|
12
|
+
<% all_keys = active_keys + inactive_keys %>
|
|
13
|
+
<% if all_keys.any? %>
|
|
14
|
+
<table>
|
|
15
|
+
<thead>
|
|
16
|
+
<tr>
|
|
17
|
+
<th>Name</th>
|
|
18
|
+
<th>API Key</th>
|
|
19
|
+
<th>Created</th>
|
|
20
|
+
<th>Expires</th>
|
|
21
|
+
<th>Last Used</th>
|
|
22
|
+
<th>Permissions</th>
|
|
23
|
+
<th>Actions</th>
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody>
|
|
27
|
+
<% active_keys.each do |key| %>
|
|
28
|
+
<%= render partial: 'api_keys/keys/key_row', locals: { key: key } %>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<% inactive_keys.each do |key| %>
|
|
32
|
+
<%= render partial: 'api_keys/keys/key_row', locals: { key: key, inactive: true } %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
<% else %>
|
|
37
|
+
<%= render partial: 'api_keys/keys/empty_state', locals: { message: "No publishable keys yet." } %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<%# Partial for displaying secret keys section %>
|
|
2
|
+
<%# Locals: active_keys (Active secret keys), inactive_keys (Inactive secret keys) %>
|
|
3
|
+
|
|
4
|
+
<section class="api-keys-section api-keys-secret-section" aria-labelledby="secret-keys-heading">
|
|
5
|
+
<h2 id="secret-keys-heading">Secret Keys</h2>
|
|
6
|
+
<p class="api-keys-section-description">
|
|
7
|
+
Keep these private. Never expose in client-side code or share publicly.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<div class="api-keys-table-wrapper">
|
|
11
|
+
<% all_keys = active_keys + inactive_keys %>
|
|
12
|
+
<% if all_keys.any? %>
|
|
13
|
+
<table>
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th>Name</th>
|
|
17
|
+
<th>API Key</th>
|
|
18
|
+
<th>Created</th>
|
|
19
|
+
<th>Expires</th>
|
|
20
|
+
<th>Last Used</th>
|
|
21
|
+
<th>Permissions</th>
|
|
22
|
+
<th>Actions</th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% active_keys.each do |key| %>
|
|
27
|
+
<%= render partial: 'api_keys/keys/key_row', locals: { key: key } %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<% inactive_keys.each do |key| %>
|
|
31
|
+
<%= render partial: 'api_keys/keys/key_row', locals: { key: key, inactive: true } %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
<% else %>
|
|
36
|
+
<%= render partial: 'api_keys/keys/empty_state', locals: { message: "No secret keys yet." } %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
<h2>Save your key</h2>
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
<% if api_key.public_key_type? %>
|
|
7
|
+
<p>Here's your <%= api_key.key_type.humanize.downcase %> key. This key is designed to be embedded in client-side applications. You can view it again anytime from your dashboard.</p>
|
|
8
|
+
<% else %>
|
|
9
|
+
<p>Please save your API key in a safe place since <strong>you won't be able to view it again</strong>. Keep it secure, as anyone with your API key can make requests on your behalf. If you lose it, you'll need to generate a new one.</p>
|
|
10
|
+
<% end %>
|
|
7
11
|
|
|
8
12
|
<p>
|
|
9
13
|
<%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Partial for displaying an API key token with optional show/copy functionality %>
|
|
2
|
+
<%# Locals: key (required) - The ApiKey record %>
|
|
3
|
+
|
|
4
|
+
<% if key.public_key_type? && key.viewable_token.present? %>
|
|
5
|
+
<span class="token-masked"><code><%= key.masked_token %></code></span>
|
|
6
|
+
<span class="token-full" style="display: none;"><code style="word-break: break-all;"><%= key.viewable_token %></code></span>
|
|
7
|
+
<button type="button" class="btn-show-token" style="margin-left: 8px; font-size: 0.75em; padding: 2px 6px; cursor: pointer;" title="Show full token">Show</button>
|
|
8
|
+
<button type="button" class="btn-copy-token" style="display: none; margin-left: 4px; font-size: 0.75em; padding: 2px 6px; cursor: pointer;" title="Copy to clipboard" data-token="<%= key.viewable_token %>">Copy</button>
|
|
9
|
+
<% else %>
|
|
10
|
+
<code><%= key.masked_token %></code>
|
|
11
|
+
<% end %>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div class="col api-keys-align-center is-right">
|
|
8
8
|
<%= link_to new_key_path, class: "button primary api-keys-align-center", role: "button" do %>
|
|
9
9
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 5a1 1 0 0 1 1 1v5h5a1 1 0 1 1 0 2h-5v5a1 1 0 1 1-2 0v-5H6a1 1 0 1 1 0-2h5V6a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
|
|
10
|
-
<span> Create new
|
|
10
|
+
<span> Create new API key</span>
|
|
11
11
|
<% end %>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -15,12 +15,44 @@
|
|
|
15
15
|
|
|
16
16
|
<div>
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
<% if key_types_feature_enabled? %>
|
|
19
|
+
<% has_publishable_keys = @publishable_keys.any? || @inactive_publishable_keys.any? %>
|
|
20
|
+
|
|
21
|
+
<p class="api-keys-info-text">
|
|
22
|
+
<% if has_publishable_keys %>
|
|
23
|
+
<strong>Secret keys</strong> should never be shared or exposed publicly.
|
|
24
|
+
<strong>Publishable keys</strong> can be safely embedded in client applications.
|
|
25
|
+
<% else %>
|
|
26
|
+
Do not share your API key with others or expose it in the browser or other client-side code.
|
|
27
|
+
<% end %>
|
|
28
|
+
<%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
|
|
29
|
+
Learn more
|
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
|
|
31
|
+
<% end %>
|
|
32
|
+
</p>
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
<%# Render secret keys section first (primary use case) %>
|
|
35
|
+
<%= render partial: 'secret_keys', locals: {
|
|
36
|
+
active_keys: @secret_keys,
|
|
37
|
+
inactive_keys: @inactive_secret_keys
|
|
38
|
+
} %>
|
|
39
|
+
|
|
40
|
+
<%# Only render publishable keys section if there are any %>
|
|
41
|
+
<% if has_publishable_keys %>
|
|
42
|
+
<%= render partial: 'publishable_keys', locals: {
|
|
43
|
+
active_keys: @publishable_keys,
|
|
44
|
+
inactive_keys: @inactive_publishable_keys
|
|
45
|
+
} %>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<% else %>
|
|
49
|
+
<p>Do not share your API key with others or expose it in the browser or other client-side code. <%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
|
|
50
|
+
Learn more
|
|
51
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
54
|
+
<%# Render the reusable table partial (legacy mode - single table) %>
|
|
55
|
+
<%= render partial: 'keys_table', locals: { active_keys: @api_keys, inactive_keys: @inactive_api_keys } %>
|
|
56
|
+
<% end %>
|
|
25
57
|
|
|
26
|
-
</div>
|
|
58
|
+
</div>
|