verify_it 0.3.0 → 0.4.1.beta

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: 9c975ad8c3016c930d9b8071d4f6c21fef89eece05332a48c5b1ebfebebf7386
4
- data.tar.gz: 40adb101cba36f8cb6b1c89f81977558e4d21ae0de87a93333a70e3bf92006d1
3
+ metadata.gz: 4f2e332e6a7ecae4c16eaf2232cb6f5826c26cd1a5c3de98c98a7cf0f1bf9968
4
+ data.tar.gz: 14e43962ebc0e2b2ca553324aadb0780aa1c2e6b22f4d4a1c6c9ab7fad285442
5
5
  SHA512:
6
- metadata.gz: e4b862df0a1be371deb7fa9ea1d4eb2ed85da2f83852771a3efd52156187cf0c7a5057c162cd5e6e04ab5f6c88571c3fa6bc9c4d0427f901a7f4dbf5f23b6fe9
7
- data.tar.gz: af94994c116729b2f0670b1664618ac1c01877755996ae00bd2b97069292fba1c0b2ef48d73f91a48d2c5653c2b49d5fc500a40c3a37e1cf304c77d7cf9d852c
6
+ metadata.gz: cc77c48de4632d3c129dcd3b3f614e909107d906dd3bdbf66d3d0771eb3609a3978916c318ccdaef873544b50f64f1a6be411f5554a8a882b1ec681eda4c0e8e
7
+ data.tar.gz: ecff61e7e94a7aaf27bf91f7ea0e13635c64dcde5413054077d366a8f36e2dd546bb049c18939374885675a8f5fdb9fe714ba82d4cd3caca3f875fe3245a9362
data/.DS_Store CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added
4
+ - `on_verify_success` callback now receives `request:` as an optional keyword argument,
5
+ 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
+ Existing proc-based callbacks are unaffected (procs ignore unknown keyword args);
8
+ lambda-based callbacks should add `request: nil` to their signature.
9
+
10
+ ### Changed
11
+ - Dropped Ruby 3.1 support; minimum required Ruby is now 3.2 (due to `connection_pool >= 3.0` transitive dependency)
12
+
13
+ ## [0.4.0] - 2026-03-07
14
+
15
+ ### Added
16
+ - **Rails Engine** (`VerifyIt::Engine`) — replaces the minimal Railtie with a full mountable engine
17
+ - **HTTP endpoints** — `POST /verify/send` and `POST /verify/confirm` served by `VerifyIt::VerificationsController`
18
+ - **I18n locale file** (`config/locales/en.yml`) — all response/error messages and SMS/email message templates are now translatable and overridable by host apps
19
+ - `config.current_record_resolver` — lambda `(request) { ... }` to resolve the authenticated record from a request; required when mounting the engine
20
+ - `config.identifier_resolver` — lambda `(record, channel) { ... }` to derive the delivery identifier (phone/email) from the record; required when mounting the engine
21
+ - `rescue_from VerifyIt::ConfigurationError` in `ApplicationController` returns a JSON 500 when resolvers are not configured
22
+ - Dummy Rails app (`spec/dummy/`) and `spec/rails_helper.rb` for request-level integration specs
23
+ - 10 new request specs covering auth, rate-limiting, locking, and error scenarios
24
+
25
+ ### Notes
26
+ - Backward compatible: standalone Ruby usage, the `verifies` DSL, and all existing sender lambdas are unchanged
27
+ - Non-mounting apps are completely unaffected by the new engine and resolver options
28
+
3
29
  ## [0.3.0] - 2026-03-03
4
30
 
5
31
  ### Security
data/README.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # VerifyIt
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/verify_it.svg)](https://badge.fury.io/rb/verify_it)
4
+
3
5
  A storage-agnostic verification system for Ruby applications.
6
+
4
7
  VerifyIt provides a flexible framework for implementing SMS and email verifications with built-in rate limiting and multiple storage backends.
5
8
 
9
+ It allows applications to generate and validate verification codes locally, helping reduce the operational cost associated with per-verification billing models.
10
+
6
11
  ## Features
7
12
 
8
13
  - **Storage Agnostic**: Redis, Memory, or Database storage
@@ -65,6 +70,10 @@ VerifyIt.configure do |config|
65
70
  )
