rails_simple_auth 1.0.2 → 1.0.4

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: fd84335df56b3bc4fbb386f841bfb66bc74eadae1dc3864873bcda904a946cd2
4
+ data.tar.gz: 00d31290be4bdccf36c5f9a326896e9506936f6b122f9298f53fabc0aadcd8a3
5
5
  SHA512:
6
- metadata.gz: 92302e8e2c6d489ebda9e82b222e0a53f31c52f0b8958bf3af2a4240f654f0de7bd81892e350ee92892df16d747ef88d4b43ae69b6af0e510ee1504925755693
7
- data.tar.gz: 07d17cb9ca09fe1f01446cf1faf4cc67fb303d5c0553b6e2c242595054700a88fff187838820aed8639c65b3fd839e3679771acb178700f7966cf17f0f1ce258
6
+ metadata.gz: 2c0cf144576a950a1aff4a1958197b9ef93a3edc405bf1de36339f3effed3c09087158b821f6e2c7617af9b9ae9b3ef184783b08c9428da0df65d79ac5a2ba3e
7
+ data.tar.gz: 0cc8bd3f8ce5094910f25daf12496f1efe6f447d3ee22f3744ad47d7ff4b65ad46209ecd2004fe9024f2fcad85c38efb6478261bd408251f728aad1c2fc89824
data/CHANGELOG.md CHANGED
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.4] - 2025-01-19
11
+
12
+ ### Added
13
+
14
+ - **`authenticates_with` DSL** - Cleaner model setup inspired by Devise syntax
15
+ ```ruby
16
+ # Before
17
+ include RailsSimpleAuth::Models::Concerns::Authenticatable
18
+ include RailsSimpleAuth::Models::Concerns::Confirmable
19
+
20
+ # After
21
+ authenticates_with :confirmable, :magic_linkable, :oauth, :temporary
22
+ ```
23
+ - **Devise comparison article** - Comprehensive comparison at `docs/devise-comparison.md`
24
+ - **Admin Users documentation** - Guide for implementing admin functionality
25
+ - **Rate Limiting documentation** - Default limits and customization guide
26
+ - **Session Management documentation** - Expiration, querying, and cleanup
27
+
28
+ ## [1.0.3] - 2025-01-19
29
+
30
+ ### Added
31
+
32
+ - **Temporary Users Support** - Allow users to try the app without signing up, then convert to permanent accounts
33
+ - `TemporaryUser` concern with `temporary?` and `permanent?` methods
34
+ - Scopes: `temporary`, `permanent`, `temporary_expired`
35
+ - `convert_to_permanent!(email:, password:)` method for account conversion
36
+ - Generator: `rails g rails_simple_auth:temporary_users` for migration
37
+ - Configuration options: `temporary_users_enabled`, `temporary_user_cleanup_days`
38
+ - Automatic cleanup of expired temporary users via `User.cleanup_expired_temporary!`
39
+ - Session invalidation on account conversion
40
+ - Automatic destruction of temporary user when signing in with different account
41
+ - **Email Reconfirmation Flow** - Support for users changing their email address
42
+ - `unconfirmed_email` column support for pending email changes
43
+ - `reconfirming?` and `unconfirmed_or_reconfirming?` helper methods
44
+ - `confirmable_email` helper returns the email needing confirmation
45
+ - Confirmation emails sent to new email address during reconfirmation
46
+ - Comprehensive test suite (126 tests, 247 assertions)
47
+
48
+ ### Fixed
49
+
50
+ - `confirm!` now uses `has_attribute?(:temporary)` instead of `respond_to?(:temporary?)` to prevent errors when `Authenticatable` is included without the temporary database column
51
+ - `confirm!` properly handles race conditions with `RecordNotUnique` rescue during reconfirmation
52
+ - `convert_to_permanent!` validates password presence to prevent users being left without credentials
53
+ - `convert_to_permanent!` reloads after transaction to check actual database state (not stale in-memory state)
54
+ - `convert_to_permanent!` resets `confirmed_at` to require email verification for new address
55
+ - `cleanup_expired_temporary!` now returns accurate count (only increments when destroy succeeds)
56
+ - `magic_link_login` checks `confirm!` return value and shows error if confirmation fails
57
+ - `confirmations_controller#show` checks `confirm!` return value and displays appropriate error message
58
+ - `destroy_temporary_user_session` skips destruction when user is re-authenticating as themselves
59
+ - `AuthMailer#confirmation` sends to `confirmable_email` for correct recipient during reconfirmation
60
+
61
+ ### Changed
62
+
63
+ - Confirmation token purpose changed from `:email_confirmation` to `:confirm_email` (**Breaking**: existing confirmation tokens will be invalidated)
64
+
65
+ ## [1.0.2] - 2025-01-18
66
+
67
+ ### Fixed
68
+
69
+ - Session invalidation and batch cleanup for temporary users
70
+
10
71
  ## [1.0.0] - 2025-01-18
