Skip to content

Commit 0342fd0

Browse files
authored
Merge pull request #155 from joyofrails/feat/magic-login
Add authentication via magic link
2 parents e6f7774 + b4847f9 commit 0342fd0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+477
-50
lines changed

app/controllers/users/confirmations_controller.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ def new
1010
end
1111

1212
def create
13-
@user = User.find_by(email: params.require(:user).permit(:email).dig(:email).to_s.downcase)
13+
email = params.require(:user).permit(:email).dig(:email).to_s.downcase
14+
@user = User.find_by(email: email)
1415

1516
if @user.blank?
1617
return redirect_to new_users_confirmation_path, alert: "We are unable to confirm that email address"
@@ -26,7 +27,7 @@ def create
2627
end
2728

2829
def edit
29-
@user = User.find_by_token_for(:confirmation, params[:confirmation_token])
30+
@user = User.find_by_token_for(:confirmation, params[:token])
3031

3132
if @user.blank?
3233
return redirect_to new_users_confirmation_path, alert: "This link is invalid or expired"
@@ -36,11 +37,11 @@ def edit
3637
return redirect_to root_path, notice: "Your account has already been confirmed"
3738
end
3839

39-
render Users::Confirmations::EditView.new(user: @user, confirmation_token: params[:confirmation_token])
40+
render Users::Confirmations::EditView.new(user: @user, confirmation_token: params[:token])
4041
end
4142

4243
def update
43-
@user = User.find_by_token_for(:confirmation, params[:confirmation_token])
44+
@user = User.find_by_token_for(:confirmation, params[:token])
4445

4546
if @user.blank?
4647
return redirect_to new_users_confirmation_path, alert: "That link is invalid or expired"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class Users::MagicSessionTokensController < ApplicationController
2+
before_action :feature_enabled!
3+
before_action :redirect_if_authenticated
4+
5+
def new
6+
render Users::MagicSessionTokens::NewView.new(user: User.new)
7+
end
8+
9+
def create
10+
email = params.require(:user).permit(:email).dig(:email).to_s.downcase
11+
@user = User.find_by(email: email)
12+
13+
if @user.present?
14+
MagicSessionTokenNotifier.deliver_to(@user)
15+
else
16+
Emails::MagicSessionMailer.no_account_found(email).deliver_later
17+
end
18+
19+
redirect_to root_path, notice: "We’ve sent an email with instructions to sign in"
20+
end
21+
22+
def show
23+
render Users::MagicSessionTokens::ShowView.new(user: User.new, magic_session_token: params[:token])
24+
end
25+
26+
private
27+
28+
def feature_enabled!
29+
redirect_to root_path, notice: "Coming soon!" unless Flipper.enabled?(:user_registration, current_admin_user)
30+
end
31+
end

app/controllers/users/passwords_controller.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ def new
99
end
1010

1111
def create
12-
@user = User.find_by(email: params.require(:user).permit(:email).dig(:email).to_s.downcase)
12+
email = params.require(:user).permit(:email).dig(:email).to_s.downcase
13+
@user = User.find_by(email: email)
1314

1415
if @user.blank?
1516
return redirect_to new_users_session_path, notice: "If that user exists we’ve sent instructions to their email"
@@ -25,7 +26,7 @@ def create
2526
end
2627

2728
def edit
28-
@user = User.find_by_token_for(:password_reset, params[:password_reset_token])
29+
@user = User.find_by_token_for(:password_reset, params[:token])
2930

3031
if @user.blank?
3132
return redirect_to new_users_password_path, alert: "That link is invalid or expired"
@@ -35,11 +36,11 @@ def edit
3536
return redirect_to new_users_confirmation_path, alert: "You must confirm your email before you can sign in"
3637
end
3738

38-
render Users::Passwords::EditView.new(user: @user, password_reset_token: params[:password_reset_token])
39+
render Users::Passwords::EditView.new(user: @user, password_reset_token: params[:token])
3940
end
4041

4142
def update
42-
@user = User.find_by_token_for(:password_reset, params[:password_reset_token])
43+
@user = User.find_by_token_for(:password_reset, params[:token])
4344

4445
if @user.blank?
4546
flash.now[:alert] = "That link is invalid or expired"
@@ -54,7 +55,7 @@ def update
5455
redirect_to new_users_session_path, notice: "Password updated! Please sign in"
5556
else
5657
flash.now[:alert] = @user.errors.full_messages.to_sentence
57-
render Users::Passwords::EditView.new(user: @user, password_reset_token: params[:password_reset_token]), status: :unprocessable_entity
58+
render Users::Passwords::EditView.new(user: @user, password_reset_token: params[:token]), status: :unprocessable_entity
5859
end
5960
end
6061

