2

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!

Community
  • 1
  • 1
Vasseurth
  • 6,354
  • 12
  • 53
  • 81

2 Answers2

3

Rails 5 comes with a new feaeture has_secure_tokenis really easy to use:

# Schema: User(token:string, auth_token:string)
class User < ActiveRecord::Base
    has_secure_token :auth_token
end

user = User.new
user.save
user.auth_token # => "pX27zsMN2ViQKta1bGfLmVJE"
user.regenerate_auth_token # => true

Since Rails 5 isn't already released, you can use the Backport has_secure_token gem

user2627938
  • 207
  • 2
  • 3
2

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
Community
  • 1
  • 1
Ken Stipek
  • 1,512
  • 8
  • 12
  • I'm running into a "Circular dependency detected error with `c.included_modules.include(Concerns::Tokenable)`. Could it be that Devise is causing this circle? – Vasseurth Dec 09 '14 at 06:59
  • 1
    I was able to solve this error by changing `include(Concerns::Tokenable)` to `include?(Tokenable)` – Vasseurth Dec 09 '14 at 07:28