verify_it 0.4.1.beta → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f2e332e6a7ecae4c16eaf2232cb6f5826c26cd1a5c3de98c98a7cf0f1bf9968
4
- data.tar.gz: 14e43962ebc0e2b2ca553324aadb0780aa1c2e6b22f4d4a1c6c9ab7fad285442
3
+ metadata.gz: be733b2c9a751f9163ffcc41c67a22c88b19a92baf8e8e5d478f0d2cfb3c1c3b
4
+ data.tar.gz: fb2580dd55fda49704fa8c327721eda31d470fff6f8f4177adb3741d24d89553
5
5
  SHA512:
6
- metadata.gz: cc77c48de4632d3c129dcd3b3f614e909107d906dd3bdbf66d3d0771eb3609a3978916c318ccdaef873544b50f64f1a6be411f5554a8a882b1ec681eda4c0e8e
7
- data.tar.gz: ecff61e7e94a7aaf27bf91f7ea0e13635c64dcde5413054077d366a8f36e2dd546bb049c18939374885675a8f5fdb9fe714ba82d4cd3caca3f875fe3245a9362
6
+ metadata.gz: d87fdcf7e40044a25259f945151644337b21d6e404e6fc793a3b9d856f745da23fe1014e34a7240d1386ca24a7e096b2233c19548e0ba8a86470faf98eb459db
7
+ data.tar.gz: bf5ef12903fe759e87de6ff9cd6bda4cb53b4abe3fe5c71aaae00c63a0efd3f2290abdff32706e0b7868e276a68580d78af847a15d82beab19531a522703e5dc
data/CHANGELOG.md CHANGED
@@ -1,14 +1,75 @@
1
- ## [Unreleased]
1
+ ## [0.5.0] - 2026-03-22
2
+
3
+ ### Added
4
+ - **IP-based rate limiting** — new opt-in configuration options `max_ip_send_attempts`
5
+ and `max_ip_verification_attempts` protect against SMS pumping, distributed brute
6
+ force, and IP-based abuse. Both default to `nil` (disabled). When configured, IP
7
+ attempts are tracked using the existing storage backends with no schema changes required.
8
+ - `Verifier#send_code` now accepts an optional `request:` keyword, matching
9
+ `verify_code`. The engine controller and `Verifiable` concern forward it automatically.
10
+ - `RateLimiter` gains `ip_send_rate_limited?`, `ip_verification_rate_limited?`,
11
+ `record_ip_send_attempt`, and `record_ip_verification_attempt` methods.
12
+ - `Verifiable#send_{channel}_code` now accepts an optional `request:` keyword.
13
+
14
+ ### Fixed
15
+ - **DatabaseStorage race conditions** — `store_code` and `increment_attempt_count`
16
+ now use atomic patterns with `rescue ActiveRecord::RecordNotUnique` + retry,
17
+ preventing duplicate records and rate limit bypass under concurrency.
18
+ - Migration template indexes on `verify_it_codes` and `verify_it_attempts` are now
19
+ `unique: true`, enforcing data integrity at the database level.
20
+ - `Attempt` model validation now accepts `ip_send` and `ip_verification` attempt types.
21
+
22
+ ### Changed
23
+ - README: generator commands now show the bare command first; `--storage` and
24
+ `--engine` are documented as opt-in flags with defaults explained
25
+ - README: moved Rails Engine section right after Rails Setup, before Plain Ruby,
26
+ so all Rails content is grouped together
27
+ - README: Plain Ruby examples use `record: user` instead of `record: current_user`
28
+ to avoid implying a Rails dependency
29
+ - README: Engine section notes that `--engine` already generates mount and resolver
30
+ config, so users don't duplicate setup
31
+ - README: Engine resolver example no longer hardcodes Redis storage; replaced with
32
+ a storage-agnostic comment pointing to the Configuration Reference
33
+
34
+ ## [0.4.2] - 2026-03-21
2
35
 
3
36
  ### Added
4
37
  - `on_verify_success` callback now receives `request:` as an optional keyword argument,
5
38
  enabling apps to access HTTP context (e.g. set session) at verification time.
6
- Thread: `VerifyIt.verify_code(request:)` → `Verifier#verify_code` → `handle_verification_success`.
7
39
  Existing proc-based callbacks are unaffected (procs ignore unknown keyword args);
8
40
  lambda-based callbacks should add `request: nil` to their signature.
