Module: ApiKeys::Models::Concerns::HasApiKeys

Extended by:
ActiveSupport::Concern
Defined in:
lib/api_keys/models/concerns/has_api_keys.rb

Overview

Concern to add API key capabilities to an owner model (e.g., User, Organization). This module provides the has_api_keys class method when extended onto ActiveRecord::Base.

Defined Under Namespace

Modules: ClassMethods Classes: DslProvider

Instance Method Summary collapse

Instance Method Details

#available_api_key_scopesArray<String>

Returns the available scopes for API keys on this owner. Uses owner-specific settings if defined, otherwise falls back to global config. Useful for populating scope checkboxes in forms.

Returns:

  • (Array<String>)

    The available scopes



103
104
105
106
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 103

def available_api_key_scopes
  owner_settings = self.class.api_keys_settings
  owner_settings&.[](:default_scopes) || ApiKeys.configuration.default_scopes || []
end

#can_create_api_key?(key_type: nil, environment: nil) ⇒ Boolean

Checks if this owner can create an API key of the given type. Returns false if the limit for this key type/environment is reached. Useful for conditional UI (e.g., hiding "Create" button when at limit).

Note: This is a best-effort check for UI purposes without locking. Concurrent requests could see stale data. The actual limit is enforced with pessimistic locking in create_api_key!, so this is safe to use for UI decisions (worst case: button shown but creation fails validation).

Examples:

if current_org.can_create_api_key?(key_type: :publishable)
  # Show "Create Publishable Key" button
end

Parameters:

  • key_type (Symbol, nil) (defaults to: nil)

    The key type to check (e.g., :publishable, :secret)

  • environment (Symbol, nil) (defaults to: nil)

    The environment to check (defaults to current_environment)

Returns:

  • (Boolean)

    true if the owner can create another key of this type



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 126

def can_create_api_key?(key_type: nil, environment: nil)
  config = ApiKeys.configuration

  # If no key_type specified, check global quota only
  return within_global_quota? if key_type.nil?

  # Resolve environment
  resolved_environment = resolve_environment(environment, key_type, config)

  # Get type-specific limit
  type_config = config.key_types&.dig(key_type.to_sym)
  return within_global_quota? unless type_config

  limit = type_config[:limit]
  return within_global_quota? unless limit

  # Count existing keys of this type/environment
  existing_count = api_keys
                    .active
                    .where(key_type: key_type.to_s)
                    .where(environment: resolved_environment.to_s)
                    .count

  existing_count < limit && within_global_quota?
end

#create_api_key!(name: nil, scopes: nil, expires_at: nil, expires_at_preset: nil, metadata: nil, key_type: nil, environment: nil) ⇒ ApiKeys::ApiKey

Creates a new API key for this owner instance and returns the ApiKey instance. Raises ActiveRecord::RecordInvalid if creation fails.

Parameters:

  • name (String) (defaults to: nil)

    The name for the new API key (required).

  • scopes (Array<String>, nil) (defaults to: nil)

    Scopes for the key. Defaults to owner/global settings. When key_type is specified, scopes are filtered to only include those allowed by the key type's permissions ceiling. Blank values are automatically removed.

  • expires_at (Time, nil) (defaults to: nil)

    Optional expiration timestamp.

  • expires_at_preset (String, nil) (defaults to: nil)

    Convenience param: "7_days", "30_days", "no_expiration", etc. If provided, this is parsed into expires_at. Takes precedence over expires_at if both given.

  • metadata (Hash, nil) (defaults to: nil)

    Optional metadata hash.

  • key_type (Symbol, nil) (defaults to: nil)

    The key type (e.g., :publishable, :secret). Must be defined in ApiKeys.configuration.key_types if provided.

  • environment (Symbol, nil) (defaults to: nil)

    The environment (e.g., :test, :live). Defaults to current_environment if key_types feature is enabled.

Returns:

  • (ApiKeys::ApiKey)

    The newly created ApiKey instance. The plaintext token is available via the #token attribute on this instance only until it's reloaded.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 170

def create_api_key!(name: nil, scopes: nil, expires_at: nil, expires_at_preset: nil, metadata: nil, key_type: nil, environment: nil)
  config = ApiKeys.configuration

  # Parse expires_at_preset if provided (takes precedence over expires_at)
  if expires_at_preset.present?
    expires_at = ApiKeys::Helpers::ExpirationOptions.parse(expires_at_preset)
  end

  # Auto-clean scopes: remove blank values from arrays (common when using form checkboxes)
  if scopes.is_a?(Array)
    scopes = scopes.reject { |s| s.blank? }
  end

  # Check for missing columns if key_types feature is enabled
  if key_types_feature_enabled?(config)
    check_required_columns!
  end

  # Use default_key_type if not specified and key_types feature is enabled
  resolved_key_type = key_type
  if resolved_key_type.nil? && key_types_feature_enabled?(config) && config.default_key_type.present?
    resolved_key_type = config.default_key_type
  end

  # Validate key_type if provided and key_types feature is enabled
  if resolved_key_type.present?
    validate_key_type!(resolved_key_type, config)
  end

  # Determine environment: use provided, or default from config
  resolved_environment = resolve_environment(environment, resolved_key_type, config)

  # Validate environment if key_types feature is enabled
  if resolved_environment.present? && key_types_feature_enabled?(config)
    validate_environment!(resolved_environment, config)
  end

  # Fetch default scopes from this owner class's settings, falling back to global config.
  owner_settings = self.class.api_keys_settings
  default_scopes = owner_settings&.[](:default_scopes) || config.default_scopes || []

  # Use provided scopes if given, otherwise use the calculated defaults.
  key_scopes = scopes.nil? ? default_scopes : Array(scopes)

  # Filter scopes based on key type permissions ceiling
  if resolved_key_type.present?
    key_scopes = filter_scopes_by_permissions(key_scopes, resolved_key_type, config)
  end

  # Create the key using the association, letting AR handle owner_id/type.
  api_key = self.api_keys.create!(
    name: name,
    scopes: key_scopes,
    expires_at: expires_at,
    metadata:  || {}, # Ensure metadata is at least an empty hash
    key_type: resolved_key_type&.to_s,
    environment: resolved_environment&.to_s
    # prefix, token_digest, digest_algorithm are set by ApiKey callbacks
  )

  # Return the ApiKey instance itself.
  # The plaintext token is available via `api_key.token` immediately after this.
  api_key
end