otp-jwt 0.2.2 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/LICENSE.txt +21 -0
- data/README.md +8 -0
- data/lib/otp/active_record.rb +2 -2
- data/lib/otp/jwt/action_controller.rb +0 -1
- data/lib/otp/jwt/active_record.rb +23 -4
- data/lib/otp/jwt/test_helpers.rb +9 -2
- data/lib/otp/jwt/version.rb +1 -1
- data/spec/dummy.rb +82 -0
- data/spec/otp/jwt/token_spec.rb +54 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/tokens_controller_spec.rb +50 -0
- data/spec/user_spec.rb +63 -0
- data/spec/users_controller_spec.rb +23 -0
- metadata +47 -22
- data/.github/main.workflow +0 -30
- data/.gitignore +0 -2
- data/.rspec +0 -3
- data/.rubocop.yml +0 -46
- data/.yardstick.yml +0 -29
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -197
- data/Rakefile +0 -30
- data/otp-jwt.gemspec +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 16fc6a7023ef65ec63c539e7a43ad48b6a5a8a64b41269687c8c1db53e321c4b
|
4
|
+
data.tar.gz: 4c7e06c0dd6e1b29ea12de1f75c4f25310c8557965c7d8bbdeaf294c244683c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7ee896f4d70eb673ce977a8db560cc8f193255802f36da5074791b883cfa453045a5e57c98f98ad919735eee0bbd191025e12cdaa253447f7b5fae71aed429a3
|
7
|
+
data.tar.gz: b9a82e89ba20bf80d5e7ebfe3d292452d8fa487b5f3645ea3b94862f77078386c20f77ac97cecdfd5e9740a1aee592480ab53dd6e5c6d50f7cbcf423cd1aea90
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Stas Suscov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -10,6 +10,8 @@ One time password (email, SMS) authentication support for HTTP APIs.
|
|
10
10
|
This project provides a couple of mixins to help you build
|
11
11
|
applications/HTTP APIs without asking your users to provide passwords.
|
12
12
|
|
13
|
+
[Your browser probably can work seamlessly with OTPs](https://web.dev/web-otp/)!!! :heart_eyes:
|
14
|
+
|
13
15
|
## About
|
14
16
|
|
15
17
|
The goal of this project is to provide support for one time passwords
|
@@ -103,6 +105,12 @@ A migration to add these two looks like this:
|
|
103
105
|
$ rails g migration add_otp_to_users otp_secret:string otp_counter:integer
|
104
106
|
```
|
105
107
|
|
108
|
+
Generate `opt_secret` by running the following in rails console if you have preexisting user data:
|
109
|
+
```
|
110
|
+
User.all.each do |u|
|
111
|
+
u.save()
|
112
|
+
end
|
113
|
+
```
|
106
114
|
#### Mailer support
|
107
115
|
|
108
116
|
You can use the built-in mailer to deliver the OTP, just require it and
|
data/lib/otp/active_record.rb
CHANGED
@@ -36,7 +36,8 @@ module OTP
|
|
36
36
|
def verify_otp(otp)
|
37
37
|
return nil if !valid? || !persisted? || otp_secret.blank?
|
38
38
|
|
39
|
-
|
39
|
+
otp_digits = self.class.const_get(:OTP_DIGITS)
|
40
|
+
hotp = ROTP::HOTP.new(otp_secret, digits: otp_digits)
|
40
41
|
transaction do
|
41
42
|
otp_status = hotp.verify(otp.to_s, otp_counter)
|
42
43
|
increment!(:otp_counter)
|
@@ -72,7 +73,6 @@ module OTP
|
|
72
73
|
end
|
73
74
|
|
74
75
|
private
|
75
|
-
|
76
76
|
# Provides a default value for the OTP secret attribute
|
77
77
|
#
|
78
78
|
# @return [String]
|
@@ -10,19 +10,38 @@ module OTP
|
|
10
10
|
# Returns a record based on the [JWT] token subject
|
11
11
|
#
|
12
12
|
# @param token [String] representing a [JWT] token
|
13
|
+
# @param claim_name [String] the claim name to be used, default is `sub`
|
13
14
|
# @return [ActiveRecord::Base] model
|
14
|
-
def from_jwt(token)
|
15
|
+
def from_jwt(token, claim_name = 'sub')
|
15
16
|
OTP::JWT::Token.decode(token) do |payload|
|
16
|
-
|
17
|
+
val = payload[claim_name]
|
18
|
+
pk_col = self.column_for_attribute(self.primary_key)
|
19
|
+
|
20
|
+
# Arel casts the values to the primary key type,
|
21
|
+
# which means that an UUID becomes an integer by default...
|
22
|
+
if self.connection.respond_to?(:lookup_cast_type_from_column)
|
23
|
+
pk_type = self.connection.lookup_cast_type_from_column(pk_col)
|
24
|
+
casted_val = pk_type.serialize(val)
|
25
|
+
else
|
26
|
+
casted_val = self.connection.type_cast(val, pk_col)
|
27
|
+
end
|
28
|
+
|
29
|
+
return if casted_val.to_s != val.to_s.strip
|
30
|
+
|
31
|
+
self.find_by(self.primary_key => val)
|
17
32
|
end
|
18
33
|
end
|
19
34
|
end
|
20
35
|
|
21
36
|
# Returns a [JWT] token for this record
|
22
37
|
#
|
38
|
+
# @param claims [Hash] extra claims to be included
|
23
39
|
# @return [ActiveRecord::Base] model
|
24
|
-
def to_jwt
|
25
|
-
OTP::JWT::Token.sign(
|
40
|
+
def to_jwt(claims = nil)
|
41
|
+
OTP::JWT::Token.sign(
|
42
|
+
sub: self.send(self.class.primary_key),
|
43
|
+
**(claims || {})
|
44
|
+
)
|
26
45
|
end
|
27
46
|
end
|
28
47
|
end
|
data/lib/otp/jwt/test_helpers.rb
CHANGED
@@ -4,16 +4,23 @@ module OTP
|
|
4
4
|
module JWT
|
5
5
|
# Helpers to help you test the [JWT] requests.
|
6
6
|
module TestHelpers
|
7
|
+
# Helper provides JSON content type headers
|
8
|
+
#
|
9
|
+
# @return [Hash] the relevant content type &co
|
10
|
+
def json_headers
|
11
|
+
{ 'Content-Type': Mime[:json].to_s }
|
12
|
+
end
|
13
|
+
|
7
14
|
# Helper to handle authentication requests easier
|
8
15
|
#
|
9
16
|
# @return [Hash] the authorization headers
|
10
17
|
def jwt_auth_header(entity_or_subject)
|
11
|
-
return
|
18
|
+
return json_headers if entity_or_subject.blank?
|
12
19
|
|
13
20
|
token = entity_or_subject.try(:to_jwt)
|
14
21
|
token ||= OTP::JWT::Token.sign(sub: entity_or_subject)
|
15
22
|
|
16
|
-
{ 'Authorization': "Bearer #{token}" }
|
23
|
+
{ 'Authorization': "Bearer #{token}" }.merge(json_headers)
|
17
24
|
end
|
18
25
|
|
19
26
|
# Parses and returns a deserialized JSON
|
data/lib/otp/jwt/version.rb
CHANGED
data/spec/dummy.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'active_record/railtie'
|
2
|
+
require 'action_controller/railtie'
|
3
|
+
require 'global_id/railtie'
|
4
|
+
require 'otp/mailer'
|
5
|
+
|
6
|
+
class Dummy < Rails::Application
|
7
|
+
secrets.secret_key_base = '_'
|
8
|
+
|
9
|
+
config.hosts << 'www.example.com' if config.respond_to?(:hosts)
|
10
|
+
|
11
|
+
config.logger = Logger.new($stdout)
|
12
|
+
config.logger.level = ENV['LOG_LEVEL'] || Logger::WARN
|
13
|
+
|
14
|
+
routes.draw do
|
15
|
+
resources :users, only: [:index]
|
16
|
+
resources :tokens, only: [:create]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
GlobalID.app = Dummy
|
21
|
+
Rails.logger = Dummy.config.logger
|
22
|
+
ActiveRecord::Base.logger = Dummy.config.logger
|
23
|
+
ActiveRecord::Base.establish_connection(
|
24
|
+
ENV['DATABASE_URL'] || 'sqlite3::memory:'
|
25
|
+
)
|
26
|
+
|
27
|
+
ActiveRecord::Schema.define do
|
28
|
+
create_table :users, force: true do |t|
|
29
|
+
t.string :email
|
30
|
+
t.string :full_name
|
31
|
+
t.string :phone_number
|
32
|
+
t.string :otp_secret
|
33
|
+
t.integer :otp_counter
|
34
|
+
t.timestamp :last_login_at
|
35
|
+
t.timestamps
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class User < ActiveRecord::Base
|
40
|
+
include GlobalID::Identification
|
41
|
+
include OTP::ActiveRecord
|
42
|
+
include OTP::JWT::ActiveRecord
|
43
|
+
|
44
|
+
def email_otp
|
45
|
+
OTP::Mailer.otp(email, otp, self).deliver_later
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class ApplicationController < ActionController::Base
|
50
|
+
include OTP::JWT::ActionController
|
51
|
+
|
52
|
+
private
|
53
|
+
def current_user
|
54
|
+
@jwt_user ||= User.from_jwt(request_authorization_header)
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_user!
|
58
|
+
current_user || raise('User authentication failed')
|
59
|
+
rescue
|
60
|
+
head(:unauthorized)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class UsersController < ApplicationController
|
65
|
+
before_action :current_user!
|
66
|
+
|
67
|
+
def index
|
68
|
+
render json: current_user
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class TokensController < ApplicationController
|
73
|
+
def create
|
74
|
+
user = User.find_by(email: params[:email])
|
75
|
+
|
76
|
+
jwt_from_otp(user, params[:otp]) do |auth_user|
|
77
|
+
auth_user.update_column(:last_login_at, DateTime.current)
|
78
|
+
|
79
|
+
render json: { token: auth_user.to_jwt }, status: :created
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe OTP::JWT::Token, type: :model do
|
4
|
+
let(:payload) { { 'sub' => FFaker::Internet.password } }
|
5
|
+
let(:token) do
|
6
|
+
JWT.encode(
|
7
|
+
payload.dup.merge(exp: Time.now.to_i + described_class.jwt_lifetime),
|
8
|
+
described_class.jwt_signature_key,
|
9
|
+
described_class.jwt_algorithm
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#sign' do
|
14
|
+
it { expect(described_class.sign(payload)).to eq(token) }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#verify' do
|
18
|
+
it do
|
19
|
+
expect(described_class.verify(token).first).to include(payload)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'with a bad token' do
|
23
|
+
expect { described_class.verify(FFaker::Internet.password) }
|
24
|
+
.to raise_error(JWT::DecodeError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'with an expired token' do
|
28
|
+
token = OTP::JWT::Token.sign(
|
29
|
+
sub: FFaker::Internet.password, exp: DateTime.now.to_i
|
30
|
+
)
|
31
|
+
expect { described_class.verify(token) }
|
32
|
+
.to raise_error(JWT::ExpiredSignature)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#decode' do
|
37
|
+
let(:user) { create_user }
|
38
|
+
let(:payload) { { 'sub' => user.id } }
|
39
|
+
|
40
|
+
it do
|
41
|
+
expect(
|
42
|
+
described_class.decode(token) { |p| User.find(p['sub']) }
|
43
|
+
).to eq(user)
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'with a bad token' do
|
47
|
+
let(:token) { FFaker::Internet.password }
|
48
|
+
|
49
|
+
it do
|
50
|
+
expect(described_class.decode(token)).to eq(nil)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'simplecov'
|
3
|
+
|
4
|
+
SimpleCov.start do
|
5
|
+
add_group 'Lib', 'lib'
|
6
|
+
add_group 'Tests', 'spec'
|
7
|
+
end
|
8
|
+
SimpleCov.minimum_coverage 90
|
9
|
+
|
10
|
+
require 'otp'
|
11
|
+
require 'otp/jwt'
|
12
|
+
require 'otp/jwt/test_helpers'
|
13
|
+
require_relative 'dummy'
|
14
|
+
require 'ffaker'
|
15
|
+
require 'rspec/rails'
|
16
|
+
|
17
|
+
OTP::JWT::Token.jwt_signature_key = '_'
|
18
|
+
OTP::Mailer.default from: '_'
|
19
|
+
ActiveJob::Base.queue_adapter = :test
|
20
|
+
ActionMailer::Base.delivery_method = :test
|
21
|
+
|
22
|
+
module OTP::JWT::FactoryHelpers
|
23
|
+
# Creates an user
|
24
|
+
#
|
25
|
+
# @return [User]
|
26
|
+
def create_user
|
27
|
+
User.create!(
|
28
|
+
full_name: FFaker::Name.name,
|
29
|
+
email: FFaker::Internet.email,
|
30
|
+
phone_number: FFaker::PhoneNumber.phone_number
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Rails4RequestMethods
|
36
|
+
[:get, :post, :put, :delete].each do |method_name|
|
37
|
+
define_method(method_name) do |path, named_args|
|
38
|
+
super(
|
39
|
+
path,
|
40
|
+
named_args.delete(:params),
|
41
|
+
named_args.delete(:headers)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
RSpec.configure do |config|
|
48
|
+
config.use_transactional_fixtures = true
|
49
|
+
config.mock_with :rspec
|
50
|
+
config.filter_run_when_matching :focus
|
51
|
+
config.disable_monkey_patching!
|
52
|
+
|
53
|
+
config.expect_with :rspec do |c|
|
54
|
+
c.syntax = :expect
|
55
|
+
end
|
56
|
+
|
57
|
+
config.include OTP::JWT::TestHelpers, type: :model
|
58
|
+
config.include OTP::JWT::FactoryHelpers, type: :model
|
59
|
+
config.include ActiveJob::TestHelper, type: :model
|
60
|
+
|
61
|
+
config.include OTP::JWT::TestHelpers, type: :request
|
62
|
+
config.include OTP::JWT::FactoryHelpers, type: :request
|
63
|
+
config.include ActiveJob::TestHelper, type: :request
|
64
|
+
config.include Dummy.routes.url_helpers, type: :request
|
65
|
+
|
66
|
+
if ::Rails::VERSION::MAJOR == 4
|
67
|
+
config.include Rails4RequestMethods, type: :request
|
68
|
+
config.include Rails4RequestMethods, type: :controller
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe TokensController, type: :request do
|
4
|
+
let(:user) { create_user }
|
5
|
+
let(:params) { }
|
6
|
+
|
7
|
+
around do |examp|
|
8
|
+
perform_enqueued_jobs(&examp)
|
9
|
+
end
|
10
|
+
|
11
|
+
before do
|
12
|
+
ActionMailer::Base.deliveries.clear
|
13
|
+
ActiveJob::Base.queue_adapter.performed_jobs.clear
|
14
|
+
post(tokens_path, params: params.to_json, headers: json_headers)
|
15
|
+
end
|
16
|
+
|
17
|
+
it { expect(response).to have_http_status(:forbidden) }
|
18
|
+
|
19
|
+
context 'with good email and no otp' do
|
20
|
+
let(:params) { { email: user.email } }
|
21
|
+
|
22
|
+
it do
|
23
|
+
expect(response).to have_http_status(:bad_request)
|
24
|
+
|
25
|
+
mail = ActionMailer::Base.deliveries.last
|
26
|
+
expect(mail.subject).to eq(OTP::Mailer.default[:subject])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with good email and bad otp' do
|
31
|
+
let(:params) { { email: user.email, otp: FFaker::Internet.password } }
|
32
|
+
|
33
|
+
it do
|
34
|
+
expect(response).to have_http_status(:forbidden)
|
35
|
+
expect(ActionMailer::Base.deliveries.size).to eq(0)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with good email and good otp' do
|
40
|
+
let(:params) { { email: user.email, otp: user.otp } }
|
41
|
+
|
42
|
+
it do
|
43
|
+
expect(response).to have_http_status(:created)
|
44
|
+
expect(User.from_jwt(response_json['token'])).to eq(user)
|
45
|
+
expect(ActionMailer::Base.deliveries.size).to eq(0)
|
46
|
+
|
47
|
+
expect(user.reload.last_login_at).not_to be_blank
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/user_spec.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe User, type: :model do
|
4
|
+
let(:user) { create_user }
|
5
|
+
|
6
|
+
it { expect(User.new.otp_secret).not_to be_blank }
|
7
|
+
it { expect(User.new.deliver_otp).to be_blank }
|
8
|
+
it { expect(User.new.otp).to be_blank }
|
9
|
+
|
10
|
+
describe '#from_jwt' do
|
11
|
+
let(:token) { user.to_jwt }
|
12
|
+
|
13
|
+
it do
|
14
|
+
expect(User.from_jwt(token)).to eq(user)
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with a cast-able subject value' do
|
18
|
+
let(:token) { OTP::JWT::Token.sign(sub: user.id.to_s + '_text') }
|
19
|
+
|
20
|
+
it do
|
21
|
+
expect(User.from_jwt(token)).to be_nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with a custom claim name' do
|
26
|
+
let(:claim_value) { FFaker::Internet.password }
|
27
|
+
let(:token) { user.to_jwt(my_claim_name: claim_value) }
|
28
|
+
|
29
|
+
it do
|
30
|
+
expect(OTP::JWT::Token.decode(token)['my_claim_name'])
|
31
|
+
.to eq(claim_value)
|
32
|
+
expect(User.from_jwt(token, 'my_claim_name')).to be_nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#otp' do
|
38
|
+
it do
|
39
|
+
expect { user.otp }.to change(user, :otp_counter).by(1)
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'without a secret' do
|
43
|
+
it do
|
44
|
+
user.update_column(:otp_secret, nil)
|
45
|
+
expect(user.otp).to be_nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#verify_otp' do
|
51
|
+
it 'increments the otp counter after verification' do
|
52
|
+
expect(user.verify_otp(user.otp)).to be_truthy
|
53
|
+
expect { user.verify_otp(user.otp) }.to change(user, :otp_counter).by(2)
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'without a secret' do
|
57
|
+
it do
|
58
|
+
user.update_column(:otp_secret, nil)
|
59
|
+
expect(user.verify_otp(rand(1000..2000).to_s)).to be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe UsersController, type: :request do
|
4
|
+
let(:user) { nil }
|
5
|
+
|
6
|
+
before do
|
7
|
+
get(users_path, headers: jwt_auth_header(user))
|
8
|
+
end
|
9
|
+
|
10
|
+
it { expect(response).to have_http_status(:unauthorized) }
|
11
|
+
|
12
|
+
context 'with known subject token' do
|
13
|
+
let(:user) { create_user }
|
14
|
+
|
15
|
+
it { expect(response).to have_http_status(:ok) }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with bad subject' do
|
19
|
+
let(:user) { FFaker::Internet.password }
|
20
|
+
|
21
|
+
it { expect(response).to have_http_status(:unauthorized) }
|
22
|
+
end
|
23
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: otp-jwt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stas Suscov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -30,28 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2
|
33
|
+
version: '2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2
|
40
|
+
version: '2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rotp
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '6'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '6'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: bundler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
126
|
name: rubocop-performance
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -168,16 +182,30 @@ dependencies:
|
|
168
182
|
name: sqlite3
|
169
183
|
requirement: !ruby/object:Gem::Requirement
|
170
184
|
requirements:
|
171
|
-
- - "
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: tzinfo-data
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
172
200
|
- !ruby/object:Gem::Version
|
173
|
-
version:
|
201
|
+
version: '0'
|
174
202
|
type: :development
|
175
203
|
prerelease: false
|
176
204
|
version_requirements: !ruby/object:Gem::Requirement
|
177
205
|
requirements:
|
178
|
-
- - "
|
206
|
+
- - ">="
|
179
207
|
- !ruby/object:Gem::Version
|
180
|
-
version:
|
208
|
+
version: '0'
|
181
209
|
- !ruby/object:Gem::Dependency
|
182
210
|
name: yardstick
|
183
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -199,15 +227,8 @@ executables: []
|
|
199
227
|
extensions: []
|
200
228
|
extra_rdoc_files: []
|
201
229
|
files:
|
202
|
-
-
|
203
|
-
- ".gitignore"
|
204
|
-
- ".rspec"
|
205
|
-
- ".rubocop.yml"
|
206
|
-
- ".yardstick.yml"
|
207
|
-
- Gemfile
|
208
|
-
- Gemfile.lock
|
230
|
+
- LICENSE.txt
|
209
231
|
- README.md
|
210
|
-
- Rakefile
|
211
232
|
- lib/otp.rb
|
212
233
|
- lib/otp/active_record.rb
|
213
234
|
- lib/otp/jwt.rb
|
@@ -219,10 +240,15 @@ files:
|
|
219
240
|
- lib/otp/mailer.rb
|
220
241
|
- lib/otp/mailer/otp.text.erb
|
221
242
|
- lib/otp/sms_job.rb
|
222
|
-
-
|
243
|
+
- spec/dummy.rb
|
244
|
+
- spec/otp/jwt/token_spec.rb
|
245
|
+
- spec/spec_helper.rb
|
246
|
+
- spec/tokens_controller_spec.rb
|
247
|
+
- spec/user_spec.rb
|
248
|
+
- spec/users_controller_spec.rb
|
223
249
|
homepage: https://github.com/stas/otp-jwt
|
224
250
|
licenses:
|
225
|
-
-
|
251
|
+
- MIT
|
226
252
|
metadata: {}
|
227
253
|
post_install_message:
|
228
254
|
rdoc_options: []
|
@@ -239,8 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
239
265
|
- !ruby/object:Gem::Version
|
240
266
|
version: '0'
|
241
267
|
requirements: []
|
242
|
-
|
243
|
-
rubygems_version: 2.5.2.2
|
268
|
+
rubygems_version: 3.2.3
|
244
269
|
signing_key:
|
245
270
|
specification_version: 4
|
246
271
|
summary: Passwordless HTTP APIs
|
data/.github/main.workflow
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
workflow "Tests" {
|
2
|
-
on = "push"
|
3
|
-
resolves = [
|
4
|
-
"rspec-ruby2.6_rails4",
|
5
|
-
"rspec-ruby2.6_rails5"
|
6
|
-
]
|
7
|
-
}
|
8
|
-
|
9
|
-
action "rspec-ruby2.6_rails4" {
|
10
|
-
uses = "docker://ruby:2.6-alpine"
|
11
|
-
env = {
|
12
|
-
RAILS_VERSION = "~> 4"
|
13
|
-
}
|
14
|
-
args = [
|
15
|
-
"sh", "-c",
|
16
|
-
"apk add -U git build-base sqlite-dev && rm Gemfile.lock && bundle install && rake"
|
17
|
-
]
|
18
|
-
}
|
19
|
-
|
20
|
-
action "rspec-ruby2.6_rails5" {
|
21
|
-
uses = "docker://ruby:2.6-alpine"
|
22
|
-
needs = ["rspec-ruby2.6_rails4"]
|
23
|
-
env = {
|
24
|
-
RAILS_VERSION = "~> 5"
|
25
|
-
}
|
26
|
-
args = [
|
27
|
-
"sh", "-c",
|
28
|
-
"apk add -U git build-base sqlite-dev && rm Gemfile.lock && bundle install && rake"
|
29
|
-
]
|
30
|
-
}
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.rubocop.yml
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
inherit_gem:
|
2
|
-
rubocop-rails_config:
|
3
|
-
- config/rails.yml
|
4
|
-
|
5
|
-
require:
|
6
|
-
- rubocop-performance
|
7
|
-
- rubocop-rspec
|
8
|
-
|
9
|
-
Rails:
|
10
|
-
Enabled: true
|
11
|
-
|
12
|
-
RSpec:
|
13
|
-
Enabled: true
|
14
|
-
|
15
|
-
RSpec/MultipleExpectations:
|
16
|
-
Enabled: false
|
17
|
-
|
18
|
-
Performance:
|
19
|
-
Enabled: true
|
20
|
-
|
21
|
-
Bundler:
|
22
|
-
Enabled: true
|
23
|
-
|
24
|
-
Gemspec:
|
25
|
-
Enabled: true
|
26
|
-
|
27
|
-
Style/StringLiterals:
|
28
|
-
Enabled: true
|
29
|
-
EnforcedStyle: single_quotes
|
30
|
-
|
31
|
-
Style/FrozenStringLiteralComment:
|
32
|
-
Enabled: false
|
33
|
-
|
34
|
-
Metrics/LineLength:
|
35
|
-
Max: 80
|
36
|
-
|
37
|
-
Metrics/BlockLength:
|
38
|
-
Exclude:
|
39
|
-
- 'spec/**/*_spec.rb'
|
40
|
-
- '**/*.gemspec'
|
41
|
-
|
42
|
-
Layout/IndentationConsistency:
|
43
|
-
EnforcedStyle: normal
|
44
|
-
|
45
|
-
Style/BlockDelimiters:
|
46
|
-
Enabled: true
|
data/.yardstick.yml
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
---
|
2
|
-
path: ['lib/**/*.rb']
|
3
|
-
threshold: 100
|
4
|
-
rules:
|
5
|
-
ApiTag::Presence:
|
6
|
-
enabled: false
|
7
|
-
ApiTag::Inclusion:
|
8
|
-
enabled: false
|
9
|
-
ApiTag::ProtectedMethod:
|
10
|
-
enabled: false
|
11
|
-
ApiTag::PrivateMethod:
|
12
|
-
enabled: false
|
13
|
-
ExampleTag:
|
14
|
-
enabled: false
|
15
|
-
ReturnTag:
|
16
|
-
enabled: true
|
17
|
-
exclude: []
|
18
|
-
Summary::Presence:
|
19
|
-
enabled: true
|
20
|
-
exclude: []
|
21
|
-
Summary::Length:
|
22
|
-
enabled: true
|
23
|
-
exclude: []
|
24
|
-
Summary::Delimiter:
|
25
|
-
enabled: true
|
26
|
-
exclude: []
|
27
|
-
Summary::SingleLine:
|
28
|
-
enabled: true
|
29
|
-
exclude: []
|
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
@@ -1,197 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
otp-jwt (0.2.2)
|
5
|
-
activesupport
|
6
|
-
jwt (~> 2.2.0.pre.beta.0)
|
7
|
-
rotp (~> 4.1)
|
8
|
-
|
9
|
-
GEM
|
10
|
-
remote: https://rubygems.org/
|
11
|
-
specs:
|
12
|
-
actioncable (5.2.3)
|
13
|
-
actionpack (= 5.2.3)
|
14
|
-
nio4r (~> 2.0)
|
15
|
-
websocket-driver (>= 0.6.1)
|
16
|
-
actionmailer (5.2.3)
|
17
|
-
actionpack (= 5.2.3)
|
18
|
-
actionview (= 5.2.3)
|
19
|
-
activejob (= 5.2.3)
|
20
|
-
mail (~> 2.5, >= 2.5.4)
|
21
|
-
rails-dom-testing (~> 2.0)
|
22
|
-
actionpack (5.2.3)
|
23
|
-
actionview (= 5.2.3)
|
24
|
-
activesupport (= 5.2.3)
|
25
|
-
rack (~> 2.0)
|
26
|
-
rack-test (>= 0.6.3)
|
27
|
-
rails-dom-testing (~> 2.0)
|
28
|
-
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
29
|
-
actionview (5.2.3)
|
30
|
-
activesupport (= 5.2.3)
|
31
|
-
builder (~> 3.1)
|
32
|
-
erubi (~> 1.4)
|
33
|
-
rails-dom-testing (~> 2.0)
|
34
|
-
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
35
|
-
activejob (5.2.3)
|
36
|
-
activesupport (= 5.2.3)
|
37
|
-
globalid (>= 0.3.6)
|
38
|
-
activemodel (5.2.3)
|
39
|
-
activesupport (= 5.2.3)
|
40
|
-
activerecord (5.2.3)
|
41
|
-
activemodel (= 5.2.3)
|
42
|
-
activesupport (= 5.2.3)
|
43
|
-
arel (>= 9.0)
|
44
|
-
activestorage (5.2.3)
|
45
|
-
actionpack (= 5.2.3)
|
46
|
-
activerecord (= 5.2.3)
|
47
|
-
marcel (~> 0.3.1)
|
48
|
-
activesupport (5.2.3)
|
49
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
50
|
-
i18n (>= 0.7, < 2)
|
51
|
-
minitest (~> 5.1)
|
52
|
-
tzinfo (~> 1.1)
|
53
|
-
addressable (2.6.0)
|
54
|
-
public_suffix (>= 2.0.2, < 4.0)
|
55
|
-
arel (9.0.0)
|
56
|
-
ast (2.4.0)
|
57
|
-
builder (3.2.3)
|
58
|
-
concurrent-ruby (1.1.5)
|
59
|
-
crass (1.0.4)
|
60
|
-
diff-lcs (1.3)
|
61
|
-
docile (1.3.1)
|
62
|
-
erubi (1.8.0)
|
63
|
-
ffaker (2.10.0)
|
64
|
-
globalid (0.4.2)
|
65
|
-
activesupport (>= 4.2.0)
|
66
|
-
i18n (1.6.0)
|
67
|
-
concurrent-ruby (~> 1.0)
|
68
|
-
jaro_winkler (1.5.2)
|
69
|
-
json (2.2.0)
|
70
|
-
jwt (2.2.0.pre.beta.0)
|
71
|
-
loofah (2.2.3)
|
72
|
-
crass (~> 1.0.2)
|
73
|
-
nokogiri (>= 1.5.9)
|
74
|
-
mail (2.7.1)
|
75
|
-
mini_mime (>= 0.1.1)
|
76
|
-
marcel (0.3.3)
|
77
|
-
mimemagic (~> 0.3.2)
|
78
|
-
method_source (0.9.2)
|
79
|
-
mimemagic (0.3.3)
|
80
|
-
mini_mime (1.0.1)
|
81
|
-
mini_portile2 (2.4.0)
|
82
|
-
minitest (5.11.3)
|
83
|
-
nio4r (2.3.1)
|
84
|
-
nokogiri (1.10.2)
|
85
|
-
mini_portile2 (~> 2.4.0)
|
86
|
-
parallel (1.16.2)
|
87
|
-
parser (2.6.2.0)
|
88
|
-
ast (~> 2.4.0)
|
89
|
-
psych (3.1.0)
|
90
|
-
public_suffix (3.0.3)
|
91
|
-
rack (2.0.6)
|
92
|
-
rack-test (1.1.0)
|
93
|
-
rack (>= 1.0, < 3)
|
94
|
-
rails (5.2.3)
|
95
|
-
actioncable (= 5.2.3)
|
96
|
-
actionmailer (= 5.2.3)
|
97
|
-
actionpack (= 5.2.3)
|
98
|
-
actionview (= 5.2.3)
|
99
|
-
activejob (= 5.2.3)
|
100
|
-
activemodel (= 5.2.3)
|
101
|
-
activerecord (= 5.2.3)
|
102
|
-
activestorage (= 5.2.3)
|
103
|
-
activesupport (= 5.2.3)
|
104
|
-
bundler (>= 1.3.0)
|
105
|
-
railties (= 5.2.3)
|
106
|
-
sprockets-rails (>= 2.0.0)
|
107
|
-
rails-dom-testing (2.0.3)
|
108
|
-
activesupport (>= 4.2.0)
|
109
|
-
nokogiri (>= 1.6)
|
110
|
-
rails-html-sanitizer (1.0.4)
|
111
|
-
loofah (~> 2.2, >= 2.2.2)
|
112
|
-
railties (5.2.3)
|
113
|
-
actionpack (= 5.2.3)
|
114
|
-
activesupport (= 5.2.3)
|
115
|
-
method_source
|
116
|
-
rake (>= 0.8.7)
|
117
|
-
thor (>= 0.19.0, < 2.0)
|
118
|
-
rainbow (3.0.0)
|
119
|
-
rake (12.3.2)
|
120
|
-
rotp (4.1.0)
|
121
|
-
addressable (~> 2.5)
|
122
|
-
rspec-core (3.8.0)
|
123
|
-
rspec-support (~> 3.8.0)
|
124
|
-
rspec-expectations (3.8.2)
|
125
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
126
|
-
rspec-support (~> 3.8.0)
|
127
|
-
rspec-mocks (3.8.0)
|
128
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
129
|
-
rspec-support (~> 3.8.0)
|
130
|
-
rspec-rails (3.8.2)
|
131
|
-
actionpack (>= 3.0)
|
132
|
-
activesupport (>= 3.0)
|
133
|
-
railties (>= 3.0)
|
134
|
-
rspec-core (~> 3.8.0)
|
135
|
-
rspec-expectations (~> 3.8.0)
|
136
|
-
rspec-mocks (~> 3.8.0)
|
137
|
-
rspec-support (~> 3.8.0)
|
138
|
-
rspec-support (3.8.0)
|
139
|
-
rubocop (0.66.0)
|
140
|
-
jaro_winkler (~> 1.5.1)
|
141
|
-
parallel (~> 1.10)
|
142
|
-
parser (>= 2.5, != 2.5.1.1)
|
143
|
-
psych (>= 3.1.0)
|
144
|
-
rainbow (>= 2.2.2, < 4.0)
|
145
|
-
ruby-progressbar (~> 1.7)
|
146
|
-
unicode-display_width (>= 1.4.0, < 1.6)
|
147
|
-
rubocop-performance (1.0.0)
|
148
|
-
rubocop (>= 0.58.0)
|
149
|
-
rubocop-rails_config (0.4.4)
|
150
|
-
railties (>= 3.0)
|
151
|
-
rubocop (~> 0.58)
|
152
|
-
rubocop-rspec (1.32.0)
|
153
|
-
rubocop (>= 0.60.0)
|
154
|
-
ruby-progressbar (1.10.0)
|
155
|
-
simplecov (0.16.1)
|
156
|
-
docile (~> 1.1)
|
157
|
-
json (>= 1.8, < 3)
|
158
|
-
simplecov-html (~> 0.10.0)
|
159
|
-
simplecov-html (0.10.2)
|
160
|
-
sprockets (3.7.2)
|
161
|
-
concurrent-ruby (~> 1.0)
|
162
|
-
rack (> 1, < 3)
|
163
|
-
sprockets-rails (3.2.1)
|
164
|
-
actionpack (>= 4.0)
|
165
|
-
activesupport (>= 4.0)
|
166
|
-
sprockets (>= 3.0.0)
|
167
|
-
sqlite3 (1.3.13)
|
168
|
-
thor (0.20.3)
|
169
|
-
thread_safe (0.3.6)
|
170
|
-
tzinfo (1.2.5)
|
171
|
-
thread_safe (~> 0.1)
|
172
|
-
unicode-display_width (1.5.0)
|
173
|
-
websocket-driver (0.7.0)
|
174
|
-
websocket-extensions (>= 0.1.0)
|
175
|
-
websocket-extensions (0.1.3)
|
176
|
-
yard (0.9.18)
|
177
|
-
yardstick (0.9.9)
|
178
|
-
yard (~> 0.8, >= 0.8.7.2)
|
179
|
-
|
180
|
-
PLATFORMS
|
181
|
-
ruby
|
182
|
-
|
183
|
-
DEPENDENCIES
|
184
|
-
bundler
|
185
|
-
ffaker
|
186
|
-
otp-jwt!
|
187
|
-
rails
|
188
|
-
rspec-rails
|
189
|
-
rubocop-performance
|
190
|
-
rubocop-rails_config
|
191
|
-
rubocop-rspec
|
192
|
-
simplecov
|
193
|
-
sqlite3 (~> 1.3.6)
|
194
|
-
yardstick
|
195
|
-
|
196
|
-
BUNDLED WITH
|
197
|
-
1.17.3
|
data/Rakefile
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
require 'bundler/gem_tasks'
|
2
|
-
require 'rspec/core/rake_task'
|
3
|
-
require 'rubocop/rake_task'
|
4
|
-
require 'yaml'
|
5
|
-
require 'yardstick'
|
6
|
-
|
7
|
-
desc('Documentation stats and measurements')
|
8
|
-
task('qa:docs') do
|
9
|
-
yaml = YAML.load_file(File.expand_path('../.yardstick.yml', __FILE__))
|
10
|
-
config = Yardstick::Config.coerce(yaml)
|
11
|
-
measure = Yardstick.measure(config)
|
12
|
-
measure.puts
|
13
|
-
coverage = Yardstick.round_percentage(measure.coverage * 100)
|
14
|
-
exit(1) if coverage < config.threshold
|
15
|
-
end
|
16
|
-
|
17
|
-
desc('Codestyle check and linter')
|
18
|
-
RuboCop::RakeTask.new('qa:code') do |task|
|
19
|
-
task.fail_on_error = true
|
20
|
-
task.patterns = [
|
21
|
-
'lib/**/*.rb',
|
22
|
-
'spec/**/*.rb'
|
23
|
-
]
|
24
|
-
end
|
25
|
-
|
26
|
-
desc('Run CI QA tasks')
|
27
|
-
task(qa: ['qa:docs', 'qa:code'])
|
28
|
-
|
29
|
-
RSpec::Core::RakeTask.new(spec: :qa)
|
30
|
-
task(default: :spec)
|
data/otp-jwt.gemspec
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
lib = File.expand_path('../lib', __FILE__)
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
|
4
|
-
require 'otp/jwt/version'
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = 'otp-jwt'
|
8
|
-
spec.version = OTP::JWT::VERSION
|
9
|
-
spec.authors = ['Stas Suscov']
|
10
|
-
spec.email = ['stas@nerd.ro']
|
11
|
-
|
12
|
-
spec.summary = 'Passwordless HTTP APIs'
|
13
|
-
spec.description = 'OTP (email, SMS) JWT authentication for HTTP APIs.'
|
14
|
-
spec.homepage = 'https://github.com/stas/otp-jwt'
|
15
|
-
spec.license = 'TBD'
|
16
|
-
|
17
|
-
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
18
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
|
19
|
-
end
|
20
|
-
spec.require_paths = ['lib']
|
21
|
-
|
22
|
-
spec.add_dependency 'activesupport'
|
23
|
-
spec.add_dependency 'jwt', '~> 2.2.0.pre.beta.0'
|
24
|
-
spec.add_dependency 'rotp', '~> 4.1'
|
25
|
-
|
26
|
-
spec.add_development_dependency 'bundler'
|
27
|
-
spec.add_development_dependency 'ffaker'
|
28
|
-
spec.add_development_dependency 'rails'
|
29
|
-
spec.add_development_dependency 'rspec-rails'
|
30
|
-
spec.add_development_dependency 'rubocop-performance'
|
31
|
-
spec.add_development_dependency 'rubocop-rails_config'
|
32
|
-
spec.add_development_dependency 'rubocop-rspec'
|
33
|
-
spec.add_development_dependency 'simplecov'
|
34
|
-
spec.add_development_dependency 'sqlite3', '~> 1.3.6'
|
35
|
-
spec.add_development_dependency 'yardstick'
|
36
|
-
end
|