41
+ - `Verifiable#verify_{channel}_code` now accepts an optional `request:` keyword,
42
+ forwarding it to `on_verify_success` so the model-level API has parity with the
43
+ engine and the direct `VerifyIt.verify_code` call.
44
+ - `current_record_resolver` and `identifier_resolver` are now listed in the
45
+ Configuration Reference table in the README.
46
+ - Install generator accepts `--engine` flag to mount engine routes and generate
47
+ resolver config. Without the flag, the installer produces a minimal initializer
48
+ with no engine routes or resolver boilerplate.
9
49
 
10
50
  ### Changed
11
- - Dropped Ruby 3.1 support; minimum required Ruby is now 3.2 (due to `connection_pool >= 3.0` transitive dependency)
51
+ - **Breaking (engine only):** `POST /verify/send` renamed to `POST /verify/request`
52
+ to avoid collision with Ruby's `Object#send`. Update client code that hits this
53
+ endpoint. The `/verify/confirm` endpoint is unchanged.
54
+ - Dropped Ruby 3.1 support; minimum required Ruby is now 3.2 (due to `connection_pool >= 3.0` transitive dependency).
55
+ - RuboCop `TargetRubyVersion` updated from 3.0 to 3.2 to match `required_ruby_version`.
56
+
57
+ ### Removed
58
+ - Engine no longer auto-includes `VerifyIt::Verifiable` into all ActiveRecord models.
59
+ Add `include VerifyIt::Verifiable` explicitly in models that need it (as shown in
60
+ the README). This avoids polluting unrelated models with verification methods.
61
+ - Install generator no longer unconditionally mounts the engine. Pass `--engine`
62
+ to opt in.
63
+
64
+ ### Fixed
65
+ - Initializer template `delivery_channel` default corrected from `:email` to `:sms`.
66
+ - Unused rescue variable in `Verifier#attempt_delivery` (RuboCop `Lint/UselessAssignment`).
67
+ - README testing example now uses correct engine params (`channel`, `auth_headers`)
68
+ instead of the outdated standalone format.
69
+ - README `on_verify_success` callback signature now includes `request: nil`.
70
+ - Install generator mount path changed from `/verify_it` to `/verify` to match
71
+ README examples.
72
+ - Initializer template `on_verify_success` callback signature now includes `request: nil`.
12
73
 
13
74
  ## [0.4.0] - 2026-03-07
14
75
 
data/README.md CHANGED
@@ -35,15 +35,21 @@ bundle install
35
35
  ### 1. Generate configuration files
36
36
 
37
37
  ```bash
38
- bundle exec verify_it install
38
+ rails generate verify_it:install
39
39
  ```
40
40
 
41
- The installer will ask you to choose a storage backend and generate:
41
+ Available options:
42
+ - `--storage=memory|redis|database` (default: `memory`)
43
+ - `--engine` — mount the VerifyIt engine and generate resolver config
42
44
 
43
- - `config/initializers/verify_it.rb` — your main configuration
44
- - A database migration (if you chose Database storage)
45
+ Examples:
45
46
 
46
- If you chose Database storage, run:
47
+ ```bash
48
+ rails generate verify_it:install --storage=redis
49
+ rails generate verify_it:install --storage=database --engine
50
+ ```
51
+
52
+ If you chose `--storage=database`, run the migration:
47
53
 
