From 2f940fab135ed300e2842d7d250a1a62d3f30953 Mon Sep 17 00:00:00 2001 From: Trevor Vallender Date: Mon, 3 Jun 2024 11:54:38 +0100 Subject: [PATCH] Add password reset functionality --- Gemfile | 1 + Gemfile.lock | 7 ++++ app/assets/stylesheets/colors.css | 4 +- app/assets/stylesheets/layout.css | 2 +- app/controllers/password_resets_controller.rb | 42 +++++++++++++++++++ app/mailers/table_invite_mailer.rb | 4 +- app/mailers/user_mailer.rb | 11 ++++- app/views/password_resets/edit.html.erb | 17 ++++++++ app/views/password_resets/new.html.erb | 15 +++++++ app/views/sessions/new.html.erb | 1 + app/views/user_mailer/password_reset.html.erb | 3 ++ app/views/user_mailer/password_reset.text.erb | 3 ++ config/environments/development.rb | 1 + config/locales/en.yml | 29 +++++++++++++ config/routes.rb | 1 + .../password_resets_controller_test.rb | 34 +++++++++++++++ todo.md | 3 ++ 17 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 app/controllers/password_resets_controller.rb create mode 100644 app/views/password_resets/edit.html.erb create mode 100644 app/views/password_resets/new.html.erb create mode 100644 app/views/user_mailer/password_reset.html.erb create mode 100644 app/views/user_mailer/password_reset.text.erb create mode 100644 test/controllers/password_resets_controller_test.rb diff --git a/Gemfile b/Gemfile index f542974..8b3fc29 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ group :development, :test do end group :development do + gem "letter_opener" gem "rubocop-rails-omakase" gem "web-console" end diff --git a/Gemfile.lock b/Gemfile.lock index c9ef976..be52154 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,6 +99,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + childprocess (5.0.0) concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) @@ -134,6 +135,11 @@ GEM activesupport (>= 5.0.0) json (2.7.2) language_server-protocol (3.17.0.3) + launchy (3.0.1) + addressable (~> 2.8) + childprocess (~> 5.0) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -327,6 +333,7 @@ DEPENDENCIES image_processing (~> 1.2) importmap-rails jbuilder + letter_opener mission_control-jobs pg propshaft diff --git a/app/assets/stylesheets/colors.css b/app/assets/stylesheets/colors.css index 116e64d..48c1727 100644 --- a/app/assets/stylesheets/colors.css +++ b/app/assets/stylesheets/colors.css @@ -1,9 +1,9 @@ :root { - --background-color: #111; + --background-color: #333; --main-background-color: --header-color: #15345b; --header-text-color: #fff; - --header-height: 150px; + --header-height: 200px; --inset-bg-color: #eee; diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css index 6d8a710..68a1fcc 100644 --- a/app/assets/stylesheets/layout.css +++ b/app/assets/stylesheets/layout.css @@ -63,7 +63,7 @@ header:before { } header h1 { - padding: .5em 0; + padding: 1em 0; } header h1 a:link, header h1 a:visited { diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb new file mode 100644 index 0000000..f27a976 --- /dev/null +++ b/app/controllers/password_resets_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class PasswordResetsController < ApplicationController + skip_before_action :authenticate + + def new + reset_session + end + + def create + user = User.find_by(username: params[:username]) + if user + token = user.generate_token_for(:password_reset) + UserMailer.with(user: user, token: token).password_reset.deliver_later + redirect_to new_session_path, notice: t(".success") and return + end + + redirect_to :root, notice: t(".error") + end + + def edit + reset_session + @user = User.find_by(username: params[:id]) + @token = params[:token] + unless @user == User.find_by_token_for(:password_reset, params[:token]) + redirect_to :root, notice: t(".invalid_token") and return + end + end + + def update + user = User.find_by(username: params[:id]) + unless user == User.find_by_token_for(:password_reset, params[:token]) + redirect_to :root, notice: t(".invalid_token") and return + end + + if user.update(password: params[:password], password_confirmation: params[:password_confirmation]) + redirect_to new_session_path, notice: t(".success") + else + redirect_to :root, notice: t(".error") + end + end +end diff --git a/app/mailers/table_invite_mailer.rb b/app/mailers/table_invite_mailer.rb index d16116f..8eef87c 100644 --- a/app/mailers/table_invite_mailer.rb +++ b/app/mailers/table_invite_mailer.rb @@ -6,12 +6,12 @@ class TableInviteMailer < ApplicationMailer def invite_user @table_invite = params[:table_invite] - mail(to: @table_invite.email, subject: "[Tabletop Companion] Invite to join a table") + mail(to: @table_invite.email, subject: t(".invite_user.subject")) end def invite_new_user @table_invite = params[:table_invite] - mail(to: @table_invite.email, subject: "[Tabletop Companion] You’ve been invited to a game!") + mail(to: @table_invite.email, subject: t(".invite_new_user.subject")) end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 94d48f8..08d945f 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -7,12 +7,19 @@ class UserMailer < ApplicationMailer @user = params[:user] @token = params[:token] - mail(to: @user.email, subject: "[Tabletop Companion] Verify your email") + mail(to: @user.email, subject: t(".email_verification.subject")) end def email_verified @user = params[:user] - mail(to: @user.email, subject: "[Tabletop Companion] Your email has been verified") + mail(to: @user.email, subject: t(".email_verified.subject")) + end + + def password_reset + @user = params[:user] + @token = params[:token] + + mail(to: @user.email, subject: t(".password_reset.subject")) end end diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb new file mode 100644 index 0000000..2559cde --- /dev/null +++ b/app/views/password_resets/edit.html.erb @@ -0,0 +1,17 @@ +<%= content_for :title, t(".reset_password") %> + +

<%= t(".reset_password") %>

+ +
+ <%= form_with url: password_reset_path(id: @user.username), method: :patch do |f| %> + <%= f.hidden_field :token, value: @token %> + + <%= f.label :password %> + <%= f.password_field :password %> + + <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation %> + + <%= f.submit t(".reset_password_button") %> + <% end %> +
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb new file mode 100644 index 0000000..afb73a5 --- /dev/null +++ b/app/views/password_resets/new.html.erb @@ -0,0 +1,15 @@ +<%= content_for :title, t(".reset_password") %> + +

<%= t(".reset_password") %>

+
+

<%= t(".forgotten_password_intro") %>

+ + <%= form_with url: password_resets_path do |f| %> + <%= f.label :username %> + <%= f.text_field :username %> + + <%= f.submit t(".reset_password_button") %> + <% end %> + +
+ diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 33427d2..bac1db4 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -14,3 +14,4 @@ <% end %> +<%= link_to(t(".forgot_password"), new_password_reset_path) %> diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb new file mode 100644 index 0000000..7b0035d --- /dev/null +++ b/app/views/user_mailer/password_reset.html.erb @@ -0,0 +1,3 @@ +<%= htmlify_email(t(".content")) %> + +<%= link_to t(".reset_password"), edit_password_reset_url(id: @user, token: @token) %> diff --git a/app/views/user_mailer/password_reset.text.erb b/app/views/user_mailer/password_reset.text.erb new file mode 100644 index 0000000..ad321d2 --- /dev/null +++ b/app/views/user_mailer/password_reset.text.erb @@ -0,0 +1,3 @@ +<%= t(".content") %> + +<%= edit_password_reset_url(id: @user, token: @token) %> diff --git a/config/environments/development.rb b/config/environments/development.rb index af49d87..dd1699a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -40,6 +40,7 @@ Rails.application.configure do # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :letter_opener config.action_mailer.perform_caching = false diff --git a/config/locales/en.yml b/config/locales/en.yml index 9a8ebd2..87f966a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -54,22 +54,42 @@ en: destroy: success: Successfully deleted “%{name}”. error: “%{name}” could not be deleted. + password_resets: + new: + reset_password: Reset your password + forgotten_password_intro: |- + If you have forgotten your password, enter your information below to reset it. + reset_password_button: Reset password + create: + success: Please check your email to reset your password. + error: Could not reset your password. + edit: + reset_password: Reset your password + invalid_token: That token seems to have expired, please try resettting your password again. + reset_password_button: Update password + update: + invalid_token: That token seems to have expired, please try resettting your password again. + success: Your password has been reset, you may now log in. + error: Failed to reset password. Please try again or contact us for help. sessions: create: success: "Hello, %{name}!" error: "Could not sign in. Please check your username and password." new: log_in: Log in + forgot_password: Forgotten your password? destroy: log_out: Log out success: "You have signed out." table_invite_mailer: invite_new_user: + subject: You’ve been invited to join a game on Tabletop Companion! content: |- You’ve been invited to join a game on Tabletop Companion. To start playing, sign up at the link below and accept your invitation! sign_up: Sign up now invite_user: + subject: You’ve been invited to a new game! content: |- You have been invited to join the table “%{table_name}” on Tabletop Companion. To respond, visit the link below. @@ -145,13 +165,22 @@ en: error: Failed to update profile user_mailer: email_verified: + subject: Email verified on Tabletop Companion content: |- Thanks for verifying your email address, and welcome to Tabletop Companion! You can now go ahead and log in. Enjoy! email_verification: + subject: Verify your email on Tabletop Companion content: |- If you did not sign up for Tabletop Companion, please ignore this email. Otherwise, please visit the link below to verify your email address. + password_reset: + subject: Reset your password on Tabletop Companion + reset_password: Reset your password + content: |- + If you did not request a password reset, please ignore this email. + + Otherwise, please visit the link below to reset your password. diff --git a/config/routes.rb b/config/routes.rb index 1dc2a8c..a21bb2f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,7 @@ Rails.application.routes.draw do resources :users, only: [ :new, :create, :show, :edit, :update ] resources :account_verifications, only: [ :show ] + resources :password_resets, only: [ :new, :create, :edit, :update ] resources :sessions, only: [ :new, :create, :destroy ] resources :table_invites, only: [ :index, :edit, :update ] diff --git a/test/controllers/password_resets_controller_test.rb b/test/controllers/password_resets_controller_test.rb new file mode 100644 index 0000000..dac4a47 --- /dev/null +++ b/test/controllers/password_resets_controller_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "test_helper" + +class PasswordResetsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get new_password_reset_path + assert_response :success + end + + test "should send a password reset email" do + user = users(:trevor) + assert_emails(+1) do + post password_resets_path, params: { username: user.username } + assert_redirected_to new_session_path + end + end + + test "should get edit" do + user = users(:trevor) + token = user.generate_token_for(:password_reset) + get edit_password_reset_path(token: token, id: user.username) + assert_response :success + end + + test "should update password" do + user = users(:trevor) + token = user.generate_token_for(:password_reset) + put password_reset_path(id: user.username, token: token), + params: { password: "password", password_confirmation: "password" } + assert_redirected_to new_session_path + assert user.reload.authenticate("password") + end +end diff --git a/todo.md b/todo.md index 6cf5e1a..5648ce7 100644 --- a/todo.md +++ b/todo.md @@ -2,6 +2,9 @@ - delete avatar - default avatars - discrete password page +- shared/private notes - notifications - Add characters to users/tables - Character sheets/prototypes +- chat +- maps