devise-security 0.13.0 → 0.14.0.rc1
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 +4 -4
- data/.codeclimate.yml +1 -1
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +30 -12
- data/Appraisals +21 -5
- data/Gemfile +8 -1
- data/README.md +47 -7
- data/app/views/devise/paranoid_verification_code/show.html.erb +1 -1
- data/app/views/devise/password_expired/show.html.erb +1 -1
- data/config/locales/de.yml +11 -11
- data/config/locales/fr.yml +13 -13
- data/config/locales/ja.yml +29 -0
- data/devise-security.gemspec +12 -6
- data/gemfiles/rails_4.2_stable.gemfile +9 -1
- data/gemfiles/rails_5.0_stable.gemfile +8 -1
- data/gemfiles/rails_5.1_stable.gemfile +8 -1
- data/gemfiles/{rails_5.2.0.gemfile → rails_5.2_stable.gemfile} +8 -1
- data/gemfiles/rails_6.0_beta.gemfile +15 -0
- data/lib/devise-security.rb +2 -4
- data/lib/devise-security/models/{old_password.rb → active_record/old_password.rb} +1 -2
- data/lib/devise-security/models/compatibility.rb +6 -15
- data/lib/devise-security/models/compatibility/active_record.rb +29 -0
- data/lib/devise-security/models/compatibility/mongoid.rb +21 -0
- data/lib/devise-security/models/database_authenticatable_patch.rb +1 -1
- data/lib/devise-security/models/expirable.rb +6 -2
- data/lib/devise-security/models/mongoid/old_password.rb +21 -0
- data/lib/devise-security/models/password_archivable.rb +16 -7
- data/lib/devise-security/models/password_expirable.rb +5 -0
- data/lib/devise-security/models/secure_validatable.rb +2 -2
- data/lib/devise-security/orm/mongoid.rb +7 -0
- data/lib/devise-security/patches/controller_security_question.rb +1 -1
- data/lib/devise-security/version.rb +1 -1
- data/lib/generators/devise_security/install_generator.rb +1 -1
- data/test/dummy/app/models/application_record.rb +8 -2
- data/test/dummy/app/models/application_user_record.rb +11 -0
- data/test/dummy/app/models/captcha_user.rb +5 -2
- data/test/dummy/app/models/mongoid/confirmable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +17 -0
- data/test/dummy/app/models/mongoid/expirable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/lockable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/mappings.rb +13 -0
- data/test/dummy/app/models/mongoid/omniauthable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/password_archivable_fields.rb +9 -0
- data/test/dummy/app/models/mongoid/password_expirable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/recoverable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/registerable_fields.rb +19 -0
- data/test/dummy/app/models/mongoid/rememberable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/security_questionable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/session_limitable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/timeoutable_fields.rb +9 -0
- data/test/dummy/app/models/mongoid/trackable_fields.rb +14 -0
- data/test/dummy/app/models/mongoid/validatable_fields.rb +7 -0
- data/test/dummy/app/models/secure_user.rb +5 -1
- data/test/dummy/app/models/security_question_user.rb +7 -4
- data/test/dummy/app/models/user.rb +5 -0
- data/test/dummy/app/models/widget.rb +4 -0
- data/test/dummy/app/mongoid/admin.rb +31 -0
- data/test/dummy/app/mongoid/one_user.rb +58 -0
- data/test/dummy/app/mongoid/shim.rb +25 -0
- data/test/dummy/app/mongoid/user_on_engine.rb +41 -0
- data/test/dummy/app/mongoid/user_on_main_app.rb +41 -0
- data/test/dummy/app/mongoid/user_with_validations.rb +37 -0
- data/test/dummy/app/mongoid/user_without_email.rb +35 -0
- data/test/dummy/config/application.rb +10 -7
- data/test/dummy/config/environments/test.rb +2 -2
- data/test/dummy/config/initializers/devise.rb +3 -4
- data/test/dummy/config/initializers/migration_class.rb +8 -6
- data/test/dummy/config/mongoid.yml +6 -0
- data/test/dummy/lib/shared_expirable_columns.rb +14 -0
- data/test/dummy/lib/shared_security_questions_fields.rb +16 -0
- data/test/dummy/lib/shared_user.rb +32 -0
- data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
- data/test/dummy/lib/shared_user_without_email.rb +28 -0
- data/test/dummy/lib/shared_user_without_omniauth.rb +15 -0
- data/test/dummy/lib/shared_verification_fields.rb +15 -0
- data/test/orm/active_record.rb +12 -0
- data/test/orm/mongoid.rb +12 -0
- data/test/support/mongoid.yml +6 -0
- data/test/test_helper.rb +16 -9
- data/test/test_install_generator.rb +1 -0
- data/test/test_password_archivable.rb +6 -7
- data/test/test_password_expirable.rb +3 -1
- data/test/test_secure_validatable.rb +11 -10
- data/test/test_security_question_controller.rb +9 -11
- metadata +125 -16
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rails", "~> 6.0.0.beta1"
|
|
6
|
+
|
|
7
|
+
group :active_record do
|
|
8
|
+
gem "sqlite3", "~> 1.3.0"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
group :mongoid do
|
|
12
|
+
gem "mongoid", "~> 6.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
data/lib/devise-security.rb
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require DEVISE_ORM.to_s if DEVISE_ORM.in? [:active_record, :mongoid]
|
|
4
4
|
require 'active_support/core_ext/integer'
|
|
5
5
|
require 'active_support/ordered_hash'
|
|
6
6
|
require 'active_support/concern'
|
|
7
7
|
require 'devise'
|
|
8
8
|
|
|
9
9
|
module Devise
|
|
10
|
-
|
|
11
10
|
# Number of seconds that passwords are valid (e.g 3.months)
|
|
12
11
|
# Disable pasword expiration with +false+
|
|
13
12
|
# Expire only on demand with +true+
|
|
@@ -104,7 +103,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
|
|
|
104
103
|
# requires
|
|
105
104
|
require 'devise-security/routes'
|
|
106
105
|
require 'devise-security/rails'
|
|
107
|
-
require
|
|
108
|
-
require 'devise-security/models/old_password'
|
|
106
|
+
require "devise-security/orm/#{DEVISE_ORM}"
|
|
109
107
|
require 'devise-security/models/database_authenticatable_patch'
|
|
110
108
|
require 'devise-security/models/paranoid_verification'
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "compatibility/#{DEVISE_ORM}"
|
|
4
|
+
|
|
3
5
|
module Devise
|
|
4
6
|
module Models
|
|
7
|
+
# These compatibility modules define methods used by devise-security
|
|
8
|
+
# that may need to be defined or re-defined for compatibility between ORMs
|
|
9
|
+
# and/or older versions of ORMs.
|
|
5
10
|
module Compatibility
|
|
6
11
|
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
# for backwards compatibility with Rails < 5.1.x
|
|
9
|
-
unless Devise.activerecord51?
|
|
10
|
-
def saved_change_to_encrypted_password?
|
|
11
|
-
encrypted_password_changed?
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def encrypted_password_before_last_save
|
|
15
|
-
previous_changes['encrypted_password'].try(:first)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def will_save_change_to_encrypted_password?
|
|
19
|
-
changed_attributes['encrypted_password'].present?
|
|
20
|
-
end
|
|
21
|
-
end
|
|
12
|
+
include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}".constantize
|
|
22
13
|
end
|
|
23
14
|
end
|
|
24
15
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Devise
|
|
2
|
+
module Models
|
|
3
|
+
module Compatibility
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
unless Devise.activerecord51?
|
|
7
|
+
# When the record was saved, was the +encrypted_password+ changed?
|
|
8
|
+
# @return [Boolean]
|
|
9
|
+
def saved_change_to_encrypted_password?
|
|
10
|
+
encrypted_password_changed?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The encrypted password that existed before the record was saved
|
|
14
|
+
# @return [String]
|
|
15
|
+
# @return [nil] if an +encrypted_password+ had not been set
|
|
16
|
+
def encrypted_password_before_last_save
|
|
17
|
+
previous_changes['encrypted_password'].try(:first)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# When the record is saved, will the +encrypted_password+ be changed?
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def will_save_change_to_encrypted_password?
|
|
23
|
+
changed_attributes['encrypted_password'].present?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Devise
|
|
2
|
+
module Models
|
|
3
|
+
module Compatibility
|
|
4
|
+
module Mongoid
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Will saving this record change the +email+ attribute?
|
|
8
|
+
# @return [Boolean]
|
|
9
|
+
def will_save_change_to_email?
|
|
10
|
+
changed.include? 'email'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Will saving this record change the +encrypted_password+ attribute?
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def will_save_change_to_encrypted_password?
|
|
16
|
+
changed.include? 'encrypted_password'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -10,7 +10,7 @@ module Devise
|
|
|
10
10
|
new_password_confirmation = params[:password_confirmation]
|
|
11
11
|
|
|
12
12
|
result = if valid_password?(current_password) && new_password.present? && new_password_confirmation.present?
|
|
13
|
-
|
|
13
|
+
update(params, *options)
|
|
14
14
|
else
|
|
15
15
|
self.assign_attributes(params, *options)
|
|
16
16
|
self.valid?
|
|
@@ -22,7 +22,11 @@ module Devise
|
|
|
22
22
|
|
|
23
23
|
# Updates +last_activity_at+, called from a Warden::Manager.after_set_user hook.
|
|
24
24
|
def update_last_activity!
|
|
25
|
-
|
|
25
|
+
if respond_to?(:update_column)
|
|
26
|
+
self.update_column(:last_activity_at, Time.now.utc)
|
|
27
|
+
elsif defined? Mongoid
|
|
28
|
+
self.update_attribute(:last_activity_at, Time.now.utc)
|
|
29
|
+
end
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
# Tells if the account has expired
|
|
@@ -103,7 +107,7 @@ module Devise
|
|
|
103
107
|
# @example Overwritten version to blank out the object.
|
|
104
108
|
# def self.delete_all_expired_for(time = 90.days)
|
|
105
109
|
# expired_for(time).each do |u|
|
|
106
|
-
# u.
|
|
110
|
+
# u.update first_name: nil, last_name: nil
|
|
107
111
|
# end
|
|
108
112
|
# end
|
|
109
113
|
def delete_all_expired_for(time)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class OldPassword
|
|
4
|
+
include Mongoid::Document
|
|
5
|
+
|
|
6
|
+
## Database authenticatable
|
|
7
|
+
field :encrypted_password, type: String
|
|
8
|
+
validates_presence_of :encrypted_password
|
|
9
|
+
field :password_salt, type: String
|
|
10
|
+
|
|
11
|
+
field :password_archivable_type, type: String
|
|
12
|
+
validates_presence_of :password_archivable_type
|
|
13
|
+
|
|
14
|
+
field :password_archivable_id, type: String
|
|
15
|
+
validates_presence_of :password_archivable_id
|
|
16
|
+
index({ password_archivable_type: 1, password_archivable_id: 1 }, name: :index_password_archivable)
|
|
17
|
+
|
|
18
|
+
include Mongoid::Timestamps
|
|
19
|
+
|
|
20
|
+
belongs_to :password_archivable, polymorphic: true
|
|
21
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'compatibility'
|
|
4
|
+
require_relative "#{DEVISE_ORM}/old_password"
|
|
4
5
|
|
|
5
6
|
module Devise
|
|
6
7
|
module Models
|
|
@@ -11,7 +12,7 @@ module Devise
|
|
|
11
12
|
include Devise::Models::DatabaseAuthenticatable
|
|
12
13
|
|
|
13
14
|
included do
|
|
14
|
-
has_many :old_passwords, as: :password_archivable, dependent: :destroy
|
|
15
|
+
has_many :old_passwords, class_name: 'OldPassword', as: :password_archivable, dependent: :destroy
|
|
15
16
|
before_update :archive_password, if: :will_save_change_to_encrypted_password?
|
|
16
17
|
validate :validate_password_archive, if: :password_present?
|
|
17
18
|
end
|
|
@@ -38,11 +39,15 @@ module Devise
|
|
|
38
39
|
# @return [true] if current password was used previously
|
|
39
40
|
# @return [false] if disabled or not previously used
|
|
40
41
|
def password_archive_included?
|
|
41
|
-
return false unless max_old_passwords
|
|
42
|
-
|
|
42
|
+
return false unless max_old_passwords.positive?
|
|
43
|
+
|
|
44
|
+
old_passwords_including_cur_change = old_passwords.order(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
|
|
43
45
|
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
|
|
44
46
|
old_passwords_including_cur_change.any? do |old_password|
|
|
45
|
-
|
|
47
|
+
# NOTE: we deliberately do not do mass assignment here so that users that
|
|
48
|
+
# rely on `protected_attributes_continued` gem can still use this extension.
|
|
49
|
+
# See issue #68
|
|
50
|
+
self.class.new.tap { |object| object.encrypted_password = old_password }.valid_password?(password)
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
|
|
@@ -60,11 +65,15 @@ module Devise
|
|
|
60
65
|
|
|
61
66
|
private
|
|
62
67
|
|
|
63
|
-
#
|
|
68
|
+
# Archive the last password before save and delete all to old passwords from archive
|
|
69
|
+
# @note we check to see if an old password has already been archived because
|
|
70
|
+
# mongoid will keep re-triggering this callback when we add an old password
|
|
64
71
|
def archive_password
|
|
65
|
-
if max_old_passwords
|
|
72
|
+
if max_old_passwords.positive?
|
|
73
|
+
return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
|
|
74
|
+
|
|
66
75
|
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
|
|
67
|
-
old_passwords.order(:
|
|
76
|
+
old_passwords.order(created_at: :desc).offset(max_old_passwords).destroy_all
|
|
68
77
|
else
|
|
69
78
|
old_passwords.destroy_all
|
|
70
79
|
end
|
|
@@ -39,6 +39,7 @@ module Devise::Models
|
|
|
39
39
|
# @return [Boolean]
|
|
40
40
|
def need_change_password!
|
|
41
41
|
return unless password_expiration_enabled?
|
|
42
|
+
|
|
42
43
|
need_change_password
|
|
43
44
|
save(validate: false)
|
|
44
45
|
end
|
|
@@ -51,6 +52,7 @@ module Devise::Models
|
|
|
51
52
|
# @return [void]
|
|
52
53
|
def need_change_password
|
|
53
54
|
return unless password_expiration_enabled?
|
|
55
|
+
|
|
54
56
|
self.password_changed_at = nil
|
|
55
57
|
end
|
|
56
58
|
alias expire_password need_change_password
|
|
@@ -70,6 +72,7 @@ module Devise::Models
|
|
|
70
72
|
def password_change_requested?
|
|
71
73
|
return false unless password_expiration_enabled?
|
|
72
74
|
return false if new_record?
|
|
75
|
+
|
|
73
76
|
password_changed_at.nil?
|
|
74
77
|
end
|
|
75
78
|
|
|
@@ -79,6 +82,7 @@ module Devise::Models
|
|
|
79
82
|
return false if new_record?
|
|
80
83
|
return false unless password_expiration_enabled?
|
|
81
84
|
return false if expire_password_on_demand?
|
|
85
|
+
|
|
82
86
|
password_changed_at < expire_password_after.seconds.ago
|
|
83
87
|
end
|
|
84
88
|
alias password_expired? password_too_old?
|
|
@@ -89,6 +93,7 @@ module Devise::Models
|
|
|
89
93
|
# @note called as a +before_save+ hook
|
|
90
94
|
def update_password_changed
|
|
91
95
|
return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
|
|
96
|
+
|
|
92
97
|
self.password_changed_at = Time.zone.now
|
|
93
98
|
end
|
|
94
99
|
|
|
@@ -90,8 +90,8 @@ module Devise
|
|
|
90
90
|
|
|
91
91
|
def has_uniqueness_validation_of_login?
|
|
92
92
|
validators.any? do |validator|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
|
|
94
|
+
validator.kind_of?(validator_orm_klass) && validator.attributes.include?(login_attribute)
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -9,6 +9,7 @@ module DeviseSecurity::Patches
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
private
|
|
12
|
+
|
|
12
13
|
def check_security_question
|
|
13
14
|
# only find via email, not login
|
|
14
15
|
resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
|
|
@@ -19,4 +20,3 @@ module DeviseSecurity::Patches
|
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
|
-
|
|
@@ -4,7 +4,7 @@ module DeviseSecurity
|
|
|
4
4
|
module Generators
|
|
5
5
|
# Generator for Rails to create or append to a Devise initializer.
|
|
6
6
|
class InstallGenerator < Rails::Generators::Base
|
|
7
|
-
LOCALES = %w[en es de fr it tr].freeze
|
|
7
|
+
LOCALES = %w[en es de fr it ja tr].freeze
|
|
8
8
|
|
|
9
9
|
source_root File.expand_path('../../templates', __FILE__)
|
|
10
10
|
desc 'Install the devise security extension'
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
if DEVISE_ORM == :active_record
|
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
5
|
+
self.abstract_class = true
|
|
6
|
+
end
|
|
7
|
+
else
|
|
8
|
+
class ApplicationRecord
|
|
9
|
+
include Mongoid::Document
|
|
10
|
+
end
|
|
5
11
|
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class CaptchaUser <
|
|
4
|
-
self.table_name = 'users'
|
|
3
|
+
class CaptchaUser < ApplicationUserRecord
|
|
5
4
|
devise :database_authenticatable, :password_archivable,
|
|
6
5
|
:paranoid_verification, :password_expirable
|
|
6
|
+
if DEVISE_ORM == :mongoid
|
|
7
|
+
require './test/dummy/app/models/mongoid/mappings'
|
|
8
|
+
include ::Mongoid::Mappings
|
|
9
|
+
end
|
|
7
10
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ConfirmableFields
|
|
2
|
+
extend ::ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
include Mongoid::Document
|
|
6
|
+
|
|
7
|
+
## Confirmable
|
|
8
|
+
field :confirmation_token, type: String
|
|
9
|
+
field :confirmed_at, type: Time
|
|
10
|
+
field :confirmation_sent_at, type: Time
|
|
11
|
+
field :unconfirmed_email, type: String # Only if using reconfirmable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module DatabaseAuthenticatableFields
|
|
2
|
+
extend ::ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
include Mongoid::Document
|
|
6
|
+
|
|
7
|
+
## Database authenticatable
|
|
8
|
+
field :username, type: String
|
|
9
|
+
field :email, type: String, default: ""
|
|
10
|
+
#validates_presence_of :email
|
|
11
|
+
|
|
12
|
+
field :encrypted_password, type: String, default: ""
|
|
13
|
+
validates_presence_of :encrypted_password
|
|
14
|
+
|
|
15
|
+
include Mongoid::Timestamps
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module LockableFields
|
|
2
|
+
extend ::ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
include Mongoid::Document
|
|
6
|
+
|
|
7
|
+
field :failed_attempts, type: Integer, default: 0 # Only if lock strategy is :failed_attempts
|
|
8
|
+
field :unlock_token, type: String # Only if unlock strategy is :email or :both
|
|
9
|
+
field :locked_at, type: Time
|
|
10
|
+
include Mongoid::Timestamps
|
|
11
|
+
index({ unlock_token: 1 }, { unique: true })
|
|
12
|
+
end
|
|
13
|
+
end
|