+<% end %>
+```
+
+#### New/Edit Form
+
+```erb
+<%= form_with(model: @api_key, url: settings_api_keys_path) do |form| %>
+ <%# Name %>
+ <%= form.text_field :name, placeholder: "e.g., Production Server" %>
+
+ <%# Expiration (new keys only) %>
+ <%= form.api_key_expiration_select(class: "form-select") %>
+
+ <%# Scopes %>
+ <%= form.api_key_scopes_checkboxes(@available_scopes) do |scope, checked| %>
+
+ <% end %>
+
+ <%= form.submit %>
+<% end %>
+```
+
+#### Success Page (Show Token Once)
+
+```erb
+<% if @token.present? %>
+
+
+ <%= api_key_environment_label_from_token(@token) %>
+
+
This key will only be shown once. Copy it now!
+<% else %>
+
Token already shown. Create a new key if needed.
+ <%= link_to "Create New Key", new_settings_api_key_path %>
+<% end %>
+```
+
+---
+
+### Best Practices
+
+Based on real production integrations:
+
+1. **Use RESTful routes** - `resources :api_keys` with member/collection actions, not custom route definitions.
+
+2. **Separate form-only params** - Access `expires_at_preset` via `params.dig(:api_key, :expires_at_preset)` rather than including it in strong params (it's not a model attribute).
+
+3. **Use `before_action` for shared setup** - Extract `@available_scopes` to a before_action rather than setting it in multiple actions.
+
+4. **Let validations handle errors** - Rescue `ActiveRecord::RecordInvalid` and re-render the form rather than pre-checking everything.
+
+5. **Use the gem's helpers consistently** - Use `api_key_status_label(key)` everywhere rather than hardcoding "Active" in some places.
+
+6. **Check limits before showing UI** - Use `can_create_api_key?(key_type:)` to conditionally show/hide create buttons.
+
+7. **Keep controllers thin** - The gem handles token generation, hashing, scope filtering, and validation. Your controller just orchestrates.
+
+See the "How it works" section below for additional model methods.
+
## How it works
diff --git a/app/controllers/api_keys/keys_controller.rb b/app/controllers/api_keys/keys_controller.rb
index 781856d..66787ec 100644
--- a/app/controllers/api_keys/keys_controller.rb
+++ b/app/controllers/api_keys/keys_controller.rb
@@ -4,6 +4,7 @@ module ApiKeys
# Controller for managing API keys belonging to the current owner.
class KeysController < ApplicationController
before_action :set_api_key, only: [:show, :edit, :update, :revoke]
+ helper_method :key_types_feature_enabled?
# GET /keys
def index
@@ -20,6 +21,14 @@ def index
@api_keys = base_scope.active.order(created_at: :desc)
# Optionally, fetch inactive ones for a separate section or filter
@inactive_api_keys = base_scope.inactive.order(created_at: :desc)
+
+ # When key_types feature is enabled, separate publishable and secret keys
+ if key_types_feature_enabled?
+ @publishable_keys = @api_keys.select(&:public_key_type?)
+ @secret_keys = @api_keys.reject(&:public_key_type?)
+ @inactive_publishable_keys = @inactive_api_keys.select(&:public_key_type?)
+ @inactive_secret_keys = @inactive_api_keys.reject(&:public_key_type?)
+ end
end
# GET /keys/:id
diff --git a/app/controllers/api_keys/security_controller.rb b/app/controllers/api_keys/security_controller.rb
index e1c38f7..9601fa2 100644
--- a/app/controllers/api_keys/security_controller.rb
+++ b/app/controllers/api_keys/security_controller.rb
@@ -6,11 +6,19 @@ class SecurityController < ApplicationController
# Skip the user authentication requirement for these static pages
# as they contain general information.
skip_before_action :authenticate_api_keys_owner!, only: [:best_practices]
+ helper_method :key_types_feature_enabled?
# GET /security/best-practices
def best_practices
# Renders app/views/api_keys/security/best_practices.html.erb
# The view will contain the static content.
end
+
+ private
+
+ # Check if key types feature is enabled
+ def key_types_feature_enabled?
+ ApiKeys.configuration.key_types.present? && ApiKeys.configuration.key_types.any?
+ end
end
end
diff --git a/app/views/api_keys/keys/_empty_state.html.erb b/app/views/api_keys/keys/_empty_state.html.erb
new file mode 100644
index 0000000..6f51828
--- /dev/null
+++ b/app/views/api_keys/keys/_empty_state.html.erb
@@ -0,0 +1,9 @@
+<%# Partial for displaying empty state when no keys exist %>
+<%# Locals: message (optional) - Custom message to display %>
+
+<% message ||= "You don't have any API keys yet!" %>
+
+
+
<%= message %>
+
Create your first API key to get started.
+
diff --git a/app/views/api_keys/keys/_key_actions.html.erb b/app/views/api_keys/keys/_key_actions.html.erb
new file mode 100644
index 0000000..d397b66
--- /dev/null
+++ b/app/views/api_keys/keys/_key_actions.html.erb
@@ -0,0 +1,20 @@
+<%# Partial for displaying key action buttons (edit, revoke) %>
+<%# Locals: key (required) - The ApiKey record %>
+
+<% if key.active? %>
+ <%= link_to api_keys.edit_key_path(key), title: "Edit Key", class: "api-keys-action-edit" do %>
+
+ <% end %>
+
+ <% if key.revocable? %>
+ <%= 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 %>
+
+ <% end %>
+ <% else %>
+
+
+
+ <% end %>
+<% else %>
+ —
+<% end %>
diff --git a/app/views/api_keys/keys/_key_badges.html.erb b/app/views/api_keys/keys/_key_badges.html.erb
new file mode 100644
index 0000000..b1dc24c
--- /dev/null
+++ b/app/views/api_keys/keys/_key_badges.html.erb
@@ -0,0 +1,17 @@
+<%# Partial for displaying key type and environment badges %>
+<%# Locals: key (required) - The ApiKey record %>
+
+<% if key.key_type.present? %>
+ <% type_config = key.key_type_config %>
+ <% is_publishable = type_config&.dig(:revocable) == false %>
+
+ <%= key.key_type.humanize %>
+
+<% end %>
+
+<% if key.environment.present? %>
+ <% is_live = key.environment == 'live' %>
+
+ <%= key.environment.upcase %>
+
+<% end %>
diff --git a/app/views/api_keys/keys/_key_row.html.erb b/app/views/api_keys/keys/_key_row.html.erb
index 60d518e..a83ac07 100644
--- a/app/views/api_keys/keys/_key_row.html.erb
+++ b/app/views/api_keys/keys/_key_row.html.erb
@@ -1,48 +1,25 @@
-
-
- <%# Status indicator (no text originally, keeping it that way unless specified otherwise) %>
- <% if key.active? %>
-
- <% elsif key.revoked? %>
- [Revoked]
- <% elsif key.expired? %>
- [Expired]
- <% end %>
+<%# Partial for displaying a single API key row in the table %>
+<%# Locals: key (required) - The ApiKey record %>
+
+
<% if key.expires_at? %>
<% if key.expired? %>
-
+
Expired <%= time_ago_in_words(key.expires_at) %> ago
<% else %>
@@ -55,46 +32,27 @@
<% end %>
-
-
+
<% if key.last_used_at? %>
<%= time_ago_in_words(key.last_used_at) %> ago
- <%# TODO: Add relative time check (e.g., "within last 3 months") %>
<% else %>
Never used
<% end %>
-
-
+
<% if key.scopes.present? %>
<% key.scopes.each do |scope| %>
- <%= scope %>
+ <%= scope %>
<% end %>
<% else %>
—
<% end %>
-
- <% if key.active? %>
- <%= link_to api_keys.edit_key_path(key), title: "Edit Key" do %>
-
- <% end %>
- <%# Only show revoke button for revocable keys %>
- <% if key.revocable? %>
- <%= 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 %>
-
- <% end %>
- <% else %>
-
-
-
- <% end %>
- <% else %>
- <%# No actions available for inactive/revoked/expired keys %>
- —
- <% end %>
+
+
+
diff --git a/app/views/api_keys/keys/_token_display.html.erb b/app/views/api_keys/keys/_token_display.html.erb
new file mode 100644
index 0000000..847d3f7
--- /dev/null
+++ b/app/views/api_keys/keys/_token_display.html.erb
@@ -0,0 +1,11 @@
+<%# Partial for displaying an API key token with optional show/copy functionality %>
+<%# Locals: key (required) - The ApiKey record %>
+
+<% if key.public_key_type? && key.viewable_token.present? %>
+ <%= key.masked_token %>
+ <%= key.viewable_token %>
+
+
+<% else %>
+ <%= key.masked_token %>
+<% end %>
diff --git a/app/views/api_keys/keys/index.html.erb b/app/views/api_keys/keys/index.html.erb
index b976ea4..ef04313 100644
--- a/app/views/api_keys/keys/index.html.erb
+++ b/app/views/api_keys/keys/index.html.erb
@@ -15,12 +15,44 @@
-
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 %>
- Learn more
-
- <% end %>
+ <% if key_types_feature_enabled? %>
+ <% has_publishable_keys = @publishable_keys.any? || @inactive_publishable_keys.any? %>
+
+
+ <% if has_publishable_keys %>
+ Secret keys should never be shared or exposed publicly.
+ Publishable keys can be safely embedded in client applications.
+ <% else %>
+ Do not share your API key with others or expose it in the browser or other client-side code.
+ <% end %>
+ <%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
+ Learn more
+
+ <% end %>
+
- <%# Render the reusable table partial %>
- <%= render partial: 'keys_table', locals: { active_keys: @api_keys, inactive_keys: @inactive_api_keys } %>
+ <%# Render secret keys section first (primary use case) %>
+ <%= render partial: 'secret_keys', locals: {
+ active_keys: @secret_keys,
+ inactive_keys: @inactive_secret_keys
+ } %>
+
+ <%# Only render publishable keys section if there are any %>
+ <% if has_publishable_keys %>
+ <%= render partial: 'publishable_keys', locals: {
+ active_keys: @publishable_keys,
+ inactive_keys: @inactive_publishable_keys
+ } %>
+ <% end %>
+
+ <% else %>
+
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 %>
+ Learn more
+
+ <% end %>
+
+ <%# Render the reusable table partial (legacy mode - single table) %>
+ <%= render partial: 'keys_table', locals: { active_keys: @api_keys, inactive_keys: @inactive_api_keys } %>
+ <% end %>
-
\ No newline at end of file
+
diff --git a/app/views/api_keys/security/best_practices.html.erb b/app/views/api_keys/security/best_practices.html.erb
index 280648e..48e608f 100644
--- a/app/views/api_keys/security/best_practices.html.erb
+++ b/app/views/api_keys/security/best_practices.html.erb
@@ -1,70 +1,96 @@
-
API Key Security Best Practices
-
Protecting your API keys is crucial for maintaining the security and integrity of your account and data.
+
API Key Security
+
Protecting your API keys is crucial for maintaining the security of your account and data.
-
+
-
-
1. Treat API Keys Like Passwords
-
Your API keys grant access to your account and potentially sensitive operations. Handle them with the same level of security you would apply to your account password or other critical credentials.
-
+ <% if key_types_feature_enabled? %>
+
+
Understanding Key Types
-
-
2. Use Unique Keys for Different Applications & Environments
-
Generate distinct API keys for different applications, services, or integrations that need access. If a key for one application is compromised, you can revoke it without disrupting others. Use separate keys for development, staging, and production environments.
-
Tip: Use the "Name" field when creating keys to easily identify their purpose (e.g., "Production Zapier Integration", "Staging iOS App").
-
+
Secret Keys
+
Secret keys provide full access to your account and should be treated like passwords.
+
+
Never expose in client-side code (browsers, mobile apps, desktop apps)
+
Never commit to version control (Git, etc.)
+
Store securely using environment variables or secrets management
+
Can be revoked immediately if compromised
+
-
-
3. Never Expose Keys in Client-Side Code
-
Never embed API keys directly in mobile apps (iOS, Android), browser-side JavaScript, desktop applications, or any code that resides on a user's device. Exposed keys can be easily extracted by malicious actors.
-
Solution: Route API requests through your own backend server. Your server can securely store and use the API key to communicate with the target API on behalf of the client.
-
+
Publishable Keys
+
Publishable keys are designed for client-side use with limited, safe permissions.
+
+
Safe to embed in browser JavaScript, mobile apps, and public code
Always visible — you can view the full key anytime in your dashboard
+
Cannot be revoked — designed to be long-lived identifiers
+
+
+ <% end %>
-
4. Never Commit Keys to Version Control (e.g., Git)
-
Committing keys to your source code repository (like Git, Mercurial, etc.) is a common and dangerous mistake. Even in private repositories, accidental pushes or repository breaches can leak your keys.
-
Solution: Store keys in environment variables or use a dedicated secrets management system. Access the key in your code via these secure methods.
-
+
Essential Practices
-
-
5. Securely Store Keys on Your Backend
-
-
Environment Variables: The simplest secure method for many applications. Set an environment variable (e.g., `YOUR_SERVICE_API_KEY`) on your server and access it in your code (e.g., `ENV['YOUR_SERVICE_API_KEY']` in Ruby/Rails).
-
Secrets Management Services: For more robust needs, especially in production or team environments, use dedicated services like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Doppler, etc. These provide encrypted storage, access control, auditing, and often automated rotation capabilities.
-
Encrypted Configuration Files: If using configuration files, ensure they are encrypted (e.g., Rails encrypted credentials `config/credentials.yml.enc` and `Rails.application.credentials`). <%= link_to "More info here", "https://guides.rubyonrails.org/security.html#custom-credentials", target: "_blank", rel: "noopener noreferrer", class: "text-primary" %>.
-
+
Treat Secret Keys Like Passwords
+
Your API keys grant access to your account. Handle them with the same care you would apply to your account password.
+
+
Use Separate Keys for Different Purposes
+
Create distinct keys for different applications and environments. If one key is compromised, you can revoke it without disrupting others.
+
Tip: Use descriptive names like "Production Backend" or "Staging iOS App" to easily identify each key's purpose.
+
+ <% unless key_types_feature_enabled? %>
+
Never Expose Keys in Client-Side Code
+
Never embed API keys in mobile apps, browser JavaScript, or desktop applications. Exposed keys can be easily extracted by malicious actors.
+
Solution: Route API requests through your own backend server, which can securely store and use the API key.
+ <% end %>
-
6. Implement the Principle of Least Privilege (Scopes)
-
If the API service supports it (and this `api_keys` gem allows for scopes), create keys with only the minimum permissions (scopes) required for their specific task. Avoid using a key with full access if only read access is needed.
-
Note: Scope availability and enforcement depend on how the host application integrates and utilizes the `scopes` attribute provided by this gem.
+
Secure Storage
+
+
Environment Variables
+
The simplest secure method. Set an environment variable on your server:
+
# In your shell or deployment config
+export YOUR_SERVICE_API_KEY="sk_..."
+
+# Access in Ruby
+ENV['YOUR_SERVICE_API_KEY']
+
+
Rails Encrypted Credentials
+
For Rails applications, use encrypted credentials:
For production environments, consider dedicated services like HashiCorp Vault, AWS Secrets Manager, or Google Secret Manager.
-
7. Monitor Usage and Rotate Keys Regularly
-
-
Monitor Usage: Regularly check API usage logs or dashboards (if provided by the service or your monitoring tools). Look for unexpected spikes in activity or requests from unusual locations, which could indicate a compromised key.
-
Rotate Keys: Periodically generate new keys and revoke old ones (key rotation). This limits the window of opportunity for attackers if a key is ever leaked undetected. How often you rotate depends on your security requirements (e.g., every 90 days, annually).
- Tip: This dashboard allows creating multiple keys, facilitating rotation. Create a new key, update your application(s), verify they work, and then revoke the old key.
-
Revoke Immediately if Compromised: If you suspect a key has been leaked or compromised, revoke it immediately using the "Revoke" button on your keys dashboard.
-
+
Monitoring & Rotation
+
+
Monitor Usage
+
Regularly check for unexpected activity spikes or requests from unusual locations, which could indicate a compromised key.
+
+
Rotate Keys Periodically
+
Generate new keys and revoke old ones on a regular schedule (e.g., every 90 days). This limits exposure if a key is leaked undetected.
+
Tip: Create a new key first, update your application, verify it works, then revoke the old key.
+
+
Revoke Immediately if Compromised
+
If you suspect a key has been leaked, revoke it immediately from your dashboard.
-
8. Use HTTPS Exclusively
-
Ensure all API requests are made over HTTPS to encrypt the connection and prevent eavesdropping. Transmitting keys over unencrypted HTTP is highly insecure.
+
Always Use HTTPS
+
Ensure all API requests are made over HTTPS. Transmitting keys over unencrypted HTTP exposes them to eavesdropping.
-
By following these best practices, you significantly reduce the risk associated with API key management.
-
- <%# Link back to the keys index if appropriate %>
- <% if defined?(api_keys.keys_path) %>
-
<%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %>
- <% end %>
+
<%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %>
-
\ No newline at end of file
+
diff --git a/app/views/layouts/api_keys/application.html.erb b/app/views/layouts/api_keys/application.html.erb
index 7369ff3..b4b5cef 100644
--- a/app/views/layouts/api_keys/application.html.erb
+++ b/app/views/layouts/api_keys/application.html.erb
@@ -12,15 +12,67 @@