verify_it 0.3.0 → 0.4.0.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +115 -8
- data/app/controllers/verify_it/application_controller.rb +9 -0
- data/app/controllers/verify_it/verifications_controller.rb +71 -0
- data/config/locales/en.yml +16 -0
- data/config/routes.rb +6 -0
- data/lib/generators/verify_it/install/install_generator.rb +46 -0
- data/lib/{verify_it/templates/migration.rb.erb → generators/verify_it/install/templates/create_verify_it_tables.rb} +1 -3
- data/lib/{verify_it → generators/verify_it/install}/templates/initializer.rb.erb +16 -6
- data/lib/verify_it/code_generator.rb +3 -3
- data/lib/verify_it/code_hasher.rb +2 -2
- data/lib/verify_it/configuration.rb +15 -1
- data/lib/verify_it/engine.rb +19 -0
- data/lib/verify_it/storage/database_storage.rb +7 -6
- data/lib/verify_it/storage/memory_storage.rb +3 -3
- data/lib/verify_it/storage/models/attempt.rb +2 -2
- data/lib/verify_it/storage/models/code.rb +2 -2
- data/lib/verify_it/storage/models/identifier_change.rb +2 -2
- data/lib/verify_it/verifiable.rb +1 -3
- data/lib/verify_it/verifier.rb +170 -121
- data/lib/verify_it/version.rb +1 -1
- data/lib/verify_it.rb +3 -1
- data/verify_it-0.1.0.gem +0 -0
- data/verify_it-0.1.1.gem +0 -0
- data/verify_it-0.2.0.gem +0 -0
- data/verify_it-0.3.0.gem +0 -0
- metadata +56 -36
- data/exe/verify_it +0 -7
- data/lib/verify_it/cli.rb +0 -141
- data/lib/verify_it/railtie.rb +0 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccaee3a9738dcd530a9afc7015401a5e13151f19b6e26e76dbf02d88b38294bd
|
|
4
|
+
data.tar.gz: 423c5f4b1354fd994be521a72303c6f61435263b5f71477177b0bbd68c9a89a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14e0947db04ce6fab19be9a0b1a475d99d6e16a6967ec079488fa162fecf45c37abdf37b7ad4e763d96a8110b1447dbca1a13923d3e0a5377d7f7e2bad5fe147
|
|
7
|
+
data.tar.gz: 3e5a97c488b04b36487c1ee8bb6591c2eb2c2987dd79dc62dbfe5b610ee1dac7813a5fd63927db745354f8ee54abd224fd67f587acbb273283731794c2f42904
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-03-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Rails Engine** (`VerifyIt::Engine`) — replaces the minimal Railtie with a full mountable engine
|
|
7
|
+
- **HTTP endpoints** — `POST /verify/send` and `POST /verify/confirm` served by `VerifyIt::VerificationsController`
|
|
8
|
+
- **I18n locale file** (`config/locales/en.yml`) — all response/error messages and SMS/email message templates are now translatable and overridable by host apps
|
|
9
|
+
- `config.current_record_resolver` — lambda `(request) { ... }` to resolve the authenticated record from a request; required when mounting the engine
|
|
10
|
+
- `config.identifier_resolver` — lambda `(record, channel) { ... }` to derive the delivery identifier (phone/email) from the record; required when mounting the engine
|
|
11
|
+
- `rescue_from VerifyIt::ConfigurationError` in `ApplicationController` returns a JSON 500 when resolvers are not configured
|
|
12
|
+
- Dummy Rails app (`spec/dummy/`) and `spec/rails_helper.rb` for request-level integration specs
|
|
13
|
+
- 10 new request specs covering auth, rate-limiting, locking, and error scenarios
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- Backward compatible: standalone Ruby usage, the `verifies` DSL, and all existing sender lambdas are unchanged
|
|
17
|
+
- Non-mounting apps are completely unaffected by the new engine and resolver options
|
|
18
|
+
|
|
3
19
|
## [0.3.0] - 2026-03-03
|
|
4
20
|
|
|
5
21
|
### Security
|
data/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# VerifyIt
|
|
2
2
|
|
|
3
|
+
[](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: :
|
|
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." }
|
|
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:
|
|
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
|
-
-
|
|
281
|
-
- Use `:redis` or `:database` storage in production
|
|
282
|
-
- Keep `code_ttl` short
|
|
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)
|
|
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,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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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) {
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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,
|
|
17
|
+
scope :for_record, lambda { |record|
|
|
18
18
|
where(record_type: record.class.name, record_id: record.id)
|
|
19
|
-
|
|
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,
|
|
16
|
+
scope :for_record, lambda { |record|
|
|
17
17
|
where(record_type: record.class.name, record_id: record.id)
|
|
18
|
-
|
|
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,
|
|
12
|
+
scope :for_record, lambda { |record|
|
|
13
13
|
where(record_type: record.class.name, record_id: record.id)
|
|
14
|
-
|
|
14
|
+
}
|
|
15
15
|
scope :recent, ->(window) { where("created_at > ?", Time.now - window) }
|
|
16
16
|
|
|
17
17
|
def self.cleanup_old(window)
|
data/lib/verify_it/verifiable.rb
CHANGED
|
@@ -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)
|