Commit ab0d9395 by Son Do Hong

Merge branch 'account-activation' into 'master'

Account activation

See merge request !10
parents 2d33396b 7c9768df
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
// Place all the styles related to the AccountActivations controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.activate
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
...@@ -6,10 +6,17 @@ class SessionsController < ApplicationController ...@@ -6,10 +6,17 @@ class SessionsController < ApplicationController
def create def create
@user = User.find_by(email: params[:session][:email].downcase) @user = User.find_by(email: params[:session][:email].downcase)
if @user && @user.authenticate(params[:session][:password]) if @user && @user.authenticate(params[:session][:password])
if @user.activated?
log_in @user log_in @user
params[:session][:remember_me] == "1" ? remember(@user) : forget(@user) params[:session][:remember_me] == "1" ? remember(@user) : forget(@user)
redirect_back_or @user redirect_back_or @user
else else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = "Invalid email/password combination" flash.now[:danger] = "Invalid email/password combination"
render "new" render "new"
end end
......
...@@ -4,11 +4,12 @@ class UsersController < ApplicationController ...@@ -4,11 +4,12 @@ class UsersController < ApplicationController
before_action :admin_user, only: :destroy before_action :admin_user, only: :destroy
def index def index
@users = User.paginate(page: params[:page]) @users = User.where(activated: true).paginate(page: params[:page])
end end
def show def show
@user = User.find(params[:id]) @user = User.find(params[:id])
redirect_to root_url and return unless @user.activated
end end
def new def new
...@@ -18,9 +19,9 @@ class UsersController < ApplicationController ...@@ -18,9 +19,9 @@ class UsersController < ApplicationController
def create def create
@user = User.new(user_params) @user = User.new(user_params)
if @user.save if @user.save
log_in @user @user.send_activation_email
flash[:success] = "Welcome to the Sample App!" flash[:info] = "Please check your email to activate your account."
redirect_to @user redirect_to root_url
else else
render "new" render "new"
end end
......
...@@ -22,7 +22,7 @@ module SessionsHelper ...@@ -22,7 +22,7 @@ module SessionsHelper
@current_user ||= User.find_by(id: user_id) @current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id]) elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token]) if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user log_in user
@current_user = user @current_user = user
end end
......
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: "from@example.com" default from: "noreply@example.com"
layout "mailer" layout "mailer"
end end
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end
class User < ApplicationRecord class User < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
attr_accessor :remember_token attr_accessor :remember_token, :activation_token
validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX } validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
validates :name, presence: true, length: { maximum: 50 }, uniqueness: { case_sensitive: false } validates :name, presence: true, length: { maximum: 50 }
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
before_save { email.downcase! } #Call Backs before_save { email.downcase! } #Call Backs
before_save :downcase_email
before_create :create_activation_digest
has_secure_password has_secure_password
...@@ -40,5 +42,34 @@ class User < ApplicationRecord ...@@ -40,5 +42,34 @@ class User < ApplicationRecord
def forget def forget
update_attribute(:remember_digest, nil) update_attribute(:remember_digest, nil)
end end
end
# Returns true if the given token matches the digest.
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# Activates an account.
def activate
update_columns(activated: true, activated_at: Time.current)
end
# Sends activation email.
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
private
# Converts email to all lower-case.
def downcase_email
self.email = email.downcase
end
# Creates and assigns the activation token and digest.
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
<h1>Sample App</h1>
<p>Hi <%= @user.name %>,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %>
Hi <%= @user.name %>,
Welcome to the Sample App! Click on the link below to activate your account:
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
<h1>User#password_reset</h1>
<p>
<%= @greeting %>, find me in app/views/user_mailer/password_reset.html.erb
</p>
User#password_reset
<%= @greeting %>, find me in app/views/user_mailer/password_reset.text.erb
...@@ -12,23 +12,26 @@ Rails.application.configure do ...@@ -12,23 +12,26 @@ Rails.application.configure do
# Show full error reports. # Show full error reports.
config.consider_all_requests_local = true config.consider_all_requests_local = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = "localhost:3000" # Don"t use this literally; use your local dev host instead
# Use this on the cloud IDE.
config.action_mailer.default_url_options = { host: host, protocol: "http" }
# Use this if developing on localhost.
# config.action_mailer.default_url_options = { host: host, protocol: "http" }
# Enable/disable caching. By default caching is disabled. # Enable/disable caching. By default caching is disabled.
if Rails.root.join("tmp/caching-dev.txt").exist? if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
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.seconds.to_i}" }
"Cache-Control" => "public, max-age=#{2.days.seconds.to_i}"
}
else else
config.action_controller.perform_caching = false config.action_controller.perform_caching = false
config.cache_store = :null_store config.cache_store = :null_store
end end
# Don"t care if the mailer can"t send. # Don"t care if the mailer can"t send.
config.action_mailer.raise_delivery_errors = false 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. # Print deprecation notices to the Rails logger.
......
...@@ -33,6 +33,7 @@ Rails.application.configure do ...@@ -33,6 +33,7 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the # The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array. # ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: "example.com" }
# Print deprecation notices to the stderr. # Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr config.active_support.deprecation = :stderr
......
...@@ -8,4 +8,5 @@ Rails.application.routes.draw do ...@@ -8,4 +8,5 @@ Rails.application.routes.draw do
post "/login", to: "sessions#create" post "/login", to: "sessions#create"
delete "/logout", to: "sessions#destroy" delete "/logout", to: "sessions#destroy"
resources :users resources :users
resources :account_activations, only: [:edit]
end end
class AddActivationToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :activation_digest, :string
add_column :users, :activated, :boolean, default: false
add_column :users, :activated_at, :datetime
end
end
...@@ -10,7 +10,7 @@ ...@@ -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.define(version: 20191115085103) do ActiveRecord::Schema.define(version: 20191119063621) do
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "name" t.string "name"
...@@ -19,7 +19,10 @@ ActiveRecord::Schema.define(version: 20191115085103) do ...@@ -19,7 +19,10 @@ ActiveRecord::Schema.define(version: 20191115085103) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "password_digest" t.string "password_digest"
t.string "remember_digest" t.string "remember_digest"
t.boolean "admin" t.boolean "admin", default: false
t.string "activation_digest"
t.boolean "activated", default: false
t.datetime "activated_at"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
end end
......
User.create!(name: "Example User", User.create!(name: "Example User",
email: "example@railstutorial.org", email: "example@railstutorial.org",
password: "foobar", password: "foobar",
password_confirmation: "foobar") password_confirmation: "foobar",
admin: true,
activated: true,
activated_at: Time.current)
99.times do |n| 99.times do |n|
name = Faker::Name.name name = Faker::Name.name
...@@ -10,5 +13,7 @@ User.create!(name: "Example User", ...@@ -10,5 +13,7 @@ User.create!(name: "Example User",
User.create!(name: name, User.create!(name: name,
email: email, email: email,
password: password, password: password,
password_confirmation: password) password_confirmation: password,
activated: true,
activated_at: Time.current)
end end
require "test_helper"
class AccountActivationsControllerTest < ActionDispatch::IntegrationTest
end
...@@ -3,25 +3,35 @@ michael: ...@@ -3,25 +3,35 @@ michael:
email: michael@example.com email: michael@example.com
password_digest: <%= User.digest("password") %> password_digest: <%= User.digest("password") %>
admin: true admin: true
activated: true
activated_at: <%= Time.current %>
archer: archer:
name: Sterling Archer name: Sterling Archer
email: duchess@example.gov email: duchess@example.gov
password_digest: <%= User.digest("password") %> password_digest: <%= User.digest("password") %>
activated: true
activated_at: <%= Time.current %>
lana: lana:
name: Lana Kane name: Lana Kane
email: hands@example.gov email: hands@example.gov
password_digest: <%= User.digest('password') %> password_digest: <%= User.digest("password") %>
activated: true
activated_at: <%= Time.current %>
malory: malory:
name: Malory Archer name: Malory Archer
email: boss@example.gov email: boss@example.gov
password_digest: <%= User.digest('password') %> password_digest: <%= User.digest("password") %>
activated: true
activated_at: <%= Time.current %>
<% 30.times do |n| %> <% 30.times do |n| %>
user_<%= n %>: user_<%= n %>:
name: <%= "User #{n}" %> name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %> email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %> password_digest: <%= User.digest("password") %>
activated: true
activated_at: <%= Time.current %>
<% end %> <% end %>
require "test_helper" require "test_helper"
class UsersSignupTest < ActionDispatch::IntegrationTest class UsersSignupTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
end
test "invalid signup information" do test "invalid signup information" do
get signup_path get signup_path
...@@ -12,14 +15,31 @@ class UsersSignupTest < ActionDispatch::IntegrationTest ...@@ -12,14 +15,31 @@ class UsersSignupTest < ActionDispatch::IntegrationTest
assert_select "div.field_with_errors" assert_select "div.field_with_errors"
end end
test "valid signup information" do test "valid signup information with account activation" do
get signup_path get signup_path
assert_difference "User.count", 1 do assert_difference "User.count", 1 do
post users_path, params: {user: {name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password"}} post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end end
assert_equal 1, ActionMailer::Base.deliveries.size
user = assigns(:user)
assert_not user.activated?
# Try to log in before activation.
log_in_as(user)
assert_not is_logged_in?
# Invalid activation token
get edit_account_activation_path("invalid token", email: user.email)
assert_not is_logged_in?
# Valid token, wrong email
get edit_account_activation_path(user.activation_token, email: "wrong")
assert_not is_logged_in?
# Valid activation token
get edit_account_activation_path(user.activation_token, email: user.email)
assert user.reload.activated?
follow_redirect! follow_redirect!
assert_template "users/show" assert_template "users/show"
assert_not flash.empty?
assert is_logged_in? assert is_logged_in?
end end
end end
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
user = users(:michael)
user.activation_token = User.new_token
mail = UserMailer.account_activation(user)
assert_equal "Account activation", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.name, mail.body.encoded
assert_match user.activation_token, mail.body.encoded
assert_match CGI.escape(user.email), mail.body.encoded
end
end
...@@ -72,6 +72,6 @@ class UserTest < ActiveSupport::TestCase ...@@ -72,6 +72,6 @@ class UserTest < ActiveSupport::TestCase
end end
test "authenticated? should return false for a user with nil digest" do test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?("") assert_not @user.authenticated?(:remember, '')
end end
end end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment