Skip to content

2.0.0 #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 64 additions & 108 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/controllers/confirmations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def create
end

def edit
@user = User.find_by(confirmation_token: params[:confirmation_token])
if @user.present? && @user.unconfirmed_or_reconfirming? && @user.confirmation_token_is_valid?
@user = User.find_signed(params[:confirmation_token], purpose: :confirm_email)
if @user.present? && @user.unconfirmed_or_reconfirming?
if @user.confirm!
login @user
redirect_to root_path, notice: "Your account has been confirmed."
Expand Down
13 changes: 5 additions & 8 deletions app/controllers/passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ def create
end

def edit
@user = User.find_by(password_reset_token: params[:password_reset_token])
@user = User.find_signed(params[:password_reset_token], purpose: :reset_password)
if @user.present? && @user.unconfirmed?
redirect_to new_confirmation_path, alert: "You must confirm your email before you can sign in."
elsif @user.nil? || @user.password_reset_token_has_expired?
elsif @user.nil?
redirect_to new_password_path, alert: "Invalid or expired token."
end
end
Expand All @@ -28,21 +28,18 @@ def new
end

def update
@user = User.find_by(password_reset_token: params[:password_reset_token])
@user = User.find_signed(params[:password_reset_token], purpose: :reset_password)
if @user
if @user.unconfirmed?
redirect_to new_confirmation_path, alert: "You must confirm your email before you can sign in."
elsif @user.password_reset_token_has_expired?
redirect_to new_password_path, alert: "Incorrect email or password."
elsif @user.update(password_params)
@user.regenerate_password_reset_token
redirect_to login_path, notice: "Password updated."
redirect_to login_path, notice: "Sign in."
else
flash.now[:alert] = @user.errors.full_messages.to_sentence
render :edit, status: :unprocessable_entity
end
else
flash.now[:alert] = "Incorrect email or password."
flash.now[:alert] = "Invalid or expired token."
render :new, status: :unprocessable_entity
end
end
Expand Down
6 changes: 4 additions & 2 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ class UserMailer < ApplicationMailer
#
# en.user_mailer.confirmation.subject
#
def confirmation(user)
def confirmation(user, confirmation_token)
@user = user
@confirmation_token = confirmation_token

mail to: @user.confirmable_email, subject: "Confirmation Instructions"
end

def password_reset(user)
def password_reset(user, password_reset_token)
@user = user
@password_reset_token = password_reset_token

mail to: @user.email, subject: "Password Reset Instructions"
end
Expand Down
27 changes: 10 additions & 17 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
class User < ApplicationRecord
CONFIRMATION_TOKEN_EXPIRATION_IN_SECONDS = 10.minutes.to_i
CONFIRMATION_TOKEN_EXPIRATION = 10.minutes
MAILER_FROM_EMAIL = "no-reply@example.com"
PASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS = 10.minutes.to_i
PASSWORD_RESET_TOKEN_EXPIRATION = 10.minutes

attr_accessor :current_password

has_secure_password
has_secure_token :confirmation_token
has_secure_token :password_reset_token
has_secure_token :remember_token
has_secure_token :session_token

Expand Down Expand Up @@ -37,7 +35,6 @@ def confirm!
if unconfirmed_email.present?
return false unless update(email: unconfirmed_email, unconfirmed_email: nil)
end
regenerate_confirmation_token
update_columns(confirmed_at: Time.current)
else
false
Expand All @@ -56,26 +53,22 @@ def confirmable_email
end
end

def confirmation_token_is_valid?
return false if confirmation_sent_at.nil?
(Time.current - confirmation_sent_at) <= User::CONFIRMATION_TOKEN_EXPIRATION_IN_SECONDS
def generate_confirmation_token
signed_id expires_in: CONFIRMATION_TOKEN_EXPIRATION, purpose: :confirm_email
end

def password_reset_token_has_expired?
return true if password_reset_sent_at.nil?
(Time.current - password_reset_sent_at) >= User::PASSWORD_RESET_TOKEN_EXPIRATION_IN_SECONDS
def generate_password_reset_token
signed_id expires_in: PASSWORD_RESET_TOKEN_EXPIRATION, purpose: :reset_password
end

def send_confirmation_email!
regenerate_confirmation_token
update_columns(confirmation_sent_at: Time.current)
UserMailer.confirmation(self).deliver_now
confirmation_token = generate_confirmation_token
UserMailer.confirmation(self, confirmation_token).deliver_now
end

