Vasseurth Vasseurth - 5 months ago 13
Ruby Question

Create random, unique tokens upon account creation in Rails

I have a Rails 4 app using Devise (the most recent) and am trying to create a random token for each user (like the ID, but longer, etc.) Using this answer I was able to come up with the follow code:

# app/models/model_name.rb
class ModelName < ActiveRecord::Base
include Tokenable
end

# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern

included do
before_create :generate_token
end

protected

def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless self.class.exists?(token: random_token)
end
end
end


This code works fantastically for tokens that are unique for any given model. I.e. All Users will have unique tokens, and all Admins will have unique tokens. But an Admin may have the same token as a User – this behavior is unwanted.

Is there an elegant way, short of abstracting the token into its own model and using "has_one" relationships, to ensure that the token does not exist in all the models it is a part of?

(I guess I could hard code
unless (User.exists? ... or Admin.exists? ... )
into the unless clause, though this seems bulky.)

Any thoughts or suggestions are appreciated! Thanks!

Answer

I would create a method that lists each of the Classes that are including my concern and then test against the token for each. Something like this:

# app/models/concerns/tokenable.rb
module Tokenable
  extend ActiveSupport::Concern

  included do
    before_create :generate_token
  end

  protected

  def included_classes
     ActiveRecord::Base.descendants.select do |c|
       c.included_modules.include(Concerns::Tokenable)}.map(&:name)
     end
  end

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless included_classes.map {|c| c.constantize.exists?(token: random_token) }.include?(true)
    end
  end
end

So include_classes is going to return an array of names as strings of each of the classes that include the Tokenable concern. And then in the loop within generate_token is going to check against each of these classes generating an array of true or false which then we just check if any are true with include?(true).

Here is were I found how to get included classes (first answer).

EDIT

In Rails 5 the included_classes looks like this (note the ApplicationRecord and not needing the Concerns::Tokenable):

  def included_classes
    ApplicationRecord.descendants.select do |c|
      c.included_modules.include?(Tokenable)
    end
  end