Skip to content

Commit 35d0f70

Browse files
authored
Merge pull request #149 from joyofrails/feat/auth
Add user authentication from scratch
2 parents ae8a320 + 0b48dfb commit 35d0f70

File tree

67 files changed

+1885
-32
lines changed

Some content is hidden

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

67 files changed

+1885
-32
lines changed

app/controllers/concerns/authentication.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,54 @@ module Authentication
44

55
included do
66
before_action :current_admin_user
7+
before_action :current_user
8+
79
helper_method :current_admin_user
10+
helper_method :current_user
11+
helper_method :user_signed_in?
812
helper_method :admin_user_signed_in?
913
end
1014

1115
def warden
1216
request.env["warden"]
1317
end
1418

19+
def authenticate_user!
20+
store_location
21+
redirect_to new_users_session_path, alert: "You need to sign in to access that page" unless user_signed_in?
22+
end
23+
1524
def redirect_admin_if_authenticated
1625
redirect_to admin_root_path, alert: "You are already logged in." if admin_user_signed_in?
1726
end
1827

28+
def redirect_if_authenticated
29+
redirect_back fallback_location: login_success_path, alert: "You are already logged in." if user_signed_in?
30+
end
31+
1932
protected
2033

34+
def login_success_path
35+
session.delete(:user_return_to) || users_dashboard_path
36+
end
37+
38+
def current_user
39+
Current.user ||= warden.user(scope: :user)
40+
end
41+
2142
def current_admin_user
2243
Current.admin_user ||= warden.user(scope: :admin_user)
2344
end
2445

46+
def user_signed_in?
47+
current_user.present?
48+
end
49+
2550
def admin_user_signed_in?
26-
Current.admin_user.present?
51+
current_admin_user.present?
52+
end
53+
54+
def store_location
55+
session[:user_return_to] = request.original_url if request.get? && request.local?
2756
end
2857
end

