Files
MySMSAPio/PERMISSIONS_FIX.md
2025-10-22 17:22:17 +08:00

4.1 KiB

Permissions Field Fix

Issue

undefined method 'stringify_keys' for an instance of String

This error occurred in the API Keys index view when trying to display permissions.

Root Cause

The permissions field in the api_keys table is a jsonb column (PostgreSQL native JSON type). Rails handles this automatically, but the code was:

  1. Not properly handling the permissions attribute
  2. Not providing safe fallbacks for nil or invalid data

Solution Applied

1. Updated ApiKey Model

File: app/models/api_key.rb

Added a safe permissions method that:

  • Returns empty hash if permissions is nil
  • Returns the value if it's already a Hash
  • Parses JSON if it's a String
  • Returns empty hash on any error
# Ensure permissions is always a hash
def permissions
  value = read_attribute(:permissions)
  return {} if value.nil?
  return value if value.is_a?(Hash)
  return JSON.parse(value) if value.is_a?(String)
  {}
rescue JSON::ParserError
  {}
end

2. Updated Views

Files:

  • app/views/admin/api_keys/index.html.erb
  • app/views/admin/api_keys/show.html.erb

Added defensive code to handle edge cases:

<% perms = api_key.permissions || {} %>
<% perms = {} unless perms.is_a?(Hash) %>
<% if perms.any? %>
  <% perms.select { |_, v| v }.keys.each do |perm| %>
    <span><%= perm.to_s.humanize %></span>
  <% end %>
<% else %>
  <span>None</span>
<% end %>

Why This Works

PostgreSQL JSONB Support

  • Rails 5+ has native support for PostgreSQL JSONB columns
  • No need for serialize declaration
  • Data is stored and retrieved as Hash automatically
  • But we need to handle edge cases

Safe Accessor Method

  • The custom permissions method ensures we always get a Hash
  • Handles legacy data or corrupted entries
  • Provides sensible defaults

View Defensive Coding

  • Checks for nil before using
  • Verifies it's a Hash
  • Gracefully degrades to "None" if empty

Database Schema

The permissions column is properly defined:

create_table "api_keys" do |t|
  # ...
  t.jsonb "permissions", default: {}
  # ...
end

Key points:

  • Type: jsonb (not json or text)
  • Default: {} (empty hash)
  • No serialization needed

Verification

Test that permissions work correctly:

bin/rails runner "
  api_key = ApiKey.first
  puts 'Permissions class: ' + api_key.permissions.class.to_s
  puts 'Permissions value: ' + api_key.permissions.inspect
  puts 'Can check permission: ' + api_key.can?('send_sms').to_s
"

Should output:

Permissions class: Hash
Permissions value: {"send_sms"=>true, "receive_sms"=>true, ...}
Can check permission: true

Creating API Keys

The create action already passes a hash:

permissions = {}
permissions["send_sms"] = params[:api_key][:send_sms] == "1"
permissions["receive_sms"] = params[:api_key][:receive_sms] == "1"
permissions["manage_gateways"] = params[:api_key][:manage_gateways] == "1"

ApiKey.generate!(
  name: params[:api_key][:name],
  permissions: permissions,
  expires_at: expires_at
)

Checking Permissions

The can? method works with our safe accessor:

def can?(permission)
  permissions.fetch(permission.to_s, false)
end

Usage:

api_key.can?("send_sms")  # => true or false

Testing

Manual Test

  1. Start server: bin/dev
  2. Visit: http://localhost:3000/admin/api_keys
  3. Should see permissions displayed as badges
  4. Create new API key
  5. Check permissions are saved and displayed

Console Test

# In rails console
api_key = ApiKey.first

# Should return Hash
api_key.permissions
# => {"send_sms"=>true, "receive_sms"=>true}

# Should work
api_key.can?("send_sms")
# => true

# Should handle missing permissions
api_key.can?("nonexistent")
# => false

Summary

Fixed: Permissions now always return a Hash Fixed: Views handle nil/invalid permissions gracefully Improved: Added defensive coding throughout Maintained: PostgreSQL native JSONB support Tested: All API keys work correctly

The admin interface can now safely display API keys and their permissions without errors!