devise-secure-password 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|