app/controllers/site_controller.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ def process_rendition(rendition)
2929
# @param rendition [Sitepress::Rendition] Rendered representatio of current_resource
3030
#
3131
def post_render(rendition)
32-
if stale?(rendition.source, last_modified: current_resource.asset.updated_at.utc, public: true)
32+
if current_path?(root_path) || stale?(rendition.source, last_modified: current_resource.asset.updated_at.utc, public: true)
3333
render body: rendition.output, content_type: rendition.mime_type
3434
end
3535
end
36+
37+
def current_path?(path)
38+
request.path == path
39+
end
3640
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
class Users::ConfirmationsController < ApplicationController
4+
before_action :feature_enabled!
5+
before_action :redirect_if_authenticated, only: [:create, :new]
6+
7+
def new
8+
@user = User.new
9+
render Users::Confirmations::NewView.new(user: @user)
10+
end
11+
12+
def create
13+
@user = User.find_by(email: params.require(:user).permit(:email).dig(:email).to_s.downcase)
14+
15+
if @user.blank?
16+
return redirect_to new_users_confirmation_path, alert: "We are unable to confirm that email address"
17+
end
18+
19+
if !@user.needs_confirmation?
20+
return redirect_to root_path, notice: "Your account has already been confirmed"
21+
end
22+
23+
EmailConfirmationNotifier.deliver_to(@user)
24+
25+
redirect_to root_path, notice: "Check your email for confirmation instructions"
26+
end
27+
28+
def edit
29+
@user = User.find_by_token_for(:confirmation, params[:confirmation_token])
30+
31+
if @user.blank?
32+
return redirect_to new_users_confirmation_path, alert: "This link is invalid or expired"
33+
end
34+
35+
if !@user.needs_confirmation?
36+
return redirect_to root_path, notice: "Your account has already been confirmed"
37+
end
38+
39+
render Users::Confirmations::EditView.new(user: @user, confirmation_token: params[:confirmation_token])
40+
end
41+
42+
def update
43+
@user = User.find_by_token_for(:confirmation, params[:confirmation_token])
44+
45+
if @user.blank?
46+
return redirect_to new_users_confirmation_path, alert: "That link is invalid or expired"
47+
end
48+
if !@user.needs_confirmation?
49+
return redirect_to root_path, notice: "Your account has already been confirmed"
50+
end
51+
52+
if @user.confirm!
53+
warden.set_user(@user, scope: :user)
54+
55+
redirect_to users_dashboard_path, notice: "Thank you for confirming your email address"
56+
else
57+
redirect_to new_users_confirmation_path, alert: "Something went wrong"
58+
end
59+
end
60+
61+
private
62+
63+
def feature_enabled!
64+
redirect_to root_path, notice: "Coming soon!" unless Flipper.enabled?(:user_registration)
65+
end
66+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Users::DashboardController < ApplicationController
2+
before_action :authenticate_user!
3+
4+
def index
5+
render Users::Dashboard::IndexView.new
6+
end
7+
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
class Users::PasswordsController < ApplicationController
4+
before_action :feature_enabled!
5+
before_action :redirect_if_authenticated
6+
7+
def new
8+
render Users::Passwords::NewView.new(user: User.new)
9+
end
10+
11+
def create
12+
@user = User.find_by(email: params.require(:user).permit(:email).dig(:email).to_s.downcase)
13+
14+
if @user.blank?
15+
return redirect_to new_users_session_path, notice: "If that user exists we’ve sent instructions to their email"
16+
end
17+
18+
if @user.unconfirmed?
19+
return redirect_to new_users_confirmation_path, alert: "Please confirm your email address first"
20+
end
21+
22+
PasswordResetNotifier.deliver_to(@user)
23+
24+
redirect_to new_users_session_path, notice: "If that user exists we’ve sent instructions to their email"
25+
end
26+
27+
def edit
28+
@user = User.find_by_token_for(:password_reset, params[:password_reset_token])
29+
30+
if @user.blank?
31+
return redirect_to new_users_password_path, alert: "That link is invalid or expired"
32+
end
33+
34+
if @user.unconfirmed?
35+
return redirect_to new_users_confirmation_path, alert: "You must confirm your email before you can sign in"
36+
end
37+
38+
render Users::Passwords::EditView.new(user: @user, password_reset_token: params[:password_reset_token])
39+
end
40+
41+
def update
42+
@user = User.find_by_token_for(:password_reset, params[:password_reset_token])
43+
44+
if @user.blank?
45+
flash.now[:alert] = "That link is invalid or expired"
46+
return render Users::Passwords::NewView.new(user: User.new), status: :unprocessable_entity
47+
end
48+
49+
if @user.unconfirmed?
50+
return redirect_to new_users_confirmation_path, alert: "Please confirm your email before you can sign in"
51+
end
52+
53+
if @user.update(password_params)
54+
redirect_to new_users_session_path, notice: "Password updated! Please sign in"
55+
else
56+
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+
end
59+
end
60+
61+
private
62+
63+
def password_params
64+
params.require(:user).permit(:password, :password_confirmation)
65+
end
66+
67+
def feature_enabled!
68+
redirect_to root_path, notice: "Coming soon!" unless Flipper.enabled?(:user_registration)
69+
end
70+
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
class Users::RegistrationsController < ApplicationController
4+
before_action :feature_enabled!
5+
before_action :redirect_if_authenticated, only: [:create, :new]
6+
before_action :authenticate_user!, only: [:edit, :update, :destroy]
7+
8+
def new
9+
@user = User.new
10+
render Users::Registrations::NewView.new(user: @user)
11+
end
12+
13+
def create
14+
create_user_params = params.require(:user).permit(:email, :password, :password_confirmation)
15+
@user = User.new(create_user_params)
16+
if @user.save
17+
EmailConfirmationNotifier.deliver_to(@user)
18+
redirect_to root_path, notice: "Welcome to Joy of Rails! Please check your email for confirmation instructions"
19+
else
20+
render Users::Registrations::NewView.new(user: @user), status: :unprocessable_entity
21+
end
22+
end
23+
24+
def edit
25+
@user = current_user
26+
@user.email_exchanges.build
27+
28+
render Users::Registrations::EditView.new(user: @user)
29+
end
30+
31+
def update
32+
update_user_params = params.require(:user).permit(:password_challenge, :password, :password_confirmation, email_exchanges_attributes: [:email])
33+
34+
@user = current_user
35+
36+
if !@user.authenticate(params[:user][:password_challenge])
37+
flash.now[:error] = "Incorrect password"
38+
return render Users::Registrations::EditView.new(user: @user), status: :unprocessable_entity
39+
end
40+
41+
if !@user.update(update_user_params)
42+
return render Users::Registrations::EditView.new(user: @user), status: :unprocessable_entity
43+
end
44+
45+
if update_user_params[:email_exchanges_attributes].present?
46+
EmailConfirmationNotifier.deliver_to(@user)
47+
redirect_to users_dashboard_path, notice: "Check your email for confirmation instructions"
48+
else
49+
redirect_to users_dashboard_path, notice: "Account updated"
50+
end
51+
end
52+
53+
def destroy
54+
current_user.destroy
55+
reset_session
56+
redirect_to root_path, notice: "Your account has been deleted"
57+
end
58+
59+
private
60+
61+
def feature_enabled!
62+
redirect_to root_path, notice: "Coming soon!" unless Flipper.enabled?(:user_registration)
63+
end
64+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
class Users::SessionsController < ApplicationController
4+
before_action :feature_enabled!
5+
before_action :redirect_if_authenticated, only: [:create, :new]
6+
before_action :authenticate_user!, only: [:destroy]
7+
8+
def new
9+
render Users::Sessions::NewView.new
10+
end
11+
12+
def create
13+
@user = warden.authenticate!(:password, scope: :user)
14+
15+
redirect_to login_success_path, notice: "Signed in successfully"
16+
end
17+
18+
def destroy
19+
warden.logout(:user)
20+
warden.clear_strategies_cache!(scope: :user)
21+
22+
redirect_to root_path, notice: "Signed out successfully"
23+
end
24+
25+
# This method is called when Warden authentication fails
26+
def fail
27+
warden_options = request.env["warden.options"] || {}
28+
warden_message = warden_options[:message]
29+
30+
message = case warden_message
31+
when :invalid
32+
"Incorrect email or password"
33+
when :unconfirmed
34+
"Please confirm your email address first"
35+
end
36+
37+
flash.now[:alert] = message
38+
39+
email = params.require(:user).permit(:email)[:email].to_s.downcase
40+
user = User.new(email: email)
41+
42+
render Users::Sessions::NewView.new(user: user), status: :unprocessable_entity
43+
end
44+
45+
private
46+
47+
def feature_enabled!
48+
redirect_to root_path, notice: "Coming soon!" unless Flipper[:user_registration].enabled?
49+
end
50+
end

app/jobs/notifications/event_job.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class Notifications::EventJob < ApplicationJob
2+
queue_as :default
3+
4+
def perform(event)
5+
# Enqueue individual deliveries
6+
event.notifications.each do |notification|
7+
event.deliver_notification(notification)
8+
end
9+
end
10+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Routes
2+
class UsersAccessConstraint
3+
def matches?(request)
4+
request.env["warden"].authenticate?(scope: :user)
5+
end
6+
end
7+
end

app/lib/warden_extensions/password_strategy.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ def valid?
55
end
66

77
def authenticate!
8-
user = scope_class.authenticate(email: scoped_params["email"], password: scoped_params["password"])
9-
user ? success!(user) : fail!(:invalid)
8+
user = scope_class.authenticate_by(email: scoped_params["email"].to_s.downcase, password: scoped_params["password"])
9+
10+
return fail!(:invalid) if !user
11+
return fail!(:unconfirmed) if user.needs_confirmation?
12+
13+
success!(user)
1014
end
1115

1216
def scoped_params

0 commit comments

Comments
 (0)