diff --git a/Gemfile b/Gemfile
index f6d4606..7f8aa8b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -41,6 +41,8 @@ group :development do
gem 'rack-mini-profiler', '~> 2.0'
gem 'listen', '~> 3.3'
gem 'bullet'
+ gem 'letter_opener'
+ gem 'letter_opener_web'
end
group :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index 0219040..161fda5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -140,6 +140,15 @@ GEM
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
+ launchy (2.5.0)
+ addressable (~> 2.7)
+ letter_opener (1.7.0)
+ launchy (~> 2.2)
+ letter_opener_web (2.0.0)
+ actionmailer (>= 5.2)
+ letter_opener (~> 1.7)
+ railties (>= 5.2)
+ rexml
listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@@ -334,6 +343,8 @@ DEPENDENCIES
jbuilder (~> 2.7)
js-routes
kaminari
+ letter_opener
+ letter_opener_web
listen (~> 3.3)
newrelic_rpm
pg (~> 1.1)
diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb
index b54f9c6..08c85f8 100644
--- a/app/controllers/api/v1/tasks_controller.rb
+++ b/app/controllers/api/v1/tasks_controller.rb
@@ -17,21 +17,30 @@ def show
def create
task = current_user.my_tasks.new(task_params)
- task.save
+
+ if task.save
+ UserMailer.with({ user: current_user, task: task }).task_created.deliver_now
+ end
respond_with(task, serializer: TaskSerializer, location: nil)
end
def update
task = Task.find(params[:id])
- task.update(task_params)
+
+ if task.update(task_params)
+ UserMailer.with({ task: task }).task_updated.deliver_now
+ end
respond_with(task, serializer: TaskSerializer)
end
def destroy
task = Task.find(params[:id])
- task.destroy
+
+ if task.destroy
+ UserMailer.with({ task: task }).task_destroyed.deliver_now
+ end
respond_with(task)
end
diff --git a/app/controllers/web/password_resets_controller.rb b/app/controllers/web/password_resets_controller.rb
new file mode 100644
index 0000000..6b5a2d0
--- /dev/null
+++ b/app/controllers/web/password_resets_controller.rb
@@ -0,0 +1,48 @@
+class Web::PasswordResetsController < Web::ApplicationController
+ def new
+ @password_reset = PasswordResetForm.new
+ end
+
+ def create
+ @password_reset = PasswordResetForm.new(password_reset_params)
+ @email = @password_reset.email
+
+ if @password_reset.valid?
+ @user = @password_reset.user
+ UserService.reset_password!(@user)
+ end
+
+ redirect_to(root_path)
+ end
+
+ def edit
+ set_user
+ end
+
+ def update
+ set_user
+
+ if @user.update(user_params)
+ UserService.clear_reset_digest(@user)
+ end
+
+ redirect_to(new_session_path)
+ end
+
+ private
+
+ def set_user
+ @user = User.find_by(reset_digest: params[:user][:reset_digest])
+ if @user.blank? || !UserService.password_reset_period_valid?(@user)
+ redirect_to(new_session_path)
+ end
+ end
+
+ def user_params
+ params.require(:user).permit(:password, :password_confirmation)
+ end
+
+ def password_reset_params
+ params.require(:password_reset_form).permit(:email)
+ end
+end
diff --git a/app/forms/password_reset_form.rb b/app/forms/password_reset_form.rb
new file mode 100644
index 0000000..1950356
--- /dev/null
+++ b/app/forms/password_reset_form.rb
@@ -0,0 +1,18 @@
+class PasswordResetForm
+ include ActiveModel::Model
+
+ attr_accessor(
+ :email,
+ )
+
+ validates :email, presence: true, format: { with: /\A\S+@.+\.\S+\z/ }
+ validate :user_exists?
+
+ def user
+ @user = User.find_by(email: email)
+ end
+
+ def user_exists?
+ errors.add(:email, :user_does_not_exist) if user.blank?
+ end
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
new file mode 100644
index 0000000..384201a
--- /dev/null
+++ b/app/mailers/user_mailer.rb
@@ -0,0 +1,30 @@
+class UserMailer < ApplicationMailer
+ default from: 'noreply@taskmanager.com'
+
+ def task_created
+ user = params[:user]
+ @task = params[:task]
+
+ mail(to: user.email, subject: 'New Task Created')
+ end
+
+ def task_updated
+ @task = params[:task]
+ author = @task.author
+
+ mail(to: author.email, subject: 'Task Updated')
+ end
+
+ def task_destroyed
+ @task = params[:task]
+ author = @task.author
+
+ mail(to: author.email, subject: 'Task Destroyed')
+ end
+
+ def reset_password
+ @user = params[:user]
+
+ mail(to: @user.email, subject: 'Reset Password')
+ end
+end
diff --git a/app/services/user_service.rb b/app/services/user_service.rb
new file mode 100644
index 0000000..4c2798f
--- /dev/null
+++ b/app/services/user_service.rb
@@ -0,0 +1,16 @@
+class UserService
+ def self.reset_password!(user)
+ token = SecureRandom.hex(10)
+ user.update!(reset_digest: token, reset_sent_at: Time.current)
+
+ UserMailer.with({ user: user }).reset_password.deliver_now
+ end
+
+ def self.password_reset_period_valid?(user)
+ user.reset_sent_at.present? && (Time.current - user.reset_sent_at) <= 1.days
+ end
+
+ def self.clear_reset_digest(user)
+ user.update!(reset_digest: nil, reset_sent_at: nil)
+ end
+end
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb
index cbd34d2..7c025b6 100644
--- a/app/views/layouts/mailer.html.erb
+++ b/app/views/layouts/mailer.html.erb
@@ -1,13 +1,37 @@
-
+
-
-
+
+ Email template
-
-
- <%= yield %>
+
+
+
+
+
+
+ |
+
+ Task Board Project
+
+ |
+
+ |
+
+ <%= yield %>
+
+ |
+
+
+ |
+
+ © 2022, TaskBoard Project
+
+ |
+
+
+ |
+
+
-
+
\ No newline at end of file
diff --git a/app/views/layouts/mailer.html.slim b/app/views/layouts/mailer.html.slim
new file mode 100644
index 0000000..e17dcf6
--- /dev/null
+++ b/app/views/layouts/mailer.html.slim
@@ -0,0 +1,3 @@
+html
+ body
+ = yield
diff --git a/app/views/layouts/mailer.text.slim b/app/views/layouts/mailer.text.slim
new file mode 100644
index 0000000..0a90f09
--- /dev/null
+++ b/app/views/layouts/mailer.text.slim
@@ -0,0 +1 @@
+= yield
diff --git a/app/views/user_mailer/reset_password.html.slim b/app/views/user_mailer/reset_password.html.slim
new file mode 100644
index 0000000..583b3be
--- /dev/null
+++ b/app/views/user_mailer/reset_password.html.slim
@@ -0,0 +1,2 @@
+p Follow link for set new password
+= link_to 'Reset password URL', edit_password_reset_url(user: { reset_digest: @user.reset_digest, email: @user.email }), target: '_blank'
diff --git a/app/views/user_mailer/task_created.html.slim b/app/views/user_mailer/task_created.html.slim
new file mode 100644
index 0000000..d0be475
--- /dev/null
+++ b/app/views/user_mailer/task_created.html.slim
@@ -0,0 +1 @@
+| Task #{@task.id} was created
\ No newline at end of file
diff --git a/app/views/user_mailer/task_destroyed.html.slim b/app/views/user_mailer/task_destroyed.html.slim
new file mode 100644
index 0000000..7aef6f4
--- /dev/null
+++ b/app/views/user_mailer/task_destroyed.html.slim
@@ -0,0 +1 @@
+| Task #{@task.id} was destroyed
\ No newline at end of file
diff --git a/app/views/user_mailer/task_updated.html.slim b/app/views/user_mailer/task_updated.html.slim
new file mode 100644
index 0000000..b580df6
--- /dev/null
+++ b/app/views/user_mailer/task_updated.html.slim
@@ -0,0 +1 @@
+| Task #{@task.id} was updated
\ No newline at end of file
diff --git a/app/views/web/password_resets/edit.html.slim b/app/views/web/password_resets/edit.html.slim
new file mode 100644
index 0000000..4a1ffc6
--- /dev/null
+++ b/app/views/web/password_resets/edit.html.slim
@@ -0,0 +1,12 @@
+h3 New Password
+= simple_form_for @user, url: password_reset_path, as: :user do |f|
+ p
+ = f.input :password
+ p
+ = f.input :password_confirmation
+ p
+ = f.button :submit, 'Submit'
+ p
+ = f.hidden_field :reset_digest, :value => @user.reset_digest
+ p
+ = f.hidden_field :email, :value => @user.email
\ No newline at end of file
diff --git a/app/views/web/password_resets/new.html.slim b/app/views/web/password_resets/new.html.slim
new file mode 100644
index 0000000..dfdd563
--- /dev/null
+++ b/app/views/web/password_resets/new.html.slim
@@ -0,0 +1,6 @@
+h4 Password Reset
+= simple_form_for @password_reset, url: password_reset_path do |f|
+ p
+ = f.input :email
+ p
+ = f.button :submit, "Reset"
\ No newline at end of file
diff --git a/app/views/web/sessions/new.html.slim b/app/views/web/sessions/new.html.slim
index c255aa8..5c4003f 100644
--- a/app/views/web/sessions/new.html.slim
+++ b/app/views/web/sessions/new.html.slim
@@ -4,4 +4,5 @@ h4 Log in
= f.input :email
= f.input :password
p
- = f.button :submit, "Sign in"
\ No newline at end of file
+ = f.button :submit, "Sign in"
+= link_to "(forgot password)", new_password_reset_path
\ No newline at end of file
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 3e9aa33..b2033dd 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -36,7 +36,10 @@
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
- config.action_mailer.perform_caching = false
+ config.action_mailer.delivery_method = :letter_opener_web
+ config.action_mailer.perform_caching = true
+
+ config.action_mailer.default_url_options = { host: 'localhost:3000' }
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 27003ca..2f0e1da 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -117,4 +117,15 @@
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.smtp_settings = {
+ user_name: ENV['MAILER_USERNAME'],
+ password: ENV['MAILER_PASSWORD'],
+ address: ENV['MAILER_ADDRESS'],
+ port: ENV['MAILER_PORT'],
+ domain: ENV['MAILER_DOMAIN'],
+ authentication: ENV['MAILER_AUTHENTICATION'],
+ enable_starttls_auto: true,
+ }
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 17ce39c..aec0a53 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -37,6 +37,8 @@
config.action_mailer.perform_caching = false
+ config.action_mailer.default_url_options = { host: 'example.com' }
+
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
diff --git a/config/routes.rb b/config/routes.rb
index 3ef0110..346b34a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,6 @@
Rails.application.routes.draw do
+ mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
+
root :to => "web/boards#show"
namespace :admin do
@@ -16,5 +18,7 @@
resource :board, only: :show
resource :session, only: [:new, :create, :destroy]
resources :developers, only: [:new, :create]
+ resource :password_reset, only: [:new, :create, :edit, :update]
+ resource :password, only: [:edit, :update]
end
end
\ No newline at end of file
diff --git a/db/migrate/20220116111008_add_reset_to_users.rb b/db/migrate/20220116111008_add_reset_to_users.rb
new file mode 100644
index 0000000..dd79fee
--- /dev/null
+++ b/db/migrate/20220116111008_add_reset_to_users.rb
@@ -0,0 +1,6 @@
+class AddResetToUsers < ActiveRecord::Migration[6.1]
+ def change
+ add_column :users, :reset_digest, :string
+ add_column :users, :reset_sent_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2aca128..8fbaa72 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_11_14_180958) do
+ActiveRecord::Schema.define(version: 2022_01_16_111008) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -35,6 +35,8 @@
t.string "type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.string "reset_digest"
+ t.datetime "reset_sent_at"
end
end
diff --git a/test/controllers/api/v1/tasks_controller_test.rb b/test/controllers/api/v1/tasks_controller_test.rb
index 9e25d08..19cf4f7 100644
--- a/test/controllers/api/v1/tasks_controller_test.rb
+++ b/test/controllers/api/v1/tasks_controller_test.rb
@@ -16,17 +16,21 @@ class Api::V1::TasksControllerTest < ActionController::TestCase
test 'should post create' do
author = create(:user)
sign_in(author)
+
assignee = create(:user)
- task_attributes = attributes_for(:task).
- merge({ assignee_id: assignee.id })
- post :create, params: { task: task_attributes, format: :json }
+ task_attributes = attributes_for(:task).merge({ assignee_id: assignee.id })
+
+ assert_emails 1 do
+ post :create, params: { task: task_attributes, format: :json }
+ end
assert_response :created
data = JSON.parse(response.body)
created_task = Task.find(data['task']['id'])
assert created_task.present?
- assert_equal task_attributes.stringify_keys, created_task.slice(*task_attributes.keys)
+ assert created_task.assignee == assignee
+ assert created_task.author == author
end
test 'should put update' do
@@ -37,7 +41,9 @@ class Api::V1::TasksControllerTest < ActionController::TestCase
merge({ author_id: author.id, assignee_id: assignee.id }).
stringify_keys
- patch :update, params: { id: task.id, format: :json, task: task_attributes }
+ assert_emails 1 do
+ patch :update, params: { id: task.id, format: :json, task: task_attributes }
+ end
assert_response :success
task.reload
diff --git a/test/controllers/web/password_resets_controller_test.rb b/test/controllers/web/password_resets_controller_test.rb
new file mode 100644
index 0000000..284740a
--- /dev/null
+++ b/test/controllers/web/password_resets_controller_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Web::PasswordResetsControllerTest < ActionController::TestCase
+ test 'new' do
+ get :new
+ assert_response :success
+ end
+
+ test 'create' do
+ user = create(:user)
+ token_before_reset = user.reset_digest
+
+ post :create, params: { password_reset_form: { email: user.email } }
+ assert_response :redirect
+
+ user.reload
+ assert token_before_reset != user.reset_digest
+ end
+
+ test 'edit' do
+ user = create(:user)
+ UserService.reset_password!(user)
+ token = user.reset_digest
+
+ get :edit, params: { user: { reset_digest: token, email: user.email } }
+ assert_response :success
+ end
+
+ test 'update' do
+ user = create(:user)
+ UserService.reset_password!(user)
+
+ new_password = generate(:string)
+
+ attrs = {
+ password: new_password,
+ password_confirmation: new_password,
+ }
+
+ patch :update, params: { user: { reset_digest: user.reset_digest, email: user.email, user: attrs } }
+ assert_response :redirect
+ user.reload
+ assert user.reset_digest.nil?
+ assert user.reset_sent_at.nil?
+ end
+end
diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb
new file mode 100644
index 0000000..adebc0f
--- /dev/null
+++ b/test/mailers/previews/user_mailer_preview.rb
@@ -0,0 +1,30 @@
+# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
+class UserMailerPreview < ActionMailer::Preview
+ def task_created
+ user = User.first
+ task = Task.first
+ params = { user: user, task: task }
+
+ UserMailer.with(params).task_created
+ end
+
+ def task_updated
+ task = Task.first
+ params = { task: task }
+
+ UserMailer.with(params).task_updated
+ end
+
+ def task_destroyed
+ task = Task.first
+ params = { task: task }
+
+ UserMailer.with(params).task_destroyed
+ end
+
+ def reset_password
+ user = User.first
+
+ UserService.reset_password!(user)
+ end
+end
diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb
new file mode 100644
index 0000000..63f7c3b
--- /dev/null
+++ b/test/mailers/user_mailer_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class UserMailerTest < ActionMailer::TestCase
+ test 'task created' do
+ user = create(:user)
+ task = create(:task, author: user)
+ params = { user: user, task: task }
+ email = UserMailer.with(params).task_created
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal ['noreply@taskmanager.com'], email.from
+ assert_equal [user.email], email.to
+ assert_equal 'New Task Created', email.subject
+ assert email.body.to_s.include?("Task #{task.id} was created")
+ end
+
+ test 'password reset created' do
+ user = create(:user)
+ token = SecureRandom.hex(10)
+ user.update!(reset_digest: token, reset_sent_at: Time.current)
+
+ email = UserMailer.with({ user: user }).reset_password
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal ['noreply@taskmanager.com'], email.from
+ assert_equal [user.email], email.to
+ assert_equal 'Reset Password', email.subject
+ assert email.body.to_s.include?(user.reset_digest.to_s)
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 274e044..9962dfc 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -23,6 +23,7 @@
require 'rails/test_help'
class ActiveSupport::TestCase
+ include ActionMailer::TestHelper
include AuthHelper
include FactoryBot::Syntax::Methods
end