66
71
  }
67
72
 
73
+ config.email_sender = ->(to:, code:, context:) {
74
+ EmailClient.new(to:).send("Your verification code is: #{code}")
75
+ }
76
+
68
77
  config.test_mode = Rails.env.test?
69
78
  config.bypass_delivery = Rails.env.test?
70
79
  end
@@ -123,10 +132,10 @@ class VerificationsController < ApplicationController
123
132
  session[:phone_verified] = true
124
133
  render json: { verified: true }
125
134
  elsif result.locked?
126
- render json: { error: "Too many failed attempts. Try again later." }, status: :locked
135
+ render json: { error: "Too many failed attempts. Try again later." }, status: :forbidden
127
136
  else
128
137
  remaining = VerifyIt.configuration.max_verification_attempts - result.attempts
129
- render json: { error: "Invalid code. #{remaining} attempts remaining." }, status: :unprocessable_entity
138
+ render json: { error: "Invalid code. #{remaining} attempts remaining." }
130
139
  end
131
140
  end
132
141
  end
@@ -138,7 +147,8 @@ Both `send_*` methods accept an optional `context:` hash that is forwarded to yo
138
147
 
139
148
  ```ruby
140
149
  current_user.send_sms_code(context: {
141
- message_template: "Your Acme login code: %{code}"
150
+ message_template: :code_verification_v1,
151
+ locale: :es
142
152
  })
143
153
 
144
154
  current_user.send_email_code(context: {
@@ -175,11 +185,109 @@ VerifyIt.cleanup(to: "+15551234567", record: current_user)
175
185
 
176
186
  ---
177
187
 
188
+ ## Rails Engine (HTTP Endpoints)
189
+
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.
192
+
193
+ ### Mount the engine
194
+
195
+ ```ruby
196
+ # config/routes.rb
197
+ mount VerifyIt::Engine, at: "/verify"
198
+ ```
199
+
200
+ This adds two routes:
201
+
202
+ | Method | Path | Action |
203
+ |---|---|---|
204
+ | POST | `/verify/send` | Send a verification code |
205
+ | POST | `/verify/confirm` | Confirm a verification code |
206
+
207
+ ### Configure the resolvers
208
+
209
+ Two additional config options are **required** when using the engine endpoints:
210
+
211
+ ```ruby
212
+ # config/initializers/verify_it.rb
213
+ VerifyIt.configure do |config|
214
+ config.secret_key_base = Rails.application.secret_key_base
215
+ config.storage = :redis
216
+ config.redis_client = Redis.new
217
+
218
+ # Resolve the authenticated record from the request (e.g. from a session or JWT).
219
+ config.current_record_resolver = ->(request) {
220
+ User.find_by(id: request.session[:user_id])
221
+ }
222
+
223
+ # Derive the delivery identifier from the record and the requested channel.
224
+ config.identifier_resolver = ->(record, channel) {
225
+ channel == :sms ? record.phone_number : record.email
226
+ }
227
+
228
+ config.sms_sender = ->(to:, code:, context:) {
229
+ Twilio::REST::Client.new.messages.create(
230
+ from: ENV["TWILIO_NUMBER"],
231
+ to: to,
232
+ body: I18n.t("verify_it.sms.default_message", code: code)
233
+ )
234
+ }
235
+ end
236
+ ```
237
+
238
+ ### Request/response examples
239
+
240
+ **Send a code:**
241
+
242
+ ```bash
243
+ POST /verify/send
244
+ Content-Type: application/json
245
+
246
+ { "channel": "sms" }
247
+
248
+ # 201 Created
249
+ { "message": "Verification code sent." }
250
+
251
+ # 429 Too Many Requests
252
+ { "error": "Too many attempts. Please try again later." }
253
+ ```
254
+
255
+ **Confirm a code:**
256
+
257
+ ```bash
258
+ POST /verify/confirm
259
+ Content-Type: application/json
260
+
261
+ { "channel": "sms", "code": "123456" }
262
+
263
+ # 200 OK
264
+ { "message": "Successfully verified." }
265
+
266
+ # 422 Unprocessable Entity
267
+ { "error": "The code you entered is invalid." }
268
+ ```
269
+
270
+ ### I18n overrides
271
+
272
+ All messages are stored in `config/locales/en.yml` and can be overridden in your
273
+ application's locale files:
274
+
275
+ ```yaml
276
+ en:
277
+ verify_it:
278
+ sms:
279
+ default_message: "%{code} is your one-time passcode."
280
+ responses:
281
+ verified: "Identity confirmed."
282
+ ```
283
+
284
+ ---
285
+
178
286
  ## Configuration Reference
179
287
 
180
288
  | Option | Default | Accepted values |
181
289
  |---|---|---|
182
- | `secret_key_base` | `nil` | Any string — **required** |
290
+ | `secret_key_base` | `nil` unless using Rails | Any string — **required** |
183
291
  | `storage` | `:memory` | `:memory`, `:redis`, `:database` |
184
292
  | `redis_client` | `nil` | A `Redis` instance (required when `storage: :redis`) |
185
293
  | `code_length` | `6` | Integer |
@@ -277,11 +385,10 @@ end
277
385
  ## Security
278
386
 
279
387
  - **`secret_key_base` is required** — codes are hashed with HMAC-SHA256 before storage
280
- - Enable rate limiting (on by default) in all environments
281
- - Use `:redis` or `:database` storage in production (`:memory` does not survive restarts)
282
- - Keep `code_ttl` short — 5 minutes is a sensible default
388
+ - Do not disable rate limiting
389
+ - Use `:redis` or `:database` storage in production
390
+ - Keep `code_ttl` short.
283
391
  - Use `on_verify_failure` to monitor and alert on repeated failures
284
- - Always serve verification endpoints over HTTPS
285
392
 
286
393
  ---
287
394
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VerifyIt
4
+ class ApplicationController < ActionController::API
5
+ rescue_from VerifyIt::ConfigurationError do |_error|
6
+ render json: { error: "Service configuration error" }, status: :internal_server_error
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VerifyIt
4
+ class VerificationsController < ApplicationController
5
+ VALID_CHANNELS = %i[sms email].freeze
6
+
7
+ before_action :validate_resolvers_configured!
8
+
9
+ # POST /send { channel: "sms"|"email" }
10
+ def create
11
+ record = resolve_record
12
+ return render json: { error: "Unauthorized" }, status: :unauthorized if record.nil?
13
+
14
+ channel = resolve_channel
15
+ identifier = resolve_identifier(record, channel)
16
+ result = VerifyIt.send_code(to: identifier, record: record, channel: channel, context: {})
17
+
18
+ if result.success?
19
+ payload = { message: I18n.t("verify_it.responses.sent") }
20
+ payload[:code] = result.code if VerifyIt.configuration.test_mode
21
+ render json: payload, status: :created
22
+ elsif result.rate_limited?
23
+ render json: { error: I18n.t("verify_it.errors.rate_limited") }, status: :too_many_requests
24
+ else
25
+ render json: { error: I18n.t("verify_it.errors.#{result.error || :delivery_failed}") },
26
+ status: :unprocessable_entity
27
+ end
28
+ end
29
+
30
+ # POST /confirm { channel: "sms"|"email", code: "123456" }
31
+ def verify
32
+ record = resolve_record
33
+ return render json: { error: "Unauthorized" }, status: :unauthorized if record.nil?
34
+
35
+ channel = resolve_channel
36
+ identifier = resolve_identifier(record, channel)
37
+ result = VerifyIt.verify_code(to: identifier, code: params[:code].to_s, record: record, request: request)
38
+
39
+ if result.success?
40
+ render json: { message: I18n.t("verify_it.responses.verified") }, status: :ok
41
+ elsif result.locked?
42
+ render json: { error: I18n.t("verify_it.errors.locked") }, status: :too_many_requests
43
+ elsif result.rate_limited?
44
+ render json: { error: I18n.t("verify_it.errors.rate_limited") }, status: :too_many_requests
45
+ else
46
+ render json: { error: I18n.t("verify_it.errors.#{result.error || :invalid_code}") },
47
+ status: :unprocessable_entity
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def validate_resolvers_configured!
54
+ unless VerifyIt.configuration.current_record_resolver
55
+ raise ConfigurationError, "VerifyIt: configure current_record_resolver to use HTTP endpoints."
56
+ end
57
+ return if VerifyIt.configuration.identifier_resolver
58
+
59
+ raise ConfigurationError, "VerifyIt: configure identifier_resolver to use HTTP endpoints."
60
+ end
61
+
62
+ def resolve_record = VerifyIt.configuration.current_record_resolver.call(request)
63
+
64
+ def resolve_channel
65
+ ch = params[:channel]&.to_sym
66
+ VALID_CHANNELS.include?(ch) ? ch : VerifyIt.configuration.delivery_channel
67
+ end
68
+
69
+ def resolve_identifier(record, channel) = VerifyIt.configuration.identifier_resolver.call(record, channel)
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ en:
2
+ verify_it:
3
+ responses:
4
+ sent: "Verification code sent."
5
+ verified: "Successfully verified."
6
+ errors:
7
+ rate_limited: "Too many attempts. Please try again later."
8
+ invalid_code: "The code you entered is invalid."
9
+ code_not_found: "No active verification code found. Please request a new one."
10
+ locked: "Your account is temporarily locked due to too many failed attempts."
11
+ delivery_failed: "Failed to deliver verification code. Please try again."
12
+ sms:
13
+ default_message: "%{code} is your verification code."
14
+ email:
15
+ default_subject: "Your verification code"
16
+ default_message: "Your verification code is %{code}."
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ VerifyIt::Engine.routes.draw do
4
+ post "send", to: "verifications#create", as: :send_verification
5
+ post "confirm", to: "verifications#verify", as: :confirm_verification
6
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "rails/generators/migration"
5
+
6
+ module VerifyIt
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Creates a VerifyIt initializer, mounts the engine in routes, " \
14
+ "and generates a migration when using database storage."
15
+
16
+ class_option :storage, type: :string, default: "memory",
17
+ desc: "Storage backend to use: memory, redis, or database"
18
+
19
+ def self.next_migration_number(dirname)
20
+ next_num = current_migration_number(dirname) + 1
21
+ ActiveRecord::Migration.next_migration_number(next_num)
22
+ end
23
+
24
+ def create_initializer
25
+ template "initializer.rb.erb", "config/initializers/verify_it.rb"
26
+ end
27
+
28
+ def mount_engine
29
+ route 'mount VerifyIt::Engine, at: "/verify_it"'
30
+ end
31
+
32
+ def generate_migration
33
+ return unless storage == "database"
34
+
35
+ migration_template "create_verify_it_tables.rb",
36
+ "db/migrate/create_verify_it_tables.rb"
37
+ end
38
+
39
+ private
40
+
41
+ def storage
42
+ options[:storage]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,6 +1,4 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateVerifyItTables < ActiveRecord::Migration[7.0]
1
+ class CreateVerifyItTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
2
  def change
5
3
  create_table :verify_it_codes do |t|
6
4
  t.string :identifier, null: false
@@ -5,16 +5,16 @@ VerifyIt.configure do |config|
5
5
  # It is not required to use the Rails secret key base.
6
6
  config.secret_key_base = Rails.application.secret_key_base
7
7
 
8
- <% if storage_type == "redis" %>
8
+ <% if storage == "redis" -%>
9
9
  config.storage = :redis
10
10
  config.redis_client = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
11
- <% elsif storage_type == "database" %>
11
+ <% elsif storage == "database" -%>
12
12
  config.storage = :database
13
- <% else %>
13
+ <% else -%>
14
14
  # config.storage = :memory # default
15
- <% end %>
15
+ <% end -%>
16
16
 
17
- # Required: configure your delivery channel(s)
17
+ # Required: configure your delivery channel(s).
18
18
  config.email_sender = ->(to:, code:, context:) {
19
19
  # VerificationMailer.send_code(to, code, context).deliver_now
20
20
  }
@@ -23,7 +23,17 @@ VerifyIt.configure do |config|
23
23
  # e.g. Twilio, Vonage, etc.
24
24
  }
25
25
 
26
- # Some other config options (with defaults):
26
+ # Required: resolve the current authenticated record from a request.
27
+ config.current_record_resolver = ->(request) {
28
+ # User.find_by(id: request.session[:user_id])
29
+ }
30
+
31
+ # Required: resolve the delivery identifier (phone/email) from the record and channel.
32
+ config.identifier_resolver = ->(record, channel) {
33
+ # channel == :sms ? record.phone_number : record.email
34
+ }
35
+
36
+ # Optional overrides (shown with defaults):
27
37
  # config.code_length = 6
28
38
  # config.code_ttl = 300
29
39
  # config.code_format = :numeric
@@ -16,17 +16,17 @@ module VerifyIt
16
16
  end
17
17
 
18
18
  def self.generate_numeric(length)
19
- Array.new(length) { rand(0..9) }.join
19
+ Array.new(length) { SecureRandom.random_number(10) }.join
20
20
  end
21
21
 
22
22
  def self.generate_alphanumeric(length)
23
23
  chars = ("0".."9").to_a + ("A".."Z").to_a
24
- Array.new(length) { chars.sample }.join
24
+ Array.new(length) { chars.sample(random: SecureRandom) }.join
25
25
  end
26
26
 
27
27
  def self.generate_alpha(length)
28
28
  chars = ("A".."Z").to_a
29
- Array.new(length) { chars.sample }.join
29
+ Array.new(length) { chars.sample(random: SecureRandom) }.join
30
30
  end
31
31
  end
32
32
  end
@@ -15,8 +15,8 @@ module VerifyIt
15
15
  def self.digest(code, secret:)
16
16
  if secret.nil? || secret.to_s.empty?
17
17
  raise ArgumentError,
18
- "VerifyIt requires secret_key_base to be configured. " \
19
- "Set config.secret_key_base in your initializer."
18
+ "VerifyIt requires secret_key_base to be configured. " \
19
+ "Set config.secret_key_base in your initializer."
20
20
  end
21
21
 
22
22
  OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, code.to_s)
@@ -20,7 +20,19 @@ module VerifyIt
20
20
  :namespace,
21
21
  :test_mode,
22
22
  :bypass_delivery,
23
- :secret_key_base
23
+ :secret_key_base,
24
+ :current_record_resolver,
25
+ :identifier_resolver
26
+
27
+ def validate!
28
+ if secret_key_base.nil? || secret_key_base.to_s.strip.empty?
29
+ raise ConfigurationError, "VerifyIt: secret_key_base must be configured before use."
30
+ end
31
+
32
+ return unless test_mode && defined?(Rails) && Rails.env.production?
33
+
34
+ raise ConfigurationError, "VerifyIt: test_mode must not be enabled in production."
35
+ end
24
36
 
25
37
  def initialize
26
38
  # Default values
@@ -43,6 +55,8 @@ module VerifyIt
43
55
  @test_mode = false
44
56
  @bypass_delivery = false
45
57
  @secret_key_base = nil
58
+ @current_record_resolver = nil
59
+ @identifier_resolver = nil
46
60
  end
47
61
  end
48
62
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module VerifyIt
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace VerifyIt
8
+
9
+ initializer "verify_it.i18n" do
10
+ config.i18n.load_path += Dir[Engine.root.join("config", "locales", "**", "*.yml").to_s]
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
+ end
19
+ end
@@ -5,9 +5,7 @@ module VerifyIt
5
5
  class DatabaseStorage < Base
6
6
  def initialize
7
7
  # Verify ActiveRecord is available
8
- unless defined?(::ActiveRecord)
9
- raise StandardError, "ActiveRecord is required for DatabaseStorage"
10
- end
8
+ raise StandardError, "ActiveRecord is required for DatabaseStorage" unless defined?(::ActiveRecord)
11
9
 
12
10
  # Load models only when needed
13
11
  require_relative "models/code"
@@ -130,10 +128,13 @@ module VerifyIt
130
128
  scope = scope.for_record(record) if record
131
129
  scope.delete_all
132
130
 
133
- # Delete attempts
131
+ # Delete attempts for this identifier
134
132
  scope = Models::Attempt.for_identifier(identifier)
135
133
  scope = scope.for_record(record) if record
136
134
  scope.delete_all
135
+
136
+ # Purge all globally expired attempt records
137
+ Models::Attempt.cleanup_expired
137
138
  end
138
139
 
139
140
  private
@@ -146,8 +147,8 @@ module VerifyIt
146
147
 
147
148
  def find_attempt(identifier:, record:, attempt_type:)
148
149
  scope = Models::Attempt
149
- .for_identifier(identifier)
150
- .where(attempt_type: attempt_type)
150
+ .for_identifier(identifier)
151
+ .where(attempt_type: attempt_type)
151
152
 
152
153
  scope = scope.for_record(record) if record
153
154
  scope.first
@@ -118,10 +118,10 @@ module VerifyIt
118
118
  end
119
119
 
120
120
  def cleanup(identifier:, record:)
121
+ keys_to_delete = %w[code attempts send_count].map do |suffix|
122
+ build_key(identifier: identifier, record: record, suffix: suffix)
123
+ end
121
124
  @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
125
  keys_to_delete.each { |key| @data.delete(key) }
126
126
  end
127
127
  end
@@ -14,9 +14,9 @@ module VerifyIt
14
14
  scope :active, -> { where("expires_at > ?", Time.now) }
15
15
  scope :expired, -> { where("expires_at <= ?", Time.now) }
16
16
  scope :for_identifier, ->(identifier) { where(identifier: identifier) }
17
- scope :for_record, ->(record) do
17
+ scope :for_record, lambda { |record|
18
18
  where(record_type: record.class.name, record_id: record.id)
19
- end
19
+ }
20
20
  scope :verification_type, -> { where(attempt_type: "verification") }
21
21
  scope :send_type, -> { where(attempt_type: "send") }
22
22
 
@@ -13,9 +13,9 @@ module VerifyIt
13
13
  scope :active, -> { where("expires_at > ?", Time.now) }
14
14
  scope :expired, -> { where("expires_at <= ?", Time.now) }
15
15
  scope :for_identifier, ->(identifier) { where(identifier: identifier) }
16
- scope :for_record, ->(record) do
16
+ scope :for_record, lambda { |record|
17
17
  where(record_type: record.class.name, record_id: record.id)
18
- end
18
+ }
19
19
 
20
20
  def expired?
21
21
  expires_at <= Time.now
@@ -9,9 +9,9 @@ module VerifyIt
9
9
  validates :identifier, presence: true
10
10
  validates :created_at, presence: true
11
11
 
12
- scope :for_record, ->(record) do
12
+ scope :for_record, lambda { |record|
13
13
  where(record_type: record.class.name, record_id: record.id)
14
- end
14
+ }
15
15
  scope :recent, ->(window) { where("created_at > ?", Time.now - window) }
16
16
 
17
17
  def self.cleanup_old(window)
@@ -18,9 +18,7 @@ module VerifyIt
18
18
  verify_method = "verify_#{channel}_code"
19
19
  cleanup_method = "cleanup_#{channel}_verification"
20
20
 
21
- if method_defined?(send_method)
22
- raise ArgumentError, "Method #{send_method} is already defined on #{name}"
23
- end
21
+ raise ArgumentError, "Method #{send_method} is already defined on #{name}" if method_defined?(send_method)
24
22
 
25
23
  define_method(send_method) do |context: {}|
26
24
  identifier = send(attribute)