class ApiKey < ApplicationRecord # Normalize permissions to always be a Hash attribute :permissions, :jsonb, default: {} before_validation :ensure_permissions_is_hash validates :name, presence: true validates :key_digest, presence: true, uniqueness: true validates :key_prefix, presence: true scope :active_keys, -> { where(active: true) } scope :expired, -> { where("expires_at IS NOT NULL AND expires_at < ?", Time.current) } scope :valid, -> { active_keys.where("expires_at IS NULL OR expires_at > ?", Time.current) } # Generate a new API key def self.generate!(name:, permissions: {}, expires_at: nil) raw_key = "api_live_#{SecureRandom.hex(32)}" key_digest = Digest::SHA256.hexdigest(raw_key) key_prefix = raw_key[0..11] # First 12 chars for identification api_key = create!( name: name, key_digest: key_digest, key_prefix: key_prefix, permissions: permissions, expires_at: expires_at ) { api_key: api_key, raw_key: raw_key } end # Authenticate with a raw key def self.authenticate(raw_key) return nil unless raw_key.present? key_digest = Digest::SHA256.hexdigest(raw_key) api_key = valid.find_by(key_digest: key_digest) if api_key api_key.touch(:last_used_at) end api_key end # Check if API key is still active and not expired def active_and_valid? active && (expires_at.nil? || expires_at > Time.current) end # Check if API key has specific permission def can?(permission) permissions.fetch(permission.to_s, false) end # Revoke API key def revoke! update!(active: false) end # Deactivate expired keys def self.deactivate_expired! expired.update_all(active: false) end private def ensure_permissions_is_hash self.permissions = {} if permissions.nil? self.permissions = {} unless permissions.is_a?(Hash) end end