rails_simple_auth 1.0.1 → 1.0.2
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/README.md +77 -5
- data/app/controllers/rails_simple_auth/confirmations_controller.rb +19 -17
- data/app/controllers/rails_simple_auth/omniauth_callbacks_controller.rb +6 -4
- data/app/controllers/rails_simple_auth/passwords_controller.rb +13 -12
- data/app/controllers/rails_simple_auth/registrations_controller.rb +6 -4
- data/app/controllers/rails_simple_auth/sessions_controller.rb +21 -16
- data/app/mailers/rails_simple_auth/auth_mailer.rb +10 -7
- data/app/views/rails_simple_auth/confirmations/new.html.erb +2 -2
- data/app/views/rails_simple_auth/passwords/new.html.erb +2 -2
- data/app/views/rails_simple_auth/registrations/new.html.erb +5 -5
- data/app/views/rails_simple_auth/sessions/magic_link_form.html.erb +2 -2
- data/app/views/rails_simple_auth/sessions/new.html.erb +2 -2
- data/lib/generators/rails_simple_auth/css/css_generator.rb +20 -20
- data/lib/generators/rails_simple_auth/install/install_generator.rb +32 -32
- data/lib/generators/rails_simple_auth/install/templates/initializer.rb +3 -3
- data/lib/generators/rails_simple_auth/install/templates/migration.rb +2 -2
- data/lib/generators/rails_simple_auth/temporary_users/USAGE +21 -0
- data/lib/generators/rails_simple_auth/temporary_users/templates/add_temporary_to_users.rb.erb +8 -0
- data/lib/generators/rails_simple_auth/temporary_users/temporary_users_generator.rb +40 -0
- data/lib/generators/rails_simple_auth/views/views_generator.rb +8 -8
- data/lib/rails_simple_auth/configuration.rb +21 -7
- data/lib/rails_simple_auth/controllers/concerns/authentication.rb +17 -18
- data/lib/rails_simple_auth/controllers/concerns/session_management.rb +24 -0
- data/lib/rails_simple_auth/engine.rb +1 -1
- data/lib/rails_simple_auth/models/concerns/authenticatable.rb +13 -5
- data/lib/rails_simple_auth/models/concerns/confirmable.rb +38 -3
- data/lib/rails_simple_auth/models/concerns/oauth_connectable.rb +5 -5
- data/lib/rails_simple_auth/models/concerns/temporary_user.rb +105 -0
- data/lib/rails_simple_auth/models/session.rb +2 -4
- data/lib/rails_simple_auth/routes.rb +15 -15
- data/lib/rails_simple_auth/version.rb +1 -1
- data/lib/rails_simple_auth.rb +14 -12
- metadata +15 -11
|
@@ -1,64 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
5
|
|
|
6
6
|
module RailsSimpleAuth
|
|
7
7
|
module Generators
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
|
9
9
|
include Rails::Generators::Migration
|
|
10
10
|
|
|
11
|
-
source_root File.expand_path(
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
12
|
|
|
13
|
-
class_option :user_model, type: :string, default:
|
|
14
|
-
|
|
13
|
+
class_option :user_model, type: :string, default: 'User',
|
|
14
|
+
desc: 'Name of your User model'
|
|
15
15
|
class_option :skip_migration, type: :boolean, default: false,
|
|
16
|
-
|
|
16
|
+
desc: 'Skip generating migration'
|
|
17
17
|
|
|
18
18
|
def self.next_migration_number(dirname)
|
|
19
19
|
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def create_initializer
|
|
23
|
-
template
|
|
23
|
+
template 'initializer.rb', 'config/initializers/rails_simple_auth.rb'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def create_migration
|
|
27
27
|
return if options[:skip_migration]
|
|
28
28
|
|
|
29
|
-
migration_template
|
|
29
|
+
migration_template 'migration.rb', 'db/migrate/add_rails_simple_auth.rb'
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def add_routes
|
|
33
|
-
route
|
|
33
|
+
route 'rails_simple_auth_routes'
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def show_instructions
|
|
37
|
-
say
|
|
38
|
-
say
|
|
39
|
-
say
|
|
40
|
-
say
|
|
41
|
-
say
|
|
42
|
-
say
|
|
37
|
+
say ''
|
|
38
|
+
say 'RailsSimpleAuth installed successfully!', :green
|
|
39
|
+
say ''
|
|
40
|
+
say 'Next steps:'
|
|
41
|
+
say ' 1. Review and edit the migration: db/migrate/xxx_add_rails_simple_auth.rb'
|
|
42
|
+
say ' 2. Run: rails db:migrate'
|
|
43
43
|
say " 3. Add concerns to your #{options[:user_model]} model:"
|
|
44
|
-
say
|
|
44
|
+
say ''
|
|
45
45
|
say " class #{options[:user_model]} < ApplicationRecord"
|
|
46
|
-
say
|
|
47
|
-
say
|
|
48
|
-
say
|
|
49
|
-
say
|
|
50
|
-
say
|
|
51
|
-
say
|
|
52
|
-
say
|
|
53
|
-
say
|
|
54
|
-
say
|
|
55
|
-
say
|
|
56
|
-
say
|
|
57
|
-
say
|
|
58
|
-
say
|
|
59
|
-
say
|
|
60
|
-
say
|
|
61
|
-
say
|
|
46
|
+
say ' include RailsSimpleAuth::Models::Concerns::Authenticatable'
|
|
47
|
+
say ' include RailsSimpleAuth::Models::Concerns::Confirmable # optional'
|
|
48
|
+
say ' include RailsSimpleAuth::Models::Concerns::MagicLinkable # optional'
|
|
49
|
+
say ' include RailsSimpleAuth::Models::Concerns::OAuthConnectable # optional'
|
|
50
|
+
say ' end'
|
|
51
|
+
say ''
|
|
52
|
+
say ' 4. Add before_action to protect routes:'
|
|
53
|
+
say ''
|
|
54
|
+
say ' class ApplicationController < ActionController::Base'
|
|
55
|
+
say ' before_action :require_authentication'
|
|
56
|
+
say ' end'
|
|
57
|
+
say ''
|
|
58
|
+
say 'Optional generators:'
|
|
59
|
+
say ' rails generate rails_simple_auth:views # Copy views for customization'
|
|
60
|
+
say ' rails generate rails_simple_auth:css # Copy CSS for styling'
|
|
61
|
+
say ''
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -58,14 +58,14 @@ RailsSimpleAuth.configure do |config|
|
|
|
58
58
|
# ============================================================================
|
|
59
59
|
|
|
60
60
|
# Layout to use for auth pages (default: "application")
|
|
61
|
-
config.layout =
|
|
61
|
+
config.layout = 'application'
|
|
62
62
|
|
|
63
63
|
# ============================================================================
|
|
64
64
|
# Mailer
|
|
65
65
|
# ============================================================================
|
|
66
66
|
|
|
67
67
|
# From address for auth emails
|
|
68
|
-
config.mailer_sender = ENV.fetch(
|
|
68
|
+
config.mailer_sender = ENV.fetch('MAILER_FROM', 'noreply@example.com')
|
|
69
69
|
|
|
70
70
|
# Custom mailer class (must implement confirmation, magic_link, password_reset methods)
|
|
71
71
|
# config.mailer_class = "RailsSimpleAuth::AuthMailer"
|
|
@@ -75,7 +75,7 @@ RailsSimpleAuth.configure do |config|
|
|
|
75
75
|
# ============================================================================
|
|
76
76
|
|
|
77
77
|
# Your user model class name
|
|
78
|
-
config.user_class_name =
|
|
78
|
+
config.user_class_name = '<%= options[:user_model] %>'
|
|
79
79
|
|
|
80
80
|
# Custom session model (if you want to use your own)
|
|
81
81
|
# config.session_class_name = "RailsSimpleAuth::Session"
|
|
@@ -11,7 +11,7 @@ class AddRailsSimpleAuth < ActiveRecord::Migration[8.0]
|
|
|
11
11
|
# Uncomment and modify as needed:
|
|
12
12
|
|
|
13
13
|
# Required fields for authentication
|
|
14
|
-
# add_column :users, :
|
|
14
|
+
# add_column :users, :email, :string, null: false
|
|
15
15
|
# add_column :users, :password_digest, :string, null: false
|
|
16
16
|
|
|
17
17
|
# Email confirmation (optional - if using Confirmable concern)
|
|
@@ -22,7 +22,7 @@ class AddRailsSimpleAuth < ActiveRecord::Migration[8.0]
|
|
|
22
22
|
# add_column :users, :admin, :boolean, default: false
|
|
23
23
|
|
|
24
24
|
# Indexes
|
|
25
|
-
# add_index :users, :
|
|
25
|
+
# add_index :users, :email, unique: true
|
|
26
26
|
# add_index :users, :confirmed_at
|
|
27
27
|
|
|
28
28
|
# ============================================================================
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Creates a migration to add temporary user support to your User model.
|
|
3
|
+
Temporary users allow guest/demo user flows where visitors can try
|
|
4
|
+
your app without signing up, then convert to permanent accounts.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
bin/rails generate rails_simple_auth:temporary_users
|
|
8
|
+
|
|
9
|
+
This will create:
|
|
10
|
+
db/migrate/YYYYMMDDHHMMSS_add_temporary_to_users.rb
|
|
11
|
+
|
|
12
|
+
After running the migration, include the concern in your User model:
|
|
13
|
+
include RailsSimpleAuth::Models::Concerns::TemporaryUser
|
|
14
|
+
|
|
15
|
+
And enable in your initializer:
|
|
16
|
+
config.temporary_users_enabled = true
|
|
17
|
+
|
|
18
|
+
The concern provides:
|
|
19
|
+
- temporary? / permanent? methods
|
|
20
|
+
- temporary / permanent / temporary_expired scopes
|
|
21
|
+
- convert_to_permanent!(email:, password:) for account conversion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddTemporaryToUsers < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
add_column :users, :temporary, :boolean, default: false, null: false
|
|
6
|
+
add_index :users, [:temporary, :created_at], name: "index_users_on_temporary_and_created_at"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
|
|
6
|
+
module RailsSimpleAuth
|
|
7
|
+
module Generators
|
|
8
|
+
class TemporaryUsersGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
|
+
|
|
13
|
+
desc 'Creates a migration to add temporary user support to your User model'
|
|
14
|
+
|
|
15
|
+
def self.next_migration_number(dirname)
|
|
16
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_migration_file
|
|
20
|
+
migration_template 'add_temporary_to_users.rb.erb',
|
|
21
|
+
'db/migrate/add_temporary_to_users.rb'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def show_instructions
|
|
25
|
+
say ''
|
|
26
|
+
say 'Temporary users migration created!', :green
|
|
27
|
+
say ''
|
|
28
|
+
say 'Next steps:', :yellow
|
|
29
|
+
say ' 1. Run: bin/rails db:migrate'
|
|
30
|
+
say ''
|
|
31
|
+
say ' 2. Include the concern in your User model:'
|
|
32
|
+
say ' include RailsSimpleAuth::Models::Concerns::TemporaryUser'
|
|
33
|
+
say ''
|
|
34
|
+
say ' 3. Enable in your initializer:'
|
|
35
|
+
say ' config.temporary_users_enabled = true'
|
|
36
|
+
say ''
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'rails/generators'
|
|
4
4
|
|
|
5
5
|
module RailsSimpleAuth
|
|
6
6
|
module Generators
|
|
7
7
|
class ViewsGenerator < Rails::Generators::Base
|
|
8
|
-
source_root File.expand_path(
|
|
8
|
+
source_root File.expand_path('../../../../app/views/rails_simple_auth', __dir__)
|
|
9
9
|
|
|
10
10
|
class_option :only, type: :array, default: [],
|
|
11
|
-
|
|
11
|
+
desc: 'Only copy specific view directories (sessions, registrations, passwords, confirmations, mailers)'
|
|
12
12
|
|
|
13
13
|
def copy_views
|
|
14
14
|
view_directories.each do |dir|
|
|
15
15
|
directory dir, "app/views/rails_simple_auth/#{dir}"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
say
|
|
19
|
-
say
|
|
20
|
-
say
|
|
21
|
-
say
|
|
18
|
+
say ''
|
|
19
|
+
say 'Views copied to app/views/rails_simple_auth/', :green
|
|
20
|
+
say ''
|
|
21
|
+
say 'You can now customize these views. Rails will use your local copies'
|
|
22
22
|
say "instead of the gem's views."
|
|
23
|
-
say
|
|
23
|
+
say ''
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
private
|
|
@@ -10,14 +10,17 @@ module RailsSimpleAuth
|
|
|
10
10
|
:mailer_sender, :mailer_class,
|
|
11
11
|
:user_class_name, :session_class_name,
|
|
12
12
|
:password_minimum_length,
|
|
13
|
-
:after_sign_in_callback, :after_sign_out_callback, :after_sign_up_callback, :after_confirmation_callback
|
|
13
|
+
:after_sign_in_callback, :after_sign_out_callback, :after_sign_up_callback, :after_confirmation_callback,
|
|
14
|
+
:temporary_users_enabled
|
|
15
|
+
|
|
16
|
+
attr_reader :temporary_user_cleanup_days
|
|
14
17
|
|
|
15
18
|
def initialize
|
|
16
19
|
@magic_link_enabled = true
|
|
17
20
|
@email_confirmation_enabled = true
|
|
18
21
|
@oauth_enabled = false
|
|
19
22
|
@oauth_providers = []
|
|
20
|
-
@oauth_link_existing_accounts = true
|
|
23
|
+
@oauth_link_existing_accounts = true # Allow OAuth to link to existing email accounts
|
|
21
24
|
|
|
22
25
|
@magic_link_expiry = 15.minutes
|
|
23
26
|
@password_reset_expiry = 15.minutes
|
|
@@ -37,14 +40,14 @@ module RailsSimpleAuth
|
|
|
37
40
|
@after_sign_up_path = :root_path
|
|
38
41
|
@after_confirmation_path = :new_session_path
|
|
39
42
|
|
|
40
|
-
@layout =
|
|
43
|
+
@layout = 'rails_simple_auth'
|
|
41
44
|
@app_name = nil
|
|
42
45
|
|
|
43
|
-
@mailer_sender =
|
|
44
|
-
@mailer_class =
|
|
46
|
+
@mailer_sender = 'noreply@example.com'
|
|
47
|
+
@mailer_class = 'RailsSimpleAuth::AuthMailer'
|
|
45
48
|
|
|
46
|
-
@user_class_name =
|
|
47
|
-
@session_class_name =
|
|
49
|
+
@user_class_name = 'User'
|
|
50
|
+
@session_class_name = 'RailsSimpleAuth::Session'
|
|
48
51
|
|
|
49
52
|
@password_minimum_length = 8
|
|
50
53
|
|
|
@@ -52,6 +55,9 @@ module RailsSimpleAuth
|
|
|
52
55
|
@after_sign_out_callback = nil
|
|
53
56
|
@after_sign_up_callback = nil
|
|
54
57
|
@after_confirmation_callback = nil
|
|
58
|
+
|
|
59
|
+
@temporary_users_enabled = false
|
|
60
|
+
@temporary_user_cleanup_days = 7
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
def user_class
|
|
@@ -93,5 +99,13 @@ module RailsSimpleAuth
|
|
|
93
99
|
def rate_limit_for(action)
|
|
94
100
|
rate_limits&.dig(action.to_sym)
|
|
95
101
|
end
|
|
102
|
+
|
|
103
|
+
def temporary_user_cleanup_days=(value)
|
|
104
|
+
unless value.is_a?(Integer) && value.positive?
|
|
105
|
+
raise ConfigurationError, 'temporary_user_cleanup_days must be a positive integer'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@temporary_user_cleanup_days = value
|
|
109
|
+
end
|
|
96
110
|
end
|
|
97
111
|
end
|
|
@@ -17,9 +17,9 @@ module RailsSimpleAuth
|
|
|
17
17
|
return unless (session_token = cookies.signed.permanent[:session_token])
|
|
18
18
|
|
|
19
19
|
session_record = RailsSimpleAuth.configuration.session_class
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
.includes(:user)
|
|
21
|
+
.active
|
|
22
|
+
.find_by(id: session_token)
|
|
23
23
|
|
|
24
24
|
if session_record
|
|
25
25
|
RailsSimpleAuth::Current.user = session_record.user
|
|
@@ -51,8 +51,8 @@ module RailsSimpleAuth
|
|
|
51
51
|
|
|
52
52
|
# SECURITY: Validate path to prevent open redirect attacks
|
|
53
53
|
# Only store relative paths that start with / but not //
|
|
54
|
-
return unless path.start_with?(
|
|
55
|
-
return if path.start_with?(
|
|
54
|
+
return unless path.start_with?('/')
|
|
55
|
+
return if path.start_with?('//')
|
|
56
56
|
|
|
57
57
|
session[:return_to] = path
|
|
58
58
|
end
|
|
@@ -63,34 +63,33 @@ module RailsSimpleAuth
|
|
|
63
63
|
|
|
64
64
|
def redirect_to_sign_in
|
|
65
65
|
respond_to do |format|
|
|
66
|
-
format.html { redirect_to new_session_path, alert:
|
|
67
|
-
format.json { render json: { error:
|
|
68
|
-
format.turbo_stream { redirect_to new_session_path, alert:
|
|
66
|
+
format.html { redirect_to new_session_path, alert: 'Please sign in to continue.' }
|
|
67
|
+
format.json { render json: { error: 'Authentication required' }, status: :unauthorized }
|
|
68
|
+
format.turbo_stream { redirect_to new_session_path, alert: 'Please sign in to continue.' }
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def client_ip
|
|
73
|
-
request.headers[
|
|
74
|
-
request.headers[
|
|
73
|
+
request.headers['CF-Connecting-IP'] ||
|
|
74
|
+
request.headers['X-Forwarded-For']&.split(',')&.first&.strip ||
|
|
75
75
|
request.remote_ip
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def resolve_path(config_key)
|
|
79
79
|
path_config = RailsSimpleAuth.configuration.public_send(config_key)
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
case path_config
|
|
82
82
|
when Symbol then send(path_config)
|
|
83
83
|
when Proc then path_config.call(self)
|
|
84
84
|
when String then path_config
|
|
85
85
|
else
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
Rails.logger.warn(
|
|
87
|
+
"[RailsSimpleAuth] Invalid path configuration for #{config_key}: " \
|
|
88
|
+
"expected Symbol, Proc, or String, got #{path_config.class.name}. " \
|
|
89
|
+
'Falling back to root_path.'
|
|
90
|
+
)
|
|
91
|
+
root_path
|
|
92
92
|
end
|
|
93
|
-
result
|
|
94
93
|
rescue NoMethodError => e
|
|
95
94
|
Rails.logger.error(
|
|
96
95
|
"[RailsSimpleAuth] Path helper '#{path_config}' not found for #{config_key}. " \
|
|
@@ -39,6 +39,30 @@ module RailsSimpleAuth
|
|
|
39
39
|
RailsSimpleAuth::Current.session = nil
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Destroy temporary user session when signing in with a different account
|
|
43
|
+
# This cleans up guest/demo users when they sign in or register
|
|
44
|
+
# @param signing_in_user [User, nil] The user being signed in (to avoid self-destruction)
|
|
45
|
+
def destroy_temporary_user_session(signing_in_user = nil)
|
|
46
|
+
return unless RailsSimpleAuth.configuration.temporary_users_enabled
|
|
47
|
+
return unless RailsSimpleAuth::Current.user&.temporary?
|
|
48
|
+
|
|
49
|
+
temp_user = RailsSimpleAuth::Current.user
|
|
50
|
+
|
|
51
|
+
# Don't destroy if the user is re-authenticating as themselves
|
|
52
|
+
return if signing_in_user && temp_user.id == signing_in_user.id
|
|
53
|
+
|
|
54
|
+
temp_user_id = temp_user.id
|
|
55
|
+
|
|
56
|
+
temp_user.transaction do
|
|
57
|
+
destroy_current_session
|
|
58
|
+
temp_user.destroy!
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Rails.logger.info("[RailsSimpleAuth] Destroyed temporary user #{temp_user_id} on sign in")
|
|
62
|
+
rescue ActiveRecord::RecordNotDestroyed => e
|
|
63
|
+
Rails.logger.error("[RailsSimpleAuth] Failed to destroy temporary user #{temp_user_id}: #{e.message}")
|
|
64
|
+
end
|
|
65
|
+
|
|
42
66
|
# Run after sign in callback if configured
|
|
43
67
|
def run_after_sign_in_callback(user)
|
|
44
68
|
run_callback(:after_sign_in_callback, user)
|
|
@@ -8,7 +8,7 @@ module RailsSimpleAuth
|
|
|
8
8
|
g.test_framework :minitest
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
initializer
|
|
11
|
+
initializer 'rails_simple_auth.helpers' do
|
|
12
12
|
ActiveSupport.on_load(:action_controller_base) do
|
|
13
13
|
include RailsSimpleAuth::Controllers::Concerns::Authentication
|
|
14
14
|
include RailsSimpleAuth::Controllers::Concerns::SessionManagement
|
|
@@ -10,25 +10,26 @@ module RailsSimpleAuth
|
|
|
10
10
|
has_secure_password
|
|
11
11
|
|
|
12
12
|
has_many :sessions,
|
|
13
|
-
class_name:
|
|
13
|
+
class_name: RailsSimpleAuth.configuration.session_class_name,
|
|
14
14
|
dependent: :destroy,
|
|
15
15
|
inverse_of: :user
|
|
16
16
|
|
|
17
|
-
validates :
|
|
17
|
+
validates :email,
|
|
18
18
|
presence: true,
|
|
19
19
|
uniqueness: { case_sensitive: false },
|
|
20
|
-
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
20
|
+
format: { with: URI::MailTo::EMAIL_REGEXP },
|
|
21
|
+
unless: :temporary?
|
|
21
22
|
|
|
22
23
|
validate :password_meets_minimum_length, if: :password_required?
|
|
23
24
|
|
|
24
|
-
normalizes :
|
|
25
|
+
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
class_methods do
|
|
28
29
|
def find_by_email(email)
|
|
29
30
|
return nil if email.blank?
|
|
30
31
|
|
|
31
|
-
find_by(
|
|
32
|
+
find_by(email: email.to_s.strip.downcase)
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -47,9 +48,16 @@ module RailsSimpleAuth
|
|
|
47
48
|
count
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
# Returns false by default. Override in TemporaryUser concern.
|
|
52
|
+
def temporary?
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
50
56
|
private
|
|
51
57
|
|
|
52
58
|
def password_required?
|
|
59
|
+
return false if temporary?
|
|
60
|
+
|
|
53
61
|
password_digest.blank? || password.present?
|
|
54
62
|
end
|
|
55
63
|
|
|
@@ -8,6 +8,7 @@ module RailsSimpleAuth
|
|
|
8
8
|
|
|
9
9
|
included do
|
|
10
10
|
# Requires `confirmed_at` datetime column
|
|
11
|
+
# Optional `unconfirmed_email` string column for reconfirmation
|
|
11
12
|
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
|
12
13
|
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
|
13
14
|
end
|
|
@@ -22,17 +23,51 @@ module RailsSimpleAuth
|
|
|
22
23
|
!confirmed?
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
# Check if user is changing their email (reconfirmation)
|
|
27
|
+
def reconfirming?
|
|
28
|
+
respond_to?(:unconfirmed_email) && unconfirmed_email.present?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if user needs confirmation (either unconfirmed or reconfirming)
|
|
32
|
+
def unconfirmed_or_reconfirming?
|
|
33
|
+
unconfirmed? || reconfirming?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the email that needs confirmation
|
|
37
|
+
def confirmable_email
|
|
38
|
+
reconfirming? ? unconfirmed_email : email
|
|
39
|
+
end
|
|
40
|
+
|
|
25
41
|
# Confirm the user's email
|
|
42
|
+
# Handles both initial confirmation and reconfirmation (email change)
|
|
43
|
+
# Also sets temporary: false if TemporaryUser concern is included
|
|
44
|
+
# Returns true on success, false on failure (with errors populated)
|
|
26
45
|
def confirm!
|
|
27
|
-
|
|
46
|
+
attrs = { confirmed_at: Time.current }
|
|
47
|
+
attrs[:temporary] = false if respond_to?(:temporary?)
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
if reconfirming?
|
|
50
|
+
# Email change confirmation - check email uniqueness first
|
|
51
|
+
if self.class.where.not(id: id).exists?(email: unconfirmed_email)
|
|
52
|
+
errors.add(:email, 'is already taken by another user')
|
|
53
|
+
return false
|
|
54
|
+
end
|
|
55
|
+
attrs[:email] = unconfirmed_email
|
|
56
|
+
attrs[:unconfirmed_email] = nil
|
|
57
|
+
update(attrs)
|
|
58
|
+
elsif unconfirmed?
|
|
59
|
+
# Initial confirmation
|
|
60
|
+
update(attrs)
|
|
61
|
+
else
|
|
62
|
+
# Already confirmed and not reconfirming
|
|
63
|
+
true
|
|
64
|
+
end
|
|
30
65
|
end
|
|
31
66
|
|
|
32
67
|
# Generate email confirmation token using Rails signed_id
|
|
33
68
|
def generate_confirmation_token
|
|
34
69
|
signed_id(
|
|
35
|
-
purpose: :
|
|
70
|
+
purpose: :confirm_email,
|
|
36
71
|
expires_in: RailsSimpleAuth.configuration.confirmation_expiry
|
|
37
72
|
)
|
|
38
73
|
end
|
|
@@ -18,13 +18,13 @@ module RailsSimpleAuth
|
|
|
18
18
|
# - true (default): Links OAuth to existing email accounts (safe for providers that verify emails)
|
|
19
19
|
# - false: Only allows OAuth for accounts created via OAuth with same provider+uid
|
|
20
20
|
def from_oauth(auth_hash)
|
|
21
|
-
email = auth_hash.dig(
|
|
22
|
-
provider = auth_hash[
|
|
23
|
-
uid = auth_hash[
|
|
21
|
+
email = auth_hash.dig('info', 'email')
|
|
22
|
+
provider = auth_hash['provider']
|
|
23
|
+
uid = auth_hash['uid']
|
|
24
24
|
|
|
25
25
|
if email.blank?
|
|
26
26
|
Rails.logger.warn(
|
|
27
|
-
|
|
27
|
+
'[RailsSimpleAuth] OAuth auth_hash missing email. ' \
|
|
28
28
|
"Provider: #{provider}, UID: #{uid}. Ensure email scope is requested."
|
|
29
29
|
)
|
|
30
30
|
return nil
|
|
@@ -62,7 +62,7 @@ module RailsSimpleAuth
|
|
|
62
62
|
|
|
63
63
|
# Create new user for new OAuth signups
|
|
64
64
|
user = new(
|
|
65
|
-
|
|
65
|
+
email: email,
|
|
66
66
|
password: SecureRandom.hex(32) # Random password for OAuth users
|
|
67
67
|
)
|
|
68
68
|
|