rails_simple_auth 1.0.2 → 1.0.3

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: 6d9299c78e7be2bfe4bc6e338150d03ec03d80e6c624616e09c30b48c5bf16c6
4
- data.tar.gz: 145ebfad74f0188f138a9e8e006bfac26a6404b89d3690a0a8484246a5b01d9f
3
+ metadata.gz: 5fb876e2f8ec9c1f40ed2dbb8d97fdd984e7e5927e11c56be6a2ac4b2aa593a4
4
+ data.tar.gz: 29a914c7c8f77a85199d6f7188ce77ab24460554ca90421b22b38646f15464f5
5
5
  SHA512:
6
- metadata.gz: 92302e8e2c6d489ebda9e82b222e0a53f31c52f0b8958bf3af2a4240f654f0de7bd81892e350ee92892df16d747ef88d4b43ae69b6af0e510ee1504925755693
7
- data.tar.gz: 07d17cb9ca09fe1f01446cf1faf4cc67fb303d5c0553b6e2c242595054700a88fff187838820aed8639c65b3fd839e3679771acb178700f7966cf17f0f1ce258
6
+ metadata.gz: 8a7d0926e4e1e3a8c7da6ec9ab4de80768ae98a1316a32b6d9bf6aa790f266fc494598344b9a47444bfac1241ab55ea6d11c4c89a56bb2d270cff816a0bb3178
7
+ data.tar.gz: b9ea5fa54ad39f51e86dae6852df477760bfb8c28a3c002cae421c40d44e5d3fb74e0ecfaa6ab6d65705cb3ba579d72e4c47061aa2b06d848d5f4639ca9f1eaf
data/CHANGELOG.md CHANGED
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.3] - 2025-01-19
11
+
12
+ ### Added
13
+
14
+ - **Temporary Users Support** - Allow users to try the app without signing up, then convert to permanent accounts
15
+ - `TemporaryUser` concern with `temporary?` and `permanent?` methods
16
+ - Scopes: `temporary`, `permanent`, `temporary_expired`
17
+ - `convert_to_permanent!(email:, password:)` method for account conversion
18
+ - Generator: `rails g rails_simple_auth:temporary_users` for migration
19
+ - Configuration options: `temporary_users_enabled`, `temporary_user_cleanup_days`
20
+ - Automatic cleanup of expired temporary users via `User.cleanup_expired_temporary!`
21
+ - Session invalidation on account conversion
22
+ - Automatic destruction of temporary user when signing in with different account
23
+ - **Email Reconfirmation Flow** - Support for users changing their email address
24
+ - `unconfirmed_email` column support for pending email changes
25
+ - `reconfirming?` and `unconfirmed_or_reconfirming?` helper methods
26
+ - `confirmable_email` helper returns the email needing confirmation
27
+ - Confirmation emails sent to new email address during reconfirmation
28
+ - Comprehensive test suite (126 tests, 247 assertions)
29
+
30
+ ### Fixed
31
+
32
+ - `confirm!` now uses `has_attribute?(:temporary)` instead of `respond_to?(:temporary?)` to prevent errors when `Authenticatable` is included without the temporary database column
33
+ - `confirm!` properly handles race conditions with `RecordNotUnique` rescue during reconfirmation
34
+ - `convert_to_permanent!` validates password presence to prevent users being left without credentials
35
+ - `convert_to_permanent!` reloads after transaction to check actual database state (not stale in-memory state)
36
+ - `convert_to_permanent!` resets `confirmed_at` to require email verification for new address
37
+ - `cleanup_expired_temporary!` now returns accurate count (only increments when destroy succeeds)
38
+ - `magic_link_login` checks `confirm!` return value and shows error if confirmation fails
39
+ - `confirmations_controller#show` checks `confirm!` return value and displays appropriate error message
40
+ - `destroy_temporary_user_session` skips destruction when user is re-authenticating as themselves
41
+ - `AuthMailer#confirmation` sends to `confirmable_email` for correct recipient during reconfirmation
42
+
43
+ ### Changed
44
+
45
+ - Confirmation token purpose changed from `:email_confirmation` to `:confirm_email` (**Breaking**: existing confirmation tokens will be invalidated)
46
+
47
+ ## [1.0.2] - 2025-01-18
48
+
49
+ ### Fixed
50
+
51
+ - Session invalidation and batch cleanup for temporary users
52
+
10
53
  ## [1.0.0] - 2025-01-18