48
54
  ```bash
49
55
  rails db:migrate
@@ -159,36 +165,14 @@ current_user.send_email_code(context: {
159
165
 
160
166
  ---
161
167
 
162
- ## Plain Ruby
163
-
164
- ```ruby
165
- require "verify_it"
166
-
167
- VerifyIt.configure do |config|
168
- config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
169
- config.storage = :memory # :memory, :redis, or :database
170
-
171
- config.sms_sender = ->(to:, code:, context:) {
172
- MySmsSender.deliver(to: to, body: "Your code: #{code}")
173
- }
174
- end
175
-
176
- # Send a code
177
- result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: current_user)
178
-
179
- # Verify a code
180
- result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: current_user)
181
-
182
- # Clean up
183
- VerifyIt.cleanup(to: "+15551234567", record: current_user)
184
- ```
185
-
186
- ---
187
-
188
168
  ## Rails Engine (HTTP Endpoints)
189
169
 
190
- VerifyIt ships a mountable Rails Engine that exposes two JSON endpoints. This is
191
- optional, if you prefer to not use the engine, skip to the next section.
170
+ VerifyIt ships a mountable Rails Engine that exposes two JSON endpoints. Use the
171
+ engine when you want drop-in endpoints without writing controller code. This is
172
+ optional - skip this section if you prefer to handle verification in your own
173
+ controllers using the `Verifiable` concern or the plain Ruby API.
174
+
175
+ > If you ran the installer with `--engine`, the mount and resolver config are already generated for you. The details below are for reference or manual setup.
192
176
 
193
177
  ### Mount the engine
194
178
 
@@ -201,7 +185,7 @@ This adds two routes:
201
185
 
202
186
  | Method | Path | Action |
203
187
  |---|---|---|
204
- | POST | `/verify/send` | Send a verification code |
188
+ | POST | `/verify/request` | Send a verification code |
205
189
  | POST | `/verify/confirm` | Confirm a verification code |
206
190
 
207
191
  ### Configure the resolvers
@@ -212,8 +196,7 @@ Two additional config options are **required** when using the engine endpoints:
212
196
  # config/initializers/verify_it.rb
213
197
  VerifyIt.configure do |config|
214
198
  config.secret_key_base = Rails.application.secret_key_base
215
- config.storage = :redis
216
- config.redis_client = Redis.new
199
+ # ... your storage config (see Configuration Reference) ...
217
200
 
218
201
  # Resolve the authenticated record from the request (e.g. from a session or JWT).
219
202
  config.current_record_resolver = ->(request) {
@@ -240,7 +223,7 @@ end
240
223
  **Send a code:**
241
224
 
242
225
  ```bash
243
- POST /verify/send
226
+ POST /verify/request
244
227
  Content-Type: application/json
245
228
 
246
229
  { "channel": "sms" }
@@ -283,6 +266,32 @@ en:
283
266
 
284
267
  ---
285
268
 
269
+ ## Plain Ruby
270
+
271
+ ```ruby
272
+ require "verify_it"
273
+
274
+ VerifyIt.configure do |config|
275
+ config.secret_key_base = ENV.fetch("VERIFY_IT_SECRET")
276
+ config.storage = :memory # :memory, :redis, or :database
277
+
278
+ config.sms_sender = ->(to:, code:, context:) {
279
+ MySmsSender.deliver(to: to, body: "Your code: #{code}")
280
+ }
281
+ end
282
+
283
+ # Send a code
284
+ result = VerifyIt.send_code(to: "+15551234567", channel: :sms, record: user)
285
+
286
+ # Verify a code
287
+ result = VerifyIt.verify_code(to: "+15551234567", code: "123456", record: user)
288
+
289
+ # Clean up
290
+ VerifyIt.cleanup(to: "+15551234567", record: user)
291
+ ```
292
+
293
+ ---
294
+
286
295
  ## Configuration Reference
287
296
 
288
297
  | Option | Default | Accepted values |
@@ -296,12 +305,17 @@ en:
296
305
  | `max_send_attempts` | `3` | Integer |
297
306
  | `max_verification_attempts` | `5` | Integer |
298
307
  | `max_identifier_changes` | `5` | Integer |
308
+ | `max_ip_send_attempts` | `nil` | Integer or `nil` (disabled) — max sends per IP per window |
309
+ | `max_ip_verification_attempts` | `nil` | Integer or `nil` (disabled) — max verifications per IP per window |
299
310
  | `rate_limit_window` | `3600` | Seconds (integer) |
311
+ | `delivery_channel` | `:sms` | `:sms`, `:email` |
300
312
  | `sms_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
301
313
  | `email_sender` | `nil` | Lambda `(to:, code:, context:) { }` |
302
314
  | `on_send` | `nil` | Lambda `(record:, identifier:, channel:) { }` |
303
- | `on_verify_success` | `nil` | Lambda `(record:, identifier:) { }` |
315
+ | `on_verify_success` | `nil` | Lambda `(record:, identifier:, request: nil) { }` |
304
316
  | `on_verify_failure` | `nil` | Lambda `(record:, identifier:, attempts:) { }` |
317
+ | `current_record_resolver` | `nil` | Lambda `(request) { }` — required when mounting the engine |
318
+ | `identifier_resolver` | `nil` | Lambda `(record, channel) { }` — required when mounting the engine |
305
319
  | `namespace` | `nil` | Lambda `(record) { }` returning a string or nil |
306
320
  | `test_mode` | `false` | Boolean — exposes `result.code` |
307
321
  | `bypass_delivery` | `false` | Boolean — skips actual delivery |
@@ -313,7 +327,7 @@ config.on_send = ->(record:, identifier:, channel:) {
313
327
  Analytics.track("verification_sent", user_id: record.id, channel: channel)
314
328
  }
315
329
 
316
- config.on_verify_success = ->(record:, identifier:) {
330
+ config.on_verify_success = ->(record:, identifier:, request: nil) {
317
331
  Analytics.track("verification_success", user_id: record.id)
318
332
  }
319
333
 
@@ -368,14 +382,14 @@ RSpec.configure do |config|
368
382
  end
369
383
  ```