11
72
 
12
73
  ### Added
data/README.md CHANGED
@@ -2,18 +2,20 @@
2
2
 
3
3
  Simple, secure authentication for Rails 8+ applications. Built on Rails primitives with no magic.
4
4
 
5
+ **Coming from Devise?** Read our [detailed comparison](docs/devise-comparison.md).
6
+
5
7
  ## Features
6
8
 
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
9
+ - [**Email/Password authentication**](#installation) - secure session-based auth
10
+ - [**Magic link authentication**](#routes) - passwordless sign-in via email
11
+ - [**Email confirmation**](#routes) - verify user email addresses
12
+ - [**Password reset**](#routes) - secure password recovery flow
13
+ - [**OAuth support**](#oauth-setup) - Google, GitHub, and more
14
+ - [**Temporary users**](#temporary-users-guest-accounts) - guest accounts that convert to permanent
15
+ - [**Rate limiting**](#rate-limiting) - built-in protection on all endpoints
16
+ - [**Session tracking**](#session-management) - IP and user agent logging
17
+ - [**Customizable styling**](#styling) - CSS variables for easy theming
18
+ - [**Custom mailers**](#mailer) - use your own branded email templates
17
19
 
18
20
  ## Installation
19
21
 
@@ -36,20 +38,31 @@ rails generate rails_simple_auth:install
36
38
  rails db:migrate
37
39
  ```
38
40
 
39
- Add concerns to your User model:
41
+ Add authentication to your User model:
40
42
 
41
43
  ```ruby
42
44
  class User < ApplicationRecord
43
- include RailsSimpleAuth::Models::Concerns::Authenticatable
44
- include RailsSimpleAuth::Models::Concerns::Confirmable # optional
45
- include RailsSimpleAuth::Models::Concerns::MagicLinkable # optional
46
- include RailsSimpleAuth::Models::Concerns::OAuthConnectable # optional
45
+ authenticates_with :confirmable, :magic_linkable, :oauth, :temporary
47
46
 
48
47
  # Your custom fields and validations
49
48
  validates :company_name, presence: true
50
49
  end
51
50
  ```
52
51
 
52
+ Available modules:
53
+ - `:confirmable` - Email confirmation for new accounts
54
+ - `:magic_linkable` - Passwordless sign-in via email
55
+ - `:oauth` - OAuth provider support (Google, GitHub, etc.)
56
+ - `:temporary` - Guest accounts that convert to permanent
57
+
58
+ For basic email/password auth only:
59
+
60
+ ```ruby
61
+ class User < ApplicationRecord
62
+ authenticates_with
63
+ end
64
+ ```
65
+
53
66
  Protect your routes:
54
67
 
55
68
  ```ruby
@@ -204,9 +217,20 @@ class User < ApplicationRecord
204
217
  end
205
218
  ```
206
219
 
207
- ## Temporary Users (Guest Mode)
220
+ ## Temporary Users (Guest Accounts)
208
221
 
209
- Allow visitors to try your app without signing up, then convert to permanent accounts later.
222
+ 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.
223
+
224
+ ### Why Use Temporary Users?
225
+
226
+ **Reduce friction**: Let users experience your app's value before asking them to sign up. This is especially useful for:
227
+
228
+ - **E-commerce**: Users can add items to cart, save preferences, then checkout as guest or create account
229
+ - **Productivity apps**: Users can create documents, try features, then save their work by signing up
230
+ - **Games**: Users can start playing immediately, then create account to save progress
231
+ - **Collaboration tools**: Users can join a shared workspace via link, then register to keep access
232
+
233
+ **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
234
 
211
235
  ### Setup
212
236
 
@@ -217,12 +241,11 @@ rails generate rails_simple_auth:temporary_users
217
241
  rails db:migrate
218
242
  ```
219
243
 
220
- 2. Include the concern in your User model:
244
+ 2. Add the `:temporary` module to your User model:
221
245
 
222
246
  ```ruby
223
247
  class User < ApplicationRecord
224
- include RailsSimpleAuth::Models::Concerns::Authenticatable
225
- include RailsSimpleAuth::Models::Concerns::TemporaryUser # Add this
248
+ authenticates_with :confirmable, :temporary
226
249
  end
227
250
  ```
228
251
 
@@ -237,42 +260,110 @@ end
237
260
 
238
261
  ### Creating Temporary Users
239
262
 
263
+ Create a temporary user when someone needs to use your app without signing up:
264
+
265
+ ```ruby
266
+ # In your controller
267
+ def try_without_account
268
+ user = User.create!(
269
+ email: "temp_#{SecureRandom.hex(8)}@temporary.local",
270
+ password: SecureRandom.hex(32),
271
+ temporary: true
272
+ )
273
+
274
+ # Sign them in
275
+ create_session_for(user)
276
+ redirect_to dashboard_path
277
+ end
278
+ ```
279
+
280
+ Or create via an invite link:
281
+
240
282
  ```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
- )
283
+ def accept_invite
284
+ # Create temporary user to access shared content
285
+ user = User.create!(temporary: true, ...)
286
+ create_session_for(user)
287
+ redirect_to shared_workspace_path(params[:workspace_id])
288
+ end
247
289
  ```
248
290
 
249
291
  ### Converting to Permanent Account
250
292
 
293
+ When a temporary user is ready to create a real account:
294
+
251
295
  ```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
296
+ # In your controller
297
+ def convert_account
298
+ if current_user.convert_to_permanent!(
299
+ email: params[:email],
300
+ password: params[:password]
301
+ )
302
+ redirect_to dashboard_path, notice: "Account created! Please check your email to confirm."
303
+ else
304
+ # Validation failed (email taken, password blank, etc.)
305
+ render :convert_form, status: :unprocessable_entity
306
+ end
307
+ end
308
+ ```
309
+
310
+ The conversion:
311
+ - Updates email and password
312
+ - Sets `temporary: false`
313
+ - Resets `confirmed_at` (requires email confirmation for new address)
314
+ - Invalidates all existing sessions (security measure)
315
+ - Sends confirmation email automatically
316
+
317
+ ### What Happens on Sign In?
318
+
319
+ When a temporary user signs in with a different account (or signs up), the temporary user is automatically destroyed:
320
+
321
+ ```
322
+ Temporary User (browsing) → Signs in with existing account → Temp user deleted
323
+ Temporary User (browsing) → Creates new account → Temp user deleted
324
+ Temporary User (browsing) → Converts their temp account → Keeps same user record
258
325
  ```
259
326
 
260
- ### Scopes
327
+ This prevents orphaned temporary records and ensures clean data.
328
+
329
+ ### Querying Users
261
330
 
262
331
  ```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
332
+ User.temporary # All temporary users
333
+ User.permanent # All permanent users
334
+ User.temporary_expired # Temporary users older than cleanup_days
335
+
336
+ current_user.temporary? # Is this a guest?
337
+ current_user.permanent? # Is this a real account?
267
338
  ```
268
339
 
269
- ### Cleanup Task
340
+ ### Cleanup
270
341
 
271
- Add to your scheduler (cron, Sidekiq, etc.):
342
+ Temporary users are automatically eligible for cleanup after `temporary_user_cleanup_days`. Run cleanup manually or via scheduled job:
272
343
 
273
344
  ```ruby
274
- # Delete expired temporary users
275
- User.temporary_expired.destroy_all
345
+ # In a rake task or background job
346
+ User.cleanup_expired_temporary!
347
+
348
+ # With custom retention period
349
+ User.cleanup_expired_temporary!(days: 14)
350
+ ```
351
+
352
+ Add to your scheduler (e.g., `config/recurring.yml` for Solid Queue):
353
+
354
+ ```yaml
355
+ cleanup_temporary_users:
356
+ schedule: every day at 3am
357
+ class: CleanupTemporaryUsersJob
358
+ ```
359
+
360
+ ```ruby
361
+ class CleanupTemporaryUsersJob < ApplicationJob
362
+ def perform
363
+ count = User.cleanup_expired_temporary!
364
+ Rails.logger.info "Cleaned up #{count} expired temporary users"
365
+ end
366
+ end
276
367
  ```
277
368
 
278
369
  ## Controller Customization
@@ -402,6 +493,178 @@ The gem adds these routes:
402
493
  | POST | `/request_magic_link` | Send magic link |
403
494
  | GET | `/magic_link` | Login via magic link |
404
495
 
496
+ ## Rate Limiting
497
+
498
+ All authentication endpoints are rate limited using Rails 8's `rate_limit` DSL to prevent brute force attacks.
499
+
500
+ ### Default Limits
501
+
502
+ | Action | Limit | Period | Scope |
503
+ |--------|-------|--------|-------|
504
+ | Sign in | 5 requests | 15 minutes | per IP |
505
+ | Sign up | 5 requests | 1 hour | per IP |
506
+ | Magic link request | 3 requests | 10 minutes | per email |
507
+ | Password reset | 3 requests | 1 hour | per IP |
508
+ | Email confirmation | 3 requests | 1 hour | per IP |
509
+
510
+ ### Customizing Limits
511
+
512
+ ```ruby
513
+ RailsSimpleAuth.configure do |config|
514
+ config.rate_limits = {
515
+ sign_in: { limit: 10, period: 30.minutes },
516
+ sign_up: { limit: 3, period: 1.hour },
517
+ magic_link: { limit: 5, period: 15.minutes },
518
+ password_reset: { limit: 5, period: 1.hour },
519
+ confirmation: { limit: 5, period: 1.hour }
520
+ }
521
+ end
522
+ ```
523
+
524
+ ### Disabling Rate Limiting
525
+
526
+ To disable rate limiting for a specific action, set it to `nil`:
527
+
528
+ ```ruby
529
+ config.rate_limits = {
530
+ sign_in: nil, # No rate limiting on sign in
531
+ sign_up: { limit: 5, period: 1.hour }
532
+ }
533
+ ```
534
+
535
+ When rate limited, users see a "Too many requests" error and must wait for the period to expire.
536
+
537
+ ## Session Management
538
+
539
+ Sessions track user authentication state with IP address and user agent for security auditing.
540
+
541
+ ### What's Tracked
542
+
543
+ Each session stores:
544
+ - **user_id** - The authenticated user
545
+ - **ip_address** - Client IP at sign-in time
546
+ - **user_agent** - Browser/device information
547
+ - **created_at** - When the session was created
548
+
549
+ ### Session Expiration
550
+
551
+ Sessions expire after 30 days by default:
552
+
553
+ ```ruby
554
+ RailsSimpleAuth.configure do |config|
555
+ config.session_expiry = 30.days # Default
556
+ # config.session_expiry = 7.days # Shorter sessions
557
+ end
558
+ ```
559
+
560
+ ### Querying Sessions
561
+
562
+ ```ruby
563
+ # All sessions for a user
564
+ current_user.sessions
565
+
566
+ # Recent sessions first
567
+ current_user.sessions.recent
568
+
569
+ # Active sessions (not expired)
570
+ current_user.sessions.active
571
+
572
+ # Expired sessions
573
+ current_user.sessions.expired
574
+ ```
575
+
576
+ ### Session Cleanup
577
+
578
+ Expired sessions can be cleaned up manually or via scheduled job:
579
+
580
+ ```ruby
581
+ # Clean up all expired sessions
582
+ RailsSimpleAuth::Session.cleanup_expired!
583
+ ```
584
+
585
+ Add to your scheduler:
586
+
587
+ ```ruby
588
+ class CleanupExpiredSessionsJob < ApplicationJob
589
+ def perform
590
+ count = RailsSimpleAuth::Session.cleanup_expired!
591
+ Rails.logger.info "Cleaned up #{count} expired sessions"
592
+ end
593
+ end
594
+ ```
595
+
596
+ ### Security Behaviors
597
+
598
+ - **Password change**: All sessions are invalidated when a user changes their password
599
+ - **Account conversion**: All sessions are invalidated when a temporary user converts to permanent
600
+ - **Sign out**: Only the current session is destroyed (other devices stay signed in)
601
+
602
+ ## Admin Users
603
+
604
+ RailsSimpleAuth uses a single table with role-based access — the Rails way. No separate admin models or authentication flows needed.
605
+
606
+ ### Setup
607
+
608
+ Add an admin column to your users table:
609
+
610
+ ```ruby
611
+ # Migration
612
+ add_column :users, :admin, :boolean, default: false
613
+ ```
614
+
615
+ Add a helper method to your model:
616
+
617
+ ```ruby
618
+ class User < ApplicationRecord
619
+ authenticates_with :confirmable
620
+
621
+ def admin?
622
+ admin == true
623
+ end
624
+ end
625
+ ```
626
+
627
+ ### Protecting Admin Routes
628
+
629
+ ```ruby
630
+ class AdminController < ApplicationController
631
+ before_action :require_admin
632
+
633
+ private
634
+
635
+ def require_admin
636
+ redirect_to root_path, alert: "Not authorized" unless current_user&.admin?
637
+ end
638
+ end
639
+
640
+ # Or as a concern
641
+ module AdminAuthentication
642
+ extend ActiveSupport::Concern
643
+
644
+ included do
645
+ before_action :require_admin
646
+ end
647
+
648
+ private
649
+
650
+ def require_admin
651
+ redirect_to root_path, alert: "Not authorized" unless current_user&.admin?
652
+ end
653
+ end
654
+ ```
655
+
656
+ ### Creating Admin Users
657
+
658
+ ```ruby
659
+ # Console
660
+ User.find_by(email: "admin@example.com").update!(admin: true)
661
+
662
+ # Seeds
663
+ User.create!(email: "admin@example.com", password: "secure123", admin: true)
664
+ ```
665
+
666
+ For more complex role systems, consider adding a `role` enum or using an authorization gem like [Pundit](https://github.com/varvet/pundit).
667
+
405
668
  ## Security Features
406
669
 
407
670
  - **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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rails_simple_auth/model'
4
+
3
5
  module RailsSimpleAuth
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace RailsSimpleAuth
@@ -14,5 +16,11 @@ module RailsSimpleAuth
14
16
  include RailsSimpleAuth::Controllers::Concerns::SessionManagement
15
17
  end
16
18
  end
19
+
20
+ initializer 'rails_simple_auth.model' do
21
+ ActiveSupport.on_load(:active_record) do
22
+ include RailsSimpleAuth::Model
23
+ end
24
+ end
17
25
  end
18
26
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSimpleAuth
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ MODULES = {
8
+ confirmable: 'RailsSimpleAuth::Models::Concerns::Confirmable',
9
+ magic_linkable: 'RailsSimpleAuth::Models::Concerns::MagicLinkable',
10
+ oauth: 'RailsSimpleAuth::Models::Concerns::OAuthConnectable',
11
+ temporary: 'RailsSimpleAuth::Models::Concerns::TemporaryUser'
12
+ }.freeze
13
+
14
+ class_methods do
15
+ # Configure authentication for this model
16
+ #
17
+ # @example Basic authentication only
18
+ # authenticates_with
19
+ #
20
+ # @example With optional modules
21
+ # authenticates_with :confirmable, :magic_linkable
22
+ #
23
+ # @example Full featured
24
+ # authenticates_with :confirmable, :magic_linkable, :oauth, :temporary
25
+ #
26
+ # Available modules:
27
+ # - :confirmable - Email confirmation for new accounts
28
+ # - :magic_linkable - Passwordless sign-in via email
29
+ # - :oauth - OAuth provider support (Google, GitHub, etc.)
30
+ # - :temporary - Guest accounts that convert to permanent
31
+ #
32
+ def authenticates_with(*modules)
33
+ # Always include base authentication
34
+ include RailsSimpleAuth::Models::Concerns::Authenticatable
35
+
36
+ # Include requested optional modules
37
+ modules.each do |mod|
38
+ mod_name = mod.to_sym
39
+ unless MODULES.key?(mod_name)
40
+ raise ArgumentError, "Unknown authentication module: #{mod.inspect}. " \
41
+ "Available modules: #{MODULES.keys.join(', ')}"
42
+ end
43
+
44
+ include MODULES[mod_name].constantize
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -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.4'
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.4
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
@@ -81,6 +82,7 @@ files:
81
82
  - lib/rails_simple_auth/controllers/concerns/authentication.rb
82
83
  - lib/rails_simple_auth/controllers/concerns/session_management.rb
83
84
  - lib/rails_simple_auth/engine.rb
85
+ - lib/rails_simple_auth/model.rb
84
86
  - lib/rails_simple_auth/models/concerns/authenticatable.rb
85
87
  - lib/rails_simple_auth/models/concerns/confirmable.rb
86
88
  - lib/rails_simple_auth/models/concerns/magic_linkable.rb
@@ -100,6 +102,7 @@ metadata:
100
102
  bug_tracker_uri: https://github.com/ivankuznetsov/rails_simple_auth/issues
101
103
  documentation_uri: https://github.com/ivankuznetsov/rails_simple_auth#readme
102
104
  rubygems_mfa_required: 'true'
105
+ post_install_message:
103
106
  rdoc_options: []
104
107
  require_paths:
105
108
  - lib
@@ -114,7 +117,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
117
  - !ruby/object:Gem::Version
115
118
  version: '0'
116
119
  requirements: []
117
- rubygems_version: 3.6.9
120
+ rubygems_version: 3.5.22
121
+ signing_key:
118
122
  specification_version: 4
119
123
  summary: Simple, secure authentication for Rails 8+ applications
120
124
  test_files: []