token_attr 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/lib/token_attr.rb +2 -87
- data/lib/token_attr/concern.rb +84 -0
- data/lib/token_attr/errors.rb +14 -0
- data/lib/token_attr/version.rb +1 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/{token_attr_spec.rb → token_attr/concern_spec.rb} +60 -20
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8883c5c7026e366c148643bd1e4bd13dc57feba4
|
4
|
+
data.tar.gz: 7f661a16d09ab1e9368680efff5b940e20c51f05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a01317959dd57120d37231389020af8e9ea669596788aad13b50c60b1f170f3cf3155ba20e640bbdd2e1076657bb08e8c218b592c4c19c33f98fada74accb73f
|
7
|
+
data.tar.gz: 404e200b4bfe0c10ddeee8cf1b9970cc39c0ee572815e7c3c5ba11c3737ffc47f9f82521faefb043120d21dbbcf4ebdd4ac415cbd1c8e56a4e57023317c4a970
|
data/README.md
CHANGED
@@ -6,13 +6,13 @@ Unique random token generator for ActiveRecord.
|
|
6
6
|
|
7
7
|
Add `token_attr` to your Gemfile:
|
8
8
|
|
9
|
-
gem 'token_attr', '~> 0.
|
9
|
+
gem 'token_attr', '~> 0.2.0'
|
10
10
|
|
11
11
|
## Usage
|
12
12
|
|
13
13
|
```
|
14
14
|
class User < ActiveRecord::Base
|
15
|
-
include TokenAttr
|
15
|
+
include TokenAttr::Concern
|
16
16
|
token_attr :token
|
17
17
|
end
|
18
18
|
|
@@ -21,7 +21,7 @@ user.valid?
|
|
21
21
|
user.token # => "b8bd30ff"
|
22
22
|
```
|
23
23
|
|
24
|
-
The token is generated in a `before_validation` callback
|
24
|
+
The token is generated in a `before_validation` callback only if the it's `nil`.
|
25
25
|
|
26
26
|
### Options
|
27
27
|
|
@@ -48,10 +48,10 @@ Accepted values:
|
|
48
48
|
- a string - a string of your choice of the characters you want to use
|
49
49
|
|
50
50
|
```
|
51
|
-
token_attr :token, alphabet: :numeric
|
52
|
-
token_attr :token, alphabet: :alphabetic
|
51
|
+
token_attr :token, alphabet: :numeric # => "82051173"
|
52
|
+
token_attr :token, alphabet: :alphabetic # => "xqnInSJa"
|
53
53
|
token_attr :token, alphabet: :alphanumeric # => "61nD0lUo"
|
54
|
-
token_attr :token, alphabet: "token"
|
54
|
+
token_attr :token, alphabet: "token" # => "ktnekoet"
|
55
55
|
```
|
56
56
|
|
57
57
|
## Contributing
|
data/lib/token_attr.rb
CHANGED
@@ -1,88 +1,3 @@
|
|
1
1
|
require 'token_attr/version'
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
|
5
|
-
module TokenAttr
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
DEFAULT_TOKEN_LENGTH = 8.freeze
|
9
|
-
ALPHABETIC_ALPHABET = [('a'..'z'),('A'..'Z')].map(&:to_a).flatten.freeze
|
10
|
-
NUMERIC_ALPHABET = [(0..9)].map(&:to_a).flatten.freeze
|
11
|
-
ALPHANUMERIC_ALPHABET = [ALPHABETIC_ALPHABET, NUMERIC_ALPHABET].flatten.freeze
|
12
|
-
|
13
|
-
class TooManyAttemptsError < StandardError
|
14
|
-
attr_reader :attribute, :token
|
15
|
-
|
16
|
-
def initialize(attr_name, token, message = nil)
|
17
|
-
@attribute = attr_name
|
18
|
-
@token = token
|
19
|
-
message ||= "Can't generate unique token for \"#{attr_name}\". Last attempt with \"#{token}\"."
|
20
|
-
super(message)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
included do
|
25
|
-
before_validation :generate_tokens
|
26
|
-
end
|
27
|
-
|
28
|
-
module ClassMethods
|
29
|
-
def token_attr(attr_name, options = {})
|
30
|
-
token_attributes << attr_name
|
31
|
-
|
32
|
-
define_method "should_generate_new_#{attr_name}_token?" do
|
33
|
-
send(attr_name).blank?
|
34
|
-
end
|
35
|
-
|
36
|
-
define_method "generate_new_#{attr_name}_token" do
|
37
|
-
token_length = options.fetch(:length, DEFAULT_TOKEN_LENGTH)
|
38
|
-
|
39
|
-
if alphabet = options[:alphabet]
|
40
|
-
alphabet_array = case alphabet
|
41
|
-
when :alphanumeric
|
42
|
-
ALPHANUMERIC_ALPHABET
|
43
|
-
when :alphabetic
|
44
|
-
ALPHABETIC_ALPHABET
|
45
|
-
when :numeric
|
46
|
-
NUMERIC_ALPHABET
|
47
|
-
else
|
48
|
-
alphabet.split('')
|
49
|
-
end
|
50
|
-
(0...token_length).map{ alphabet_array.sample }.join
|
51
|
-
else
|
52
|
-
hex_length = (token_length / 2.0).ceil # 2 characters per length
|
53
|
-
SecureRandom.hex(hex_length).slice(0...token_length)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def token_attributes
|
59
|
-
@token_attributes ||= []
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def generate_tokens
|
64
|
-
self.class.token_attributes.each do |attr_name|
|
65
|
-
if send("should_generate_new_#{attr_name}_token?")
|
66
|
-
new_token = nil
|
67
|
-
try_count = 0
|
68
|
-
begin
|
69
|
-
raise TooManyAttemptsError.new(attr_name, new_token) if try_count == 5
|
70
|
-
new_token = send("generate_new_#{attr_name}_token")
|
71
|
-
try_count += 1
|
72
|
-
end until token_is_unique?(attr_name, new_token)
|
73
|
-
|
74
|
-
send "#{attr_name}=", new_token
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def token_is_unique?(attr_name, token)
|
80
|
-
scope = self.class.where(attr_name => token)
|
81
|
-
scope = scope.where(id != self.id) if self.persisted?
|
82
|
-
!scope.exists?
|
83
|
-
end
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
# Uncomment to auto-extend ActiveRecord, probably not a good idea
|
88
|
-
# ActiveRecord::Base.send(:extend, TokenAttr)
|
2
|
+
require 'token_attr/concern'
|
3
|
+
require 'token_attr/errors'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module TokenAttr
|
5
|
+
DEFAULT_TOKEN_LENGTH = 8.freeze
|
6
|
+
ALPHABETIC_ALPHABET = [('a'..'z'),('A'..'Z')].map(&:to_a).flatten.freeze
|
7
|
+
NUMERIC_ALPHABET = [(0..9)].map(&:to_a).flatten.freeze
|
8
|
+
ALPHANUMERIC_ALPHABET = [ALPHABETIC_ALPHABET, NUMERIC_ALPHABET].flatten.freeze
|
9
|
+
|
10
|
+
TokenDefinition = Struct.new(:attr_name, :scope_attr)
|
11
|
+
|
12
|
+
module Concern
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
before_validation :generate_tokens
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def token_attr(attr_name, options = {})
|
21
|
+
token_definitions << TokenDefinition.new(attr_name, options[:scope])
|
22
|
+
|
23
|
+
define_method "should_generate_new_#{attr_name}?" do
|
24
|
+
send(attr_name).blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method "generate_new_#{attr_name}" do
|
28
|
+
token_length = options.fetch(:length, DEFAULT_TOKEN_LENGTH)
|
29
|
+
|
30
|
+
if alphabet = options[:alphabet]
|
31
|
+
alphabet_array = case alphabet
|
32
|
+
when :alphanumeric
|
33
|
+
ALPHANUMERIC_ALPHABET
|
34
|
+
when :alphabetic
|
35
|
+
ALPHABETIC_ALPHABET
|
36
|
+
when :numeric
|
37
|
+
NUMERIC_ALPHABET
|
38
|
+
else
|
39
|
+
alphabet.split('')
|
40
|
+
end
|
41
|
+
(0...token_length).map{ alphabet_array.sample }.join
|
42
|
+
else
|
43
|
+
hex_length = (token_length / 2.0).ceil # 2 characters per length
|
44
|
+
SecureRandom.hex(hex_length).slice(0...token_length)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def token_definitions
|
50
|
+
@token_definitions ||= []
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_tokens
|
55
|
+
self.class.token_definitions.each do |td|
|
56
|
+
if send("should_generate_new_#{td.attr_name}?")
|
57
|
+
new_token = nil
|
58
|
+
try_count = 0
|
59
|
+
begin
|
60
|
+
raise TooManyAttemptsError.new(td.attr_name, new_token) if try_count == 5
|
61
|
+
new_token = send("generate_new_#{td.attr_name}")
|
62
|
+
try_count += 1
|
63
|
+
end until token_is_unique?(td, new_token)
|
64
|
+
|
65
|
+
send "#{td.attr_name}=", new_token
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def token_is_unique?(token_definition, token)
|
71
|
+
attr_name = token_definition.attr_name
|
72
|
+
scope_attr = token_definition.scope_attr
|
73
|
+
|
74
|
+
scope = self.class.where(attr_name => token)
|
75
|
+
scope = scope.where.not(id: self.id) if self.persisted?
|
76
|
+
scope = scope.where(scope_attr => read_attribute(scope_attr)) if scope_attr
|
77
|
+
!scope.exists?
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Uncomment to auto-extend ActiveRecord, probably not a good idea
|
84
|
+
# ActiveRecord::Base.send(:extend, TokenAttr)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module TokenAttr
|
2
|
+
|
3
|
+
class TooManyAttemptsError < StandardError
|
4
|
+
attr_reader :attribute, :token
|
5
|
+
|
6
|
+
def initialize(attr_name, token, message = nil)
|
7
|
+
@attribute = attr_name
|
8
|
+
@token = token
|
9
|
+
message ||= "Can't generate unique token for \"#{attr_name}\". Last attempt with \"#{token}\"."
|
10
|
+
super(message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/lib/token_attr/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -20,6 +20,12 @@ ActiveRecord::Schema.define do
|
|
20
20
|
create_table :models, force: true do |t|
|
21
21
|
t.string :token
|
22
22
|
t.string :private_token
|
23
|
+
t.integer :scope_id
|
23
24
|
end
|
24
25
|
|
25
26
|
end
|
27
|
+
|
28
|
+
class BaseModel < ActiveRecord::Base
|
29
|
+
self.table_name = 'models'
|
30
|
+
include TokenAttr::Concern
|
31
|
+
end
|
@@ -2,48 +2,47 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe TokenAttr do
|
4
4
|
|
5
|
-
class Model <
|
6
|
-
include TokenAttr
|
5
|
+
class Model < BaseModel
|
7
6
|
token_attr :token
|
8
7
|
end
|
9
8
|
|
10
|
-
class ModelWithLength <
|
11
|
-
self.table_name = 'models'
|
12
|
-
include TokenAttr
|
9
|
+
class ModelWithLength < BaseModel
|
13
10
|
token_attr :token, length: 13
|
14
11
|
end
|
15
12
|
|
16
|
-
class ModelWithAlphabet <
|
17
|
-
self.table_name = 'models'
|
18
|
-
include TokenAttr
|
13
|
+
class ModelWithAlphabet < BaseModel
|
19
14
|
token_attr :token, alphabet: 'abc123'
|
20
15
|
end
|
21
16
|
|
22
|
-
class ModelWithAlphanumericAlphabet <
|
23
|
-
self.table_name = 'models'
|
24
|
-
include TokenAttr
|
17
|
+
class ModelWithAlphanumericAlphabet < BaseModel
|
25
18
|
token_attr :token, alphabet: :alphanumeric
|
26
19
|
end
|
27
20
|
|
28
|
-
class ModelWithAlphabeticAlphabet <
|
29
|
-
self.table_name = 'models'
|
30
|
-
include TokenAttr
|
21
|
+
class ModelWithAlphabeticAlphabet < BaseModel
|
31
22
|
token_attr :token, alphabet: :alphabetic
|
32
23
|
end
|
33
24
|
|
34
|
-
class ModelWithNumericAlphabet <
|
35
|
-
self.table_name = 'models'
|
36
|
-
include TokenAttr
|
25
|
+
class ModelWithNumericAlphabet < BaseModel
|
37
26
|
token_attr :token, alphabet: :numeric
|
38
27
|
end
|
39
28
|
|
40
|
-
class ModelWithMultipleTokens <
|
41
|
-
self.table_name = 'models'
|
42
|
-
include TokenAttr
|
29
|
+
class ModelWithMultipleTokens < BaseModel
|
43
30
|
token_attr :token
|
44
31
|
token_attr :private_token
|
45
32
|
end
|
46
33
|
|
34
|
+
class ModelWithOverride < BaseModel
|
35
|
+
token_attr :token
|
36
|
+
|
37
|
+
def should_generate_new_token?
|
38
|
+
token == '1234'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ModelWithScope < BaseModel
|
43
|
+
token_attr :token, scope: :scope_id
|
44
|
+
end
|
45
|
+
|
47
46
|
describe ".token_attr" do
|
48
47
|
let(:model) { Model.new }
|
49
48
|
|
@@ -136,6 +135,47 @@ describe TokenAttr do
|
|
136
135
|
end
|
137
136
|
end
|
138
137
|
|
138
|
+
context "when the should_generate_new_[attr_name]? method is overridden" do
|
139
|
+
let(:model) { ModelWithOverride.new }
|
140
|
+
|
141
|
+
it "generates a token when the condition is satisfied" do
|
142
|
+
SecureRandom.should_receive(:hex).with(4).and_return('newtoken')
|
143
|
+
model.token = '1234'
|
144
|
+
model.valid?
|
145
|
+
model.token.should == 'newtoken'
|
146
|
+
end
|
147
|
+
|
148
|
+
it "does not generate a new token when the condition is not satisfied" do
|
149
|
+
model.valid?
|
150
|
+
model.token.should be_nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "with scope" do
|
155
|
+
let(:model) { ModelWithScope.new }
|
156
|
+
|
157
|
+
context "when the scope attributes are different" do
|
158
|
+
it "allows duplicate tokens" do
|
159
|
+
SecureRandom.should_receive(:hex).twice.with(4).and_return('12345678')
|
160
|
+
ModelWithScope.create(scope_id: 1)
|
161
|
+
model.scope_id = 2
|
162
|
+
model.valid?
|
163
|
+
model.token.should == '12345678'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "when the scope attributes are the same" do
|
168
|
+
it "regenerates a duplicate token" do
|
169
|
+
SecureRandom.should_receive(:hex).twice.with(4).and_return('12345678')
|
170
|
+
SecureRandom.should_receive(:hex).once.with(4).and_return('asdfghjk')
|
171
|
+
ModelWithScope.create(scope_id: 1)
|
172
|
+
model.scope_id = 1
|
173
|
+
model.valid?
|
174
|
+
model.token.should == 'asdfghjk'
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
139
179
|
context "when token is not blank" do
|
140
180
|
before { model.token = 'not blank' }
|
141
181
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: token_attr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michel Billard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-05-
|
11
|
+
date: 2014-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -109,9 +109,11 @@ files:
|
|
109
109
|
- README.md
|
110
110
|
- Rakefile
|
111
111
|
- lib/token_attr.rb
|
112
|
+
- lib/token_attr/concern.rb
|
113
|
+
- lib/token_attr/errors.rb
|
112
114
|
- lib/token_attr/version.rb
|
113
115
|
- spec/spec_helper.rb
|
114
|
-
- spec/
|
116
|
+
- spec/token_attr/concern_spec.rb
|
115
117
|
- token_attr.gemspec
|
116
118
|
homepage: http://github.com/mbillard/token_attr
|
117
119
|
licenses:
|
@@ -139,4 +141,4 @@ specification_version: 4
|
|
139
141
|
summary: Unique random token generator for ActiveRecord
|
140
142
|
test_files:
|
141
143
|
- spec/spec_helper.rb
|
142
|
-
- spec/
|
144
|
+
- spec/token_attr/concern_spec.rb
|