Skip to content

Commit

Permalink
User - Reset Password
Browse files Browse the repository at this point in the history
  • Loading branch information
ntritin62 committed Oct 4, 2024
1 parent 972795e commit 805d99b
Show file tree
Hide file tree
Showing 20 changed files with 200 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
.DS_Store
.env
# Ignore bundler config.
/.bundle

Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ruby "3.2.2"
gem "bcrypt", "~> 3.1.7"
gem "bootsnap", require: false
gem "config"
gem "dotenv-rails", groups: [:development, :test]
gem "faker"
gem "font-awesome-sass"
gem "foreman", "~> 0.87.2"
Expand Down
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ GEM
reline (>= 0.3.1)
deep_merge (1.2.2)
diff-lcs (1.5.1)
dotenv (3.1.4)
dotenv-rails (3.1.4)
dotenv (= 3.1.4)
railties (>= 6.1)
erubi (1.12.0)
execjs (2.9.1)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
ffi (1.17.0-aarch64-linux-gnu)
Expand Down Expand Up @@ -310,6 +313,7 @@ DEPENDENCIES
capybara
config
debug
dotenv-rails
faker
font-awesome-sass
foreman (~> 0.87.2)
Expand Down
65 changes: 65 additions & 0 deletions app/controllers/password_resets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
class PasswordResetsController < ApplicationController
before_action :load_user, :valid_user, :check_expiration,
only: %i(update edit)
def new; end

def edit; end

def create
@user = User.find_by email: params.dig(:password_reset, :email)&.downcase
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = t ".check_email"

redirect_to root_url
else
flash.now[:danger] = t ".invalid_user"
render :new, status: :unprocessable_entity
end
end

def update
if params.dig(:user, :password).empty?
@user.errors.add :password, t(".cannot_empty")
return render :edit, status: :unprocessable_entity
end

if @user.update user_params
log_in @user
@user.update_column :reset_digest, nil
flash[:success] = t ".reset_success"
return redirect_to @user
end

render :edit, status: :unprocessable_entity
end

private

def user_params
params.require(:user).permit User::RESET_PARAMS
end

def load_user
return if @user = User.find_by(email: params[:email])

flash[:danger] = t "password_resets.invalid_email"
redirect_to root_path
end

def check_expiration
return unless @user.password_reset_expired?

flash[:danger] = t "password_resets.expired_link"
redirect_to root_path
end

def valid_user
return if @user.activated? && @user.try(:authenticated?, "reset",
params[:id])

flash[:danger] = t "password_resets.invalid_user"
redirect_to root_path
end
end
2 changes: 2 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def message_class message_type
"text-green-600 bg-green-200"
when "danger"
"text-red-600 bg-red-200"
when "info"
"text-blue-600 bg-blue-200"
else
"text-yellow-600 bg-yellow-200"
end
Expand Down
2 changes: 1 addition & 1 deletion app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
default from: "noreply@example.com"
layout "mailer"
end
6 changes: 6 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class UserMailer < ApplicationMailer
def password_reset user
@user = user
mail to: user.email, subject: t(".password_reset")
end
end
17 changes: 16 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
class User < ApplicationRecord
SIGN_UP_REQUIRE_ATTRIBUTES = %i(user_name email password
password_confirmation avatar).freeze
attr_accessor :remember_token
RESET_PARAMS = %i(password password_confirmation).freeze
attr_accessor :remember_token, :reset_token

