devise-secure-password 1.1.1
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 +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Changelog.md +98 -0
- data/Dockerfile +44 -0
- data/Dockerfile.prev +44 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +272 -0
- data/LICENSE.txt +21 -0
- data/README.md +382 -0
- data/Rakefile +11 -0
- data/app/controllers/devise/passwords_with_policy_controller.rb +93 -0
- data/app/views/devise/passwords_with_policy/edit.html.erb +20 -0
- data/bin/console +14 -0
- data/bin/setup +6 -0
- data/config/locales/en.yml +84 -0
- data/devise-secure-password.gemspec +57 -0
- data/docker-entrypoint.sh +6 -0
- data/gemfiles/rails_5_1.gemfile +21 -0
- data/gemfiles/rails_5_2.gemfile +22 -0
- data/lib/devise/secure_password.rb +71 -0
- data/lib/devise/secure_password/controllers/devise_helpers.rb +23 -0
- data/lib/devise/secure_password/controllers/helpers.rb +58 -0
- data/lib/devise/secure_password/grammar.rb +13 -0
- data/lib/devise/secure_password/models/password_disallows_frequent_changes.rb +62 -0
- data/lib/devise/secure_password/models/password_disallows_frequent_reuse.rb +73 -0
- data/lib/devise/secure_password/models/password_has_required_content.rb +170 -0
- data/lib/devise/secure_password/models/password_requires_regular_updates.rb +54 -0
- data/lib/devise/secure_password/models/previous_password.rb +20 -0
- data/lib/devise/secure_password/routes.rb +11 -0
- data/lib/devise/secure_password/version.rb +5 -0
- data/lib/generators/devise/secure_password/install_generator.rb +30 -0
- data/lib/generators/devise/templates/README.txt +21 -0
- data/lib/generators/devise/templates/secure_password.rb +43 -0
- data/lib/support/string/character_counter.rb +55 -0
- data/pkg/devise-secure-password-1.1.1.gem +0 -0
- metadata +429 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
<h2><%= t('.titles.section_title') %></h2>
|
2
|
+
|
3
|
+
<%= form_for(resource, as: resource_name, url: [resource_name, :password_with_policy], html: { method: :put }) do |f| %>
|
4
|
+
<% if resource.errors.full_messages.count.positive? %>
|
5
|
+
<%= devise_error_messages! %>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<%= f.hidden_field :update_action, value: :change_password %>
|
9
|
+
|
10
|
+
<p><%= f.label :current_password, t('.labels.current_password') %><br />
|
11
|
+
<%= f.password_field :current_password %></p>
|
12
|
+
|
13
|
+
<p><%= f.label :password, t('.labels.new_password') %><br />
|
14
|
+
<%= f.password_field :password %></p>
|
15
|
+
|
16
|
+
<p><%= f.label :password_confirmation, t('.labels.confirm_new_password') %><br />
|
17
|
+
<%= f.password_field :password_confirmation %></p>
|
18
|
+
|
19
|
+
<p><%= f.submit t('.buttons.change_my_password') %></p>
|
20
|
+
<% end %>
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "devise/secure_password"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
en:
|
2
|
+
secure_password:
|
3
|
+
character:
|
4
|
+
one: "character"
|
5
|
+
other: "characters"
|
6
|
+
|
7
|
+
types:
|
8
|
+
uppercase: "uppercase"
|
9
|
+
downcase: "downcase"
|
10
|
+
number: "number"
|
11
|
+
special: "special"
|
12
|
+
|
13
|
+
password_has_required_content:
|
14
|
+
errors:
|
15
|
+
messages:
|
16
|
+
unknown_characters: "contains %{count} invalid %{subject}"
|
17
|
+
minimum_characters: "must contain at least %{count} %{type} %{subject}"
|
18
|
+
maximum_characters: "must contain less than %{count} %{type} %{subject}"
|
19
|
+
minimum_length: "must contain at least %{count} %{subject}"
|
20
|
+
maximum_length: "must contain less than %{count} %{subject}"
|
21
|
+
password_disallows_frequent_reuse:
|
22
|
+
errors:
|
23
|
+
messages:
|
24
|
+
password_is_recent: "Last %{count} passwords may not be reused"
|
25
|
+
password_disallows_frequent_changes:
|
26
|
+
errors:
|
27
|
+
messages:
|
28
|
+
password_is_recent: "Password cannot be changed more than once per %{timeframe}"
|
29
|
+
password_requires_regular_updates:
|
30
|
+
alerts:
|
31
|
+
messages:
|
32
|
+
password_updated: "Your password has been updated."
|
33
|
+
errors:
|
34
|
+
messages:
|
35
|
+
password_expired: "Your password has expired. Passwords must be changed every %{timeframe}"
|
36
|
+
datetime:
|
37
|
+
# update distance_in_words translations to remove the determiner words:
|
38
|
+
# about, almost, over, less than, etc.
|
39
|
+
precise_distance_in_words:
|
40
|
+
half_a_minute: "half a minute"
|
41
|
+
less_than_x_seconds:
|
42
|
+
one: "1 second" # default was: "less than 1 second"
|
43
|
+
other: "%{count} seconds" # default was: "less than %{count} seconds"
|
44
|
+
x_seconds:
|
45
|
+
one: "1 second"
|
46
|
+
other: "%{count} seconds"
|
47
|
+
less_than_x_minutes:
|
48
|
+
one: "a minute" # default was: "less than a minute"
|
49
|
+
other: "%{count} minutes" # default was: "less than %{count} minutes"
|
50
|
+
x_minutes:
|
51
|
+
one: "1 minute"
|
52
|
+
other: "%{count} minutes"
|
53
|
+
about_x_hours:
|
54
|
+
one: "1 hour" # default was: "about 1 hour"
|
55
|
+
other: "%{count} hours" # default was: "about %{count} hours"
|
56
|
+
x_days:
|
57
|
+
one: "1 day"
|
58
|
+
other: "%{count} days"
|
59
|
+
about_x_months:
|
60
|
+
one: "1 month" # default was: "about 1 month"
|
61
|
+
other: "%{count} months" # default was: "about %{count} months"
|
62
|
+
x_months:
|
63
|
+
one: "1 month"
|
64
|
+
other: "%{count} months"
|
65
|
+
about_x_years:
|
66
|
+
one: "1 year" # default was: "about 1 year"
|
67
|
+
other: "%{count} years" # default was: "about %{count} years"
|
68
|
+
over_x_years:
|
69
|
+
one: "1 year" # default was: "over 1 year"
|
70
|
+
other: "%{count} years" # default was: "over %{count} years"
|
71
|
+
almost_x_years:
|
72
|
+
one: "1 year" # default was: "almost 1 year"
|
73
|
+
other: "%{count} years" # default was: "almost %{count} years"
|
74
|
+
devise:
|
75
|
+
passwords_with_policy:
|
76
|
+
edit:
|
77
|
+
titles:
|
78
|
+
section_title: "Change your password"
|
79
|
+
labels:
|
80
|
+
current_password: "Current password"
|
81
|
+
new_password: "New password"
|
82
|
+
confirm_new_password: "Confirm new password"
|
83
|
+
buttons:
|
84
|
+
change_my_password: "Change my password"
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#
|
2
|
+
# devise-secure_password.gemspec
|
3
|
+
#
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
|
7
|
+
require 'date'
|
8
|
+
require 'devise/secure_password/version'
|
9
|
+
|
10
|
+
Gem::Specification.new do |spec|
|
11
|
+
spec.name = 'devise-secure-password'
|
12
|
+
spec.version = Devise::SecurePassword::VERSION.dup
|
13
|
+
spec.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
spec.authors = ['Mark Eissler']
|
16
|
+
spec.email = ['mark.eissler@valimail.com']
|
17
|
+
|
18
|
+
spec.summary = 'A devise password policy enforcement extension.'
|
19
|
+
spec.description = 'Adds configurable password policy enforcement to devise.'
|
20
|
+
|
21
|
+
spec.homepage = 'https://github.com/valimail/devise-secure_password'
|
22
|
+
spec.license = 'MIT'
|
23
|
+
|
24
|
+
spec.files = Dir['./**/*'].reject do |f|
|
25
|
+
f.match(%r{^./(test|spec|features|lib/tasks)/|Gemfile.lock.ci})
|
26
|
+
end
|
27
|
+
spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
28
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
29
|
+
spec.require_paths = ['lib']
|
30
|
+
|
31
|
+
spec.add_runtime_dependency 'devise', '>= 4.0.0', '< 5.0.0'
|
32
|
+
spec.add_runtime_dependency 'railties', '>= 5.0.0', '< 7.0.0'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'bundler', '>= 1.16.1'
|
35
|
+
spec.add_development_dependency 'capybara', '>= 2.16.1', '~> 3.11'
|
36
|
+
spec.add_development_dependency 'capybara-screenshot', '>= 1.0.18'
|
37
|
+
spec.add_development_dependency 'coffee-rails', '~> 4.2'
|
38
|
+
spec.add_development_dependency 'database_cleaner', '~> 1.6', '>= 1.6.2'
|
39
|
+
spec.add_development_dependency 'devise', '~> 4.0'
|
40
|
+
spec.add_development_dependency 'flay', '>= 2.10.0'
|
41
|
+
spec.add_development_dependency 'launchy', '>= 2.4.3'
|
42
|
+
spec.add_development_dependency 'rails', '~> 5.2', '>= 5.2.0'
|
43
|
+
spec.add_development_dependency 'rake', '>= 12.3'
|
44
|
+
spec.add_development_dependency 'rspec', '>= 3.7'
|
45
|
+
spec.add_development_dependency 'rspec-rails', '>= 3.7'
|
46
|
+
spec.add_development_dependency 'rspec_junit_formatter', '>= 0.3'
|
47
|
+
spec.add_development_dependency 'rubocop', '>= 0.74.0'
|
48
|
+
spec.add_development_dependency 'rubocop-rails', '>= 2.3.2'
|
49
|
+
spec.add_development_dependency 'rubocop-rspec', '>= 1.35.0'
|
50
|
+
spec.add_development_dependency 'ruby2ruby', '>= 2.4.0'
|
51
|
+
spec.add_development_dependency 'selenium-webdriver', '>= 3.7.0'
|
52
|
+
spec.add_development_dependency 'simplecov', '~> 0.18.2'
|
53
|
+
spec.add_development_dependency 'simplecov-console', '>= 0.4.2'
|
54
|
+
spec.add_development_dependency 'sqlite3', '>= 1.3.13'
|
55
|
+
|
56
|
+
spec.required_ruby_version = '>= 2.4'
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
ENV['RAILS_TARGET'] ||= '5.1'
|
6
|
+
|
7
|
+
gemspec path: '../'
|
8
|
+
|
9
|
+
gem 'rails', '~> 5.1.0'
|
10
|
+
gem 'sass-rails', '~> 5.0'
|
11
|
+
gem 'sqlite3', '~> 1.3.11'
|
12
|
+
gem 'therubyracer', '~> 0.12.3', platforms: :ruby
|
13
|
+
|
14
|
+
group :development, :test do
|
15
|
+
gem 'byebug', '>= 0'
|
16
|
+
end
|
17
|
+
|
18
|
+
group :test do
|
19
|
+
gem 'codecov', require: false
|
20
|
+
gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
ENV['RAILS_TARGET'] ||= '5.2'
|
6
|
+
|
7
|
+
gemspec path: '../'
|
8
|
+
|
9
|
+
gem 'bootsnap', '>= 1.1.0', require: false
|
10
|
+
gem 'mini_racer', '~> 0.2.0', platforms: :ruby
|
11
|
+
gem 'rails', '~> 5.2.0'
|
12
|
+
gem 'sassc-rails', '~> 1.3.0'
|
13
|
+
gem 'sqlite3', '~> 1.3.13'
|
14
|
+
|
15
|
+
group :development, :test do
|
16
|
+
gem 'byebug', '>= 0'
|
17
|
+
end
|
18
|
+
|
19
|
+
group :test do
|
20
|
+
gem 'codecov', require: false
|
21
|
+
gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
|
22
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#
|
2
|
+
# lib/devise-secure_password.rb
|
3
|
+
#
|
4
|
+
require 'active_support/concern'
|
5
|
+
require 'devise'
|
6
|
+
require 'devise/secure_password/routes'
|
7
|
+
require 'devise/secure_password/version'
|
8
|
+
require 'devise/secure_password/models/password_has_required_content'
|
9
|
+
require 'devise/secure_password/models/password_disallows_frequent_reuse'
|
10
|
+
require 'devise/secure_password/models/password_disallows_frequent_changes'
|
11
|
+
require 'devise/secure_password/models/password_requires_regular_updates'
|
12
|
+
require 'devise/secure_password/grammar'
|
13
|
+
|
14
|
+
module Devise
|
15
|
+
# password_content_enforcement configuration parameters
|
16
|
+
@password_required_uppercase_count = 1
|
17
|
+
@password_required_lowercase_count = 1
|
18
|
+
@password_required_number_count = 1
|
19
|
+
@password_required_special_character_count = 1
|
20
|
+
|
21
|
+
# password_frequent_reuse_prevention configuration parameters
|
22
|
+
@password_previously_used_count = 8
|
23
|
+
|
24
|
+
# password_frequent_change_prevention configuration parameters
|
25
|
+
@password_minimum_age = 1.day
|
26
|
+
|
27
|
+
# password_regular_update_enforcement configuration parameters
|
28
|
+
@password_maximum_age = 180.days
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_accessor :password_required_uppercase_count
|
32
|
+
attr_accessor :password_required_lowercase_count
|
33
|
+
attr_accessor :password_required_number_count
|
34
|
+
attr_accessor :password_required_special_character_count
|
35
|
+
attr_accessor :password_previously_used_count
|
36
|
+
attr_accessor :password_minimum_age
|
37
|
+
attr_accessor :password_maximum_age
|
38
|
+
end
|
39
|
+
|
40
|
+
module SecurePassword
|
41
|
+
module Controllers
|
42
|
+
autoload :Helpers, 'devise/secure_password/controllers/helpers'
|
43
|
+
autoload :DeviseHelpers, 'devise/secure_password/controllers/devise_helpers'
|
44
|
+
end
|
45
|
+
|
46
|
+
class Engine < ::Rails::Engine
|
47
|
+
ActiveSupport.on_load(:devise_controller) do
|
48
|
+
include ActionView::Helpers::DateHelper
|
49
|
+
include Devise::SecurePassword::Controllers::DeviseHelpers
|
50
|
+
end
|
51
|
+
ActiveSupport.on_load(:action_controller) do
|
52
|
+
include ActionView::Helpers::DateHelper
|
53
|
+
include Devise::SecurePassword::Controllers::Helpers
|
54
|
+
end
|
55
|
+
|
56
|
+
# add exceptions to the inflector so it doesn't get tripped up by our concerns that end in an 's'
|
57
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
58
|
+
inflect.uncountable :password_disallows_frequent_changes
|
59
|
+
inflect.uncountable :password_requires_regular_updates
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# modules
|
66
|
+
Devise.add_module :password_has_required_content, model: 'devise/secure_password/models/password_has_required_content'
|
67
|
+
Devise.add_module :password_disallows_frequent_reuse, model: 'devise/secure_password/models/password_disallows_frequent_reuse'
|
68
|
+
Devise.add_module :password_disallows_frequent_changes, model: 'devise/secure_password/models/password_disallows_frequent_changes'
|
69
|
+
Devise.add_module :password_requires_regular_updates,
|
70
|
+
model: 'devise/secure_password/models/password_requires_regular_updates',
|
71
|
+
controller: :passwords_with_policy, route: :passwords_with_policy
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Devise
|
2
|
+
module SecurePassword
|
3
|
+
module Controllers
|
4
|
+
module DeviseHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# rubocop:disable Style/ClassAndModuleChildren
|
8
|
+
class ::DeviseController
|
9
|
+
alias devise_sign_in sign_in
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def sign_in(*args)
|
14
|
+
devise_sign_in(*args).tap do
|
15
|
+
set_devise_secure_password_expired! if warden_user_has_password_expiration?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# rubocop:enable Style/ClassAndModuleChildren
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Devise
|
2
|
+
module SecurePassword
|
3
|
+
module Controllers
|
4
|
+
module Helpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include Devise::SecurePassword::Grammar
|
9
|
+
|
10
|
+
before_action :authenticate_secure_password!, unless: :devise_controller?
|
11
|
+
end
|
12
|
+
|
13
|
+
def authenticate_secure_password_expired?
|
14
|
+
return false if devise_controller?
|
15
|
+
|
16
|
+
session[:devise_secure_password_expired] == true
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticate_secure_password!
|
20
|
+
return unless authenticate_secure_password_expired?
|
21
|
+
|
22
|
+
redirect_to authenticate_secure_password_path, alert: "#{error_string_for_password_expired}."
|
23
|
+
end
|
24
|
+
|
25
|
+
def authenticate_secure_password_path
|
26
|
+
return unless warden.user
|
27
|
+
|
28
|
+
:"edit_#{devise_secure_password_scope}_password_with_policy"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def devise_secure_password_scope
|
34
|
+
Devise::Mapping.find_scope!(warden.user)
|
35
|
+
end
|
36
|
+
|
37
|
+
def error_string_for_password_expired
|
38
|
+
I18n.t(
|
39
|
+
'secure_password.password_requires_regular_updates.errors.messages.password_expired',
|
40
|
+
timeframe: precise_distance_of_time_in_words(warden.user.class.password_maximum_age)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_devise_secure_password_expired!
|
45
|
+
session[:devise_secure_password_expired] = warden.user.password_expired?
|
46
|
+
end
|
47
|
+
|
48
|
+
def unset_devise_secure_password_expired!
|
49
|
+
session.delete(:devise_secure_password_expired)
|
50
|
+
end
|
51
|
+
|
52
|
+
def warden_user_has_password_expiration?
|
53
|
+
warden&.user&.respond_to?(:password_expired?)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# lib/support/string/grammar.rb
|
2
|
+
#
|
3
|
+
module Devise
|
4
|
+
module SecurePassword
|
5
|
+
module Grammar
|
6
|
+
# distance_in_words without determiner words: about, almost, over, etc.
|
7
|
+
def precise_distance_of_time_in_words(from_time, to_time = 0, options = {})
|
8
|
+
precise_options = { scope: :'secure_password.datetime.precise_distance_in_words' }
|
9
|
+
distance_of_time_in_words(from_time, to_time, options.merge!(precise_options))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module PasswordDisallowsFrequentChanges
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class ConfigurationError < RuntimeError; end
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ActionView::Helpers::DateHelper
|
10
|
+
include Devise::SecurePassword::Grammar
|
11
|
+
|
12
|
+
validate :validate_password_frequent_change, if: :password_required?
|
13
|
+
|
14
|
+
set_callback(:initialize, :before, :before_resource_initialized)
|
15
|
+
set_callback(:initialize, :after, :after_resource_initialized)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_password_frequent_change
|
19
|
+
if encrypted_password_changed? && password_recent?
|
20
|
+
error_string = I18n.t(
|
21
|
+
'secure_password.password_disallows_frequent_changes.errors.messages.password_is_recent',
|
22
|
+
timeframe: precise_distance_of_time_in_words(self.class.password_minimum_age)
|
23
|
+
)
|
24
|
+
errors.add(:base, error_string)
|
25
|
+
end
|
26
|
+
|
27
|
+
errors.count.zero?
|
28
|
+
end
|
29
|
+
|
30
|
+
def password_recent?
|
31
|
+
last_password = previous_passwords.first
|
32
|
+
last_password&.fresh?(self.class.password_minimum_age)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def before_resource_initialized
|
38
|
+
return if self.class.respond_to?(:password_previously_used_count)
|
39
|
+
|
40
|
+
raise ConfigurationError, <<-ERROR.strip_heredoc
|
41
|
+
|
42
|
+
The password_disallows_frequent_changes module depends on the
|
43
|
+
password_disallows_frequent_reuse module. Verify that you have
|
44
|
+
added both modules to your model, for example:
|
45
|
+
|
46
|
+
devise :database_authenticatable, :registerable,
|
47
|
+
:password_disallows_frequent_reuse,
|
48
|
+
:password_disallows_frequent_changes
|
49
|
+
ERROR
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_resource_initialized
|
53
|
+
raise ConfigurationError, 'invalid type for password_minimum_age' \
|
54
|
+
unless self.class.password_minimum_age.is_a?(::ActiveSupport::Duration)
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
::Devise::Models.config(self, :password_minimum_age)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|