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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +36 -0
  3. data/AGENTS.md +5 -0
  4. data/Appraisals +17 -0
  5. data/CHANGELOG.md +9 -0
  6. data/CLAUDE.md +5 -0
  7. data/README.md +767 -3
  8. data/Rakefile +9 -4
  9. data/app/controllers/api_keys/keys_controller.rb +43 -11
  10. data/app/controllers/api_keys/security_controller.rb +8 -0
  11. data/app/views/api_keys/keys/_empty_state.html.erb +9 -0
  12. data/app/views/api_keys/keys/_form.html.erb +31 -2
  13. data/app/views/api_keys/keys/_key_actions.html.erb +20 -0
  14. data/app/views/api_keys/keys/_key_badges.html.erb +17 -0
  15. data/app/views/api_keys/keys/_key_row.html.erb +21 -35
  16. data/app/views/api_keys/keys/_key_status.html.erb +10 -0
  17. data/app/views/api_keys/keys/_keys_table.html.erb +2 -7
  18. data/app/views/api_keys/keys/_publishable_keys.html.erb +40 -0
  19. data/app/views/api_keys/keys/_secret_keys.html.erb +39 -0
  20. data/app/views/api_keys/keys/_show_token.html.erb +5 -1
  21. data/app/views/api_keys/keys/_token_display.html.erb +11 -0
  22. data/app/views/api_keys/keys/index.html.erb +40 -8
  23. data/app/views/api_keys/security/best_practices.html.erb +73 -47
  24. data/app/views/layouts/api_keys/application.html.erb +113 -7
  25. data/context7.json +4 -0
  26. data/gemfiles/rails_7.2.gemfile +21 -0
  27. data/gemfiles/rails_8.0.gemfile +21 -0
  28. data/gemfiles/rails_8.1.gemfile +21 -0
  29. data/lib/api_keys/configuration.rb +77 -0
  30. data/lib/api_keys/errors.rb +73 -0
  31. data/lib/api_keys/form_builder_extensions.rb +158 -0
  32. data/lib/api_keys/helpers/expiration_options.rb +131 -0
  33. data/lib/api_keys/helpers/token_session.rb +68 -0
  34. data/lib/api_keys/helpers/view_helpers.rb +216 -0
  35. data/lib/api_keys/models/api_key.rb +229 -17
  36. data/lib/api_keys/models/concerns/has_api_keys.rb +183 -3
  37. data/lib/api_keys/services/authenticator.rb +45 -2
  38. data/lib/api_keys/services/digestor.rb +6 -2
  39. data/lib/api_keys/tenant_resolution.rb +3 -1
  40. data/lib/api_keys/version.rb +1 -1
  41. data/lib/api_keys.rb +12 -0
  42. data/lib/generators/api_keys/add_key_types_generator.rb +68 -0
  43. data/lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb +18 -0
  44. data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +9 -0
  45. data/lib/generators/api_keys/templates/initializer.rb +242 -120
  46. 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
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20
- load "rails/tasks/engine.rake"
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
- load "rails/tasks/statistics.rake"
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
- # Fetch only active keys for the main list, maybe sorted by creation date
11
- @api_keys = current_api_keys_owner.api_keys.active.order(created_at: :desc)
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 = current_api_keys_owner.api_keys.inactive.order(created_at: :desc)
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
- if @api_key.revoke!
85
- redirect_to keys_path, notice: "API key revoked successfully."
86
- else
87
- # This shouldn't typically fail unless there's a callback issue
88
- redirect_to keys_path, alert: "Failed to revoke API key."
89
- end
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}", scope.humanize, class: "form-check-label" %>
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", scope.humanize, class: "form-check-label" %>
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
+ &mdash;
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
- <tr>
2
- <td>
3
- <%# Status indicator (no text originally, keeping it that way unless specified otherwise) %>
4
- <% if key.active? %>
5
- <span style="color: green;"></span>
6
- <% elsif key.revoked? %>
7
- <span style="color: orange;">[Revoked]</span>
8
- <% elsif key.expired? %>
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: red;" title="<%= key.expires_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
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
  &mdash;
57
52
  <% end %>
58
53
  </td>
59
- <td class="api-keys-action-buttons">
60
- <% if key.active? %>
61
- <%= link_to api_keys.edit_key_path(key), title: "Edit Key" do %>
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
- &mdash;
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>Secret Key</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
- <div style="text-align: center; padding: 2em;">
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
- <p>Please save your secret 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 do lose it, you'll need to generate a new one.</p>
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>&nbsp;Create new secret key</span>
10
+ <span>&nbsp;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
- <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 %>
19
- Learn more&nbsp;
20
- <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>
21
- <% end %>
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&nbsp;
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
- <%# Render the reusable table partial %>
24
- <%= render partial: 'keys_table', locals: { active_keys: @api_keys, inactive_keys: @inactive_api_keys } %>
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&nbsp;
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>