370
384
 
371
- In a request spec:
385
+ In a request spec (with the engine mounted):
372
386
 
373
387
  ```ruby
374
388
  it "verifies phone number" do
375
- post "/verify/send", params: { phone: "+15551234567" }
389
+ post "/verify/request", params: { channel: "sms" }, headers: auth_headers
376
390
  code = JSON.parse(response.body)["code"] # available in test_mode
377
391
 
378
- post "/verify/confirm", params: { phone: "+15551234567", code: code }
392
+ post "/verify/confirm", params: { channel: "sms", code: code }, headers: auth_headers
379
393
  expect(response).to have_http_status(:ok)
380
394
  end
381
395
  ```
@@ -389,6 +403,16 @@ end
389
403
  - Use `:redis` or `:database` storage in production
390
404
  - Keep `code_ttl` short.
391
405
  - Use `on_verify_failure` to monitor and alert on repeated failures
406
+ - **IP-based rate limiting** — enable `max_ip_send_attempts` and `max_ip_verification_attempts` to protect against SMS pumping and distributed brute force attacks:
407
+
408
+ ```ruby
409
+ VerifyIt.configure do |config|
410
+ config.max_ip_send_attempts = 10 # max 10 sends per IP per window
411
+ config.max_ip_verification_attempts = 20 # max 20 verifications per IP per window
412
+ end
413
+ ```
414
+
415
+ IP rate limiting requires passing `request:` to `send_code`/`verify_code`. The engine controller does this automatically. For custom controllers, pass `request:` explicitly.
392
416
 
393
417
  ---
394
418
 
@@ -13,7 +13,7 @@ module VerifyIt
13
13
 
14
14
  channel = resolve_channel
15
15
  identifier = resolve_identifier(record, channel)
16
- result = VerifyIt.send_code(to: identifier, record: record, channel: channel, context: {})
16
+ result = VerifyIt.send_code(to: identifier, record: record, channel: channel, context: {}, request: request)
17
17
 
18
18
  if result.success?
19
19
  payload = { message: I18n.t("verify_it.responses.sent") }
@@ -9,6 +9,7 @@ en:
9
9
  code_not_found: "No active verification code found. Please request a new one."
10
10
  locked: "Your account is temporarily locked due to too many failed attempts."
11
11
  delivery_failed: "Failed to deliver verification code. Please try again."
12
+ ip_rate_limited: "Too many requests from this IP address. Please try again later."
12
13
  sms:
13
14
  default_message: "%{code} is your verification code."
14
15
  email:
data/config/routes.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  VerifyIt::Engine.routes.draw do
4
- post "send", to: "verifications#create", as: :send_verification
4
+ post "request", to: "verifications#create", as: :request_verification
5
5
  post "confirm", to: "verifications#verify", as: :confirm_verification
6
6
  end
@@ -10,11 +10,13 @@ module VerifyIt
10
10
 
11
11
  source_root File.expand_path("templates", __dir__)
12
12
 
13
- desc "Creates a VerifyIt initializer, mounts the engine in routes, " \
13
+ desc "Creates a VerifyIt initializer and optionally mounts the engine routes " \
14
14
  "and generates a migration when using database storage."
15
15
 
16
16
  class_option :storage, type: :string, default: "memory",
17
17
  desc: "Storage backend to use: memory, redis, or database"
18
+ class_option :engine, type: :boolean, default: false,
19
+ desc: "Mount the VerifyIt engine and generate resolver config"
18
20
 
19
21
  def self.next_migration_number(dirname)
20
22
  next_num = current_migration_number(dirname) + 1