11
54
 
12
55
  ### Added
data/README.md CHANGED
@@ -4,16 +4,16 @@ Simple, secure authentication for Rails 8+ applications. Built on Rails primitiv
4
4
 
5
5
  ## Features
6
6
 
7
- - **Email/Password authentication** with bcrypt
8
- - **Magic link** (passwordless) authentication
9
- - **Email confirmation** with signed tokens
10
- - **Password reset** with signed tokens
11
- - **OAuth support** (Google, GitHub, etc.)
12
- - **Temporary users** (guest mode) with conversion to permanent
13
- - **Rate limiting** built-in
14
- - **Session tracking** with IP and user agent
15
- - **Customizable styling** via CSS variables
16
- - **No dependencies** beyond Rails and bcrypt
7
+ - [**Email/Password authentication**](#installation) - secure session-based auth
8
+ - [**Magic link authentication**](#routes) - passwordless sign-in via email
9
+ - [**Email confirmation**](#routes) - verify user email addresses
10
+ - [**Password reset**](#routes) - secure password recovery flow
11
+ - [**OAuth support**](#oauth-setup) - Google, GitHub, and more
12
+ - [**Temporary users**](#temporary-users-guest-accounts) - guest accounts that convert to permanent
13
+ - [**Rate limiting**](#rate-limiting) - built-in protection on all endpoints
14
+ - [**Session tracking**](#session-management) - IP and user agent logging
15
+ - [**Customizable styling**](#styling) - CSS variables for easy theming
16
+ - [**Custom mailers**](#mailer) - use your own branded email templates
17
17
 
18
18
  ## Installation
19
19
 
@@ -204,9 +204,20 @@ class User < ApplicationRecord
204
204
  end
205
205
  ```
206
206
 
207
- ## Temporary Users (Guest Mode)
207
+ ## Temporary Users (Guest Accounts)
208
208
 
209
- Allow visitors to try your app without signing up, then convert to permanent accounts later.
209
+ Temporary users allow visitors to try your app without creating an account. They get a real user record with full functionality, then can convert to a permanent account later by providing email and password.
210
+
211
+ ### Why Use Temporary Users?
212
+
213
+ **Reduce friction**: Let users experience your app's value before asking them to sign up. This is especially useful for:
214
+
215
+ - **E-commerce**: Users can add items to cart, save preferences, then checkout as guest or create account
216
+ - **Productivity apps**: Users can create documents, try features, then save their work by signing up
217
+ - **Games**: Users can start playing immediately, then create account to save progress
218
+ - **Collaboration tools**: Users can join a shared workspace via link, then register to keep access
219
+
220
+ **Preserve data**: Unlike anonymous sessions, temporary users have real database records. When they convert, all their data (orders, documents, settings) stays linked to their account.
210
221
 
211
222
  ### Setup
212
223
 
@@ -217,7 +228,7 @@ rails generate rails_simple_auth:temporary_users
217
228
  rails db:migrate
218
229
  ```
219
230
 
220
- 2. Include the concern in your User model:
231
+ 2. Add the concern to your User model:
221
232
 
222
233
  ```ruby
223
234
  class User < ApplicationRecord
@@ -237,42 +248,110 @@ end
237
248
 
238
249
  ### Creating Temporary Users
239
250
 
251
+ Create a temporary user when someone needs to use your app without signing up:
252
+
253
+ ```ruby
254
+ # In your controller
255
+ def try_without_account
256
+ user = User.create!(
257
+ email: "temp_#{SecureRandom.hex(8)}@temporary.local",
258
+ password: SecureRandom.hex(32),
259
+ temporary: true
260
+ )
261
+
262
+ # Sign them in
263
+ create_session_for(user)
264
+ redirect_to dashboard_path
265
+ end
266
+ ```
267
+
268
+ Or create via an invite link:
269
+
240
270
  ```ruby
241
- # Create a temporary user (no email/password required)
242
- temp_user = User.create!(
243
- email: "temp_#{SecureRandom.hex(8)}@temp.local",
244
- password: SecureRandom.hex(16),
245
- temporary: true
246
- )
271
+ def accept_invite
272
+ # Create temporary user to access shared content
273
+ user = User.create!(temporary: true, ...)
274
+ create_session_for(user)
275
+ redirect_to shared_workspace_path(params[:workspace_id])
276
+ end
247
277
  ```
248
278
 
249
279
  ### Converting to Permanent Account
250
280
 
281
+ When a temporary user is ready to create a real account:
282
+
283
+ ```ruby
284
+ # In your controller
285
+ def convert_account
286
+ if current_user.convert_to_permanent!(
287
+ email: params[:email],
288
+ password: params[:password]
289
+ )
290
+ redirect_to dashboard_path, notice: "Account created! Please check your email to confirm."
291
+ else
292
+ # Validation failed (email taken, password blank, etc.)
293
+ render :convert_form, status: :unprocessable_entity
294
+ end
295
+ end
296
+ ```
297
+
298
+ The conversion:
299
+ - Updates email and password
300
+ - Sets `temporary: false`
301
+ - Resets `confirmed_at` (requires email confirmation for new address)
302
+ - Invalidates all existing sessions (security measure)
303
+ - Sends confirmation email automatically
304
+
305
+ ### What Happens on Sign In?
306
+
307
+ When a temporary user signs in with a different account (or signs up), the temporary user is automatically destroyed:
308
+
309
+ ```
310
+ Temporary User (browsing) → Signs in with existing account → Temp user deleted
311
+ Temporary User (browsing) → Creates new account → Temp user deleted
312
+ Temporary User (browsing) → Converts their temp account → Keeps same user record
313
+ ```
314
+
315
+ This prevents orphaned temporary records and ensures clean data.
316
+
317
+ ### Querying Users
318
+
251
319
  ```ruby
252
- # When user decides to sign up for real
253
- temp_user.convert_to_permanent!(
254
- email: "real@example.com",
255
- password: "secure_password"
256
- )
257
- # Sends confirmation email automatically if email confirmation is enabled
320
+ User.temporary # All temporary users
321
+ User.permanent # All permanent users
322
+ User.temporary_expired # Temporary users older than cleanup_days
323
+
324
+ current_user.temporary? # Is this a guest?
325
+ current_user.permanent? # Is this a real account?
258
326
  ```
259
327
 
260
- ### Scopes
328
+ ### Cleanup
329
+
330
+ Temporary users are automatically eligible for cleanup after `temporary_user_cleanup_days`. Run cleanup manually or via scheduled job:
261
331
 
262
332
  ```ruby
263
- User.temporary # All temporary users
264
- User.permanent # All permanent users
265
- User.temporary_expired # Temporary users older than cleanup_days
266
- User.temporary_expired(14) # Custom days
333
+ # In a rake task or background job
334
+ User.cleanup_expired_temporary!
335
+
336
+ # With custom retention period
337
+ User.cleanup_expired_temporary!(days: 14)
267
338
  ```
268
339
 
269
- ### Cleanup Task
340
+ Add to your scheduler (e.g., `config/recurring.yml` for Solid Queue):
270
341
 
271
- Add to your scheduler (cron, Sidekiq, etc.):
342
+ ```yaml
343
+ cleanup_temporary_users:
344
+ schedule: every day at 3am
345
+ class: CleanupTemporaryUsersJob
346
+ ```
272
347
 
273
348
  ```ruby
274
- # Delete expired temporary users
275
- User.temporary_expired.destroy_all
349
+ class CleanupTemporaryUsersJob < ApplicationJob
350
+ def perform
351
+ count = User.cleanup_expired_temporary!
352
+ Rails.logger.info "Cleaned up #{count} expired temporary users"
353
+ end
354
+ end
276
355
  ```
277
356
 
278
357
  ## Controller Customization
@@ -402,6 +481,112 @@ The gem adds these routes:
402
481
  | POST | `/request_magic_link` | Send magic link |
403
482
  | GET | `/magic_link` | Login via magic link |
404
483
 
484
+ ## Rate Limiting
485
+
486
+ All authentication endpoints are rate limited using Rails 8's `rate_limit` DSL to prevent brute force attacks.
487
+
488
+ ### Default Limits
489
+
490
+ | Action | Limit | Period | Scope |
491
+ |--------|-------|--------|-------|
492
+ | Sign in | 5 requests | 15 minutes | per IP |
493
+ | Sign up | 5 requests | 1 hour | per IP |
494
+ | Magic link request | 3 requests | 10 minutes | per email |
495
+ | Password reset | 3 requests | 1 hour | per IP |
496
+ | Email confirmation | 3 requests | 1 hour | per IP |
497
+
498
+ ### Customizing Limits
499
+
500
+ ```ruby
501
+ RailsSimpleAuth.configure do |config|
502
+ config.rate_limits = {
503
+ sign_in: { limit: 10, period: 30.minutes },
504
+ sign_up: { limit: 3, period: 1.hour },
505
+ magic_link: { limit: 5, period: 15.minutes },
506
+ password_reset: { limit: 5, period: 1.hour },
507
+ confirmation: { limit: 5, period: 1.hour }
508
+ }
509
+ end
510
+ ```
511
+
512
+ ### Disabling Rate Limiting
513
+
514
+ To disable rate limiting for a specific action, set it to `nil`:
515
+
516
+ ```ruby
517
+ config.rate_limits = {
518
+ sign_in: nil, # No rate limiting on sign in
519
+ sign_up: { limit: 5, period: 1.hour }
520
+ }
521
+ ```
522
+
523
+ When rate limited, users see a "Too many requests" error and must wait for the period to expire.
524
+
525
+ ## Session Management
526
+
527
+ Sessions track user authentication state with IP address and user agent for security auditing.
528
+
529
+ ### What's Tracked
530
+
531
+ Each session stores:
532
+ - **user_id** - The authenticated user
533
+ - **ip_address** - Client IP at sign-in time
534
+ - **user_agent** - Browser/device information
535
+ - **created_at** - When the session was created
536
+
537
+ ### Session Expiration
538
+
539
+ Sessions expire after 30 days by default:
540
+
541
+ ```ruby
542
+ RailsSimpleAuth.configure do |config|
543
+ config.session_expiry = 30.days # Default
544
+ # config.session_expiry = 7.days # Shorter sessions
545
+ end
546
+ ```
547
+
548
+ ### Querying Sessions
549
+
550
+ ```ruby
551
+ # All sessions for a user
552
+ current_user.sessions
553
+
554
+ # Recent sessions first
555
+ current_user.sessions.recent
556
+
557
+ # Active sessions (not expired)
558
+ current_user.sessions.active
559
+
560
+ # Expired sessions
561
+ current_user.sessions.expired
562
+ ```
563
+
564
+ ### Session Cleanup
565
+
566
+ Expired sessions can be cleaned up manually or via scheduled job:
567
+
568
+ ```ruby
569
+ # Clean up all expired sessions
570
+ RailsSimpleAuth::Session.cleanup_expired!
571
+ ```
572
+
573
+ Add to your scheduler:
574
+
575
+ ```ruby
576
+ class CleanupExpiredSessionsJob < ApplicationJob
577
+ def perform
578
+ count = RailsSimpleAuth::Session.cleanup_expired!
579
+ Rails.logger.info "Cleaned up #{count} expired sessions"
580
+ end
581
+ end
582
+ ```
583
+
584
+ ### Security Behaviors
585
+
586
+ - **Password change**: All sessions are invalidated when a user changes their password
587
+ - **Account conversion**: All sessions are invalidated when a temporary user converts to permanent
588
+ - **Sign out**: Only the current session is destroyed (other devices stay signed in)
589
+
405
590
  ## Security Features
406
591
 
407
592
  - **BCrypt password hashing** with salts
@@ -15,9 +15,15 @@ module RailsSimpleAuth
15
15
  user = user_class.find_signed(params[:token], purpose: :confirm_email)
16
16
 
17
17
  if user
18
- user.confirm! if user.respond_to?(:confirm!)
19
- run_after_confirmation_callback(user)
20
- redirect_to resolve_path(:after_confirmation_path), notice: 'Email confirmed! You can now sign in.'
18
+ confirmed = user.respond_to?(:confirm!) ? user.confirm! : true
19
+
20
+ if confirmed
21
+ run_after_confirmation_callback(user)
22
+ redirect_to resolve_path(:after_confirmation_path), notice: 'Email confirmed! You can now sign in.'
23
+ else
24
+ error_message = user.errors.full_messages.first || 'Could not confirm email.'
25
+ redirect_to new_confirmation_path, alert: error_message
26
+ end
21
27
  else
22
28
  redirect_to new_confirmation_path, alert: 'Invalid or expired confirmation link.'
23
29
  end
@@ -70,7 +70,13 @@ module RailsSimpleAuth
70
70
  user = user_class.find_signed(params[:token], purpose: :magic_link)
71
71
 
72
72
  if user
73
- user.confirm! if user.respond_to?(:confirm!) && user.respond_to?(:unconfirmed?) && user.unconfirmed?
73
+ # Auto-confirm unconfirmed users via magic link (email ownership verified)
74
+ if user.respond_to?(:confirm!) && user.respond_to?(:unconfirmed?) && user.unconfirmed? && !user.confirm!
75
+ # Confirmation failed (e.g., email already taken during reconfirmation)
76
+ error_message = user.errors.full_messages.first || 'Could not confirm email.'
77
+ redirect_to new_session_path, alert: error_message
78
+ return
79
+ end
74
80
  sign_in_and_redirect(user)
75
81
  else
76
82
  redirect_to new_session_path, alert: 'Invalid or expired magic link.'
@@ -4,6 +4,9 @@ module RailsSimpleAuth
4
4
  class AuthMailer < ApplicationMailer
5
5
  default from: -> { RailsSimpleAuth.configuration.mailer_sender }
6
6
 
7
+ # Use the mailers folder for templates instead of auth_mailer
8
+ self.mailer_name = 'rails_simple_auth/mailers'
9
+
7
10
  def confirmation(user, token)
8
11
  @user = user
9
12
  @token = token
@@ -44,7 +44,7 @@ module RailsSimpleAuth
44
44
  # Returns true on success, false on failure (with errors populated)
45
45
  def confirm!
46
46
  attrs = { confirmed_at: Time.current }
47
- attrs[:temporary] = false if respond_to?(:temporary?)
47
+ attrs[:temporary] = false if has_attribute?(:temporary)
48
48
 
49
49
  if reconfirming?
50
50
  # Email change confirmation - check email uniqueness first
@@ -62,6 +62,10 @@ module RailsSimpleAuth
62
62
  # Already confirmed and not reconfirming
63
63
  true
64
64
  end
65
+ rescue ActiveRecord::RecordNotUnique
66
+ # Race condition: email was taken between check and update
67
+ errors.add(:email, 'is already taken by another user')
68
+ false
65
69
  end
66
70
 
67
71
  # Generate email confirmation token using Rails signed_id
@@ -28,6 +28,12 @@ module RailsSimpleAuth
28
28
  # Convert a temporary user to a permanent user with email and password
29
29
  # Returns self on success, false on failure (with errors populated)
30
30
  def convert_to_permanent!(email:, password:)
31
+ # Validate password presence upfront
32
+ if password.blank?
33
+ errors.add(:password, "can't be blank")
34
+ return false
35
+ end
36
+
31
37
  # Validate email uniqueness upfront (better UX than failing inside transaction)
32
38
  if self.class.where.not(id: id).exists?(email: email)
33
39
  errors.add(:email, 'has already been taken')
@@ -55,7 +61,11 @@ module RailsSimpleAuth
55
61
  raise ActiveRecord::Rollback unless update(attrs)
56
62
  end
57
63
 
58
- # Check if transaction was rolled back
64
+ # Reload to get actual database state after transaction
65
+ # (in-memory attributes may be stale if transaction was rolled back)
66
+ reload
67
+
68
+ # Check if conversion actually succeeded
59
69
  return false if errors.any? || temporary?
60
70
 
61
71
  invalidate_all_sessions!
@@ -75,8 +85,7 @@ module RailsSimpleAuth
75
85
  def cleanup_expired_temporary!(days: nil, batch_size: 100)
76
86
  count = 0
77
87
  temporary_expired(days).find_each(batch_size: batch_size) do |user|
78
- user.destroy
79
- count += 1
88
+ count += 1 if user.destroy
80
89
  end
81
90
  Rails.logger.info("[RailsSimpleAuth] Cleaned up #{count} expired temporary users")
82
91
  count
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsSimpleAuth
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.3'
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_simple_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuznetsov
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-01-19 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: bcrypt
@@ -100,6 +101,7 @@ metadata:
100
101
  bug_tracker_uri: https://github.com/ivankuznetsov/rails_simple_auth/issues
101
102
  documentation_uri: https://github.com/ivankuznetsov/rails_simple_auth#readme
102
103
  rubygems_mfa_required: 'true'
104
+ post_install_message:
103
105
  rdoc_options: []
104
106
  require_paths:
105
107
  - lib
@@ -114,7 +116,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
116
  - !ruby/object:Gem::Version
115
117
  version: '0'
116
118
  requirements: []
117
- rubygems_version: 3.6.9
119
+ rubygems_version: 3.5.22
120
+ signing_key:
118
121
  specification_version: 4
119
122
  summary: Simple, secure authentication for Rails 8+ applications
120
123
  test_files: []