def send_password_reset_email!
regenerate_password_reset_token
update_columns(password_reset_sent_at: Time.current)
UserMailer.password_reset(self).deliver_now
password_reset_token = generate_password_reset_token
UserMailer.password_reset(self, password_reset_token).deliver_now
end

def reconfirming?
Expand Down
2 changes: 1 addition & 1 deletion app/views/passwords/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= form_with url: password_path(@user.password_reset_token), scope: :user, method: :put do |form| %>
<%= form_with url: password_path(params[:password_reset_token]), scope: :user, method: :put do |form| %>
<div>
<%= form.label :password %>
<%= form.password_field :password, required: true %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/user_mailer/confirmation.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<h1>Confirmation Instructions</h1>

<%= link_to "Click here to confirm your email.", edit_confirmation_url(@user.confirmation_token) %>
<%= link_to "Click here to confirm your email.", edit_confirmation_url(@confirmation_token) %>
2 changes: 1 addition & 1 deletion app/views/user_mailer/confirmation.text.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Confirmation Instructions

<%= edit_confirmation_url(@user.confirmation_token) %>
<%= edit_confirmation_url(@confirmation_token) %>
2 changes: 1 addition & 1 deletion app/views/user_mailer/password_reset.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<h1>Password Reset Instructions</h1>

<%= link_to "Click here to reset your password.", edit_password_url(@user.password_reset_token) %>
<%= link_to "Click here to reset your password.", edit_password_url(@password_reset_token) %>
2 changes: 1 addition & 1 deletion app/views/user_mailer/password_reset.text.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Password Reset Instructions

<%= edit_password_url(@user.password_reset_token) %>
<%= edit_password_url(@password_reset_token) %>
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
class AddConfirmationAndPasswordColumnsToUsers < ActiveRecord::Migration[6.1]
def change
# This will be used to identify a user in a secure way. We don't ever want it to be empty.
# This value will automatically be set on save.
# https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token
add_column :users, :confirmation_token, :string, null: false
add_column :users, :confirmation_sent_at, :datetime
add_column :users, :confirmed_at, :datetime

# This will store the hashed value of the password. We don't ever want it to be empty.
# https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password
add_column :users, :password_digest, :string, null: false

# This will ensure the confirmation_token is unique.
add_index :users, :confirmation_token, unique: true
end
end

This file was deleted.

6 changes: 0 additions & 6 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 50 additions & 47 deletions test/controllers/confirmations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,49 @@