@@ -26,7 +28,9 @@ module VerifyIt
26
28
  end
27
29
 
28
30
  def mount_engine
29
- route 'mount VerifyIt::Engine, at: "/verify_it"'
31
+ return unless options[:engine]
32
+
33
+ route 'mount VerifyIt::Engine, at: "/verify"'
30
34
  end
31
35
 
32
36
  def generate_migration
@@ -8,7 +8,7 @@ class CreateVerifyItTables < ActiveRecord::Migration[<%= ActiveRecord::Migration
8
8
  t.datetime :expires_at, null: false
9
9
  t.timestamps
10
10
 
11
- t.index [:identifier, :record_type, :record_id], name: "index_verify_it_codes_on_record"
11
+ t.index [:identifier, :record_type, :record_id], name: "index_verify_it_codes_on_record", unique: true
12
12
  t.index :expires_at
13
13
  end
14
14
 
@@ -22,7 +22,7 @@ class CreateVerifyItTables < ActiveRecord::Migration[<%= ActiveRecord::Migration
22
22
  t.timestamps
23
23
 
24
24
  t.index [:identifier, :record_type, :record_id, :attempt_type],
25
- name: "index_verify_it_attempts_on_record_and_type"
25
+ name: "index_verify_it_attempts_on_record_and_type", unique: true
26
26
  t.index :expires_at
27
27
  end
28
28
 
@@ -23,24 +23,27 @@ VerifyIt.configure do |config|
23
23
  # e.g. Twilio, Vonage, etc.
24
24
  }
25
25
 
26
- # Required: resolve the current authenticated record from a request.
26
+ <% if options[:engine] -%>
27
+ # Required when mounting the engine (VerifyIt::Engine):
28
+ # Resolve the current authenticated record from a request.
27
29
  config.current_record_resolver = ->(request) {
28
30
  # User.find_by(id: request.session[:user_id])
29
31
  }
30
32
 
31
- # Required: resolve the delivery identifier (phone/email) from the record and channel.
33
+ # Resolve the delivery identifier (phone/email) from the record and channel.
32
34
  config.identifier_resolver = ->(record, channel) {
33
35
  # channel == :sms ? record.phone_number : record.email
34
36
  }
35
37
 
38
+ <% end -%>
36
39
  # Optional overrides (shown with defaults):
37
40
  # config.code_length = 6
38
41
  # config.code_ttl = 300
39
42
  # config.code_format = :numeric
40
43
  # config.max_send_attempts = 3
41
44
  # config.max_verification_attempts = 5
42
- # config.delivery_channel = :email
45
+ # config.delivery_channel = :sms
43
46
  # config.on_send = ->(record:, identifier:, channel:) {}
44
- # config.on_verify_success = ->(record:, identifier:) {}
47
+ # config.on_verify_success = ->(record:, identifier:, request: nil) {}
45
48
  # config.on_verify_failure = ->(record:, identifier:, attempts:) {}
46
49
  end
@@ -21,6 +21,8 @@ module VerifyIt
21
21
  :test_mode,
22
22
  :bypass_delivery,
23
23
  :secret_key_base,
24
+ :max_ip_send_attempts,
25
+ :max_ip_verification_attempts,
24
26
  :current_record_resolver,
25
27
  :identifier_resolver
26
28
 
@@ -55,6 +57,8 @@ module VerifyIt
55
57
  @test_mode = false
56
58
  @bypass_delivery = false
57
59
  @secret_key_base = nil
60
+ @max_ip_send_attempts = nil
61
+ @max_ip_verification_attempts = nil
58
62
  @current_record_resolver = nil
59
63
  @identifier_resolver = nil
60
64
  end
@@ -9,11 +9,5 @@ module VerifyIt
9
9
  initializer "verify_it.i18n" do
10
10
  config.i18n.load_path += Dir[Engine.root.join("config", "locales", "**", "*.yml").to_s]
11
11
  end
12
-
13
- initializer "verify_it.active_record" do
14
- ActiveSupport.on_load(:active_record) do
15
- include VerifyIt::Verifiable
16
- end
17
- end
18
12
  end
19
13
  end
@@ -64,5 +64,29 @@ module VerifyIt
64
64
  def reset_verification_attempts(identifier:, record:)
65
65
  @storage.reset_attempts(identifier: identifier, record: record)
66
66
  end
67
+
68
+ def ip_send_rate_limited?(ip:)
69
+ max = VerifyIt.configuration.max_ip_send_attempts
70
+ return false unless max
71
+
72
+ count = @storage.send_count(identifier: ip, record: nil)
73
+ count >= max
74
+ end
75
+
76
+ def ip_verification_rate_limited?(ip:)
77
+ max = VerifyIt.configuration.max_ip_verification_attempts
78
+ return false unless max
79
+
80
+ count = @storage.attempts(identifier: ip, record: nil)
81
+ count >= max
82
+ end
83
+
84
+ def record_ip_send_attempt(ip:)
85
+ @storage.increment_send_count(identifier: ip, record: nil)
86
+ end
87
+
88
+ def record_ip_verification_attempt(ip:)
89
+ @storage.increment_attempts(identifier: ip, record: nil)
90
+ end
67
91
  end
68
92
  end
@@ -25,13 +25,14 @@ module VerifyIt
25
25
  attrs[:record_id] = record.id
26
26
  end
27
27
 
28
- # Find existing or create new
29
28
  existing = find_code(identifier: identifier, record: record)
30
29
  if existing
31
- existing.update!(attrs)
30
+ existing.update!(code: code, expires_at: expires_at)
32
31
  else
33
32
  Models::Code.create!(attrs)
34
33
  end
34
+ rescue ActiveRecord::RecordNotUnique
35
+ retry
35
36
  end
36
37
 
37
38
  def fetch_code(identifier:, record:)
@@ -146,48 +147,41 @@ module VerifyIt
146
147
  end
147
148
 
148
149
  def find_attempt(identifier:, record:, attempt_type:)
150
+ build_attempt_scope(identifier:, record:, attempt_type:).first
151
+ end
152
+
153
+ def build_attempt_scope(identifier:, record:, attempt_type:)
149
154
  scope = Models::Attempt
150
155
  .for_identifier(identifier)
151
156
  .where(attempt_type: attempt_type)
152
157
 
153
158
  scope = scope.for_record(record) if record
154
- scope.first
159
+ scope
155
160
  end
156
161
 
157
162
  def increment_attempt_count(identifier:, record:, attempt_type:)
158
- attempt = find_attempt(
159
- identifier: identifier,
160
- record: record,
161
- attempt_type: attempt_type
162
- )
163
+ # Clean expired attempts first
164
+ build_attempt_scope(identifier:, record:, attempt_type:).expired.delete_all
163
165
 
164
166
  window = VerifyIt.configuration.rate_limit_window
165
167
  expires_at = Time.now + window
168
+ attrs = {
169
+ identifier: identifier.to_s,
170
+ attempt_type: attempt_type,
171
+ count: 0,
172
+ expires_at: expires_at
173
+ }
166
174
 
167
- if attempt.nil? || attempt.expired?
168
- # Create new attempt record
169
- attrs = {
170
- identifier: identifier.to_s,
171
- attempt_type: attempt_type,
172
- count: 1,
173
- expires_at: expires_at
174
- }
175
-
176
- if record
177
- attrs[:record_type] = record.class.name
178
- attrs[:record_id] = record.id
179
- end
180
-
181
- # Delete old expired attempt if exists
182
- attempt&.destroy
183
-
184
- Models::Attempt.create!(attrs)
185
- 1
186
- else
187
- # Increment existing
188
- attempt.increment!(:count)
189
- attempt.count
175
+ if record
176
+ attrs[:record_type] = record.class.name
177
+ attrs[:record_id] = record.id
190
178
  end
179
+
180
+ attempt = build_attempt_scope(identifier:, record:, attempt_type:).first_or_create!(attrs)
181
+ attempt.increment!(:count)
182
+ attempt.count
183
+ rescue ActiveRecord::RecordNotUnique
184
+ retry
191
185
  end
192
186
 
193
187
  def get_attempt_count(identifier:, record:, attempt_type:)
@@ -7,7 +7,7 @@ module VerifyIt
7
7
  self.table_name = "verify_it_attempts"
8
8
 
9
9
  validates :identifier, presence: true
10
- validates :attempt_type, presence: true, inclusion: { in: %w[verification send] }
10
+ validates :attempt_type, presence: true, inclusion: { in: %w[verification send ip_send ip_verification] }
11
11
  validates :count, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
12
12
  validates :expires_at, presence: true
13
13
 
@@ -20,22 +20,24 @@ module VerifyIt
20
20
 
21
21
  raise ArgumentError, "Method #{send_method} is already defined on #{name}" if method_defined?(send_method)
22
22
 
23
- define_method(send_method) do |context: {}|
23
+ define_method(send_method) do |context: {}, request: nil|
24
24
  identifier = send(attribute)
25
25
  VerifyIt.send_code(
26
26
  to: identifier,
27
27
  record: self,
28
28
  channel: channel,
29
- context: context
29
+ context: context,
30
+ request: request
30
31
  )
31
32
  end
32
33
 
33
- define_method(verify_method) do |code|
34
+ define_method(verify_method) do |code, request: nil|
34
35
  identifier = send(attribute)
35
36
  VerifyIt.verify_code(
36
37
  to: identifier,
37
38
  code: code,
38
- record: self
39
+ record: self,
40
+ request: request
39
41
  )
40
42
  end
41
43
 
@@ -11,9 +11,14 @@ module VerifyIt
11
11
  @rate_limiter = RateLimiter.new(storage)
12
12
  end
13
13
 
14
- def send_code(to:, record:, channel: nil, context: {})
14
+ def send_code(to:, record:, channel: nil, context: {}, request: nil)
15
15
  channel ||= config.delivery_channel
16
16
 
17
+ ip = extract_ip(request)
18
+ if ip && rate_limiter.ip_send_rate_limited?(ip: ip)
19
+ return rate_limited_result("IP address rate limited for sending")
20
+ end
21
+
17
22
  rate_limit_result = check_send_rate_limits(to: to, record: record)
18
23
  return rate_limit_result if rate_limit_result
19
24
 
@@ -27,10 +32,17 @@ module VerifyIt
27
32
  )
