verify_it 0.1.0
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/.DS_Store +0 -0
- data/AGENTS.md +140 -0
- data/CHANGELOG.md +32 -0
- data/CLAUDE.md +132 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +414 -0
- data/Rakefile +12 -0
- data/exe/verify_it +7 -0
- data/lib/verify_it/cli.rb +135 -0
- data/lib/verify_it/code_generator.rb +32 -0
- data/lib/verify_it/configuration.rb +46 -0
- data/lib/verify_it/delivery/base.rb +11 -0
- data/lib/verify_it/delivery/email_delivery.rb +16 -0
- data/lib/verify_it/delivery/sms_delivery.rb +16 -0
- data/lib/verify_it/railtie.rb +14 -0
- data/lib/verify_it/rate_limiter.rb +68 -0
- data/lib/verify_it/result.rb +36 -0
- data/lib/verify_it/storage/base.rb +71 -0
- data/lib/verify_it/storage/database_storage.rb +225 -0
- data/lib/verify_it/storage/memory_storage.rb +136 -0
- data/lib/verify_it/storage/models/attempt.rb +33 -0
- data/lib/verify_it/storage/models/code.rb +30 -0
- data/lib/verify_it/storage/models/identifier_change.rb +23 -0
- data/lib/verify_it/storage/redis_storage.rb +85 -0
- data/lib/verify_it/templates/initializer.rb.erb +79 -0
- data/lib/verify_it/templates/migration.rb.erb +41 -0
- data/lib/verify_it/verifiable.rb +38 -0
- data/lib/verify_it/verifier.rb +174 -0
- data/lib/verify_it/version.rb +5 -0
- data/lib/verify_it.rb +82 -0
- data/pkg/verify_it-0.1.0.gem +0 -0
- data/sig/verify_it.rbs +4 -0
- metadata +207 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
class RateLimiter
|
|
5
|
+
def initialize(storage)
|
|
6
|
+
@storage = storage
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def check_send_limit(identifier:, record:)
|
|
10
|
+
current_count = @storage.send_count(identifier: identifier, record: record)
|
|
11
|
+
max_attempts = VerifyIt.configuration.max_send_attempts
|
|
12
|
+
|
|
13
|
+
if current_count >= max_attempts
|
|
14
|
+
{
|
|
15
|
+
allowed: false,
|
|
16
|
+
reason: "Maximum send attempts (#{max_attempts}) exceeded within rate limit window"
|
|
17
|
+
}
|
|
18
|
+
else
|
|
19
|
+
{ allowed: true }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def check_verification_limit(identifier:, record:)
|
|
24
|
+
current_attempts = @storage.attempts(identifier: identifier, record: record)
|
|
25
|
+
max_attempts = VerifyIt.configuration.max_verification_attempts
|
|
26
|
+
|
|
27
|
+
if current_attempts >= max_attempts
|
|
28
|
+
{
|
|
29
|
+
allowed: false,
|
|
30
|
+
locked: true,
|
|
31
|
+
reason: "Maximum verification attempts (#{max_attempts}) exceeded"
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
{ allowed: true }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_identifier_change_limit(record:)
|
|
39
|
+
changes = @storage.identifier_changes(record: record)
|
|
40
|
+
max_changes = VerifyIt.configuration.max_identifier_changes
|
|
41
|
+
|
|
42
|
+
if changes >= max_changes
|
|
43
|
+
{
|
|
44
|
+
allowed: false,
|
|
45
|
+
reason: "Maximum identifier changes (#{max_changes}) exceeded within rate limit window"
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
{ allowed: true }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def record_send(identifier:, record:)
|
|
53
|
+
@storage.increment_send_count(identifier: identifier, record: record)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_verification_attempt(identifier:, record:)
|
|
57
|
+
@storage.increment_attempts(identifier: identifier, record: record)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def record_identifier_change(record:, identifier:)
|
|
61
|
+
@storage.track_identifier_change(record: record, identifier: identifier)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reset_verification_attempts(identifier:, record:)
|
|
65
|
+
@storage.reset_attempts(identifier: identifier, record: record)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :error, :message, :code, :expires_at, :attempts
|
|
6
|
+
|
|
7
|
+
def initialize(success:, error: nil, message: nil, code: nil, expires_at: nil, attempts: 0,
|
|
8
|
+
rate_limited: false, locked: false, verified: false)
|
|
9
|
+
@success = success
|
|
10
|
+
@error = error
|
|
11
|
+
@message = message
|
|
12
|
+
@code = code
|
|
13
|
+
@expires_at = expires_at
|
|
14
|
+
@attempts = attempts
|
|
15
|
+
@rate_limited = rate_limited
|
|
16
|
+
@locked = locked
|
|
17
|
+
@verified = verified
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def success?
|
|
21
|
+
@success
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def rate_limited?
|
|
25
|
+
@rate_limited
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def locked?
|
|
29
|
+
@locked
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def verified?
|
|
33
|
+
@verified
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Storage
|
|
5
|
+
class Base
|
|
6
|
+
def store_code(identifier:, record:, code:, expires_at:)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def fetch_code(identifier:, record:)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def delete_code(identifier:, record:)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def increment_attempts(identifier:, record:)
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attempts(identifier:, record:)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset_attempts(identifier:, record:)
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def increment_send_count(identifier:, record:)
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def send_count(identifier:, record:)
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reset_send_count(identifier:, record:)
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def track_identifier_change(record:, identifier:)
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def identifier_changes(record:)
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cleanup(identifier:, record:)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
def build_key(identifier:, record:, suffix:)
|
|
57
|
+
namespace = VerifyIt.configuration.namespace
|
|
58
|
+
ns = namespace.respond_to?(:call) ? namespace.call(record) : nil
|
|
59
|
+
|
|
60
|
+
parts = []
|
|
61
|
+
parts << ns if ns
|
|
62
|
+
parts << record.class.name if record
|
|
63
|
+
parts << record.id if record.respond_to?(:id)
|
|
64
|
+
parts << identifier
|
|
65
|
+
parts << suffix
|
|
66
|
+
|
|
67
|
+
parts.join(":")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Storage
|
|
5
|
+
class DatabaseStorage < Base
|
|
6
|
+
def initialize
|
|
7
|
+
# Verify ActiveRecord is available
|
|
8
|
+
unless defined?(::ActiveRecord)
|
|
9
|
+
raise StandardError, "ActiveRecord is required for DatabaseStorage"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Load models only when needed
|
|
13
|
+
require_relative "models/code"
|
|
14
|
+
require_relative "models/attempt"
|
|
15
|
+
require_relative "models/identifier_change"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def store_code(identifier:, record:, code:, expires_at:)
|
|
19
|
+
attrs = {
|
|
20
|
+
identifier: identifier.to_s,
|
|
21
|
+
code: code,
|
|
22
|
+
expires_at: expires_at
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if record
|
|
26
|
+
attrs[:record_type] = record.class.name
|
|
27
|
+
attrs[:record_id] = record.id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find existing or create new
|
|
31
|
+
existing = find_code(identifier: identifier, record: record)
|
|
32
|
+
if existing
|
|
33
|
+
existing.update!(attrs)
|
|
34
|
+
else
|
|
35
|
+
Models::Code.create!(attrs)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch_code(identifier:, record:)
|
|
40
|
+
code_record = find_code(identifier: identifier, record: record)
|
|
41
|
+
return nil unless code_record
|
|
42
|
+
return nil if code_record.expired?
|
|
43
|
+
|
|
44
|
+
code_record.code
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete_code(identifier:, record:)
|
|
48
|
+
code_record = find_code(identifier: identifier, record: record)
|
|
49
|
+
code_record&.destroy
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def increment_attempts(identifier:, record:)
|
|
53
|
+
increment_attempt_count(
|
|
54
|
+
identifier: identifier,
|
|
55
|
+
record: record,
|
|
56
|
+
attempt_type: "verification"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def attempts(identifier:, record:)
|
|
61
|
+
get_attempt_count(
|
|
62
|
+
identifier: identifier,
|
|
63
|
+
record: record,
|
|
64
|
+
attempt_type: "verification"
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def reset_attempts(identifier:, record:)
|
|
69
|
+
reset_attempt_count(
|
|
70
|
+
identifier: identifier,
|
|
71
|
+
record: record,
|
|
72
|
+
attempt_type: "verification"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def increment_send_count(identifier:, record:)
|
|
77
|
+
increment_attempt_count(
|
|
78
|
+
identifier: identifier,
|
|
79
|
+
record: record,
|
|
80
|
+
attempt_type: "send"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def send_count(identifier:, record:)
|
|
85
|
+
get_attempt_count(
|
|
86
|
+
identifier: identifier,
|
|
87
|
+
record: record,
|
|
88
|
+
attempt_type: "send"
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reset_send_count(identifier:, record:)
|
|
93
|
+
reset_attempt_count(
|
|
94
|
+
identifier: identifier,
|
|
95
|
+
record: record,
|
|
96
|
+
attempt_type: "send"
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def track_identifier_change(record:, identifier:)
|
|
101
|
+
attrs = {
|
|
102
|
+
identifier: identifier.to_s,
|
|
103
|
+
created_at: Time.now
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if record
|
|
107
|
+
attrs[:record_type] = record.class.name
|
|
108
|
+
attrs[:record_id] = record.id
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Models::IdentifierChange.create!(attrs)
|
|
112
|
+
|
|
113
|
+
# Cleanup old changes outside the rate limit window
|
|
114
|
+
cleanup_old_identifier_changes(record: record)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def identifier_changes(record:)
|
|
118
|
+
return 0 unless record
|
|
119
|
+
|
|
120
|
+
window = VerifyIt.configuration.rate_limit_window
|
|
121
|
+
Models::IdentifierChange
|
|
122
|
+
.for_record(record)
|
|
123
|
+
.recent(window)
|
|
124
|
+
.count
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def cleanup(identifier:, record:)
|
|
128
|
+
# Delete codes
|
|
129
|
+
scope = Models::Code.for_identifier(identifier)
|
|
130
|
+
scope = scope.for_record(record) if record
|
|
131
|
+
scope.delete_all
|
|
132
|
+
|
|
133
|
+
# Delete attempts
|
|
134
|
+
scope = Models::Attempt.for_identifier(identifier)
|
|
135
|
+
scope = scope.for_record(record) if record
|
|
136
|
+
scope.delete_all
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def find_code(identifier:, record:)
|
|
142
|
+
scope = Models::Code.for_identifier(identifier)
|
|
143
|
+
scope = scope.for_record(record) if record
|
|
144
|
+
scope.first
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def find_attempt(identifier:, record:, attempt_type:)
|
|
148
|
+
scope = Models::Attempt
|
|
149
|
+
.for_identifier(identifier)
|
|
150
|
+
.where(attempt_type: attempt_type)
|
|
151
|
+
|
|
152
|
+
scope = scope.for_record(record) if record
|
|
153
|
+
scope.first
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def increment_attempt_count(identifier:, record:, attempt_type:)
|
|
157
|
+
attempt = find_attempt(
|
|
158
|
+
identifier: identifier,
|
|
159
|
+
record: record,
|
|
160
|
+
attempt_type: attempt_type
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
window = VerifyIt.configuration.rate_limit_window
|
|
164
|
+
expires_at = Time.now + window
|
|
165
|
+
|
|
166
|
+
if attempt.nil? || attempt.expired?
|
|
167
|
+
# Create new attempt record
|
|
168
|
+
attrs = {
|
|
169
|
+
identifier: identifier.to_s,
|
|
170
|
+
attempt_type: attempt_type,
|
|
171
|
+
count: 1,
|
|
172
|
+
expires_at: expires_at
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if record
|
|
176
|
+
attrs[:record_type] = record.class.name
|
|
177
|
+
attrs[:record_id] = record.id
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Delete old expired attempt if exists
|
|
181
|
+
attempt&.destroy
|
|
182
|
+
|
|
183
|
+
Models::Attempt.create!(attrs)
|
|
184
|
+
1
|
|
185
|
+
else
|
|
186
|
+
# Increment existing
|
|
187
|
+
attempt.increment!(:count)
|
|
188
|
+
attempt.count
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def get_attempt_count(identifier:, record:, attempt_type:)
|
|
193
|
+
attempt = find_attempt(
|
|
194
|
+
identifier: identifier,
|
|
195
|
+
record: record,
|
|
196
|
+
attempt_type: attempt_type
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return 0 unless attempt
|
|
200
|
+
return 0 if attempt.expired?
|
|
201
|
+
|
|
202
|
+
attempt.count
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def reset_attempt_count(identifier:, record:, attempt_type:)
|
|
206
|
+
attempt = find_attempt(
|
|
207
|
+
identifier: identifier,
|
|
208
|
+
record: record,
|
|
209
|
+
attempt_type: attempt_type
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
attempt&.destroy
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def cleanup_old_identifier_changes(record:)
|
|
216
|
+
return unless record
|
|
217
|
+
|
|
218
|
+
window = VerifyIt.configuration.rate_limit_window
|
|
219
|
+
Models::IdentifierChange
|
|
220
|
+
.for_record(record)
|
|
221
|
+
.cleanup_old(window)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/time"
|
|
4
|
+
|
|
5
|
+
module VerifyIt
|
|
6
|
+
module Storage
|
|
7
|
+
class MemoryStorage < Base
|
|
8
|
+
def initialize
|
|
9
|
+
@data = {}
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def store_code(identifier:, record:, code:, expires_at:)
|
|
14
|
+
key = build_key(identifier: identifier, record: record, suffix: "code")
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
@data[key] = { code: code, expires_at: expires_at }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch_code(identifier:, record:)
|
|
21
|
+
key = build_key(identifier: identifier, record: record, suffix: "code")
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
data = @data[key]
|
|
24
|
+
return nil unless data
|
|
25
|
+
return nil if data[:expires_at] < Time.now
|
|
26
|
+
|
|
27
|
+
data[:code]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_code(identifier:, record:)
|
|
32
|
+
key = build_key(identifier: identifier, record: record, suffix: "code")
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
@data.delete(key)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def increment_attempts(identifier:, record:)
|
|
39
|
+
key = build_key(identifier: identifier, record: record, suffix: "attempts")
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@data[key] ||= { count: 0, expires_at: Time.now + VerifyIt.configuration.rate_limit_window }
|
|
42
|
+
if @data[key][:expires_at] < Time.now
|
|
43
|
+
@data[key] = { count: 1, expires_at: Time.now + VerifyIt.configuration.rate_limit_window }
|
|
44
|
+
else
|
|
45
|
+
@data[key][:count] += 1
|
|
46
|
+
end
|
|
47
|
+
@data[key][:count]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def attempts(identifier:, record:)
|
|
52
|
+
key = build_key(identifier: identifier, record: record, suffix: "attempts")
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
data = @data[key]
|
|
55
|
+
return 0 unless data
|
|
56
|
+
return 0 if data[:expires_at] < Time.now
|
|
57
|
+
|
|
58
|
+
data[:count]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reset_attempts(identifier:, record:)
|
|
63
|
+
key = build_key(identifier: identifier, record: record, suffix: "attempts")
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
@data.delete(key)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def increment_send_count(identifier:, record:)
|
|
70
|
+
key = build_key(identifier: identifier, record: record, suffix: "send_count")
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
@data[key] ||= { count: 0, expires_at: Time.now + VerifyIt.configuration.rate_limit_window }
|
|
73
|
+
if @data[key][:expires_at] < Time.now
|
|
74
|
+
@data[key] = { count: 1, expires_at: Time.now + VerifyIt.configuration.rate_limit_window }
|
|
75
|
+
else
|
|
76
|
+
@data[key][:count] += 1
|
|
77
|
+
end
|
|
78
|
+
@data[key][:count]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def send_count(identifier:, record:)
|
|
83
|
+
key = build_key(identifier: identifier, record: record, suffix: "send_count")
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
data = @data[key]
|
|
86
|
+
return 0 unless data
|
|
87
|
+
return 0 if data[:expires_at] < Time.now
|
|
88
|
+
|
|
89
|
+
data[:count]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def reset_send_count(identifier:, record:)
|
|
94
|
+
key = build_key(identifier: identifier, record: record, suffix: "send_count")
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
@data.delete(key)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def track_identifier_change(record:, identifier:)
|
|
101
|
+
key = build_key(identifier: "", record: record, suffix: "identifier_changes")
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
@data[key] ||= []
|
|
104
|
+
@data[key] << { identifier: identifier, timestamp: Time.now }
|
|
105
|
+
# Keep only changes within the rate limit window
|
|
106
|
+
cutoff = Time.now - VerifyIt.configuration.rate_limit_window
|
|
107
|
+
@data[key].select! { |change| change[:timestamp] > cutoff }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def identifier_changes(record:)
|
|
112
|
+
key = build_key(identifier: "", record: record, suffix: "identifier_changes")
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
changes = @data[key] || []
|
|
115
|
+
cutoff = Time.now - VerifyIt.configuration.rate_limit_window
|
|
116
|
+
changes.select { |change| change[:timestamp] > cutoff }.count
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def cleanup(identifier:, record:)
|
|
121
|
+
@mutex.synchronize do
|
|
122
|
+
keys_to_delete = @data.keys.select do |key|
|
|
123
|
+
key.include?(identifier.to_s) && (record.nil? || key.include?(record.class.name))
|
|
124
|
+
end
|
|
125
|
+
keys_to_delete.each { |key| @data.delete(key) }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def clear_all
|
|
130
|
+
@mutex.synchronize do
|
|
131
|
+
@data.clear
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Storage
|
|
5
|
+
module Models
|
|
6
|
+
class Attempt < ::ActiveRecord::Base
|
|
7
|
+
self.table_name = "verify_it_attempts"
|
|
8
|
+
|
|
9
|
+
validates :identifier, presence: true
|
|
10
|
+
validates :attempt_type, presence: true, inclusion: { in: %w[verification send] }
|
|
11
|
+
validates :count, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
12
|
+
validates :expires_at, presence: true
|
|
13
|
+
|
|
14
|
+
scope :active, -> { where("expires_at > ?", Time.now) }
|
|
15
|
+
scope :expired, -> { where("expires_at <= ?", Time.now) }
|
|
16
|
+
scope :for_identifier, ->(identifier) { where(identifier: identifier) }
|
|
17
|
+
scope :for_record, ->(record) do
|
|
18
|
+
where(record_type: record.class.name, record_id: record.id)
|
|
19
|
+
end
|
|
20
|
+
scope :verification_type, -> { where(attempt_type: "verification") }
|
|
21
|
+
scope :send_type, -> { where(attempt_type: "send") }
|
|
22
|
+
|
|
23
|
+
def expired?
|
|
24
|
+
expires_at <= Time.now
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.cleanup_expired
|
|
28
|
+
expired.delete_all
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Storage
|
|
5
|
+
module Models
|
|
6
|
+
class Code < ::ActiveRecord::Base
|
|
7
|
+
self.table_name = "verify_it_codes"
|
|
8
|
+
|
|
9
|
+
validates :identifier, presence: true
|
|
10
|
+
validates :code, presence: true
|
|
11
|
+
validates :expires_at, presence: true
|
|
12
|
+
|
|
13
|
+
scope :active, -> { where("expires_at > ?", Time.now) }
|
|
14
|
+
scope :expired, -> { where("expires_at <= ?", Time.now) }
|
|
15
|
+
scope :for_identifier, ->(identifier) { where(identifier: identifier) }
|
|
16
|
+
scope :for_record, ->(record) do
|
|
17
|
+
where(record_type: record.class.name, record_id: record.id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def expired?
|
|
21
|
+
expires_at <= Time.now
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.cleanup_expired
|
|
25
|
+
expired.delete_all
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Storage
|
|
5
|
+
module Models
|
|
6
|
+
class IdentifierChange < ::ActiveRecord::Base
|
|
7
|
+
self.table_name = "verify_it_identifier_changes"
|
|
8
|
+
|
|
9
|
+
validates :identifier, presence: true
|
|
10
|
+
validates :created_at, presence: true
|
|
11
|
+
|
|
12
|
+
scope :for_record, ->(record) do
|
|
13
|
+
where(record_type: record.class.name, record_id: record.id)
|
|
14
|
+
end
|
|
15
|
+
scope :recent, ->(window) { where("created_at > ?", Time.now - window) }
|
|
16
|
+
|
|
17
|
+
def self.cleanup_old(window)
|
|
18
|
+
where("created_at <= ?", Time.now - window).delete_all
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|