diff --git a/app/assets/stylesheets/messages.css b/app/assets/stylesheets/messages.css index 88bd1519..672e324f 100644 --- a/app/assets/stylesheets/messages.css +++ b/app/assets/stylesheets/messages.css @@ -593,3 +593,9 @@ img.message__attachment { inline-size: 1.4em; } } + +/* Reset password */ +.reset-password-alert { + color: var(--color-negative); + visibility: hidden; +} diff --git a/app/controllers/sessions/password_resets_controller.rb b/app/controllers/sessions/password_resets_controller.rb new file mode 100644 index 00000000..6ed819fc --- /dev/null +++ b/app/controllers/sessions/password_resets_controller.rb @@ -0,0 +1,55 @@ +class Sessions::PasswordResetsController < ApplicationController + allow_unauthenticated_access + + before_action :require_smpt + + def index + end + def new + end + + def show + @password_reset_id = params[:id] + @user = User.find_by_password_reset_id(@password_reset_id) + + redirect_to root_url unless @user + end + + def update + @user = User.find_by_password_reset_id(password_reset_params[:password_reset_id]) + + redirect_to root_url unless @user + redirect_to root_url unless password_match? + + @user.update(password: password_reset_params[:new_password]) + + redirect_to new_session_path + end + + def create + email = params[:email_address] + password_reset_url = session_password_reset_url(find_user_by_email(email).password_reset_id) + + PasswordResetMailer.with(email: email, url: password_reset_url).password_reset_email.deliver_later + + redirect_to new_session_password_reset_path + end + + private + + def require_smpt + redirect_to root_url unless helpers.smtp_enabled? + end + + def find_user_by_email(email) + User.find_by(email_address: email) + end + + def password_match? + password_reset_params[:new_password] == password_reset_params[:confirm_new_password] + end + + def password_reset_params + params.require(:user).permit(:new_password, :confirm_new_password, :password_reset_id) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index db434972..302e02a8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -35,6 +35,17 @@ def link_back_to(destination) end end + def smtp_enabled? + Rails.application.config.respond_to?(:feature_enable_smtp) && + Rails.application.config.feature_enable_smtp.present? && + Rails.application.config.action_mailer.smtp_settings.present? && + Rails.application.config.action_mailer.smtp_settings[:address].present? && + Rails.application.config.action_mailer.smtp_settings[:port].present? && + Rails.application.config.action_mailer.smtp_settings[:domain].present? && + Rails.application.config.action_mailer.smtp_settings[:user_name].present? && + Rails.application.config.action_mailer.smtp_settings[:password].present? + end + private def admin_body_class "admin" if Current.user&.can_administer? diff --git a/app/javascript/controllers/password_reset_controller.js b/app/javascript/controllers/password_reset_controller.js new file mode 100644 index 00000000..16c64f5a --- /dev/null +++ b/app/javascript/controllers/password_reset_controller.js @@ -0,0 +1,38 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ "resetPasswordInput", "resetPasswordConfirmInput", "resetPasswordError", "resetPasswordSubmit"] + + resetPasswordCheckInputs() { + if(this.#checkResetInputsValues()) { + this.#hideErrorMessage() + this.#enableSubmitButton() + } else { + this.#showErrorMessage() + this.#disableSubmitButton() + } + } + + #showErrorMessage() { + this.resetPasswordErrorTarget.style.visibility = "visible" + } + + #hideErrorMessage() { + this.resetPasswordErrorTarget.style.visibility = "hidden" + } + + #enableSubmitButton() { + this.resetPasswordSubmitTarget.disabled = false + } + + #disableSubmitButton() { + this.resetPasswordSubmitTarget.disabled = true + } + + #checkResetInputsValues() { + if (this.resetPasswordInputTarget.value.length < 0 || this.resetPasswordConfirmInputTarget.value.length < 0) return false + if (this.resetPasswordInputTarget.value !== this.resetPasswordConfirmInputTarget.value) return false + + return true + } +} diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..b43e1447 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +class ApplicationMailer < ActionMailer::Base + DEFAULT_BASE_FROM="noreply@default.com" + + default from: ENV.fetch("SMTP_INFO_EMAIL_FROM", DEFAULT_BASE_FROM) + layout "mailer" +end diff --git a/app/mailers/password_reset_mailer.rb b/app/mailers/password_reset_mailer.rb new file mode 100644 index 00000000..d54f0ee4 --- /dev/null +++ b/app/mailers/password_reset_mailer.rb @@ -0,0 +1,9 @@ +class PasswordResetMailer < ApplicationMailer + default from: ENV.fetch("SMTP_PASSWORD_RESET_EMAIL_FROM", DEFAULT_BASE_FROM) + + def password_reset_email + @email = params[:email] + @url = params[:url] + mail(to: @email, subject: "Campfire Reset Password") + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 20d05dd1..31ce90cc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord - include Avatar, Bannable, Bot, Mentionable, Role, Transferable + include Avatar, Bannable, Bot, Mentionable, Role, Transferable, Resettable has_many :memberships, dependent: :delete_all has_many :rooms, through: :memberships diff --git a/app/models/user/resettable.rb b/app/models/user/resettable.rb new file mode 100644 index 00000000..c9e6e9a2 --- /dev/null +++ b/app/models/user/resettable.rb @@ -0,0 +1,15 @@ +module User::Resettable + extend ActiveSupport::Concern + + RESET_PASSWORD_LINK_EXPIRY_DURATION = 5.hours + + class_methods do + def find_by_password_reset_id(id) + find_signed(id, purpose: :password_reset) + end + end + + def password_reset_id + signed_id(purpose: :password_reset, expires_in: RESET_PASSWORD_LINK_EXPIRY_DURATION) + end +end diff --git a/app/views/password_reset_mailer/password_reset_email.html.erb b/app/views/password_reset_mailer/password_reset_email.html.erb new file mode 100644 index 00000000..4e3e2eac --- /dev/null +++ b/app/views/password_reset_mailer/password_reset_email.html.erb @@ -0,0 +1,9 @@ +
Hello,
+You have requested a password reset for your Campfire account.
++ To reset your Campfire password, please click on this: <%= link_to 'link', @url %>. +
+Campfire
diff --git a/app/views/password_reset_mailer/password_reset_email.text.erb b/app/views/password_reset_mailer/password_reset_email.text.erb new file mode 100644 index 00000000..8159dc07 --- /dev/null +++ b/app/views/password_reset_mailer/password_reset_email.text.erb @@ -0,0 +1,9 @@ +Reset Your Campfire Password +=============================================== +Hello, + +You have requested a password reset for your Campfire account. + +To reset your Campfire password, please click on this: <%= link_to 'link', @url %>. + +Campfire \ No newline at end of file diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index e15850c2..fbe57cef 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -25,6 +25,15 @@ + <% if smtp_enabled? %> +Hello,
+You have requested a password reset for your Campfire account.
++ To reset your Campfire password, please click on this: link. +
+Campfire
+ + + diff --git a/test/mailers/fixture_templates/password_reset_text_fixture.txt b/test/mailers/fixture_templates/password_reset_text_fixture.txt new file mode 100644 index 00000000..c0f5ffa5 --- /dev/null +++ b/test/mailers/fixture_templates/password_reset_text_fixture.txt @@ -0,0 +1,9 @@ +Reset Your Campfire Password +=============================================== +Hello, + +You have requested a password reset for your Campfire account. + +To reset your Campfire password, please click on this: link. + +Campfire diff --git a/test/mailers/password_reset_mailer_test.rb b/test/mailers/password_reset_mailer_test.rb new file mode 100644 index 00000000..45ffe63b --- /dev/null +++ b/test/mailers/password_reset_mailer_test.rb @@ -0,0 +1,19 @@ +require "test_helper" + +class PasswordResetMailerTest < ActionMailer::TestCase + test "password reset" do + email = PasswordResetMailer.with(email: "me@campfire.com", url: "http://www.example.com").password_reset_email + + assert_emails 1 do + email.deliver_now + end + + assert_equal [ ENV.fetch("SMTP_PASSWORD_RESET_EMAIL_FROM", ApplicationMailer::DEFAULT_BASE_FROM) ], email.from + assert_equal [ "me@campfire.com" ], email.to + assert_equal "Campfire Reset Password", email.subject + + # fixtures for mails have to be outside default fixtures directory, since there are no tables in DB for the mails + assert_equal file_fixture("../../mailers/fixture_templates/password_reset_text_fixture.txt").read, email.text_part.body.to_s + assert_equal file_fixture("../../mailers/fixture_templates/password_reset_html_fixture.txt").read, email.html_part.body.to_s + end +end diff --git a/test/mailers/previews/password_reset_mailer_preview.rb b/test/mailers/previews/password_reset_mailer_preview.rb new file mode 100644 index 00000000..e4eeea03 --- /dev/null +++ b/test/mailers/previews/password_reset_mailer_preview.rb @@ -0,0 +1,6 @@ +# Preview all emails at http://localhost:3000/rails/mailers/password_mailer +class PasswordResetMailerPreview < ActionMailer::Preview + def reset_password_email + PasswordResetMailer.with(user: "test@test.com", url: "http://localhost:3000").password_reset_email + end +end