class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
setup do
@reconfirmed_user = User.create!(email: "reconfirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: 1.week.ago, confirmation_sent_at: 1.week.ago, unconfirmed_email: "unconfirmed_email@example.com")
@confirmed_user = User.create!(email: "confirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: 1.week.ago, confirmation_sent_at: 1.week.ago)
@unconfirmed_user = User.create!(email: "unconfirmed_user@example.com", confirmation_sent_at: Time.current, password: "password", password_confirmation: "password")
@reconfirmed_user = User.create!(email: "reconfirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: 1.week.ago, unconfirmed_email: "unconfirmed_email@example.com")
@confirmed_user = User.create!(email: "confirmed_user@example.com", password: "password", password_confirmation: "password", confirmed_at: 1.week.ago)
@unconfirmed_user = User.create!(email: "unconfirmed_user@example.com", password: "password", password_confirmation: "password")
end

test "should confirm unconfirmed user" do
freeze_time
freeze_time do
confirmation_token = @unconfirmed_user.generate_confirmation_token

assert_changes "@unconfirmed_user.reload.confirmation_token" do
get edit_confirmation_path(@unconfirmed_user.confirmation_token)
end
get edit_confirmation_path(confirmation_token)

assert_equal Time.current, @unconfirmed_user.reload.confirmed_at
assert @unconfirmed_user.reload.confirmed?
assert_redirected_to root_path
assert_not_nil flash[:notice]
assert @unconfirmed_user.reload.confirmed?
assert_equal Time.now, @unconfirmed_user.confirmed_at
assert_redirected_to root_path
assert_not_nil flash[:notice]
end
end

test "should reconfirm confirmed user" do
unconfirmed_email = @reconfirmed_user.unconfirmed_email
freeze_time

@reconfirmed_user.send_confirmation_email!
freeze_time do
confirmation_token = @reconfirmed_user.generate_confirmation_token

get edit_confirmation_path(@reconfirmed_user.confirmation_token)
get edit_confirmation_path(confirmation_token)

assert_equal Time.current, @reconfirmed_user.reload.confirmed_at
assert @reconfirmed_user.reload.confirmed?
assert_equal unconfirmed_email, @reconfirmed_user.reload.email
assert_nil @reconfirmed_user.reload.unconfirmed_email
assert_redirected_to root_path
assert_not_nil flash[:notice]
assert @reconfirmed_user.reload.confirmed?
assert_equal Time.current, @reconfirmed_user.reload.confirmed_at
assert_equal unconfirmed_email, @reconfirmed_user.reload.email
assert_nil @reconfirmed_user.reload.unconfirmed_email
assert_redirected_to root_path
assert_not_nil flash[:notice]
end
end

test "should not update email address if already taken" do
original_email = @reconfirmed_user.email
@reconfirmed_user.update(unconfirmed_email: @confirmed_user.email)

freeze_time do
@reconfirmed_user.send_confirmation_email!
confirmation_token = @reconfirmed_user.generate_confirmation_token

get edit_confirmation_path(@reconfirmed_user.confirmation_token)
get edit_confirmation_path(confirmation_token)

assert_equal original_email, @reconfirmed_user.reload.email
assert_redirected_to new_confirmation_path
Expand All @@ -52,14 +53,16 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
end

test "should redirect if confirmation link expired" do
travel_to 601.seconds.from_now
confirmation_token = @unconfirmed_user.generate_confirmation_token

get edit_confirmation_path(@unconfirmed_user.confirmation_token)
travel_to 601.seconds.from_now do
get edit_confirmation_path(confirmation_token)

assert_nil @unconfirmed_user.reload.confirmed_at
assert_not @unconfirmed_user.reload.confirmed?
assert_redirected_to new_confirmation_path
assert_not_nil flash[:alert]
assert_nil @unconfirmed_user.reload.confirmed_at
assert_not @unconfirmed_user.reload.confirmed?
assert_redirected_to new_confirmation_path
assert_not_nil flash[:alert]
end
end

test "should redirect if confirmation link is incorrect" do
Expand Down Expand Up @@ -91,39 +94,39 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
end

test "should prevent authenticated user from confirming" do
freeze_time
freeze_time do
confirmation_token = @confirmed_user.generate_confirmation_token

@reconfirmed_user.send_confirmation_email!
login @confirmed_user
login @confirmed_user

get edit_confirmation_path(@confirmed_user.confirmation_token)
get edit_confirmation_path(confirmation_token)

assert_not_equal Time.current, @confirmed_user.reload.confirmed_at
assert_redirected_to new_confirmation_path
assert_not_nil flash[:alert]
assert_not_equal Time.current, @confirmed_user.reload.confirmed_at
assert_redirected_to new_confirmation_path
assert_not_nil flash[:alert]
end
end

test "should not prevent authenticated user confirming their unconfirmed_email" do
unconfirmed_email = @reconfirmed_user.unconfirmed_email
freeze_time

login(@reconfirmed_user)
freeze_time do
login @reconfirmed_user

@reconfirmed_user.send_confirmation_email!
confirmation_token = @reconfirmed_user.generate_confirmation_token

get edit_confirmation_path(@reconfirmed_user.confirmation_token)
get edit_confirmation_path(confirmation_token)

assert_equal Time.current, @reconfirmed_user.reload.confirmed_at
assert @reconfirmed_user.reload.confirmed?
assert_equal unconfirmed_email, @reconfirmed_user.reload.email
assert_nil @reconfirmed_user.reload.unconfirmed_email
assert_redirected_to root_path
assert_not_nil flash[:notice]
assert_equal Time.current, @reconfirmed_user.reload.confirmed_at
assert @reconfirmed_user.reload.confirmed?
assert_equal unconfirmed_email, @reconfirmed_user.reload.email
assert_nil @reconfirmed_user.reload.unconfirmed_email
assert_redirected_to root_path
assert_not_nil flash[:notice]
end
end

test "should prevent authenticated user from submitting the confirmation form" do
freeze_time

login @confirmed_user

get new_confirmation_path
Expand Down
Loading