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
data/README.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# VerifyIt
|
|
2
|
+
|
|
3
|
+
A production-grade, storage-agnostic verification system for Ruby applications. VerifyIt provides a flexible framework for implementing SMS, email, and other verification workflows with built-in rate limiting and multiple storage backends.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Storage Agnostic**: Redis, Memory, or Database storage
|
|
8
|
+
- **Delivery Agnostic**: Bring your own SMS/email provider (Twilio, SendGrid, etc.)
|
|
9
|
+
- **Built-in Rate Limiting**: Configurable limits for sends and verifications
|
|
10
|
+
- **Thread-Safe**: Designed for concurrent access
|
|
11
|
+
- **Test Mode**: Expose codes for testing without delivery
|
|
12
|
+
- **Rails Optional**: Works standalone or with Rails
|
|
13
|
+
- **Zero Runtime Dependencies**: Only ActiveSupport required
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'verify_it'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install verify_it
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Rails Installation
|
|
36
|
+
|
|
37
|
+
For Rails applications, use the installer to generate configuration files:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle exec verify_it install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This will:
|
|
44
|
+
1. Prompt you to select a storage backend (Memory, Redis, or Database)
|
|
45
|
+
2. Generate an initializer at `config/initializers/verify_it.rb`
|
|
46
|
+
3. Generate a migration (if Database storage is selected)
|
|
47
|
+
|
|
48
|
+
If you selected Database storage, run the migration:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rails db:migrate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### Basic Configuration
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require 'verify_it'
|
|
60
|
+
|
|
61
|
+
VerifyIt.configure do |config|
|
|
62
|
+
# Storage backend
|
|
63
|
+
config.storage = :memory # or :redis, :database
|
|
64
|
+
|
|
65
|
+
# For Redis storage
|
|
66
|
+
# config.redis_client = Redis.new(url: ENV['REDIS_URL'])
|
|
67
|
+
|
|
68
|
+
# Code settings
|
|
69
|
+
config.code_length = 6
|
|
70
|
+
config.code_ttl = 300 # 5 minutes
|
|
71
|
+
config.code_format = :numeric # or :alphanumeric, :alpha
|
|
72
|
+
|
|
73
|
+
# Rate limiting
|
|
74
|
+
config.max_send_attempts = 3
|
|
75
|
+
config.max_verification_attempts = 5
|
|
76
|
+
config.max_identifier_changes = 5
|
|
77
|
+
config.rate_limit_window = 3600 # 1 hour
|
|
78
|
+
|
|
79
|
+
# Delivery (SMS example with Twilio)
|
|
80
|
+
config.sms_sender = ->(to:, code:, context:) {
|
|
81
|
+
# Use custom template from context, or default
|
|
82
|
+
message = if context[:message_template]
|
|
83
|
+
context[:message_template] % { code: code }
|
|
84
|
+
else
|
|
85
|
+
"Your verification code is: #{code}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
twilio_client.messages.create(
|
|
89
|
+
to: to,
|
|
90
|
+
from: '+15551234567',
|
|
91
|
+
body: message
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Email delivery
|
|
96
|
+
config.email_sender = ->(to:, code:, context:) {
|
|
97
|
+
# Context available for customization
|
|
98
|
+
UserMailer.verification_code(to, code, context).deliver_now
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Sending a Verification Code
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
result = VerifyIt.send_code(
|
|
107
|
+
to: "+15551234567",
|
|
108
|
+
record: current_user,
|
|
109
|
+
channel: :sms,
|
|
110
|
+
context: { user_id: current_user.id }
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if result.success?
|
|
114
|
+
puts "Code sent successfully!"
|
|
115
|
+
puts "Expires at: #{result.expires_at}"
|
|
116
|
+
else
|
|
117
|
+
puts "Error: #{result.message}"
|
|
118
|
+
puts "Rate limited!" if result.rate_limited?
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Verifying a Code
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
result = VerifyIt.verify_code(
|
|
126
|
+
to: "+15551234567",
|
|
127
|
+
code: params[:code],
|
|
128
|
+
record: current_user
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if result.verified?
|
|
132
|
+
# Success! Code is valid
|
|
133
|
+
session[:verified] = true
|
|
134
|
+
redirect_to dashboard_path
|
|
135
|
+
elsif result.locked?
|
|
136
|
+
# Too many failed attempts
|
|
137
|
+
flash[:error] = "Account locked due to too many attempts"
|
|
138
|
+
elsif result.success? == false
|
|
139
|
+
# Invalid code
|
|
140
|
+
flash[:error] = "Invalid verification code. #{5 - result.attempts} attempts remaining"
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Cleanup
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# Clean up verification data for a user
|
|
148
|
+
VerifyIt.cleanup(
|
|
149
|
+
to: "+15551234567",
|
|
150
|
+
record: current_user
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Configuration Options
|
|
155
|
+
|
|
156
|
+
### Storage Backends
|
|
157
|
+
|
|
158
|
+
#### Memory Storage (Default)
|
|
159
|
+
Perfect for testing and development:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
config.storage = :memory
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Redis Storage
|
|
166
|
+
Recommended for production:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
config.storage = :redis
|
|
170
|
+
config.redis_client = Redis.new(url: ENV['REDIS_URL'])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### Database Storage
|
|
174
|
+
Uses ActiveRecord models for persistent storage:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
config.storage = :database
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Requires running the migration:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
rails generate verify_it:install
|
|
184
|
+
rails db:migrate
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Creates three tables:
|
|
188
|
+
- `verify_it_codes` - Stores verification codes
|
|
189
|
+
- `verify_it_attempts` - Tracks verification and send attempts
|
|
190
|
+
- `verify_it_identifier_changes` - Tracks identifier change history
|
|
191
|
+
|
|
192
|
+
### Code Settings
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
config.code_length = 6 # Length of verification code
|
|
196
|
+
config.code_ttl = 300 # Time-to-live in seconds (5 minutes)
|
|
197
|
+
config.code_format = :numeric # :numeric, :alphanumeric, or :alpha
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Rate Limiting
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
config.max_send_attempts = 3 # Max sends per window
|
|
204
|
+
config.max_verification_attempts = 5 # Max verification tries per window
|
|
205
|
+
config.max_identifier_changes = 5 # Max identifier changes per window
|
|
206
|
+
config.rate_limit_window = 3600 # Window duration in seconds
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Callbacks
|
|
210
|
+
|
|
211
|
+
Hook into the verification lifecycle:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
config.on_send = ->(record:, identifier:, channel:) {
|
|
215
|
+
Analytics.track('verification_sent', user_id: record.id)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
config.on_verify_success = ->(record:, identifier:) {
|
|
219
|
+
Analytics.track('verification_success', user_id: record.id)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
config.on_verify_failure = ->(record:, identifier:, attempts:) {
|
|
223
|
+
Analytics.track('verification_failure', user_id: record.id, attempts: attempts)
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Namespacing
|
|
228
|
+
|
|
229
|
+
Isolate verification data by tenant or organization:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
config.namespace = ->(record) {
|
|
233
|
+
record.respond_to?(:organization_id) ? "org:#{record.organization_id}" : nil
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Test Mode
|
|
238
|
+
|
|
239
|
+
Expose codes in test environment:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
config.test_mode = true # Includes code in Result object
|
|
243
|
+
config.bypass_delivery = true # Skip actual delivery
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Rails Integration
|
|
247
|
+
|
|
248
|
+
VerifyIt automatically integrates with Rails when detected.
|
|
249
|
+
|
|
250
|
+
### Model Integration
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
class User < ApplicationRecord
|
|
254
|
+
include VerifyIt::Verifiable
|
|
255
|
+
|
|
256
|
+
verifies :phone_number, channel: :sms
|
|
257
|
+
verifies :email, channel: :email
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Usage
|
|
261
|
+
user = User.find(params[:id])
|
|
262
|
+
|
|
263
|
+
# Send code (no context needed)
|
|
264
|
+
result = user.send_sms_code
|
|
265
|
+
|
|
266
|
+
# Send with custom message template
|
|
267
|
+
result = user.send_sms_code(context: {
|
|
268
|
+
message_template: "Your #{user.company_name} verification code is: %{code}"
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
# Send with tracking metadata
|
|
272
|
+
result = user.send_email_code(context: {
|
|
273
|
+
ip: request.ip,
|
|
274
|
+
user_agent: request.user_agent,
|
|
275
|
+
action: 'password_reset'
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
# Verify code
|
|
279
|
+
result = user.verify_sms_code(params[:code])
|
|
280
|
+
|
|
281
|
+
# Cleanup
|
|
282
|
+
user.cleanup_sms_verification
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Rails Configuration
|
|
286
|
+
|
|
287
|
+
Create an initializer `config/initializers/verify_it.rb`:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
VerifyIt.configure do |config|
|
|
291
|
+
config.storage = :redis
|
|
292
|
+
config.redis_client = Redis.new(url: ENV['REDIS_URL'])
|
|
293
|
+
|
|
294
|
+
config.sms_sender = ->(to:, code:, context:) {
|
|
295
|
+
TwilioService.send_sms(to: to, body: "Your code is: #{code}")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
config.test_mode = Rails.env.test?
|
|
299
|
+
config.bypass_delivery = Rails.env.test?
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Result Object API
|
|
304
|
+
|
|
305
|
+
All operations return a `VerifyIt::Result` object:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
result.success? # true if operation succeeded
|
|
309
|
+
result.verified? # true if code was verified successfully
|
|
310
|
+
result.locked? # true if max attempts exceeded
|
|
311
|
+
result.rate_limited? # true if rate limit hit
|
|
312
|
+
result.error # Symbol error code (:invalid_code, :rate_limited, etc.)
|
|
313
|
+
result.message # Human-readable message
|
|
314
|
+
result.code # Verification code (only in test_mode)
|
|
315
|
+
result.expires_at # Time when code expires
|
|
316
|
+
result.attempts # Number of verification attempts
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Testing
|
|
320
|
+
|
|
321
|
+
### RSpec Example
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
RSpec.describe "Phone Verification", type: :request do
|
|
325
|
+
before do
|
|
326
|
+
VerifyIt.configure do |config|
|
|
327
|
+
config.storage = :memory
|
|
328
|
+
config.test_mode = true
|
|
329
|
+
config.bypass_delivery = true
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "verifies phone number" do
|
|
334
|
+
post '/verify/send', params: { phone: '+15551234567' }
|
|
335
|
+
|
|
336
|
+
# Get code from response (test mode)
|
|
337
|
+
code = JSON.parse(response.body)['code']
|
|
338
|
+
|
|
339
|
+
post '/verify/confirm', params: { phone: '+15551234567', code: code }
|
|
340
|
+
expect(response).to have_http_status(:success)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Thread Safety
|
|
346
|
+
|
|
347
|
+
VerifyIt is designed to be thread-safe:
|
|
348
|
+
|
|
349
|
+
- Memory storage uses `Mutex` for synchronization
|
|
350
|
+
- Redis operations are atomic
|
|
351
|
+
- No shared mutable state in core logic
|
|
352
|
+
|
|
353
|
+
## Performance Considerations
|
|
354
|
+
|
|
355
|
+
### Redis Storage
|
|
356
|
+
- Uses key expiration for TTL
|
|
357
|
+
- Sorted sets for identifier tracking
|
|
358
|
+
- Atomic operations for counters
|
|
359
|
+
|
|
360
|
+
### Memory Storage
|
|
361
|
+
- Fast for testing and development
|
|
362
|
+
- Not recommended for production
|
|
363
|
+
- Data lost on restart
|
|
364
|
+
|
|
365
|
+
## Error Handling
|
|
366
|
+
|
|
367
|
+
VerifyIt uses explicit error states in Result objects:
|
|
368
|
+
|
|
369
|
+
- `:rate_limited` - Rate limit exceeded
|
|
370
|
+
- `:invalid_code` - Code doesn't match
|
|
371
|
+
- `:code_not_found` - No code stored or expired
|
|
372
|
+
- `:locked` - Max attempts reached
|
|
373
|
+
- `:delivery_failed` - Delivery provider error
|
|
374
|
+
|
|
375
|
+
## Security Best Practices
|
|
376
|
+
|
|
377
|
+
1. **Always use rate limiting** in production
|
|
378
|
+
2. **Use Redis storage** for distributed systems
|
|
379
|
+
3. **Set short TTLs** (5 minutes recommended)
|
|
380
|
+
4. **Monitor failed attempts** via callbacks
|
|
381
|
+
5. **Use HTTPS** for all verification endpoints
|
|
382
|
+
6. **Sanitize phone numbers** before storage
|
|
383
|
+
|
|
384
|
+
## Development
|
|
385
|
+
|
|
386
|
+
After checking out the repo:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
bin/setup # Install dependencies
|
|
390
|
+
bundle exec rspec # Run tests
|
|
391
|
+
bin/console # Interactive console
|
|
392
|
+
bundle exec rake build # Build gem
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Roadmap
|
|
396
|
+
|
|
397
|
+
- [ ] Database storage adapter
|
|
398
|
+
- [ ] Voice verification support
|
|
399
|
+
- [ ] WebAuthn integration
|
|
400
|
+
- [ ] Backup code generation
|
|
401
|
+
- [ ] Multi-factor authentication helpers
|
|
402
|
+
- [ ] Rails generators
|
|
403
|
+
|
|
404
|
+
## Contributing
|
|
405
|
+
|
|
406
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/JeremasPosta/verify_it. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/JeremasPosta/verify_it/blob/main/CODE_OF_CONDUCT.md).
|
|
407
|
+
|
|
408
|
+
## License
|
|
409
|
+
|
|
410
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
411
|
+
|
|
412
|
+
## Code of Conduct
|
|
413
|
+
|
|
414
|
+
Everyone interacting in the VerifyIt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/JeremasPosta/verify_it/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/exe/verify_it
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "erb"
|
|
5
|
+
require_relative "../verify_it" unless defined?(VerifyIt::VERSION)
|
|
6
|
+
|
|
7
|
+
module VerifyIt
|
|
8
|
+
class CLI
|
|
9
|
+
def self.start(args)
|
|
10
|
+
new(args).run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(args)
|
|
14
|
+
@args = args
|
|
15
|
+
@command = args[0]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
case @command
|
|
20
|
+
when "install"
|
|
21
|
+
install
|
|
22
|
+
when "version", "-v", "--version"
|
|
23
|
+
puts "VerifyIt #{VerifyIt::VERSION}"
|
|
24
|
+
when "help", "-h", "--help", nil
|
|
25
|
+
show_help
|
|
26
|
+
else
|
|
27
|
+
puts "Unknown command: #{@command}"
|
|
28
|
+
show_help
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def install
|
|
36
|
+
puts "VerifyIt Installer"
|
|
37
|
+
puts "=" * 50
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
check_rails_environment
|
|
41
|
+
|
|
42
|
+
storage_type = prompt_storage_type
|
|
43
|
+
generate_initializer(storage_type)
|
|
44
|
+
generate_migration if storage_type == "database"
|
|
45
|
+
|
|
46
|
+
puts
|
|
47
|
+
puts "✓ Installation complete!"
|
|
48
|
+
puts
|
|
49
|
+
puts "Next steps:"
|
|
50
|
+
puts " 1. Review the generated initializer"
|
|
51
|
+
if storage_type == "database"
|
|
52
|
+
puts " 2. Run: rails db:migrate"
|
|
53
|
+
puts " 3. Configure your delivery method in the initializer"
|
|
54
|
+
else
|
|
55
|
+
puts " 2. Configure your delivery method in the initializer"
|
|
56
|
+
puts " 3. Start using VerifyIt in your models!"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def check_rails_environment
|
|
61
|
+
unless defined?(Rails)
|
|
62
|
+
puts "Error: Rails environment not detected."
|
|
63
|
+
puts "Please run this command from your Rails application root."
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def prompt_storage_type
|
|
69
|
+
puts "Select storage backend:"
|
|
70
|
+
puts " 1) Memory (for development/testing)"
|
|
71
|
+
puts " 2) Redis (recommended for production)"
|
|
72
|
+
puts " 3) Database (uses ActiveRecord)"
|
|
73
|
+
puts
|
|
74
|
+
|
|
75
|
+
loop do
|
|
76
|
+
print "Enter choice (1-3): "
|
|
77
|
+
choice = $stdin.gets.chomp
|
|
78
|
+
|
|
79
|
+
case choice
|
|
80
|
+
when "1"
|
|
81
|
+
return "memory"
|
|
82
|
+
when "2"
|
|
83
|
+
return "redis"
|
|
84
|
+
when "3"
|
|
85
|
+
return "database"
|
|
86
|
+
else
|
|
87
|
+
puts "Invalid choice. Please enter 1, 2, or 3."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def generate_initializer(storage_type)
|
|
93
|
+
template_path = File.expand_path("../templates/initializer.rb.erb", __FILE__)
|
|
94
|
+
output_path = Rails.root.join("config", "initializers", "verify_it.rb")
|
|
95
|
+
|
|
96
|
+
if File.exist?(output_path)
|
|
97
|
+
print "Initializer already exists. Overwrite? (y/n): "
|
|
98
|
+
return unless $stdin.gets.chomp.downcase == "y"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
template = ERB.new(File.read(template_path))
|
|
102
|
+
content = template.result(binding)
|
|
103
|
+
|
|
104
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
105
|
+
File.write(output_path, content)
|
|
106
|
+
|
|
107
|
+
puts "✓ Created initializer: config/initializers/verify_it.rb"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def generate_migration
|
|
111
|
+
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
112
|
+
template_path = File.expand_path("../templates/migration.rb.erb", __FILE__)
|
|
113
|
+
output_path = Rails.root.join("db", "migrate", "#{timestamp}_create_verify_it_tables.rb")
|
|
114
|
+
|
|
115
|
+
template = ERB.new(File.read(template_path))
|
|
116
|
+
content = template.result(binding)
|
|
117
|
+
|
|
118
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
119
|
+
File.write(output_path, content)
|
|
120
|
+
|
|
121
|
+
puts "✓ Created migration: db/migrate/#{timestamp}_create_verify_it_tables.rb"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def show_help
|
|
125
|
+
puts "VerifyIt - Verification system for Ruby applications"
|
|
126
|
+
puts
|
|
127
|
+
puts "Usage:"
|
|
128
|
+
puts " verify_it install Generate initializer and optional migration"
|
|
129
|
+
puts " verify_it version Show version number"
|
|
130
|
+
puts " verify_it help Show this help message"
|
|
131
|
+
puts
|
|
132
|
+
puts "For more information, visit: https://github.com/JeremasPosta/verify_it"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
class CodeGenerator
|
|
5
|
+
def self.generate(length:, format:)
|
|
6
|
+
case format
|
|
7
|
+
when :numeric
|
|
8
|
+
generate_numeric(length)
|
|
9
|
+
when :alphanumeric
|
|
10
|
+
generate_alphanumeric(length)
|
|
11
|
+
when :alpha
|
|
12
|
+
generate_alpha(length)
|
|
13
|
+
else
|
|
14
|
+
raise ArgumentError, "Invalid code format: #{format}. Must be :numeric, :alphanumeric, or :alpha"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.generate_numeric(length)
|
|
19
|
+
Array.new(length) { rand(0..9) }.join
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.generate_alphanumeric(length)
|
|
23
|
+
chars = ("0".."9").to_a + ("A".."Z").to_a
|
|
24
|
+
Array.new(length) { chars.sample }.join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.generate_alpha(length)
|
|
28
|
+
chars = ("A".."Z").to_a
|
|
29
|
+
Array.new(length) { chars.sample }.join
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :storage,
|
|
6
|
+
:redis_client,
|
|
7
|
+
:delivery_channel,
|
|
8
|
+
:code_length,
|
|
9
|
+
:code_ttl,
|
|
10
|
+
:code_format,
|
|
11
|
+
:max_send_attempts,
|
|
12
|
+
:max_verification_attempts,
|
|
13
|
+
:max_identifier_changes,
|
|
14
|
+
:rate_limit_window,
|
|
15
|
+
:sms_sender,
|
|
16
|
+
:email_sender,
|
|
17
|
+
:on_send,
|
|
18
|
+
:on_verify_success,
|
|
19
|
+
:on_verify_failure,
|
|
20
|
+
:namespace,
|
|
21
|
+
:test_mode,
|
|
22
|
+
:bypass_delivery
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
# Default values
|
|
26
|
+
@storage = :memory
|
|
27
|
+
@redis_client = nil
|
|
28
|
+
@delivery_channel = :sms
|
|
29
|
+
@code_length = 6
|
|
30
|
+
@code_ttl = 300 # 5 minutes
|
|
31
|
+
@code_format = :numeric
|
|
32
|
+
@max_send_attempts = 3
|
|
33
|
+
@max_verification_attempts = 5
|
|
34
|
+
@max_identifier_changes = 5
|
|
35
|
+
@rate_limit_window = 3600 # 1 hour
|
|
36
|
+
@sms_sender = nil
|
|
37
|
+
@email_sender = nil
|
|
38
|
+
@on_send = nil
|
|
39
|
+
@on_verify_success = nil
|
|
40
|
+
@on_verify_failure = nil
|
|
41
|
+
@namespace = nil
|
|
42
|
+
@test_mode = false
|
|
43
|
+
@bypass_delivery = false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Delivery
|
|
5
|
+
class EmailDelivery < Base
|
|
6
|
+
def deliver(to:, code:, context:)
|
|
7
|
+
return if VerifyIt.configuration.bypass_delivery
|
|
8
|
+
|
|
9
|
+
sender = VerifyIt.configuration.email_sender
|
|
10
|
+
raise ConfigurationError, "Email sender not configured" unless sender
|
|
11
|
+
|
|
12
|
+
sender.call(to: to, code: code, context: context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifyIt
|
|
4
|
+
module Delivery
|
|
5
|
+
class SmsDelivery < Base
|
|
6
|
+
def deliver(to:, code:, context:)
|
|
7
|
+
return if VerifyIt.configuration.bypass_delivery
|
|
8
|
+
|
|
9
|
+
sender = VerifyIt.configuration.sms_sender
|
|
10
|
+
raise ConfigurationError, "SMS sender not configured" unless sender
|
|
11
|
+
|
|
12
|
+
sender.call(to: to, code: code, context: context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module VerifyIt
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer "verify_it.active_record" do
|
|
8
|
+
ActiveSupport.on_load(:active_record) do
|
|
9
|
+
require_relative "verifiable"
|
|
10
|
+
include VerifyIt::Verifiable
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|