app/controllers/users/sessions_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def new
1010
end
1111

1212
def create
13-
@user = warden.authenticate!(:password, scope: :user)
13+
@user = warden.authenticate!(scope: :user)
1414

1515
redirect_to login_success_path, notice: "Signed in successfully"
1616
end

app/lib/warden_extensions/setup.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,23 @@ def configure_manager
1111
class_name.constantize.find(id)
1212
end
1313

14-
# Hooks
15-
# Warden::Manager.after_set_user do |user, auth, opts|
16-
# unless user.active?
17-
# auth.logout
18-
# throw(:warden, :message => "User not active")
19-
# end
20-
# end
14+
Warden::Manager.after_authentication do |user, auth, opts|
15+
case opts[:scope]
16+
when :user
17+
user.signed_in!
18+
when :admin_user
19+
# no op
20+
end
21+
22+
case auth.winning_strategy&.key
23+
when :magic_session
24+
user.confirm!
25+
when :password
26+
# no op
27+
else # nil, as with test helpers
28+
# no op
29+
end
30+
end
2131
end
2232
end
2333
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module WardenExtensions::Strategies
2+
class MagicSession < ::Warden::Strategies::Base
3+
def self.key
4+
name.demodulize.underscore.to_sym
5+
end
6+
7+
def key
8+
self.class.key
9+
end
10+
11+
def valid?
12+
!!token
13+
end
14+
15+
def authenticate!
16+
user = scope_class.find_by_token_for(:magic_session, token)
17+
18+
return fail!(:invalid) if !user
19+
20+
success!(user)
21+
end
22+
23+
private
24+
25+
def token
26+
params["token"]
27+
end
28+
29+
def scope_class
30+
return User unless scope
31+
scope.to_s.classify.constantize
32+
end
33+
end
34+
end
35+
36+
::Warden::Strategies.add(:magic_session, WardenExtensions::Strategies::MagicSession)
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
module WardenExtensions
2-
class PasswordStrategy < ::Warden::Strategies::Base
1+
module WardenExtensions::Strategies
2+
class Password < ::Warden::Strategies::Base
3+
def self.key
4+
name.demodulize.underscore.to_sym
5+
end
6+
7+
def key
8+
self.class.key
9+
end
10+
311
def valid?
4-
scoped_params["email"] && scoped_params["password"]
12+
!!(scoped_params["email"] && scoped_params["password"])
513
end
614

715
def authenticate!
@@ -13,14 +21,17 @@ def authenticate!
1321
success!(user)
1422
end
1523

24+
private
25+
1626
def scoped_params
17-
params[scope.to_s] || {}
27+
params[scope.to_s] || params
1828
end
1929

2030
def scope_class
31+
return User unless scope
2132
scope.to_s.classify.constantize
2233
end
2334
end
2435
end
2536

26-
::Warden::Strategies.add(:password, WardenExtensions::PasswordStrategy)
37+
::Warden::Strategies.add(:password, WardenExtensions::Strategies::Password)

app/mailers/application_mailer.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1+
# frozen_string_literal: true
2+
13
class ApplicationMailer < ActionMailer::Base
2-
default from: "[email protected]"
4+
prepend_view_path "app/views/emails"
5+
6+
SUPPORT_EMAIL = "[email protected]"
7+
8+
default from: email_address_with_name(SUPPORT_EMAIL, "Joy of Rails")
39
layout "emails/mailer"
10+
11+
def support_email
12+
email_address_with_name(SUPPORT_EMAIL, "Joy of Rails")
13+
end
14+
15+
helper_method :support_email
416
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Emails::MagicSessionMailer < ApplicationMailer
2+
def sign_in_link(user, magic_session_token)
3+
@user = user
4+
@magic_session_token = magic_session_token
5+
6+
mail to: @user.email, subject: "Your sign-in link"
7+
end
8+
9+
def no_account_found(email)
10+
mail to: email, subject: "No account found", support_email: support_email
11+
end
12+
end

app/mailers/emails/user_mailer.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

33
class Emails::UserMailer < ApplicationMailer
4-
default from: "[email protected]"
5-
64
# Subject can be set in your I18n file at config/locales/en.yml
75
# with the following lookup:
86
#

0 commit comments

Comments
 (0)