feat: add account activation feature
- Implement AccountActivationsController for activation logic - Create UserMailer for sending activation emails - Update SessionsController to handle unactivated users - Modify UsersController to restrict access to activated users - Add activation fields to User model and database migration - Create views for account activation emails - Add tests for account activation functionality
This commit is contained in:
parent
286ca3419f
commit
a54ebdbf23
15
app/controllers/account_activations_controller.rb
Normal file
15
app/controllers/account_activations_controller.rb
Normal file
@ -0,0 +1,15 @@
|
||||
class AccountActivationsController < ApplicationController
|
||||
include SessionsHelper
|
||||
def edit
|
||||
user = User.find_by(email: params[:email])
|
||||
if user && !user.activated? && user.authenticated?(:activation, params[:id])
|
||||
user.send(:activate)
|
||||
log_in user
|
||||
flash[:success] = "Account activated!"
|
||||
redirect_to user
|
||||
else
|
||||
flash[:danger] = "Invalid activation link"
|
||||
redirect_to root_url
|
||||
end
|
||||
end
|
||||
end
|
@ -7,11 +7,18 @@ class SessionsController < ApplicationController
|
||||
user = User.find_by(email: params[:session][:email].downcase)
|
||||
# if user && user.authenticate(params[:session][:password])
|
||||
if user&.authenticate(params[:session][:password])
|
||||
if user.activated?
|
||||
forwarding_url = session[:forwarding_url]
|
||||
reset_session
|
||||
params[:session][:remember_me] == "1" ? remember(user) : forget(user)
|
||||
log_in user
|
||||
redirect_to forwarding_url || user
|
||||
else
|
||||
message = "Account not activated. "
|
||||
message += "Check your email for the activation link."
|
||||
flash[:danger] = message
|
||||
redirect_to root_url
|
||||
end
|
||||
else
|
||||
flash.now[:danger] = "Invalid email/password combination"
|
||||
render "new", status: :unprocessable_entity
|
||||
|
@ -7,11 +7,12 @@ class UsersController < ApplicationController
|
||||
def index
|
||||
# @users = User.all
|
||||
# @users = User.order(:name).page(params[:page])
|
||||
@users = User.page(params[:page])
|
||||
@users = User.where(activated: true).page(params[:page])
|
||||
end
|
||||
|
||||
def show
|
||||
@user = User.find(params[:id])
|
||||
redirect_to root_url and return unless @user.activated?
|
||||
# debugger
|
||||
end
|
||||
def new
|
||||
@ -22,11 +23,14 @@ class UsersController < ApplicationController
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
if @user.save
|
||||
reset_session
|
||||
log_in @user
|
||||
flash[:success] = "Welcome to the Sample App!"
|
||||
redirect_to @user
|
||||
# reset_session
|
||||
# log_in @user
|
||||
# flash[:success] = "Welcome to the Sample App!"
|
||||
# redirect_to @user
|
||||
# redirect_to user_url(@user)
|
||||
@user.send(:send_activation_email)
|
||||
flash[:info] = "Please check your email to activate your account."
|
||||
redirect_to root_url
|
||||
else
|
||||
render "new"
|
||||
end
|
||||
|
2
app/helpers/account_activations_helper.rb
Normal file
2
app/helpers/account_activations_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module AccountActivationsHelper
|
||||
end
|
@ -19,7 +19,7 @@ module SessionsHelper
|
||||
end
|
||||
elsif (user_id = cookies.encrypted[: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
|
||||
|
@ -1,4 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: "from@example.com"
|
||||
default from: "noreply@mail.frytea.com"
|
||||
layout "mailer"
|
||||
end
|
||||
|
22
app/mailers/user_mailer.rb
Normal file
22
app/mailers/user_mailer.rb
Normal file
@ -0,0 +1,22 @@
|
||||
class UserMailer < ApplicationMailer
|
||||
# Subject can be set in your I18n file at config/locales/en.yml
|
||||
# with the following lookup:
|
||||
#
|
||||
# en.user_mailer.account_activation.subject
|
||||
#
|
||||
def account_activation(user)
|
||||
@user = user
|
||||
mail to: user.email, subject: "Account activation"
|
||||
end
|
||||
|
||||
# Subject can be set in your I18n file at config/locales/en.yml
|
||||
# with the following lookup:
|
||||
#
|
||||
# en.user_mailer.password_reset.subject
|
||||
#
|
||||
def password_reset
|
||||
@greeting = "Hi"
|
||||
|
||||
mail to: "to@example.org"
|
||||
end
|
||||
end
|
@ -1,7 +1,9 @@
|
||||
class User < ApplicationRecord
|
||||
attr_accessor :remember_token
|
||||
attr_accessor :remember_token, :activation_token
|
||||
# before_save { self.email = email.downcase }
|
||||
before_save { email.downcase! }
|
||||
# before_save { email.downcase! }
|
||||
before_save :downcase_email
|
||||
before_create :create_activation_digest
|
||||
validates :name, presence: true, length: { maximum: 50 }
|
||||
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
||||
validates :email, presence: true, length: { maximum: 255 },
|
||||
@ -44,12 +46,36 @@ class User < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def authenticated?(remember_token)
|
||||
return false if remember_digest.nil?
|
||||
BCrypt::Password.new(remember_digest).is_password?(remember_token)
|
||||
# powered by metaprogramming
|
||||
def authenticated?(attribute, token)
|
||||
digest = send("#{attribute}_digest")
|
||||
return false if digest.nil?
|
||||
BCrypt::Password.new(digest).is_password?(token)
|
||||
end
|
||||
|
||||
def forget
|
||||
update_attribute(:remember_digest, nil)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def downcase_email
|
||||
# self.email = email.downcase
|
||||
email.downcase!
|
||||
end
|
||||
|
||||
def create_activation_digest
|
||||
self.activation_token = User.new_token
|
||||
self.activation_digest = User.digest(activation_token)
|
||||
end
|
||||
|
||||
def activate
|
||||
# update_attribute(:activated, true)
|
||||
# update_attribute(:activated_at, Time.zone.now)
|
||||
update_columns(activated: true, activated_at: Time.zone.now)
|
||||
end
|
||||
|
||||
def send_activation_email
|
||||
UserMailer.account_activation(self).deliver_now
|
||||
end
|
||||
end
|
||||
|
15
app/views/user_mailer/account_activation.html.erb
Normal file
15
app/views/user_mailer/account_activation.html.erb
Normal file
@ -0,0 +1,15 @@
|
||||
<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) %>
|
||||
|
||||
<p>
|
||||
<%= edit_account_activation_url(@user.activation_token,
|
||||
email:@user.email) %>
|
||||
</p>
|
5
app/views/user_mailer/account_activation.text.erb
Normal file
5
app/views/user_mailer/account_activation.text.erb
Normal file
@ -0,0 +1,5 @@
|
||||
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) %>
|
5
app/views/user_mailer/password_reset.html.erb
Normal file
5
app/views/user_mailer/password_reset.html.erb
Normal file
@ -0,0 +1,5 @@
|
||||
<h1>User#password_reset</h1>
|
||||
|
||||
<p>
|
||||
<%= @greeting %>, find me in app/views/user_mailer/password_reset.html.erb
|
||||
</p>
|
3
app/views/user_mailer/password_reset.text.erb
Normal file
3
app/views/user_mailer/password_reset.text.erb
Normal file
@ -0,0 +1,3 @@
|
||||
User#password_reset
|
||||
|
||||
<%= @greeting %>, find me in app/views/user_mailer/password_reset.text.erb
|
@ -18,6 +18,7 @@ Rails.application.routes.draw do
|
||||
delete "/logout", to: "sessions#destroy"
|
||||
|
||||
resources :users
|
||||
resources :account_activations, only: [:edit]
|
||||
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
|
7
db/migrate/20250106091017_add_activation_to_users.rb
Normal file
7
db/migrate/20250106091017_add_activation_to_users.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class AddActivationToUsers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :users, :activation_digest, :string
|
||||
add_column :users, :activated, :boolean, default: false
|
||||
add_column :users, :activated_at, :datetime
|
||||
end
|
||||
end
|
5
db/schema.rb
generated
5
db/schema.rb
generated
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_05_095157) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_06_091017) do
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "email"
|
||||
@ -19,6 +19,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_05_095157) do
|
||||
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
|
||||
end
|
||||
|
@ -12,7 +12,9 @@ User.create!(name: "Example User",
|
||||
email: "example@example.com",
|
||||
password: "foobar",
|
||||
password_confirmation: "foobar",
|
||||
admin: true)
|
||||
admin: true,
|
||||
activated: true,
|
||||
activated_at: Time.zone.now)
|
||||
|
||||
99.times do |n|
|
||||
name = Faker::Name.name
|
||||
@ -21,5 +23,7 @@ User.create!(name: "Example User",
|
||||
User.create!(name: name,
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password)
|
||||
password_confirmation: password,
|
||||
activated: true,
|
||||
activated_at: Time.zone.now)
|
||||
end
|
||||
|
7
test/controllers/account_activations_controller_test.rb
Normal file
7
test/controllers/account_activations_controller_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class AccountActivationsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
17
test/fixtures/users.yml
vendored
17
test/fixtures/users.yml
vendored
@ -1,23 +1,32 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
name: MyString
|
||||
email: MyString
|
||||
|
||||
michael:
|
||||
name: Michael Example
|
||||
email: michael@example.com
|
||||
admin: true
|
||||
password_digest: <%= User.digest('password') %>
|
||||
activated: true
|
||||
activated_at: <%= Time.zone.now %>
|
||||
|
||||
inactive:
|
||||
name: Inactive User
|
||||
email: inactive@example.com
|
||||
admin: false
|
||||
password_digest: <%= User.digest('password') %>
|
||||
activated: false
|
||||
|
||||
archer:
|
||||
name: Sterling Archer
|
||||
email: suchess@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 %>
|
20
test/integration/user_show_test.rb
Normal file
20
test/integration/user_show_test.rb
Normal file
@ -0,0 +1,20 @@
|
||||
require "test_helper"
|
||||
|
||||
class UserShowTest < ActionDispatch::IntegrationTest
|
||||
def setup
|
||||
@inactive_user = users(:inactive)
|
||||
@active_user = users(:archer)
|
||||
end
|
||||
|
||||
test "should redirect when user not activated" do
|
||||
get user_path(@inactive_user)
|
||||
assert_response :redirect
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
||||
test "should display user when activated" do
|
||||
get user_path(@active_user)
|
||||
assert_response :success
|
||||
assert_template 'users/show'
|
||||
end
|
||||
end
|
@ -13,7 +13,9 @@ class UsersIndexTest < ActionDispatch::IntegrationTest
|
||||
assert_select "ul.pagination"
|
||||
|
||||
first_page_of_users = User.page(1)
|
||||
first_page_of_users.first.toggle!(:activated)
|
||||
first_page_of_users.each do |user|
|
||||
# assert_not user.activated?
|
||||
assert_select "a[href=?]", user_path(user), text: user.name
|
||||
unless user == @admin
|
||||
assert_select "a[href=?]", user_path(user), text: "delete"
|
||||
|
@ -1,6 +1,11 @@
|
||||
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 +19,7 @@ class UsersSignupTest < ActionDispatch::IntegrationTest
|
||||
assert_select "div.alert-danger"
|
||||
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,9 +27,28 @@ class UsersSignupTest < ActionDispatch::IntegrationTest
|
||||
password: "password",
|
||||
password_confirmation: "password" } }
|
||||
end
|
||||
# follow_redirect!
|
||||
# assert_template "users/show"
|
||||
# assert_not flash.notice
|
||||
# assert is_logged_in?
|
||||
|
||||
assert_equal 1, ActionMailer::Base.deliveries.size
|
||||
user = assigns(:user)
|
||||
assert_not user.activated?
|
||||
# 尝试激活前登陆
|
||||
log_in_as(user)
|
||||
assert_not is_logged_in?
|
||||
# 激活令牌无效
|
||||
get edit_account_activation_path("invalid token", email: user.email)
|
||||
assert_not is_logged_in?
|
||||
# 令牌有效,邮箱错误
|
||||
get edit_account_activation_path(user.activation_token, email: 'wrong')
|
||||
assert_not is_logged_in?
|
||||
# 令牌有效
|
||||
get edit_account_activation_path(user.activation_token, email: user.email)
|
||||
assert user.reload.activated?
|
||||
follow_redirect!
|
||||
assert_template "users/show"
|
||||
assert_not flash.notice
|
||||
assert_template 'users/show'
|
||||
assert is_logged_in?
|
||||
end
|
||||
end
|
||||
|
14
test/mailers/previews/user_mailer_preview.rb
Normal file
14
test/mailers/previews/user_mailer_preview.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# 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
|
23
test/mailers/user_mailer_test.rb
Normal file
23
test/mailers/user_mailer_test.rb
Normal file
@ -0,0 +1,23 @@
|
||||
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@mail.frytea.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
|
||||
|
||||
test "password_reset" do
|
||||
mail = UserMailer.password_reset
|
||||
assert_equal "Password reset", mail.subject
|
||||
assert_equal [ "to@example.org" ], mail.to
|
||||
assert_equal [ "noreply@mail.frytea.com" ], mail.from
|
||||
assert_match "Hi", mail.body.encoded
|
||||
end
|
||||
end
|
@ -75,6 +75,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
|
||||
|
Loading…
Reference in New Issue
Block a user