devision 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43c30705818f3d105f21c7270c92e7f1824225b0
4
+ data.tar.gz: aa8bb3b3b720f5336b42f13e7ff1ad18450c76cd
5
+ SHA512:
6
+ metadata.gz: c9da084babbbe7878ca794570626f27b8d4b15d02b91c1b209be41de3edcca642809bc0ad96fc908abfd1752a9628e75cb7f1517c06a916850853beabf974bfc
7
+ data.tar.gz: 73f23e9e1aeb2f8c8bcabd1444a9c97dabe0745938e641fdc099e2b3c37ca5b340a5c7e729218a3031c260cbad66b6615892de251bbfcc783ae01cf3f2e4930f
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ *.sw[op]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devision.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 John Faucett
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Devision
2
+
3
+ This is a gem that is heavily based off of Devise, think of it as Devise's little cousin.
4
+ Basically, it provides a lot of the functionality but doesn't make many assumptions about
5
+ how you want to want to structure your app, especially on the session handling end.
6
+
7
+ Here's some of the things it does not provide.
8
+ 1. No routing
9
+ 2. No automatic mailing or setup for mailers
10
+ 3. No controller helpers and session/cookie stuff.
11
+
12
+ So what does it provide, is probably your next question. It basically covers just the domain
13
+ model end, things like reset_password, etc.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'devision'
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install devision
28
+
29
+ ## Usage
30
+
31
+ TODO: Write usage instructions here
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it ( https://github.com/[my-github-username]/devision/fork )
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/devision.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $:.unshift(lib) unless $:.include?(lib)
4
+ require 'devision/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'devision'
8
+ spec.version = Devision::VERSION::STRING
9
+ spec.authors = ['John Faucett']
10
+ spec.email = ['jwaterfaucett@gmail.com']
11
+ spec.summary = 'Devise on a Diet'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency 'bundler', '~> 1.6'
20
+ spec.add_development_dependency 'rake'
21
+
22
+ spec.add_development_dependency 'rspec', '~> 3.0.0'
23
+
24
+ spec.add_dependency 'railties', '>= 3.2.6', '< 5'
25
+ spec.add_dependency 'orm_adapter', '~> 0.5'
26
+ spec.add_dependency 'bcrypt', '~> 3.1'
27
+ spec.add_dependency 'thread_safe', '~> 0.3'
28
+
29
+ end
data/lib/devision.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'active_support'
2
+ require 'orm_adapter'
3
+ require 'active_support/core_ext/numeric/time'
4
+ require 'securerandom'
5
+
6
+ module Devision
7
+ autoload :TokenGenerator, 'devision/token_generator'
8
+
9
+ module Models
10
+ autoload :Config, 'devision/models/config'
11
+ autoload :Confirmable, 'devision/models/confirmable'
12
+ autoload :DatabaseAuthenticatable, 'devision/models/database_authenticatable'
13
+ autoload :Recoverable, 'devision/models/recoverable'
14
+ autoload :Rememberable, 'devision/models/rememberable'
15
+ end
16
+
17
+ # Devision configuration settings (for initializers)
18
+ require 'devision/configuration_options'
19
+
20
+
21
+ # Generate a user friendly 'nice' string randomly to be used as token.
22
+ def self.nice_token
23
+ SecureRandom.urlsafe_base64(15).tr('lIO0', 'sxyz')
24
+ end
25
+
26
+ # constant-time comparison algorithm to prevent timing attacks
27
+ def self.secure_compare(a, b)
28
+ return false if a.blank? || b.blank? || a.bytesize != b.bytesize
29
+ l = a.unpack "C#{a.bytesize}"
30
+
31
+ res = 0
32
+ b.each_byte { |byte| res |= byte ^ l.shift }
33
+ res == 0
34
+ end
35
+
36
+ # Digests a password using bcrypt.
37
+ def self.bcrypt(klass, password)
38
+ ::BCrypt::Password.create("#{password}#{klass.pepper}", cost: klass.stretches).to_s
39
+ end
40
+
41
+ # Default way to setup Devision. Just call this in an initializer
42
+ def self.setup
43
+ yield(self)
44
+ end
45
+
46
+
47
+ end
@@ -0,0 +1,118 @@
1
+ module Devision
2
+
3
+ # =========================
4
+ # AUTHENTICATABLE OPTIONS
5
+ # =========================
6
+
7
+ # Keys used when authenticating a user.
8
+ mattr_accessor :authentication_keys
9
+ @@authentication_keys = [ :email ]
10
+
11
+ # =========================
12
+ # RECOVERABLE OPTIONS
13
+ # =========================
14
+
15
+ # Defines which key will be used when recovering the password for an account
16
+ mattr_accessor :reset_password_keys
17
+ @@reset_password_keys = [ :email ]
18
+
19
+ # Time interval you can reset your password with a reset password key
20
+ mattr_accessor :reset_password_within
21
+ @@reset_password_within = 6.hours
22
+
23
+
24
+ # Email regex used to validate email formats. It simply asserts that
25
+ # an one (and only one) @ exists in the given string. This is mainly
26
+ # to give user feedback and not to assert the e-mail validity.
27
+ mattr_accessor :email_regexp
28
+ @@email_regexp = /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
29
+
30
+ # Range validation for password length
31
+ mattr_accessor :password_length
32
+ @@password_length = 6..128
33
+
34
+ # ======================
35
+ # REMEMBERABLE OPTIONS
36
+ # ======================
37
+
38
+ # Custom domain or key for cookies. Not set by default
39
+ # mattr_accessor :rememberable_options
40
+ # @@rememberable_options = {}
41
+
42
+ # The time the user will be remembered without asking for credentials again.
43
+ mattr_accessor :remember_for
44
+ @@remember_for = 2.weeks
45
+
46
+ # If true, extends the user's remember period when remembered via cookie.
47
+ mattr_accessor :extend_remember_period
48
+ @@extend_remember_period = false
49
+
50
+ # ========================
51
+ # CONFIRMABLE OPTIONS
52
+ # ========================
53
+
54
+ # Time interval you can access your account before confirming your account.
55
+ # nil - allows unconfirmed access for unlimited time
56
+ mattr_accessor :allow_unconfirmed_access_for
57
+ @@allow_unconfirmed_access_for = 0.days
58
+
59
+ # Time interval the confirmation token is valid. nil = unlimited
60
+ mattr_accessor :confirm_within
61
+ @@confirm_within = nil
62
+
63
+ # Defines which key will be used when confirming an account.
64
+ mattr_accessor :confirmation_keys
65
+ @@confirmation_keys = [ :email ]
66
+
67
+ # Defines if email should be reconfirmable.
68
+ # False by default for backwards compatibility.
69
+ mattr_accessor :reconfirmable
70
+ @@reconfirmable = false
71
+
72
+ # ====================
73
+ # LOCKABLE OPTIONS
74
+ # ====================
75
+
76
+ # Defines which strategy can be used to lock an account.
77
+ # Values: :failed_attempts, :none
78
+ mattr_accessor :lock_strategy
79
+ @@lock_strategy = :failed_attempts
80
+
81
+ # Defines which key will be used when locking and unlocking an account
82
+ mattr_accessor :unlock_keys
83
+ @@unlock_keys = [ :email ]
84
+
85
+ # Defines which strategy can be used to unlock an account.
86
+ # Values: :email, :time, :both
87
+ mattr_accessor :unlock_strategy
88
+ @@unlock_strategy = :both
89
+
90
+ # Number of authentication tries before locking an account
91
+ mattr_accessor :maximum_attempts
92
+ @@maximum_attempts = 20
93
+
94
+ # Time interval to unlock the account if :time is defined as unlock_strategy.
95
+ mattr_accessor :unlock_in
96
+ @@unlock_in = 1.hour
97
+
98
+ # ======================
99
+ # ENCRYPTION OPTIONS
100
+ # ======================
101
+
102
+ # Stores the token generator
103
+ mattr_accessor :token_generator
104
+ @@token_generator = nil
105
+
106
+ # Secret key used by the key generator
107
+ mattr_accessor :secret_key
108
+ @@secret_key = nil
109
+
110
+ # The number of times to encrypt password.
111
+ mattr_accessor :stretches
112
+ @@stretches = 10
113
+
114
+ # Used to encrypt password. Please generate one with rake secret.
115
+ mattr_accessor :pepper
116
+ @@pepper = nil
117
+
118
+ end
@@ -0,0 +1,44 @@
1
+ module Devision
2
+ module Authenticatable
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.required_fields(klass)
7
+ []
8
+ end
9
+
10
+
11
+ module ClassMethods
12
+
13
+ def find_first_by_auth_conditions(tainted_conditions, options = {})
14
+ to_adapter.find_first(tainted_conditions.merge(options))
15
+ end
16
+
17
+ def find_or_initialize_with_error_by(attribute, value, error=:invalid)
18
+ find_or_initialize_with_errors([attribute], { attribute => value }, error)
19
+ end
20
+
21
+ def find_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
22
+ attributes = attributes.slice(*required_attributes)
23
+ attributes.delete_if { |key, value| value.blank? }
24
+
25
+ if attributes.size == required_attributes.size
26
+ record = find_first_by_auth_conditions(attributes)
27
+ end
28
+
29
+ unless record
30
+ record = new
31
+ required_attributes.each do |key|
32
+ value = attributes[key]
33
+ record.public_send("#{key}=", value)
34
+ record.errors.add(key, value.present? ? error : :blank)
35
+ end
36
+ end
37
+
38
+ record
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ module Devision
2
+ module Models
3
+
4
+ module Config
5
+
6
+ def self.add_options(mod, *accessors)
7
+ class << mod; attr_accessor :available_configs; end
8
+ mod.available_configs = accessors
9
+
10
+ accessors.each do |accessor|
11
+ mod.class_eval <<-METHOD, __FILE__, __LINE__ + 1
12
+ def #{accessor}
13
+ if defined?(@#{accessor})
14
+ @#{accessor}
15
+ elsif superclass.respond_to?(:#{accessor})
16
+ superclass.#{accessor}
17
+ else
18
+ Devise.#{accessor}
19
+ end
20
+ end
21
+
22
+ def #{accessor}=(value)
23
+ @#{accessor} = value
24
+ end
25
+ METHOD
26
+ end
27
+ end
28
+
29
+ end # Config
30
+
31
+ end # Models
32
+ end # Devision
@@ -0,0 +1,86 @@
1
+ module Devision
2
+ module Models
3
+ module Confirmable
4
+ extend ActiveSupport::Concern
5
+
6
+ # Fields required on the target Model
7
+ def self.required_fields(klass)
8
+ [:confirmation_token, :confirmed_at, :confirmation_sent_at]
9
+ end
10
+
11
+ included do
12
+
13
+ attr_reader :raw_confirmation_token
14
+
15
+ def confirm!
16
+ pending_any_confirmation do
17
+ if confirmation_period_expired?
18
+ self.errors.add(:email, :confirmation_period_expired)
19
+ return false
20
+ end
21
+ self.confirmation_token = nil
22
+ self.confirmed_at = Time.now.utc
23
+ save(validate: false)
24
+ end
25
+ end
26
+
27
+ def confirmed?
28
+ !!confirmed_at
29
+ end
30
+
31
+ def generate_confirmation_tokens
32
+ raw, enc = Devision.token_generator.generate(self.class, :confirmation_token)
33
+ @raw_confirmation_token = raw
34
+ self.confirmation_token = enc
35
+ self.confirmation_sent_at = Time.now.utc
36
+ end
37
+
38
+ def generate_confirmation_tokens!
39
+ generate_confirmation_tokens && save(validate: false)
40
+ end
41
+
42
+ def clear_confirmation_token
43
+ @raw_confirmation_token = nil
44
+ self.confirmation_token = nil
45
+ self.confirmation_sent_at = nil
46
+ end
47
+
48
+ # Checks whether the record requires any confirmation.
49
+ def pending_any_confirmation
50
+ if !confirmed?
51
+ yield
52
+ else
53
+ self.errors.add(:email, :already_confirmed)
54
+ false
55
+ end
56
+ end
57
+
58
+ def confirmation_period_expired?
59
+ self.class.confirm_within && (Time.now > self.confirmation_sent_at + self.class.confirm_within )
60
+ end
61
+
62
+ def confirmation_period_valid?
63
+ self.class.allow_unconfirmed_access_for.nil? || (confirmation_sent_at && confirmation_sent_at.utc >= self.class.allow_unconfirmed_access_for.ago)
64
+ end
65
+
66
+ end
67
+
68
+ module ClassMethods
69
+
70
+ def confirm_by_token(raw_confirmation_token)
71
+ original_token = raw_confirmation_token
72
+ saved_token = Devision.token_generator.digest(self, :confirmation_token, original_token)
73
+
74
+ confirmable = find_or_initialize_by(confirmation_token: saved_token)
75
+ confirmable.confirm! if confirmable.persisted?
76
+ confirmable.confirmation_token = original_token
77
+ confirmable
78
+ end
79
+
80
+ Devision::Models::Config.add_options(self, :allow_unconfirmed_access_for, :confirmation_keys, :reconfirmable, :confirm_within)
81
+ end
82
+
83
+
84
+ end # Confirmable
85
+ end # Models
86
+ end # Devision
@@ -0,0 +1,81 @@
1
+ require 'bcrypt'
2
+
3
+ module Devision
4
+
5
+ module Models
6
+ # Authenticatable Module, responsible for encrypting password and validating
7
+ # authenticity of a user while signing in.
8
+ #
9
+ # == Options
10
+ #
11
+ # == Examples
12
+ #
13
+ # User.find(1).valid_password?('password123') # returns true/false
14
+ #
15
+ module DatabaseAuthenticatable
16
+ extend ActiveSupport::Concern
17
+
18
+ # Fields required on the target Model
19
+ def self.required_fields(klass)
20
+ [:encrypted_password] + klass.authentication_keys
21
+ end
22
+
23
+ included do
24
+ attr_reader :password, :current_password
25
+ attr_accessor :password_confirmation
26
+ end
27
+
28
+ # Generates password encryption based on the given value.
29
+ def password=(new_password)
30
+ @password = new_password
31
+ self.encrypted_password = password_digest(@password) if @password.present?
32
+ end
33
+
34
+ # Verifies whether an password (ie from sign in) is the user password.
35
+ def valid_password?(password)
36
+ return false if encrypted_password.blank?
37
+ bcrypt = ::BCrypt::Password.new(encrypted_password)
38
+ password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt)
39
+ Devision.secure_compare(password, encrypted_password)
40
+ end
41
+
42
+ # Set password and password confirmation to nil
43
+ def clean_up_passwords
44
+ self.password = self.password_confirmation = nil
45
+ end
46
+
47
+ # A callback initiated after successfully authenticating. This can be
48
+ # used to insert your own logic that is only run after the user successfully
49
+ # authenticates.
50
+ #
51
+ # Example:
52
+ #
53
+ # def after_database_authentication
54
+ # self.update_attribute(:invite_code, nil)
55
+ # end
56
+ #
57
+ def after_database_authentication
58
+ end
59
+
60
+ # A reliable way to expose the salt regardless of the implementation.
61
+ def authenticatable_salt
62
+ encrypted_password[0,29] if encrypted_password
63
+ end
64
+
65
+ protected
66
+
67
+ # Digests the password using bcrypt. Custom encryption should override
68
+ # this method to apply their own algorithm.
69
+ #
70
+ # See https://github.com/plataformatec/devise-encryptable for examples
71
+ # of other encryption engines.
72
+ def password_digest(password)
73
+ Devision.bcrypt(self.class, password)
74
+ end
75
+
76
+ module ClassMethods
77
+ Devision::Models::Config.add_options(self, :pepper, :stretches)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,67 @@
1
+ module Devision
2
+ module Lockable
3
+ extend ActiveSupport::Concern
4
+
5
+ delegate :lock_strategy_enabled?, :unlock_strategy_enabled?, to: 'self.class'
6
+
7
+ def self.required_fields(klass)
8
+ attributes = []
9
+ attributes << :failed_attempts if klass.lock_strategy_enabled?(:failed_attempts)
10
+ attributes << :locked_at if klass.unlock_strategy_enabled?(:time)
11
+ attributes << :unlock_token if klass.unlock_strategy_enabled?(:email)
12
+ attributes
13
+ end
14
+
15
+ def lock_access!
16
+ self.locked_at = Time.now.utc
17
+ save(validate: false)
18
+ end
19
+
20
+ def unlock_access!
21
+ self.locked_at = nil
22
+ self.failed_attempts = 0 if respond_to?(:failed_attempts=)
23
+ self.unlock_token = nil if respond_to?(:unlock_token=)
24
+ save(validate: false)
25
+ end
26
+
27
+ def access_locked?
28
+ !!locked_at && !lock_expired?
29
+ end
30
+
31
+ protected
32
+ def attempts_exceeded?
33
+ self.failed_attempts >= self.class.maximum_attempts
34
+ end
35
+
36
+ def last_attempt?
37
+ self.failed_attempts == (self.class.maximum_attempts - 1)
38
+ end
39
+
40
+ def lock_expired?
41
+ if unlock_strategy_enabled?(:time)
42
+ locked_at && locked_at < self.class.unlock_in.ago
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ def unlock_access_by_token(unlock_token)
50
+ original_token = unlock_token
51
+ unlock_token = Devision.token_generator.digest(self, :unlock_token, unlock_token)
52
+
53
+ lockable = find_or_initialize_with_error_by(:unlock_token, unlock_token)
54
+ lockable.unlock_access! if lockable.persisted?
55
+ lockable.unlock_token = original_token
56
+ lockable
57
+ end
58
+
59
+ def unlock_strategy_enabled?(strategy)
60
+ self.lock_strategy == strategy
61
+ end
62
+
63
+ Devision::Models::Config.add_options(self, :maximum_attempts, :lock_strategy, :unlock_strategy, :unlock_in, :unlock_keys)
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,137 @@
1
+ module Devision
2
+ module Models
3
+
4
+ # Recoverable takes care of resetting the user password and send reset instructions.
5
+ #
6
+ # ==Options
7
+ #
8
+ # Recoverable adds the following options to a model
9
+ #
10
+ # * +reset_password_keys+: the keys you want to use when recovering the password for an account
11
+ #
12
+ # == Examples
13
+ #
14
+ # # resets the user password and save the record, true if valid passwords are given, otherwise false
15
+ # User.find(1).reset_password!('password123', 'password123')
16
+ #
17
+ # # only resets the user password, without saving the record
18
+ # user = User.find(1)
19
+ # user.reset_password('password123', 'password123')
20
+ #
21
+ # # creates a new token and send it with instructions about how to reset the password
22
+ # User.find(1).send_reset_password_instructions
23
+ #
24
+ module Recoverable
25
+ extend ActiveSupport::Concern
26
+
27
+ def self.required_fields(klass)
28
+ [:reset_password_sent_at, :reset_password_token]
29
+ end
30
+
31
+ included do
32
+ attr_accessor :raw_reset_password_token
33
+ end
34
+
35
+ # Update password saving the record and clearing token. Returns true if
36
+ # the passwords are valid and the record was saved, false otherwise.
37
+ def reset_password!(new_password, new_password_confirmation)
38
+ self.password = new_password
39
+ self.password_confirmation = new_password_confirmation
40
+
41
+ if valid?
42
+ clear_reset_password_token
43
+ after_password_reset
44
+ end
45
+
46
+ save
47
+ end
48
+
49
+ # Resets reset password token and send reset password instructions by email.
50
+ # Returns the token which can then be sent via email
51
+ def generate_password_tokens
52
+ @raw_password_reset_token, enc = Devision.token_generator.generate(self.class, :reset_password_token)
53
+
54
+ self.reset_password_token = enc
55
+ self.reset_password_sent_at = Time.now.utc
56
+ @raw_password_reset_token
57
+ end
58
+
59
+ def generate_password_tokens!
60
+ generate_password_tokens && save(validate: false)
61
+ end
62
+
63
+ # Checks if the reset password token sent is within the limit time.
64
+ # We do this by calculating if the difference between today and the
65
+ # sending date does not exceed the confirm in time configured.
66
+ # Returns true if the resource is not responding to reset_password_sent_at at all.
67
+ # reset_password_within is a model configuration, must always be an integer value.
68
+ #
69
+ # Example:
70
+ #
71
+ # # reset_password_within = 1.day and reset_password_sent_at = today
72
+ # reset_password_period_valid? # returns true
73
+ #
74
+ # # reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
75
+ # reset_password_period_valid? # returns true
76
+ #
77
+ # # reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
78
+ # reset_password_period_valid? # returns false
79
+ #
80
+ # # reset_password_within = 0.days
81
+ # reset_password_period_valid? # will always return false
82
+ #
83
+ def reset_password_period_valid?
84
+ reset_password_sent_at && reset_password_sent_at.utc >= self.class.reset_password_within.ago
85
+ end
86
+
87
+ protected
88
+
89
+ # Removes reset_password token
90
+ def clear_reset_password_token
91
+ self.raw_reset_password_token = nil
92
+ self.reset_password_token = nil
93
+ self.reset_password_sent_at = nil
94
+ end
95
+
96
+ def after_password_reset
97
+ end
98
+
99
+ module ClassMethods
100
+ # Attempt to find a user by its email. If a record is found, send new
101
+ # password instructions to it. If user is not found, returns a new user
102
+ # with an email not found error.
103
+ # Attributes must contain the user's email
104
+ def send_reset_password_instructions(attributes={})
105
+ recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
106
+ recoverable.send_reset_password_instructions if recoverable.persisted?
107
+ recoverable
108
+ end
109
+
110
+ # Attempt to find a user by its reset_password_token to reset its
111
+ # password. If a user is found and token is still valid, reset its password and automatically
112
+ # try saving the record. If not user is found, returns a new user
113
+ # containing an error in reset_password_token attribute.
114
+ # Attributes must contain reset_password_token, password and confirmation
115
+ def reset_password_by_token(attributes={})
116
+ original_token = attributes[:reset_password_token]
117
+ reset_password_token = Devision.token_generator.digest(self, :reset_password_token, original_token)
118
+
119
+ recoverable = find_or_initialize_with_errors(:reset_password_token, reset_password_token)
120
+
121
+ if recoverable.persisted?
122
+ if recoverable.reset_password_period_valid?
123
+ recoverable.reset_password!(attributes[:password], attributes[:password_confirmation])
124
+ else
125
+ recoverable.errors.add(:reset_password_token, :expired)
126
+ end
127
+ end
128
+
129
+ recoverable.reset_password_token = original_token
130
+ recoverable
131
+ end
132
+
133
+ Devision::Models::Config.add_options(self, :reset_password_keys, :reset_password_within)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,114 @@
1
+ module Devision
2
+ module Models
3
+ # Rememberable manages generating and clearing token for remember the user
4
+ # from a saved cookie. Rememberable also has utility methods for dealing
5
+ # with serializing the user into the cookie and back from the cookie, trying
6
+ # to lookup the record based on the saved information.
7
+ # You probably wouldn't use rememberable methods directly, they are used
8
+ # mostly internally for handling the remember token.
9
+ #
10
+ # == Options
11
+ #
12
+ # Rememberable adds the following options in devise_for:
13
+ #
14
+ # * +remember_for+: the time you want the user will be remembered without
15
+ # asking for credentials. After this time the user will be blocked and
16
+ # will have to enter their credentials again. This configuration is also
17
+ # used to calculate the expires time for the cookie created to remember
18
+ # the user. By default remember_for is 2.weeks.
19
+ #
20
+ # * +extend_remember_period+: if true, extends the user's remember period
21
+ # when remembered via cookie. False by default.
22
+ #
23
+ # * +rememberable_options+: configuration options passed to the created cookie.
24
+ #
25
+ # == Examples
26
+ #
27
+ # User.find(1).remember_me! # regenerating the token
28
+ # User.find(1).forget_me! # clearing the token
29
+ #
30
+ # # generating info to put into cookies
31
+ # User.serialize_into_cookie(user)
32
+ #
33
+ # # lookup the user based on the incoming cookie information
34
+ # User.serialize_from_cookie(cookie_string)
35
+ module Rememberable
36
+ extend ActiveSupport::Concern
37
+
38
+ attr_accessor :remember_me, :extend_remember_period
39
+
40
+ def self.required_fields(klass)
41
+ [:remember_created_at]
42
+ end
43
+
44
+ # Generate a new remember token and save the record without validations
45
+ # unless remember_across_browsers is true and the user already has a valid token.
46
+ def remember_me!(extend_period=false)
47
+ self.remember_token = self.class.remember_token if generate_remember_token?
48
+ self.remember_created_at = Time.now.utc if generate_remember_timestamp?(extend_period)
49
+ save(validate: false) if self.changed?
50
+ end
51
+
52
+ # If the record is persisted, remove the remember token (but only if
53
+ # it exists), and save the record without validations.
54
+ def forget_me!
55
+ return unless persisted?
56
+ self.remember_token = nil if respond_to?(:remember_token=)
57
+ self.remember_created_at = nil
58
+ save(validate: false)
59
+ end
60
+
61
+ # Remember token should be expired if expiration time not overpass now.
62
+ def remember_expired?
63
+ remember_created_at.nil? || (remember_expires_at <= Time.now.utc)
64
+ end
65
+
66
+ # Remember token expires at created time + remember_for configuration
67
+ def remember_expires_at
68
+ remember_created_at + self.class.remember_for
69
+ end
70
+
71
+ def rememberable_value
72
+ if respond_to?(:remember_token)
73
+ remember_token
74
+ elsif respond_to?(:authenticatable_salt) && (salt = authenticatable_salt)
75
+ salt
76
+ else
77
+ raise "authenticable_salt returned nil for the #{self.class.name} model. " \
78
+ "In order to use rememberable, you must ensure a password is always set " \
79
+ "or have a remember_token column in your model or implement your own " \
80
+ "rememberable_value in the model with custom logic."
81
+ end
82
+ end
83
+
84
+ def rememberable_options
85
+ self.class.rememberable_options
86
+ end
87
+
88
+ protected
89
+
90
+ def generate_remember_token? #:nodoc:
91
+ respond_to?(:remember_token) && remember_expired?
92
+ end
93
+
94
+ # Generate a timestamp if extend_remember_period is true, if no remember_token
95
+ # exists, or if an existing remember token has expired.
96
+ def generate_remember_timestamp?(extend_period) #:nodoc:
97
+ extend_period || remember_created_at.nil? || remember_expired?
98
+ end
99
+
100
+ module ClassMethods
101
+
102
+ # Generate a token checking if one does not already exist in the database.
103
+ def remember_token #:nodoc:
104
+ loop do
105
+ token = Devision.nice_token
106
+ break token unless to_adapter.find_first({ remember_token: token })
107
+ end
108
+ end
109
+
110
+ Devise::Models::Config.add_options(self, :remember_for, :extend_remember_period, :rememberable_options)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,72 @@
1
+ # Deprecate: Copied verbatim from Rails source, remove once we move to Rails 4 only.
2
+ require 'thread_safe'
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ module Devision
7
+
8
+ class TokenGenerator
9
+ def initialize(key_generator, digest="SHA256")
10
+ @key_generator = key_generator
11
+ @digest = digest
12
+ end
13
+
14
+ def digest(klass, column, value)
15
+ value.present? && OpenSSL::HMAC.hexdigest(@digest, key_for(column), value.to_s)
16
+ end
17
+
18
+ def generate(klass, column)
19
+ key = key_for(column)
20
+
21
+ loop do
22
+ raw = Devision.nice_token
23
+ enc = OpenSSL::HMAC.hexdigest(@digest, key, raw)
24
+ break [raw, enc] unless klass.to_adapter.find_first({ column => enc })
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def key_for(column)
31
+ @key_generator.generate_key("Devision #{column}")
32
+ end
33
+ end # TokenGenerator
34
+
35
+ # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2
36
+ # It can be used to derive a number of keys for various purposes from a given secret.
37
+ # This lets Rails applications have a single secure secret, but avoid reusing that
38
+ # key in multiple incompatible contexts.
39
+ class KeyGenerator
40
+ def initialize(secret, options = {})
41
+ @secret = secret
42
+ # The default iterations are higher than required for our key derivation uses
43
+ # on the off chance someone uses this for password storage
44
+ @iterations = options[:iterations] || 2**16
45
+ end
46
+
47
+ # Returns a derived key suitable for use. The default key_size is chosen
48
+ # to be compatible with the default settings of ActiveSupport::MessageVerifier.
49
+ # i.e. OpenSSL::Digest::SHA1#block_length
50
+ def generate_key(salt, key_size=64)
51
+ OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
52
+ end
53
+ end # KeyGenerator
54
+
55
+ # CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
56
+ # re-executing the key generation process when it's called using the same salt and
57
+ # key_size
58
+ class CachingKeyGenerator
59
+ def initialize(key_generator)
60
+ @key_generator = key_generator
61
+ @cache_keys = ThreadSafe::Cache.new
62
+ end
63
+
64
+ # Returns a derived key suitable for use. The default key_size is chosen
65
+ # to be compatible with the default settings of ActiveSupport::MessageVerifier.
66
+ # i.e. OpenSSL::Digest::SHA1#block_length
67
+ def generate_key(salt, key_size=64)
68
+ @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
69
+ end
70
+ end # CachingKeyGenerator
71
+
72
+ end # Devision
@@ -0,0 +1,13 @@
1
+ module Devision
2
+ module VERSION
3
+
4
+ def self.gem_version
5
+ @gem_version ||= Gem::Version.new(STRING)
6
+ end
7
+
8
+ MAJOR = 0
9
+ MINOR = 0
10
+ PATCH = 0
11
+ STRING = [MAJOR,MINOR,PATCH].join('.').freeze
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devision
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - John Faucett
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: railties
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.6
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '5'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 3.2.6
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '5'
75
+ - !ruby/object:Gem::Dependency
76
+ name: orm_adapter
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.5'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.5'
89
+ - !ruby/object:Gem::Dependency
90
+ name: bcrypt
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.1'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.1'
103
+ - !ruby/object:Gem::Dependency
104
+ name: thread_safe
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.3'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.3'
117
+ description:
118
+ email:
119
+ - jwaterfaucett@gmail.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - Gemfile
126
+ - LICENSE.txt
127
+ - README.md
128
+ - Rakefile
129
+ - devision.gemspec
130
+ - lib/devision.rb
131
+ - lib/devision/configuration_options.rb
132
+ - lib/devision/models/authenticatable.rb
133
+ - lib/devision/models/config.rb
134
+ - lib/devision/models/confirmable.rb
135
+ - lib/devision/models/database_authenticatable.rb
136
+ - lib/devision/models/lockable.rb
137
+ - lib/devision/models/recoverable.rb
138
+ - lib/devision/models/rememberable.rb
139
+ - lib/devision/token_generator.rb
140
+ - lib/devision/version.rb
141
+ homepage:
142
+ licenses:
143
+ - MIT
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.4.1
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: Devise on a Diet
165
+ test_files: []