has_one_attached :avatar do |attachable|
attachable.variant :display, resize_to_limit: [Settings.ui.avatar_size,
Expand Down Expand Up @@ -63,6 +64,20 @@ def forget
update_attribute :remember_digest, nil
end

def create_reset_digest
self.reset_token = User.new_token
update_columns reset_digest: User.digest(reset_token),
reset_sent_at: Time.zone.now
end

def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end

def password_reset_expired?
reset_sent_at < Settings.value.reset_expired_time.hours.ago
end

private
def downcase_email
email.downcase!
Expand Down
4 changes: 2 additions & 2 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<body class="min-h-svh flex flex-col">
<%= render "layouts/header" %>
<main class="container my-[30px]">
<main class="container my-[30px] flex-1">
<%= render "layouts/notification" %>
<%= yield %>
</main>
Expand Down
17 changes: 17 additions & 0 deletions app/views/password_resets/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<% provide :title, t(".password_reset") %>
<div class="flex flex-col items-center justify-center">
<h1 class="text-2xl font-bold"><%= t ".password_reset" %></h1>
<div class="mt-5">
<%= form_with model: @user, url: password_reset_path(params[:id]), class: "flex flex-col" do |f| %>
<%= render "shared/error_messages", object: f.object %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password, t(".password"), class: "text-base font-bold" %>
<%= f.password_field :password, class: "rounded-lg mt-2" %>
<%= f.label :password_confirmation, t(".confirm"), class: "text-base font-bold" %>
<%= f.password_field :password_confirmation, class: "rounded-lg mt-2" %>
<button type="submit" class="mt-2 px-4 py-2 bg-primary text-white rounded-sm hover:opacity-70 text-lg flex items-center justify-center">
<%= t ".submit" %>
</button>
<% end %>
</div>
</div>
13 changes: 13 additions & 0 deletions app/views/password_resets/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% provide :title, t(".forgot_password") %>
<div class="flex flex-col justify-center items-center">
<h1 class="text-2xl font-bold"><%= t ".forgot_password" %></h1>
<div class="mt-5">
<%= form_for :password_reset, url: password_resets_path, class: "flex flex-col" do |f| %>
<%= f.label :email, class: "text-base font-bold" %>
<%= f.email_field :email, class: "rounded-lg mt-2" %>
<button type="submit" class="mt-2 px-4 py-2 bg-primary text-white rounded-sm hover:opacity-70 text-lg flex items-center justify-center">
<%= t ".submit" %>
</button>
<% end %>
</div>
</div>
1 change: 1 addition & 0 deletions app/views/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<%= f.label t(".password"), class: "text-lg font-bold" %>
<%= f.password_field :password, class: "form-control" %>
</div>
<%= link_to t(".forgot_password"), new_password_reset_path, class: "text-primary hover:underline" %>
<div class="flex flex-col w-full">
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
Expand Down
4 changes: 4 additions & 0 deletions app/views/user_mailer/password_reset.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h1>B-World</h1>
<p><%= t ".greeting", deep_interpolation: true, user_name: @user.user_name %>, </p>
<p><%= t".instruction", deep_interpolation: true, expire_time: Settings.value.reset_expired_time %></p>
<%= link_to t(".reset"), edit_password_reset_url(id: @user.reset_token, email: @user.email) %>
5 changes: 5 additions & 0 deletions app/views/user_mailer/password_reset.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Sample App
<%= t ".greeting", deep_interpolation: true, user_name: @user.user_name %>
<%= t".instruction", deep_interpolation: true, expire_time: Settings.value.reset_expired_time %>
<%= edit_password_reset_url(id: @user.reset_token, email: @user.email) %>
<%= link_to t(".reset"), edit_password_reset_url(id: @user.reset_token, email: @user.email) %>
4 changes: 4 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

if Rails.env.development? || Rails.env.test?
Dotenv::Railtie.load
end

module RailsTutorial
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
Expand Down
12 changes: 12 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,16 @@

# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { host: 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
user_name: ENV['MAILTRAP_USERNAME'],
password: ENV['MAILTRAP_PASSWORD'],
address: ENV['MAILTRAP_ADDRESS'],
host: ENV['MAILTRAP_HOST'],
port: ENV['MAILTRAP_PORT'],
authentication: :login
}
Rails.application.routes.default_url_options[:host] = 'localhost:3000'
end
19 changes: 19 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,22 @@ en:
cart_item:
category: "Category"
remove: "Remove"
password_resets:
invalid_email: "Invalid email"
new:
forgot_password: "Forgot password"
submit: "Submit"
create:
invalid_user: "Invalid user"
check_email: "Please check your email to reset password"
edit:
submit: "Submit"
password_reset: "Reset password"
update:
cannot_empty: "cannot empty"
reset_success: "Password reset successfully"
user_mailer:
password_reset:
password_reset: "Password reset"
greeting: "Hi %{user_name}"
instruction: "Click the link below to reset your password. The link will expire in %{expire_time} hour."
24 changes: 24 additions & 0 deletions config/locales/vi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,27 @@ vi:
cart_item:
category: "Thể loại"
remove: "Xoá"
password_resets:
invalid_email: "Email không hợp lệ"
expired_link: "Đương dẫn đã hết hạn"
invalid_user: "Người dùng không hợp lệ"
new:
forgot_password: "Quên mật khẩu"
submit: "Gửi"
create:
invalid_user: "Người dùng không hợp lệ"
check_email: "Kiểm tra email để cài đặt lại mật khẩu"
update:
cannot_empty: "không được để trống"
reset_success: "Cài đặt lại mật khẩu thành công"
edit:
password: "Mật khẩu"
confirm: "Xác nhận mật khẩu"
submit: "Xác nhận"
password_reset: "Đặt lại mật khẩu"
user_mailer:
password_reset:
password_reset: "Đặt lại mật khẩu"
greeting: "Chào %{user_name}"
instruction: "Nhấn vào đường dẫn bên dưới để đặt lại mật khẩu. Đường dẫn sẽ khả dụng trong vòng %{expire_time} giờ."
reset: "Đặt lại mật khẩu"
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
end
end
resources :products, only: %i(show index)
resources :password_resets, only: %i(new create edit update)
end
end
1 change: 1 addition & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ value:
comment_length: 1000
min_numeric: 0
rate_min: 1
reset_expired_time: 2
rate_max: 5
ui:
avatar_size: 150
Expand Down

0 comments on commit 805d99b

Please sign in to comment.