Commit 1990642f by Ba Toi Dang

Merge branch 'account-activation' into 'master'

Account activation

See merge request !13
parents 2d1be627 db1c54ad
......@@ -14,23 +14,6 @@ gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'mini_racer', platforms: :ruby
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false
......@@ -66,4 +49,7 @@ end
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'bootstrap-sass', '~> 3.3', '>= 3.3.7'
gem 'bcrypt', '~> 3.1', '>= 3.1.12'
gem 'jquery-rails', '~> 4.3', '>= 4.3.3'
\ No newline at end of file
gem 'jquery-rails', '~> 4.3', '>= 4.3.3'
gem 'faker', '~> 1.9', '>= 1.9.1'
gem 'will_paginate', '~> 3.1', '>= 3.1.6'
gem 'bootstrap-will_paginate', '~> 1.0'
\ No newline at end of file
......@@ -56,6 +56,8 @@ GEM
bootstrap-sass (3.3.7)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap-will_paginate (1.0.0)
will_paginate
builder (3.2.3)
byebug (10.0.2)
capybara (3.4.2)
......@@ -70,26 +72,18 @@ GEM
chromedriver-helper (1.2.0)
archive-zip (~> 0.10)
nokogiri (~> 1.8)
coffee-rails (4.2.2)
coffee-script (>= 2.2.0)
railties (>= 4.0.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.0.5)
crass (1.0.4)
erubi (1.7.1)
execjs (2.7.0)
faker (1.9.1)
i18n (>= 0.7)
ffi (1.9.25)
globalid (0.4.1)
activesupport (>= 4.2.0)
i18n (1.0.1)
concurrent-ruby (~> 1.0)
io-like (0.3.0)
jbuilder (2.7.0)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
jquery-rails (4.3.3)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
......@@ -111,7 +105,6 @@ GEM
mini_portile2 (2.3.0)
minitest (5.11.3)
msgpack (1.2.4)
multi_json (1.13.1)
nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
......@@ -185,9 +178,6 @@ GEM
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
turbolinks (5.1.1)
turbolinks-source (~> 5.1)
turbolinks-source (5.1.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uglifier (4.1.17)
......@@ -200,6 +190,7 @@ GEM
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
will_paginate (3.1.6)
xpath (3.1.0)
nokogiri (~> 1.8)
......@@ -210,11 +201,11 @@ DEPENDENCIES
bcrypt (~> 3.1, >= 3.1.12)
bootsnap (>= 1.1.0)
bootstrap-sass (~> 3.3, >= 3.3.7)
bootstrap-will_paginate (~> 1.0)
byebug
capybara (>= 2.15, < 4.0)
chromedriver-helper
coffee-rails (~> 4.2)
jbuilder (~> 2.5)
faker (~> 1.9, >= 1.9.1)
jquery-rails (~> 4.3, >= 4.3.3)
listen (>= 3.0.5, < 3.2)
pg (~> 1.0)
......@@ -226,10 +217,10 @@ DEPENDENCIES
spring
spring-watcher-listen (~> 2.0.0)
sqlite3
turbolinks (~> 5)
tzinfo-data
uglifier (>= 1.3.0)
web-console (>= 3.3.0)
will_paginate (~> 3.1, >= 3.1.6)
RUBY VERSION
ruby 2.5.1p57
......
......@@ -2,5 +2,4 @@
//= require bootstrap
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .
......@@ -184,4 +184,15 @@ input {
#session_remember_me {
width: auto;
margin-left: 0;
}
/* User index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
\ No newline at end of file
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
......@@ -5,9 +5,15 @@ class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_to user
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. Check your email for the activation link."
flash[:message] = message
redirect_to root_url
end
else
flash.now[:danger] = "Invalid email/password combination"
render 'new'
......
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: [:destroy]
before_action :find_user, only: [:show, :edit, :update, :destroy]
def index
@users = User.paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
end
def new
......@@ -10,20 +18,58 @@ class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
def edit
@user = User.find params[:id]
end
def update
if @user.update_attributes user_params
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
def destroy
@user.destroy
flash[:success] = "User deleted"
redirect_to users_url
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def find_user
@user = User.find(params[:id])
end
#confirm a logged-in user
# if user don't login and they're in GET request, it store this request to session[:forwarding_url]
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in"
redirect_to login_url
end
end
# confirm the correct user
def correct_user
redirect_to root_url unless current_user? find_user
end
# confirm an admin user
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end
module AccountActivationsHelper
end
......@@ -10,6 +10,9 @@ module SessionsHelper
cookies.permanent[:remember_token] = user.remember_token
end
def current_user?(user)
user == current_user
end
# return the user corresponding to the remember token cookie
def current_user
return @current_user if @current_user
......@@ -18,7 +21,7 @@ module SessionsHelper
@current_user = User.find_by(id: user_id)
elsif user_id = cookies.signed[: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
@current_user = user
end
......@@ -42,4 +45,18 @@ module SessionsHelper
session.delete(:user_id)
@current_user = nil
end
# friendly forwarding - stored location of expected page and redirect to it after login
# evaluate to session[:forwarding_url] unless it's nil
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete :forwarding_url
end
# stores the URL trying to be accessed.
# only use for GET to prevent storing submiting a form when not log in
# original_url and get? is contained Request object of Rails
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
end
module UsersHelper
def gravatar_for(user, size: 80)
def gravatar_for(user, options = { size: 80 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
size = options[:size]
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
......
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
default from: 'noreply@example.com'
layout 'mailer'
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
attr_accessor :remember_token
attr_accessor :remember_token, :activation_token
before_save { email.downcase! }
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
......@@ -10,7 +11,7 @@ class User < ApplicationRecord
format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
......@@ -28,12 +29,33 @@ class User < ApplicationRecord
update_attribute :remember_digest, User.digest(remember_token)
end
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
def authenticated?(attribute, token)
digest = self.send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
def forget
update_attribute(:remember_digest, nil)
end
# activates an account
def activate
update_columns(activated: true, activated_at: Time.zone.now)
end
#send activation email
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
private
def downcase_email
self.email = email.downcase
end
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
......@@ -6,14 +6,14 @@
<li><%= link_to 'Home', root_path %></li>
<li><%= link_to 'Help', help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li><%= link_to "Users", users_path %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", "#" %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
......
<% if @user.errors.any? %>
<% if user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
The form contains <%= pluralize(user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<% user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
......
<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) %>
\ No newline at end of file
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) %>
\ No newline at end of file
<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
<%= form_for @user do |f| %>
<%= render 'shared/error_messages', user: @user %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>
\ No newline at end of file
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
<%= link_to 'delete', user, method: :delete, data: { confirm: "You sure?" } %>
<% end %>
</li>
\ No newline at end of file
<% provide(:title, "Edit user") %>
<% provide(:button_text, 'Save changes') %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
\ No newline at end of file
<% provide :title, 'All users' %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %>
</ul>
<%= will_paginate %>
\ No newline at end of file
<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offet-3">
<%= form_for @user, url: signup_path do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
<%= render 'form' %>
</div>
</div>
\ No newline at end of file
......@@ -31,7 +31,10 @@ Rails.application.configure do
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { address: 'localhost', port: '1025' }
config.action_mailer.default_url_options = { host: 'localhost:3000', protocol: 'http' }
config.action_mailer.perform_caching = false
......
......@@ -37,7 +37,7 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'example.com' }
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
......
......@@ -12,4 +12,7 @@ Rails.application.routes.draw do
delete '/logout', to: 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
end
class AddAdminToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :admin, :boolean, default: false
end
end
class AddActivationToUsers < ActiveRecord::Migration[5.2]
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 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_08_06_093756) do
ActiveRecord::Schema.define(version: 2018_08_12_093253) do
create_table "users", force: :cascade do |t|
t.string "name"
......@@ -19,6 +19,10 @@ ActiveRecord::Schema.define(version: 2018_08_06_093756) do
t.datetime "updated_at", null: false
t.string "password_digest"
t.string "remember_digest"
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
end
......
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
User.create! name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true,
activated: true,
activated_at: Time.zone.now
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = 'password'
User.create! name: name,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now
end
\ No newline at end of file
require 'test_helper'
class AccountActivationsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get signup_path
assert_response :success
def setup
@user = users :michael
@other_user = users :archer
end
test "should redirect edit when logged in as wrong user" do
log_in_as @other_user
get edit_user_path @user
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect update when logged in as wrong user" do
log_in_as @other_user
patch user_path @user, params: { user: { name: @user.name,
email: @user.email } }
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect index when not logged in" do
get users_path
assert_redirected_to login_url
end
test "should redirect destroy when not logged in" do
assert_no_difference 'User.count' do
delete user_path(@user)
end
assert_redirected_to login_url
end
test "should redirect destroy when logged in as a non-admin" do
log_in_as @other_user
assert_no_difference 'User.count' do
delete user_path @user
end
assert_redirected_to root_url
end
end
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
\ No newline at end of file
password_digest: <%= User.digest('password') %>
admin: true
activated: true,
activated_at: <%= Time.zone.now %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
activated: true,
activated_at: <%= Time.zone.now %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
activated: true,
activated_at: <%= Time.zone.now %>
malory:
name: Malory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
activated: true,
activated_at: <%= Time.zone.now %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
activated: true,
activated_at: <%= Time.zone.now %>
<% end %>
\ No newline at end of file
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users :michael
end
test "unsuccessful edit" do
log_in_as @user
get edit_user_path @user
assert_template 'users/edit'
patch user_path(@user), params: { user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
assert_template 'users/edit'
end
test "successful edit" do
log_in_as @user
get edit_user_path @user
assert_template 'users/edit'
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
# redirecting to edit page when non-logged-in user visit edit page,
# have already logged in instead of show page
test 'successful edit with friendly forwarding' do
get edit_user_path @user
log_in_as @user
assert_redirected_to edit_user_url @user
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@admin = users :michael
@non_admin = users :archer
end
test "index as admin including pagination and delete links" do
log_in_as @admin
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
first_page_of_users = User.paginate(page: 1)
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
unless user == @admin
assert_select 'a[href=?]', user_path(user), text: 'delete'
end
end
assert_difference 'User.count', -1 do
delete user_path @non_admin
end
end
test "index as non-admin" do
log_in_as @non_admin
get users_path
assert_select 'a', text: 'delete', count: 0
end
end
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base::deliveries.clear
end
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
......@@ -14,7 +18,7 @@ class UsersSignupTest < ActionDispatch::IntegrationTest
assert_select 'div.field_with_errors'
end
test "valid signup information" do
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
......@@ -22,6 +26,21 @@ class UsersSignupTest < ActionDispatch::IntegrationTest
password: "password",
password_confirmation: "password" } }
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!
assert_template 'users/show'
assert is_logged_in?
......
# 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
......@@ -73,6 +73,6 @@ class UserTest < ActiveSupport::TestCase
end
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?('')
assert_not @user.authenticated?(:remember, '')
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