28
33
  return delivery_result if delivery_result
29
34
 
35
+ rate_limiter.record_ip_send_attempt(ip: ip) if ip
36
+
30
37
  finalize_send(to: to, record: record, channel: channel, code_data: code_data)
31
38
  end
32
39
 
33
40
  def verify_code(to:, code:, record:, request: nil)
41
+ ip = extract_ip(request)
42
+ if ip && rate_limiter.ip_verification_rate_limited?(ip: ip)
43
+ return rate_limited_result("IP address rate limited for verification")
44
+ end
45
+
34
46
  rate_limit_result = check_verification_rate_limit(to: to, record: record)
35
47
  return rate_limit_result if rate_limit_result
36
48
 
@@ -38,6 +50,7 @@ module VerifyIt
38
50
  return code_not_found_result unless stored_code
39
51
 
40
52
  current_attempts = rate_limiter.record_verification_attempt(identifier: to, record: record)
53
+ rate_limiter.record_ip_verification_attempt(ip: ip) if ip
41
54
 
42
55
  if code_matches?(code, stored_code)
43
56
  handle_verification_success(to: to, record: record, attempts: current_attempts, request: request)
@@ -100,7 +113,7 @@ module VerifyIt
100
113
  delivery = get_delivery_adapter(channel)
101
114
  delivery.deliver(to: to, code: code, context: context)
102
115
  nil
103
- rescue StandardError => e
116
+ rescue StandardError
104
117
  delivery_failed_result
105
118
  end
106
119
 
@@ -159,6 +172,16 @@ module VerifyIt
159
172
  end
160
173
  end
161
174
 
175
+ def extract_ip(request)
176
+ return nil unless request
177
+
178
+ if request.respond_to?(:remote_ip)
179
+ request.remote_ip
180
+ elsif request.respond_to?(:ip)
181
+ request.ip
182
+ end
183
+ end
184
+
162
185
  def invoke_callback(callback, **args)
163
186
  callback&.call(**args)
164
187
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VerifyIt
4
- VERSION = "0.4.1.beta"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/verify_it.rb CHANGED
@@ -33,8 +33,8 @@ module VerifyIt
33
33
  yield(configuration)
34
34
  end
35
35
 
36
- def send_code(to:, record:, channel: nil, context: {})
37
- verifier.send_code(to: to, record: record, channel: channel, context: context)
36
+ def send_code(to:, record:, channel: nil, context: {}, request: nil)
37
+ verifier.send_code(to: to, record: record, channel: channel, context: context, request: request)
38
38
  end
39
39
 
40
40
  def verify_code(to:, code:, record:, request: nil)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verify_it
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1.beta
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremias Ramirez