rails_authentication 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/README.md +108 -0
- data/lib/generators/authentication/authentication_generator.rb +126 -0
- data/lib/generators/authentication/features/confirmable.rb +25 -0
- data/lib/generators/authentication/features/invitable.rb +26 -0
- data/lib/generators/authentication/features/lockable.rb +25 -0
- data/lib/generators/authentication/features/recoverable.rb +25 -0
- data/lib/generators/authentication/features/registerable.rb +17 -0
- data/lib/generators/authentication/features/rememberable.rb +16 -0
- data/lib/generators/authentication/features/timeoutable.rb +17 -0
- data/lib/generators/authentication/features/trackable.rb +17 -0
- data/lib/generators/authentication/features/validatable.rb +15 -0
- data/lib/generators/authentication/templates/app/controllers/concerns/authentication.rb.tt +96 -0
- data/lib/generators/authentication/templates/app/controllers/confirmations_controller.rb.tt +27 -0
- data/lib/generators/authentication/templates/app/controllers/invitations_controller.rb.tt +36 -0
- data/lib/generators/authentication/templates/app/controllers/passwords_controller.rb.tt +39 -0
- data/lib/generators/authentication/templates/app/controllers/registrations_controller.rb.tt +56 -0
- data/lib/generators/authentication/templates/app/controllers/sessions_controller.rb.tt +64 -0
- data/lib/generators/authentication/templates/app/controllers/unlocks_controller.rb.tt +23 -0
- data/lib/generators/authentication/templates/app/mailers/confirmations_mailer.rb.tt +10 -0
- data/lib/generators/authentication/templates/app/mailers/invitations_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/mailers/passwords_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/mailers/unlocks_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/models/concerns/confirmable_concern.rb.tt +74 -0
- data/lib/generators/authentication/templates/app/models/concerns/invitable_concern.rb.tt +86 -0
- data/lib/generators/authentication/templates/app/models/concerns/lockable_concern.rb.tt +48 -0
- data/lib/generators/authentication/templates/app/models/concerns/recoverable_concern.rb.tt +33 -0
- data/lib/generators/authentication/templates/app/models/concerns/rememberable_concern.rb.tt +15 -0
- data/lib/generators/authentication/templates/app/models/concerns/timeoutable_concern.rb.tt +9 -0
- data/lib/generators/authentication/templates/app/models/concerns/trackable_concern.rb.tt +7 -0
- data/lib/generators/authentication/templates/app/models/concerns/validatable_concern.rb.tt +40 -0
- data/lib/generators/authentication/templates/app/models/user_auth.rb.tt +18 -0
- data/lib/generators/authentication/templates/app/views/confirmations/new.html.erb.tt +8 -0
- data/lib/generators/authentication/templates/app/views/confirmations_mailer/confirmation_instructions.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/confirmations_mailer/confirmation_instructions.text.erb.tt +4 -0
- data/lib/generators/authentication/templates/app/views/invitations/edit.html.erb.tt +9 -0
- data/lib/generators/authentication/templates/app/views/invitations/new.html.erb.tt +9 -0
- data/lib/generators/authentication/templates/app/views/invitations_mailer/invitation_instructions.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/invitations_mailer/invitation_instructions.text.erb.tt +4 -0
- data/lib/generators/authentication/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/passwords_mailer/reset.text.erb.tt +4 -0
- data/lib/generators/authentication/templates/app/views/registrations/edit.html.erb.tt +22 -0
- data/lib/generators/authentication/templates/app/views/registrations/new.html.erb.tt +21 -0
- data/lib/generators/authentication/templates/app/views/sessions/new.html.erb.tt +23 -0
- data/lib/generators/authentication/templates/app/views/unlocks/new.html.erb.tt +8 -0
- data/lib/generators/authentication/templates/app/views/unlocks_mailer/unlock_instructions.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/unlocks_mailer/unlock_instructions.text.erb.tt +6 -0
- data/lib/generators/authentication/templates/db/migrate/add_confirmable_to_users.rb.tt +12 -0
- data/lib/generators/authentication/templates/db/migrate/add_invitable_to_users.rb.tt +12 -0
- data/lib/generators/authentication/templates/db/migrate/add_lockable_to_users.rb.tt +9 -0
- data/lib/generators/authentication/templates/db/migrate/add_recoverable_to_users.rb.tt +8 -0
- data/lib/generators/authentication/templates/db/migrate/add_rememberable_to_users.rb.tt +5 -0
- data/lib/generators/authentication/templates/db/migrate/create_user_auths.rb.tt +14 -0
- data/lib/rails_authentication/version.rb +5 -0
- data/lib/rails_authentication.rb +6 -0
- metadata +112 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class RegistrationsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access only: %i[ new create ]
|
|
3
|
+
|
|
4
|
+
def new
|
|
5
|
+
@user = User.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
@user = User.new(user_params)
|
|
10
|
+
|
|
11
|
+
if @user.save
|
|
12
|
+
<% if confirmable? -%>
|
|
13
|
+
@user.send_confirmation_instructions
|
|
14
|
+
redirect_to new_session_path, notice: "Welcome! Check your email to confirm your account."
|
|
15
|
+
<% else -%>
|
|
16
|
+
start_new_session_for @user
|
|
17
|
+
redirect_to after_authentication_url, notice: "Welcome! You have signed up successfully."
|
|
18
|
+
<% end -%>
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_content
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit
|
|
25
|
+
@user = Current.user
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
@user = Current.user
|
|
30
|
+
|
|
31
|
+
if @user.update(user_params_for_update)
|
|
32
|
+
redirect_to edit_registration_path, notice: "Your account has been updated."
|
|
33
|
+
else
|
|
34
|
+
render :edit, status: :unprocessable_content
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def destroy
|
|
39
|
+
user = Current.user
|
|
40
|
+
terminate_session
|
|
41
|
+
user.destroy
|
|
42
|
+
redirect_to new_session_path, notice: "Your account has been deleted.", status: :see_other
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def user_params
|
|
48
|
+
params.permit(:email_address, :password, :password_confirmation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def user_params_for_update
|
|
52
|
+
permitted = user_params
|
|
53
|
+
permitted = permitted.except(:password, :password_confirmation) if permitted[:password].blank?
|
|
54
|
+
permitted
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class SessionsController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access only: %i[ new create ]
|
|
3
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
|
|
4
|
+
|
|
5
|
+
def new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
user = User.authenticate_by(params.permit(:email_address, :password))
|
|
10
|
+
|
|
11
|
+
if user.nil?
|
|
12
|
+
<% if lockable? || trackable? -%>
|
|
13
|
+
account = User.find_by(email_address: params[:email_address])
|
|
14
|
+
<% end -%>
|
|
15
|
+
<% if lockable? -%>
|
|
16
|
+
account&.register_failed_attempt!
|
|
17
|
+
<% end -%>
|
|
18
|
+
<% if trackable? -%>
|
|
19
|
+
record_authentication_attempt(account, success: false, failure_reason: "invalid_credentials")
|
|
20
|
+
<% end -%>
|
|
21
|
+
redirect_to new_session_path, alert: "Try another email address or password."
|
|
22
|
+
<% if lockable? -%>
|
|
23
|
+
elsif user.locked?
|
|
24
|
+
<% if trackable? -%>
|
|
25
|
+
record_authentication_attempt(user, success: false, failure_reason: "locked")
|
|
26
|
+
<% end -%>
|
|
27
|
+
redirect_to new_session_path, alert: "Your account is locked. Check your email for unlock instructions."
|
|
28
|
+
<% end -%>
|
|
29
|
+
<% if confirmable? -%>
|
|
30
|
+
elsif !user.confirmed?
|
|
31
|
+
<% if trackable? -%>
|
|
32
|
+
record_authentication_attempt(user, success: false, failure_reason: "unconfirmed")
|
|
33
|
+
<% end -%>
|
|
34
|
+
redirect_to new_session_path, alert: "You must confirm your email address before signing in."
|
|
35
|
+
<% end -%>
|
|
36
|
+
else
|
|
37
|
+
<% if lockable? -%>
|
|
38
|
+
user.reset_failed_attempts!
|
|
39
|
+
<% end -%>
|
|
40
|
+
<% if trackable? -%>
|
|
41
|
+
record_authentication_attempt(user, success: true)
|
|
42
|
+
<% end -%>
|
|
43
|
+
<% if rememberable? -%>
|
|
44
|
+
start_new_session_for user, remember: params[:remember_me] == "1"
|
|
45
|
+
<% else -%>
|
|
46
|
+
start_new_session_for user
|
|
47
|
+
<% end -%>
|
|
48
|
+
redirect_to after_authentication_url
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def destroy
|
|
53
|
+
terminate_session
|
|
54
|
+
redirect_to new_session_path, status: :see_other
|
|
55
|
+
end
|
|
56
|
+
<% if trackable? -%>
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def record_authentication_attempt(user, success:, failure_reason: nil)
|
|
61
|
+
UserAuth.record(user, request, success: success, failure_reason: failure_reason)
|
|
62
|
+
end
|
|
63
|
+
<% end -%>
|
|
64
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class UnlocksController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access
|
|
3
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_unlock_path, alert: "Try again later." }
|
|
4
|
+
|
|
5
|
+
def new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
user = User.find_by(email_address: params[:email_address])
|
|
10
|
+
user.send_unlock_instructions if user&.locked?
|
|
11
|
+
|
|
12
|
+
redirect_to new_session_path, notice: "Unlock instructions sent (if user with that email address exists and is locked)."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
if user = User.find_by_valid_unlock_token(params[:token])
|
|
17
|
+
user.unlock!
|
|
18
|
+
redirect_to new_session_path, notice: "Your account has been unlocked. Please sign in."
|
|
19
|
+
else
|
|
20
|
+
redirect_to new_unlock_path, alert: "Unlock link is invalid or has expired."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class ConfirmationsMailer < ApplicationMailer
|
|
2
|
+
def confirmation_instructions(user)
|
|
3
|
+
@user = user
|
|
4
|
+
<% if reconfirmable? -%>
|
|
5
|
+
mail subject: "Confirm your email address", to: user.unconfirmed_email.presence || user.email_address
|
|
6
|
+
<% else -%>
|
|
7
|
+
mail subject: "Confirm your email address", to: user.email_address
|
|
8
|
+
<% end -%>
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module ConfirmableConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
CONFIRMATION_TOKEN_EXPIRES_IN = 24.hours
|
|
5
|
+
<% if reconfirmable? -%>
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_update :postpone_email_address_change, if: :postponing_email_address_change?
|
|
9
|
+
after_update_commit :deliver_reconfirmation_instructions, if: -> { saved_change_to_unconfirmed_email? && unconfirmed_email.present? }
|
|
10
|
+
end
|
|
11
|
+
<% end -%>
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def find_by_valid_confirmation_token(token)
|
|
15
|
+
return if token.blank?
|
|
16
|
+
|
|
17
|
+
user = find_by(confirmation_token: token)
|
|
18
|
+
user if user&.confirmation_sent_at&.after?(CONFIRMATION_TOKEN_EXPIRES_IN.ago)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def confirmed?
|
|
23
|
+
confirmed_at.present?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def confirm!
|
|
27
|
+
<% if reconfirmable? -%>
|
|
28
|
+
@confirming = true
|
|
29
|
+
self.email_address = unconfirmed_email if unconfirmed_email.present?
|
|
30
|
+
self.unconfirmed_email = nil
|
|
31
|
+
self.confirmed_at = Time.current
|
|
32
|
+
self.confirmation_token = nil
|
|
33
|
+
self.confirmation_sent_at = nil
|
|
34
|
+
save!
|
|
35
|
+
ensure
|
|
36
|
+
@confirming = false
|
|
37
|
+
<% else -%>
|
|
38
|
+
update!(confirmed_at: Time.current, confirmation_token: nil, confirmation_sent_at: nil)
|
|
39
|
+
<% end -%>
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def send_confirmation_instructions
|
|
43
|
+
generate_confirmation_token!
|
|
44
|
+
ConfirmationsMailer.confirmation_instructions(self).deliver_later
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def generate_confirmation_token!
|
|
48
|
+
update!(confirmation_token: generate_confirmable_token, confirmation_sent_at: Time.current)
|
|
49
|
+
end
|
|
50
|
+
<% if reconfirmable? -%>
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
def postponing_email_address_change?
|
|
54
|
+
!@confirming && email_address_changed? && confirmed?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def postpone_email_address_change
|
|
58
|
+
self.unconfirmed_email = email_address
|
|
59
|
+
self.email_address = email_address_in_database
|
|
60
|
+
self.confirmation_token = generate_confirmable_token
|
|
61
|
+
self.confirmation_sent_at = Time.current
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def deliver_reconfirmation_instructions
|
|
65
|
+
ConfirmationsMailer.confirmation_instructions(self).deliver_later
|
|
66
|
+
end
|
|
67
|
+
<% end -%>
|
|
68
|
+
|
|
69
|
+
protected
|
|
70
|
+
|
|
71
|
+
def generate_confirmable_token
|
|
72
|
+
SecureRandom.urlsafe_base64(32)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module InvitableConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
INVITATION_TOKEN_EXPIRES_IN = 7.days
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
belongs_to :invited_by, polymorphic: true, optional: true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def invite!(email_address, invited_by: nil)
|
|
12
|
+
user = find_by(email_address: email_address)
|
|
13
|
+
|
|
14
|
+
if user
|
|
15
|
+
# Re-inviting an existing pending invitee refreshes their token; anyone else
|
|
16
|
+
# is returned untouched (and no email is sent).
|
|
17
|
+
if user.invitation_pending?
|
|
18
|
+
user.update!(invitation_token: generate_invitable_token, invitation_created_at: Time.current)
|
|
19
|
+
user.send_invitation_instructions
|
|
20
|
+
invited_by.increment!(:invitations_count) if invited_by.respond_to?(:invitations_count)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return user
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
user = new(
|
|
27
|
+
email_address: email_address,
|
|
28
|
+
password: generate_temporary_password,
|
|
29
|
+
invited_by: invited_by,
|
|
30
|
+
invitation_token: generate_invitable_token,
|
|
31
|
+
invitation_created_at: Time.current
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if user.save
|
|
35
|
+
user.send_invitation_instructions
|
|
36
|
+
invited_by.increment!(:invitations_count) if invited_by.respond_to?(:invitations_count)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
user
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_by_valid_invitation_token(token)
|
|
43
|
+
return if token.blank?
|
|
44
|
+
|
|
45
|
+
user = find_by(invitation_token: token)
|
|
46
|
+
user if user&.invitation_created_at&.after?(INVITATION_TOKEN_EXPIRES_IN.ago)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
protected
|
|
50
|
+
|
|
51
|
+
def generate_invitable_token
|
|
52
|
+
SecureRandom.urlsafe_base64(32)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def generate_temporary_password
|
|
56
|
+
lower_letter = ("a".."z").to_a.sample(2)
|
|
57
|
+
upper_letter = ("A".."Z").to_a.sample(2)
|
|
58
|
+
digits = (1..5).to_a.sample(2)
|
|
59
|
+
special = %w[~ ! @ % & _ + -].sample(2)
|
|
60
|
+
prefix = (lower_letter + upper_letter + digits + special).join
|
|
61
|
+
SecureRandom.base58(16).sub(/\A.{#{prefix.length}}/, prefix)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def invitation_pending?
|
|
66
|
+
invitation_token.present? && invitation_accepted_at.nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def send_invitation_instructions
|
|
70
|
+
update!(invitation_sent_at: Time.current)
|
|
71
|
+
InvitationsMailer.invitation_instructions(self).deliver_later
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def accept_invitation!(password:, password_confirmation:)
|
|
75
|
+
assign_attributes(
|
|
76
|
+
password: password,
|
|
77
|
+
password_confirmation: password_confirmation,
|
|
78
|
+
invitation_token: nil,
|
|
79
|
+
invitation_accepted_at: Time.current
|
|
80
|
+
)
|
|
81
|
+
<% if confirmable? -%>
|
|
82
|
+
self.confirmed_at ||= Time.current # accepting the invite proves the email address
|
|
83
|
+
<% end -%>
|
|
84
|
+
save
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module LockableConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
MAXIMUM_ATTEMPTS = 5
|
|
5
|
+
UNLOCK_IN = 1.hour
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def find_by_valid_unlock_token(token)
|
|
9
|
+
return if token.blank?
|
|
10
|
+
|
|
11
|
+
find_by(unlock_token: token)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Locks expire on their own after UNLOCK_IN; the stale lock is cleared on the
|
|
16
|
+
# next successful sign-in by reset_failed_attempts!.
|
|
17
|
+
def locked?
|
|
18
|
+
locked_at.present? && locked_at.after?(UNLOCK_IN.ago)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register_failed_attempt!
|
|
22
|
+
increment!(:failed_attempts)
|
|
23
|
+
lock! if failed_attempts >= MAXIMUM_ATTEMPTS && !locked?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lock!
|
|
27
|
+
update!(locked_at: Time.current, unlock_token: generate_lockable_token)
|
|
28
|
+
send_unlock_instructions
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unlock!
|
|
32
|
+
update!(locked_at: nil, unlock_token: nil, failed_attempts: 0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset_failed_attempts!
|
|
36
|
+
update!(failed_attempts: 0, locked_at: nil, unlock_token: nil) if failed_attempts.positive? || locked_at.present?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def send_unlock_instructions
|
|
40
|
+
UnlocksMailer.unlock_instructions(self).deliver_later
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def generate_lockable_token
|
|
46
|
+
SecureRandom.urlsafe_base64(32)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module RecoverableConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
RESET_PASSWORD_TOKEN_EXPIRES_IN = 4.hours
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
def find_by_valid_reset_password_token(token)
|
|
8
|
+
return if token.blank?
|
|
9
|
+
|
|
10
|
+
user = find_by(reset_password_token: token)
|
|
11
|
+
user if user&.reset_password_sent_at&.after?(RESET_PASSWORD_TOKEN_EXPIRES_IN.ago)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send_reset_password_instructions
|
|
16
|
+
generate_reset_password_token!
|
|
17
|
+
PasswordsMailer.reset(self).deliver_later
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_reset_password_token!
|
|
21
|
+
update!(reset_password_token: generate_recoverable_token, reset_password_sent_at: Time.current)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clear_reset_password_token!
|
|
25
|
+
update!(reset_password_token: nil, reset_password_sent_at: nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
def generate_recoverable_token
|
|
31
|
+
SecureRandom.urlsafe_base64(32)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module RememberableConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
def remembered?
|
|
5
|
+
remember_created_at.present?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def remember_me!
|
|
9
|
+
update!(remember_created_at: Time.current)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def forget_me!
|
|
13
|
+
update!(remember_created_at: nil) if remember_created_at.present?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module ValidatableConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
EMAIL_ADDRESS_REGEXP = /\A[^@\s]+@[^@\s]+\z/
|
|
5
|
+
PASSWORD_LENGTH = 6..72
|
|
6
|
+
PASSWORD_DIGIT_REGEXP = /\d+/
|
|
7
|
+
PASSWORD_UPPER_REGEXP = /[A-Z]+/
|
|
8
|
+
PASSWORD_LOWER_REGEXP = /[a-z]+/
|
|
9
|
+
PASSWORD_SPECIAL_REGEXP = /[^a-zA-Z\d\s]+/
|
|
10
|
+
PASSWORD_MINIMUM_COMPLEXITY = 2 # Minumum number of complexities to match to be valid
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
validates :email_address, presence: true,
|
|
14
|
+
format: { with: EMAIL_ADDRESS_REGEXP },
|
|
15
|
+
uniqueness: { case_sensitive: false }
|
|
16
|
+
validates :password, length: { in: PASSWORD_LENGTH }, allow_nil: true
|
|
17
|
+
|
|
18
|
+
validate :password_complexity
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
def password_complexity
|
|
24
|
+
return if password.blank? || valid_password_complexity?
|
|
25
|
+
|
|
26
|
+
errors.add(:password, "must meet password complexity standards")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def valid_password_complexity?
|
|
30
|
+
return false if password.blank?
|
|
31
|
+
|
|
32
|
+
conditions = [
|
|
33
|
+
PASSWORD_DIGIT_REGEXP.match?(password),
|
|
34
|
+
PASSWORD_UPPER_REGEXP.match?(password),
|
|
35
|
+
PASSWORD_LOWER_REGEXP.match?(password),
|
|
36
|
+
PASSWORD_SPECIAL_REGEXP.match?(password)
|
|
37
|
+
]
|
|
38
|
+
conditions.count(true) >= PASSWORD_MINIMUM_COMPLEXITY
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class UserAuth < ApplicationRecord
|
|
2
|
+
# user is optional so failed attempts against unknown email addresses still get recorded
|
|
3
|
+
belongs_to :user, optional: true
|
|
4
|
+
|
|
5
|
+
scope :successful, -> { where(success: true) }
|
|
6
|
+
scope :failed, -> { where(success: false) }
|
|
7
|
+
|
|
8
|
+
def self.record(user, request, success:, failure_reason: nil)
|
|
9
|
+
create!(
|
|
10
|
+
user: user,
|
|
11
|
+
ip: request.remote_ip,
|
|
12
|
+
user_agent: request.user_agent,
|
|
13
|
+
referrer: request.referer,
|
|
14
|
+
success: success,
|
|
15
|
+
failure_reason: failure_reason
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<h1>Resend confirmation instructions</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
|
|
5
|
+
<%%= form_with url: confirmations_path do |form| %>
|
|
6
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
|
7
|
+
<%%= form.submit "Resend confirmation instructions" %>
|
|
8
|
+
<%% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<h1>Accept your invitation</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
|
|
5
|
+
<%%= form_with url: invitation_path(params[:token]), method: :patch do |form| %>
|
|
6
|
+
<%%= form.password_field :password, required: true, autofocus: true, autocomplete: "new-password", placeholder: "Choose a password", maxlength: 72 %><br>
|
|
7
|
+
<%%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat the password", maxlength: 72 %><br>
|
|
8
|
+
<%%= form.submit "Set my password" %>
|
|
9
|
+
<%% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<h1>Invite someone</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
|
5
|
+
|
|
6
|
+
<%%= form_with url: invitations_path do |form| %>
|
|
7
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, placeholder: "Enter their email address" %><br>
|
|
8
|
+
<%%= form.submit "Send invitation" %>
|
|
9
|
+
<%% end %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
You have been invited. Set your password and activate your account on
|
|
3
|
+
<%%= link_to "this invitation page", edit_invitation_url(@user.invitation_token) %>.
|
|
4
|
+
|
|
5
|
+
This link will expire in <%%= distance_of_time_in_words(0, InvitableConcern::INVITATION_TOKEN_EXPIRES_IN) %>.
|
|
6
|
+
</p>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<h1>Your account</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
|
5
|
+
|
|
6
|
+
<%% if @user.errors.any? %>
|
|
7
|
+
<ul style="color:red">
|
|
8
|
+
<%% @user.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%%= message %></li>
|
|
10
|
+
<%% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
<%% end %>
|
|
13
|
+
|
|
14
|
+
<%%= form_with url: registration_path, method: :patch do |form| %>
|
|
15
|
+
<%%= form.email_field :email_address, required: true, autocomplete: "username", value: @user.email_address %><br>
|
|
16
|
+
<%%= form.password_field :password, autocomplete: "new-password", placeholder: "New password (leave blank to keep current)", maxlength: 72 %><br>
|
|
17
|
+
<%%= form.password_field :password_confirmation, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
|
|
18
|
+
<%%= form.submit "Update" %>
|
|
19
|
+
<%% end %>
|
|
20
|
+
<br>
|
|
21
|
+
|
|
22
|
+
<%%= button_to "Delete my account", registration_path, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<h1>Sign up</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
|
|
5
|
+
<%% if @user.errors.any? %>
|
|
6
|
+
<ul style="color:red">
|
|
7
|
+
<%% @user.errors.full_messages.each do |message| %>
|
|
8
|
+
<li><%%= message %></li>
|
|
9
|
+
<%% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
<%% end %>
|
|
12
|
+
|
|
13
|
+
<%%= form_with url: registration_path do |form| %>
|
|
14
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: @user.email_address %><br>
|
|
15
|
+
<%%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter a password", maxlength: 72 %><br>
|
|
16
|
+
<%%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat the password", maxlength: 72 %><br>
|
|
17
|
+
<%%= form.submit "Sign up" %>
|
|
18
|
+
<%% end %>
|
|
19
|
+
<br>
|
|
20
|
+
|
|
21
|
+
<%%= link_to "Sign in", new_session_path %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
2
|
+
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
|
3
|
+
|
|
4
|
+
<%%= form_with url: session_path do |form| %>
|
|
5
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
|
6
|
+
<%%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
|
|
7
|
+
<% if rememberable? -%>
|
|
8
|
+
<%%= form.check_box :remember_me %> <%%= form.label :remember_me, "Remember me" %><br>
|
|
9
|
+
<% end -%>
|
|
10
|
+
<%%= form.submit "Sign in" %>
|
|
11
|
+
<%% end %>
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
<%%= link_to "Forgot password?", new_password_path %>
|
|
15
|
+
<% if registerable? -%>
|
|
16
|
+
<br><%%= link_to "Sign up", new_registration_path %>
|
|
17
|
+
<% end -%>
|
|
18
|
+
<% if confirmable? -%>
|
|
19
|
+
<br><%%= link_to "Resend confirmation instructions", new_confirmation_path %>
|
|
20
|
+
<% end -%>
|
|
21
|
+
<% if lockable? -%>
|
|
22
|
+
<br><%%= link_to "Resend unlock instructions", new_unlock_path %>
|
|
23
|
+
<% end -%>
|