diff --git a/README.md b/README.md index faa8ce4..efacbbc 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,522 @@ To make the experience between your app and the `api_keys` dashboard more seamle You can check out the dashboard on the [live demo website](https://apikeys.rameerez.com). +### Customizing the Dashboard + +The gem provides two levels of customization for the mounted dashboard: + +#### Level 1: Use the stock dashboard (default) +Works out of the box with good defaults. No configuration needed. + +#### Level 2: Override CSS variables +Tweak colors and spacing by overriding CSS variables in your application's stylesheet: + +```css +:root { + --api-keys-primary-color: #your-brand-color; + --api-keys-danger-color: #dc3545; + --api-keys-success-color: #28a745; + --api-keys-badge-secret-bg: #e7f1ff; + --api-keys-badge-publishable-bg: #fef3cd; + /* See layout file for all available variables */ +} +``` + +#### Building Custom Integrations + +If you need complete control over the UI (e.g., to match your design system with Tailwind, Bootstrap, etc.), you can build your own views and controllers while using the gem's model layer and helpers. + +The gem provides a comprehensive set of helpers specifically designed for custom integrations. These patterns are battle-tested from real production integrations. + +##### What You'll Need + +A complete custom integration typically requires: + +| Component | Purpose | +|-----------|---------| +| Initializer | Configure the gem + opt into form helpers | +| Routes | RESTful resources (~6 lines) | +| Controller | Handle CRUD operations (~90 lines) | +| Views | index, new, edit, success pages | +| Helper include | One line in ApplicationHelper | + +##### Quick Setup + +**1. Initializer** (`config/initializers/api_keys.rb`): + +```ruby +# Include form builder extensions for cleaner forms +Rails.application.config.to_prepare do + ActionView::Helpers::FormBuilder.include(ApiKeys::FormBuilderExtensions) +end + +ApiKeys.configure do |config| + config.current_owner_method = :current_organization + config.authenticate_owner_method = :authenticate_organization! + # ... other config +end +``` + +**2. Routes** (`config/routes.rb`): + +```ruby +namespace :settings do + resources :api_keys, only: [:index, :new, :create, :edit, :update] do + post :revoke, on: :member + get :success, on: :collection + post :create_publishable, on: :collection # If using key types + end +end +``` + +**3. Helper** (`app/helpers/application_helper.rb`): + +```ruby +module ApplicationHelper + include ApiKeys::ViewHelpers +end +``` + +**4. Controller** - See the complete example below. + +--- + +### Complete Controller Example + +Here's a production-ready controller (~90 lines) that handles all API key operations: + +```ruby +# app/controllers/settings/api_keys_controller.rb +module Settings + class ApiKeysController < ApplicationController + before_action :set_api_key, only: [:edit, :update, :revoke] + before_action :set_available_scopes, only: [:new, :create, :edit, :update] + + def index + @publishable_key = current_organization.api_keys.publishable.active.first + @secret_keys = current_organization.api_keys.secret.active.order(created_at: :desc) + @inactive_keys = current_organization.api_keys.secret.inactive.order(created_at: :desc) + end + + def new + @api_key = current_organization.api_keys.build(key_type: :secret) + end + + def create + @api_key = current_organization.create_api_key!( + name: api_key_params[:name], + key_type: :secret, + scopes: api_key_params[:scopes], + expires_at_preset: params.dig(:api_key, :expires_at_preset) + ) + + ApiKeys::TokenSession.store(session, @api_key) + redirect_to success_settings_api_keys_path + rescue ActiveRecord::RecordInvalid => e + @api_key = e.record + flash.now[:alert] = "Failed to create API key." + render :new, status: :unprocessable_entity + end + + def success + @token = ApiKeys::TokenSession.retrieve_once(session) + redirect_to settings_api_keys_path, alert: "Token can only be shown once." and return if @token.blank? + end + + def edit + end + + def update + if @api_key.update(api_key_params) + redirect_to settings_api_keys_path, notice: "API key updated." + else + flash.now[:alert] = "Failed to update API key." + render :edit, status: :unprocessable_entity + end + end + + def create_publishable + unless current_organization.can_create_api_key?(key_type: :publishable) + redirect_to settings_api_keys_path, alert: "You already have a publishable key." + return + end + + current_organization.create_api_key!(name: "SDK Key", key_type: :publishable) + redirect_to settings_api_keys_path, notice: "Publishable key created!" + rescue ActiveRecord::RecordInvalid => e + redirect_to settings_api_keys_path, alert: "Failed to create key." + end + + def revoke + if @api_key.revocable? + @api_key.revoke! + redirect_to settings_api_keys_path, notice: "API key revoked." + else + redirect_to settings_api_keys_path, alert: "This key cannot be revoked." + end + end + + private + + def set_api_key + @api_key = current_organization.api_keys.find(params[:id]) + end + + def set_available_scopes + @available_scopes = current_organization.available_api_key_scopes + end + + def api_key_params + params.require(:api_key).permit(:name, scopes: []) + end + end +end +``` + +--- + +### Model Scopes + +Filter keys by type and status: + +```ruby +# By key type (when using key_types feature) +@org.api_keys.publishable # Only publishable keys +@org.api_keys.secret # Secret keys (and legacy keys without type) + +# By status +@org.api_keys.active # Not revoked and not expired +@org.api_keys.inactive # Revoked or expired +@org.api_keys.expired # Past expiration date +@org.api_keys.revoked # Manually revoked + +# Chain them +@org.api_keys.publishable.active +@org.api_keys.secret.inactive.order(created_at: :desc) +``` + +--- + +### Owner Instance Methods + +Methods available on any model with `has_api_keys`: + +```ruby +# Get available scopes for forms +@available_scopes = current_org.available_api_key_scopes +# Returns owner-specific scopes, or falls back to global config + +# Check if owner can create a key (respects limits) +current_org.can_create_api_key?(key_type: :publishable) +# => false if limit reached + +# Create a key with all options +@api_key = current_org.create_api_key!( + name: "My Key", + key_type: :secret, # or :publishable + scopes: ["read", "write"], # Blank values auto-removed + expires_at: 30.days.from_now, # Explicit date + expires_at_preset: "30_days", # OR use preset (takes precedence) + environment: :live, # Defaults to current_environment + metadata: { team: "backend" } # Optional JSON metadata +) +``` + +--- + +### API Key Instance Methods + +Methods available on `ApiKeys::ApiKey` instances: + +```ruby +# Token (only available immediately after creation) +@api_key.token # => "sk_live_abc123..." (plaintext, once only) + +# Display +@api_key.masked_token # => "sk_live_••••abc1" (safe for UI) +@api_key.viewable_token # => full token if public key type, nil otherwise + +# Status checks +@api_key.active? # => true if not revoked and not expired +@api_key.expired? # => true if past expires_at +@api_key.revoked? # => true if manually revoked +@api_key.revocable? # => false for non-revocable key types + +# Type checks (when using key_types) +@api_key.public_key_type? # => true if token can be viewed again +@api_key.key_type # => "publishable", "secret", or nil +@api_key.environment # => "test", "live", or nil + +# Actions +@api_key.revoke! # Revoke the key (raises if not revocable) + +# Scopes +@api_key.scopes # => ["read", "write"] +@api_key.allows_scope?("read") # => true + +# Metadata +@api_key.name # => "Production Server" +@api_key.created_at +@api_key.expires_at +@api_key.last_used_at +@api_key.requests_count # If tracking enabled +``` + +--- + +### Token Session Helper + +Manages the "show token once" pattern for secret keys: + +```ruby +# Store token after creation +ApiKeys::TokenSession.store(session, @api_key) + +# Retrieve and clear (returns nil on subsequent calls) +@token = ApiKeys::TokenSession.retrieve_once(session) + +# With custom session key (if managing multiple token types) +ApiKeys::TokenSession.store(session, @api_key, key: :my_custom_key) +@token = ApiKeys::TokenSession.retrieve_once(session, key: :my_custom_key) +``` + +--- + +### Expiration Options Helper + +For building expiration dropdowns: + +```ruby +# Get options for select +ApiKeys::ExpirationOptions.for_select +# => [["No Expiration", "no_expiration"], ["7 days", "7_days"], ["30 days", "30_days"], ...] + +# Get default value +ApiKeys::ExpirationOptions.default_value +# => "no_expiration" + +# Parse a preset to a date +ApiKeys::ExpirationOptions.parse("30_days") +# => 30.days.from_now + +ApiKeys::ExpirationOptions.parse("no_expiration") +# => nil + +# Exclude "no expiration" option +ApiKeys::ExpirationOptions.for_select(include_no_expiration: false) +``` + +--- + +### Form Builder Extensions (Opt-in) + +Add to your initializer to enable: + +```ruby +Rails.application.config.to_prepare do + ActionView::Helpers::FormBuilder.include(ApiKeys::FormBuilderExtensions) +end +``` + +#### `api_key_expiration_select` + +Renders a select dropdown with all expiration presets: + +```erb +<%# Basic usage %> +<%= form.api_key_expiration_select %> + +<%# With CSS classes (Tailwind example) %> +<%= form.api_key_expiration_select(class: "w-full px-4 py-3 border rounded-lg") %> + +<%# With custom default selection %> +<%= form.api_key_expiration_select(selected: "30_days") %> +``` + +#### `api_key_scopes_checkboxes` + +Renders scope checkboxes with a block for custom markup: + +```erb +<%# With block - you control the HTML, gem handles the logic %> +<%= form.api_key_scopes_checkboxes(@available_scopes) do |scope, checked| %> + +<% end %> + +<%# For new records, all scopes are checked by default %> +<%# For existing records, only the key's current scopes are checked %> + +<%# Override checked state %> +<%= form.api_key_scopes_checkboxes(@scopes, checked: :none) do |scope, checked| %> + ... +<% end %> + +<%# checked options: :all, :none, or an array of specific scopes %> +``` + +#### `api_key_token_data` + +Returns structured data for building token display UIs: + +```erb +<% data = form.api_key_token_data %> +<%= data[:masked] %> + +<% if data[:viewable] %> + +<% end %> + +<%# Returns: { masked:, full:, viewable:, type:, environment: } %> +``` + +--- + +### View Helpers + +Include in your ApplicationHelper: + +```ruby +module ApplicationHelper + include ApiKeys::ViewHelpers +end +``` + +#### Status Helpers + +```erb +<%# Get status as symbol %> +<%= api_key_status(@key) %> +<%# => :active, :expired, or :revoked %> + +<%# Get human-readable label %> +<%= api_key_status_label(@key) %> +<%# => "Active", "Expired", or "Revoked" %> + +<%# Get full status info for styling %> +<% info = api_key_status_info(@key) %> + + <%= info[:label] %> + +<%# Returns: { status: :active, label: "Active", color: :green } %> +<%# Colors: :green (active), :red (revoked), :gray (expired) %> +``` + +#### Type & Environment Helpers + +```erb +<%# Key type label %> +<%= api_key_type_label(@key) %> +<%# => "Publishable", "Secret", or nil %> + +<%# Environment label %> +<%= api_key_environment_label(@key) %> +<%# => "Test", "Live", or "Default" %> + +<%# Type checks %> +<%= api_key_publishable?(@key) %> <%# => true/false %> +<%= api_key_secret?(@key) %> <%# => true/false %> + +<%# Get environment from a token string (useful on success page) %> +<%= api_key_environment_from_token(@token) %> +<%# => :test, :live, or nil %> + +<%= api_key_environment_label_from_token(@token) %> +<%# => "Test mode", "Live mode", or "Default" %> +``` + +--- + +### View Examples + +#### Index Page (Key List) + +```erb +<%# Publishable key section %> +<% if @publishable_key %> + <%= @publishable_key.viewable_token || @publishable_key.masked_token %> + <%= api_key_environment_label(@publishable_key) %> mode +<% else %> + <%= button_to create_publishable_settings_api_keys_path, method: :post do %> + Create Publishable Key + <% end %> +<% end %> + +<%# Secret keys table %> +<% @secret_keys.each do |key| %> + + <%= key.name || "Unnamed key" %> + <%= key.masked_token %> + <%= api_key_status_label(key) %> + + <%= link_to "Edit", edit_settings_api_key_path(key) %> + <%= button_to "Revoke", revoke_settings_api_key_path(key), method: :post %> + + +<% 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 %> + + + + <%= render partial: 'api_keys/keys/key_status', locals: { key: key } %> <%= key.name.presence || (key.key_type.present? ? "#{key.key_type.humanize} key" : "API key") %> - <%# Key type and environment badges %> - <% if key.key_type.present? %> - <% type_config = key.key_type_config %> - - <%= key.key_type.humanize %> - - <% end %> - <% if key.environment.present? %> - - <%= key.environment.upcase %> - - <% end %> - - - <%= key.masked_token %> - <% if key.public_key_type? && key.viewable_token.present? %> - - - <%= key.viewable_token %> - - - <% end %> + <%= render partial: 'api_keys/keys/key_badges', locals: { key: key } %> + + <%= render partial: 'api_keys/keys/token_display', locals: { key: key } %> + - + <%= time_ago_in_words(key.created_at) %> ago - - + <% 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 %> + + + <%= render partial: 'api_keys/keys/key_actions', locals: { key: key } %> - \ No newline at end of file + diff --git a/app/views/api_keys/keys/_key_status.html.erb b/app/views/api_keys/keys/_key_status.html.erb new file mode 100644 index 0000000..d55b904 --- /dev/null +++ b/app/views/api_keys/keys/_key_status.html.erb @@ -0,0 +1,10 @@ +<%# Partial for displaying key status indicator %> +<%# Locals: key (required) - The ApiKey record %> + +<% if key.active? %> + +<% elsif key.revoked? %> + [Revoked] +<% elsif key.expired? %> + [Expired] +<% end %> diff --git a/app/views/api_keys/keys/_keys_table.html.erb b/app/views/api_keys/keys/_keys_table.html.erb index 683b0e4..15845ee 100644 --- a/app/views/api_keys/keys/_keys_table.html.erb +++ b/app/views/api_keys/keys/_keys_table.html.erb @@ -38,12 +38,7 @@ <% else %> -
-

You don't have any API keys yet!

-

Create your first API key to get started.

- <%# Consider adding a primary "Create Key" button here %> - <%#= link_to "Create New API Key", new_key_path, class: "button primary" %> -
+ <%= render partial: 'api_keys/keys/empty_state' %> <% end %> diff --git a/app/views/api_keys/keys/_publishable_keys.html.erb b/app/views/api_keys/keys/_publishable_keys.html.erb new file mode 100644 index 0000000..b70deaf --- /dev/null +++ b/app/views/api_keys/keys/_publishable_keys.html.erb @@ -0,0 +1,40 @@ +<%# Partial for displaying publishable keys section %> +<%# Locals: active_keys (Active publishable keys), inactive_keys (Inactive publishable keys) %> + +
+

Publishable Keys

+

+ These keys are safe to embed in client-side applications and browser code. + You can view them anytime. +

+ +
+ <% all_keys = active_keys + inactive_keys %> + <% if all_keys.any? %> + + + + + + + + + + + + + + <% active_keys.each do |key| %> + <%= render partial: 'api_keys/keys/key_row', locals: { key: key } %> + <% end %> + + <% inactive_keys.each do |key| %> + <%= render partial: 'api_keys/keys/key_row', locals: { key: key, inactive: true } %> + <% end %> + +
NameAPI KeyCreatedExpiresLast UsedPermissionsActions
+ <% else %> + <%= render partial: 'api_keys/keys/empty_state', locals: { message: "No publishable keys yet." } %> + <% end %> +
+
diff --git a/app/views/api_keys/keys/_secret_keys.html.erb b/app/views/api_keys/keys/_secret_keys.html.erb new file mode 100644 index 0000000..335809f --- /dev/null +++ b/app/views/api_keys/keys/_secret_keys.html.erb @@ -0,0 +1,39 @@ +<%# Partial for displaying secret keys section %> +<%# Locals: active_keys (Active secret keys), inactive_keys (Inactive secret keys) %> + +
+

Secret Keys

+

+ Keep these private. Never expose in client-side code or share publicly. +

+ +
+ <% all_keys = active_keys + inactive_keys %> + <% if all_keys.any? %> + + + + + + + + + + + + + + <% active_keys.each do |key| %> + <%= render partial: 'api_keys/keys/key_row', locals: { key: key } %> + <% end %> + + <% inactive_keys.each do |key| %> + <%= render partial: 'api_keys/keys/key_row', locals: { key: key, inactive: true } %> + <% end %> + +
NameAPI KeyCreatedExpiresLast UsedPermissionsActions
+ <% else %> + <%= render partial: 'api_keys/keys/empty_state', locals: { message: "No secret keys yet." } %> + <% 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 %> + + + +<% 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
  • +
  • Limited access — cannot perform sensitive operations
  • +
  • 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:

+
# Edit credentials
+bin/rails credentials:edit
+
+# Access in code
+Rails.application.credentials.your_service_api_key
+

<%= link_to "Rails Security Guide", "https://guides.rubyonrails.org/security.html#custom-credentials", target: "_blank", rel: "noopener noreferrer", class: "text-primary" %>

+ +

Secrets Management Services

+

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 @@