Email confirmation workflow
This commit is contained in:
parent
c57fe587c2
commit
107c2e62a8
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EmailConfirmationsController < ApplicationController
|
||||||
|
def confirm
|
||||||
|
@user = User.find_by(email: params[:email])
|
||||||
|
if params[:confirmation_string] == @user.email_confirmation_string
|
||||||
|
@user.update(email_confirmation_string: nil)
|
||||||
|
redirect_to new_session_path, notice: t(".email_confirmed")
|
||||||
|
else
|
||||||
|
redirect_to root_path, notice: t(".email_confirmation_failed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SessionsController < ApplicationController
|
class SessionsController < ApplicationController
|
||||||
|
before_action :set_user, only: [:create]
|
||||||
|
before_action :ensure_email_confirmed, only: [:create]
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@user = User.find_by(username: params[:username])
|
|
||||||
|
|
||||||
if @user.present? && @user.authenticate(params[:password])
|
if @user.present? && @user.authenticate(params[:password])
|
||||||
session[:user_id] = @user.id
|
session[:user_id] = @user.id
|
||||||
redirect_to @user, notice: t(".logged_in")
|
redirect_to @user, notice: t(".logged_in")
|
||||||
|
@ -19,4 +20,17 @@ class SessionsController < ApplicationController
|
||||||
reset_session
|
reset_session
|
||||||
redirect_to root_path, notice: t(".logged_out")
|
redirect_to root_path, notice: t(".logged_out")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
@user = User.find_by(username: params[:username])
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_email_confirmed
|
||||||
|
return unless @user.requires_confirmation?
|
||||||
|
|
||||||
|
flash.alert = t(".account_not_confirmed")
|
||||||
|
redirect_to new_session_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,6 @@ class UsersController < ApplicationController
|
||||||
def create
|
def create
|
||||||
@user = User.new(user_params)
|
@user = User.new(user_params)
|
||||||
if @user.save
|
if @user.save
|
||||||
# TODO: Add email confirmation workflow
|
|
||||||
session[:user_id] = @user.id
|
session[:user_id] = @user.id
|
||||||
redirect_to root_path, notice: t(".account_created")
|
redirect_to root_path, notice: t(".account_created")
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: "from@example.com"
|
default from: "hello@summonplayer.com"
|
||||||
layout "mailer"
|
layout "mailer"
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserMailer < ApplicationMailer
|
||||||
|
def confirm_email
|
||||||
|
@user = params[:user]
|
||||||
|
mail(to: @user.email, subject: t(".confirm_email"))
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "securerandom"
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
has_secure_password
|
has_secure_password
|
||||||
|
|
||||||
|
@ -26,6 +28,8 @@ class User < ApplicationRecord
|
||||||
presence: true,
|
presence: true,
|
||||||
if: :validate_password?
|
if: :validate_password?
|
||||||
|
|
||||||
|
after_create :require_confirmation
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
username
|
username
|
||||||
end
|
end
|
||||||
|
@ -34,8 +38,17 @@ class User < ApplicationRecord
|
||||||
"#{first_name} #{last_names}"
|
"#{first_name} #{last_names}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requires_confirmation?
|
||||||
|
email_confirmation_string.present?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def require_confirmation
|
||||||
|
update(email_confirmation_string: SecureRandom.uuid)
|
||||||
|
UserMailer.with(user: self).confirm_email.deliver_later
|
||||||
|
end
|
||||||
|
|
||||||
def validate_password?
|
def validate_password?
|
||||||
new_record? || password_digest_changed?
|
new_record? || password_digest_changed?
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
Please visit the following link to confirm your email and access your new
|
||||||
|
account at Summon Player:
|
||||||
|
|
||||||
|
<%= confirm_email_url(email: @user.email, confirmation_string: @user.email_confirmation_string) %>
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
|
||||||
|
The Summon Player team.
|
|
@ -3,20 +3,9 @@
|
||||||
require "active_support/core_ext/integer/time"
|
require "active_support/core_ext/integer/time"
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
|
||||||
|
|
||||||
# In the development environment your application's code is reloaded any time
|
|
||||||
# it changes. This slows down response time but is perfect for development
|
|
||||||
# since you don't have to restart the web server when you make code changes.
|
|
||||||
config.cache_classes = false
|
config.cache_classes = false
|
||||||
|
|
||||||
# Do not eager load code on boot.
|
|
||||||
config.eager_load = false
|
config.eager_load = false
|
||||||
|
|
||||||
# Show full error reports.
|
|
||||||
config.consider_all_requests_local = true
|
config.consider_all_requests_local = true
|
||||||
|
|
||||||
# Enable server timing
|
|
||||||
config.server_timing = true
|
config.server_timing = true
|
||||||
|
|
||||||
# Enable/disable caching. By default caching is disabled.
|
# Enable/disable caching. By default caching is disabled.
|
||||||
|
@ -27,7 +16,7 @@ Rails.application.configure do
|
||||||
|
|
||||||
config.cache_store = :memory_store
|
config.cache_store = :memory_store
|
||||||
config.public_file_server.headers = {
|
config.public_file_server.headers = {
|
||||||
"Cache-Control" => "public, max-age=#{2.days.to_i}"
|
"Cache-Control" => "public, max-age=#{2.days.to_i}",
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
config.action_controller.perform_caching = false
|
config.action_controller.perform_caching = false
|
||||||
|
@ -35,38 +24,30 @@ Rails.application.configure do
|
||||||
config.cache_store = :null_store
|
config.cache_store = :null_store
|
||||||
end
|
end
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
|
||||||
config.active_storage.service = :local
|
config.active_storage.service = :local
|
||||||
|
|
||||||
# Don't care if the mailer can't send.
|
config.action_mailer.raise_delivery_errors = true
|
||||||
config.action_mailer.raise_delivery_errors = false
|
|
||||||
|
|
||||||
config.action_mailer.perform_caching = false
|
config.action_mailer.perform_caching = false
|
||||||
|
|
||||||
# Print deprecation notices to the Rails logger.
|
|
||||||
config.active_support.deprecation = :log
|
config.active_support.deprecation = :log
|
||||||
|
|
||||||
# Raise exceptions for disallowed deprecations.
|
|
||||||
config.active_support.disallowed_deprecation = :raise
|
config.active_support.disallowed_deprecation = :raise
|
||||||
|
|
||||||
# Tell Active Support which deprecation messages to disallow.
|
|
||||||
config.active_support.disallowed_deprecation_warnings = []
|
config.active_support.disallowed_deprecation_warnings = []
|
||||||
|
|
||||||
# Raise an error on page load if there are pending migrations.
|
|
||||||
config.active_record.migration_error = :page_load
|
config.active_record.migration_error = :page_load
|
||||||
|
|
||||||
# Highlight code that triggered database queries in logs.
|
|
||||||
config.active_record.verbose_query_logs = true
|
config.active_record.verbose_query_logs = true
|
||||||
|
|
||||||
# Suppress logger output for asset requests.
|
|
||||||
config.assets.quiet = true
|
config.assets.quiet = true
|
||||||
|
|
||||||
# Raises error for missing translations.
|
# Raises error for missing translations.
|
||||||
config.i18n.raise_on_missing_translations = true
|
config.i18n.raise_on_missing_translations = true
|
||||||
|
|
||||||
# Annotate rendered view with file names.
|
# Annotate rendered view with file names.
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
# Uncomment if you wish to allow Action Cable access from any origin.
|
# Use Mailhog
|
||||||
# config.action_cable.disable_request_forgery_protection = true
|
config.action_mailer.perform_deliveries = true
|
||||||
|
config.action_mailer.smtp_settings = {
|
||||||
|
address: "localhost",
|
||||||
|
port: 1025,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
en:
|
||||||
|
email_confirmations:
|
||||||
|
email_confirmed: Thanks, your email has been confirmed.
|
||||||
|
email_confirmation_failed: Sorry, we could not confirm your email.
|
|
@ -3,3 +3,4 @@ en:
|
||||||
logged_in: You’ve been logged in.
|
logged_in: You’ve been logged in.
|
||||||
login_fail: Sorry, we couldn’t log you in.
|
login_fail: Sorry, we couldn’t log you in.
|
||||||
logged_out: You have been logged out.
|
logged_out: You have been logged out.
|
||||||
|
account_not_confirmed: Please confirm your email to log in.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
en:
|
en:
|
||||||
users:
|
users:
|
||||||
create_failed: Could not create user account, please correct the errors below.
|
create_failed: Could not create user account, please correct the errors below.
|
||||||
account_created: Account created!
|
account_created: Account created! Please check your email to confirm.
|
||||||
account_updated: Your details have been updated.
|
account_updated: Your details have been updated.
|
||||||
update_failed: Could not update your details.
|
update_failed: Could not update your details.
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
en:
|
||||||
|
user_mailer:
|
||||||
|
confirm_email: Please confirm your email.
|
|
@ -1,9 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
|
default_url_options :host => "summonplayer.com"
|
||||||
|
|
||||||
root "sessions#new"
|
root "sessions#new"
|
||||||
|
|
||||||
resources :users, only: [:new, :create, :show, :edit, :update]
|
resources :users, only: [:new, :create, :show, :edit, :update]
|
||||||
resources :sessions, only: [:new, :create]
|
resources :sessions, only: [:new, :create]
|
||||||
delete "log_out", to: "sessions#destroy_session"
|
delete "log_out", to: "sessions#destroy_session"
|
||||||
|
get "confirm_email", to: "email_confirmations#confirm"
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddEmailConfirmationStringToUser < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :email_confirmation_string, :uuid, default: nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_08_08_190158) do
|
ActiveRecord::Schema[7.0].define(version: 2023_08_18_201128) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_08_190158) do
|
||||||
t.string "email", null: false
|
t.string "email", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.uuid "email_confirmation_string"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["username"], name: "index_users_on_username", unique: true
|
t.index ["username"], name: "index_users_on_username", unique: true
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,4 +25,8 @@
|
||||||
CREATE ROLE summon_player WITH LOGIN PASSWORD 'postgres' SUPERUSER;
|
CREATE ROLE summon_player WITH LOGIN PASSWORD 'postgres' SUPERUSER;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.mailhog = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class EmailConfirmationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
test "should confirm user account" do
|
||||||
|
user = users(:unconfirmed_user)
|
||||||
|
assert user.requires_confirmation?
|
||||||
|
get confirm_email_url(
|
||||||
|
email: user.email,
|
||||||
|
confirmation_string: user.email_confirmation_string,
|
||||||
|
)
|
||||||
|
follow_redirect!
|
||||||
|
assert_includes @response.body, I18n.t("email_confirmations.email_confirmed")
|
||||||
|
user.reload
|
||||||
|
assert_not user.requires_confirmation?
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,3 +4,12 @@ user:
|
||||||
first_name: Gimli
|
first_name: Gimli
|
||||||
last_names: son of Glóin
|
last_names: son of Glóin
|
||||||
email: gimli@example.com
|
email: gimli@example.com
|
||||||
|
|
||||||
|
unconfirmed_user:
|
||||||
|
username: pippin
|
||||||
|
password_digest: <%= BCrypt::Password.create('tolkien-abercrombie-hobb-barker', cost: 5) %>
|
||||||
|
first_name: Peregrin
|
||||||
|
last_names: Took
|
||||||
|
email: pippin@example.com
|
||||||
|
email_confirmation_string: 5a68514b-7cd2-4ac8-bec1-22c2526f9a52
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class UserTest < ActiveSupport::TestCase
|
class UserTest < ActiveSupport::TestCase
|
||||||
|
include ActionMailer::TestHelper
|
||||||
|
|
||||||
test "email must resemble an email" do
|
test "email must resemble an email" do
|
||||||
user = users(:user)
|
user = users(:user)
|
||||||
user.email = "foobar"
|
user.email = "foobar"
|
||||||
assert_not user.valid?
|
assert_not user.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "a new user requires confirmation" do
|
||||||
|
user = User.create(
|
||||||
|
username: "gorkamorka",
|
||||||
|
email: "gorka@morka.com",
|
||||||
|
first_name: "Ork",
|
||||||
|
last_names: "Boyz",
|
||||||
|
password: "snotlings-are-for-squashing",
|
||||||
|
password_confirmation: "snotlings-are-for-squashing",
|
||||||
|
)
|
||||||
|
assert user.requires_confirmation?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a new user requests confirmation" do
|
||||||
|
assert_emails 1 do
|
||||||
|
User.create(
|
||||||
|
username: "gorkamorka",
|
||||||
|
email: "gorka@morka.com",
|
||||||
|
first_name: "Ork",
|
||||||
|
last_names: "Boyz",
|
||||||
|
password: "snotlings-are-for-squashing",
|
||||||
|
password_confirmation: "snotlings-are-for-squashing",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,17 +3,23 @@
|
||||||
require "application_system_test_case"
|
require "application_system_test_case"
|
||||||
|
|
||||||
class SessionsTest < ApplicationSystemTestCase
|
class SessionsTest < ApplicationSystemTestCase
|
||||||
setup do
|
|
||||||
@user = users(:user)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "can log in and log out existing user" do
|
test "can log in and log out existing user" do
|
||||||
|
user = users(:user)
|
||||||
visit new_session_path
|
visit new_session_path
|
||||||
fill_in "username", with: @user.username
|
fill_in "username", with: user.username
|
||||||
fill_in "password", with: "tolkien-abercrombie-hobb-barker"
|
fill_in "password", with: "tolkien-abercrombie-hobb-barker"
|
||||||
click_button I18n.t("log_in")
|
click_button I18n.t("log_in")
|
||||||
assert_text I18n.t("sessions.logged_in")
|
assert_text I18n.t("sessions.logged_in")
|
||||||
click_link I18n.t("log_out")
|
click_link I18n.t("log_out")
|
||||||
assert_text I18n.t("sessions.logged_out")
|
assert_text I18n.t("sessions.logged_out")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "cannot log in as an unconfirmed user" do
|
||||||
|
user = users(:unconfirmed_user)
|
||||||
|
visit new_session_path
|
||||||
|
fill_in "username", with: user.username
|
||||||
|
fill_in "password", with: "tolkien-abercrombie-hobb-barker"
|
||||||
|
click_button I18n.t("log_in")
|
||||||
|
assert_text I18n.t("sessions.account_not_confirmed")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "application_system_test_case"
|
require "application_system_test_case"
|
||||||
|
|
||||||
class RegisterUsersTest < ApplicationSystemTestCase
|
class RegisterUsersTest < ApplicationSystemTestCase
|
||||||
|
|
Loading…
Reference in New Issue